Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions frontend/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { test, expect } from "@playwright/test";

test.describe("authentication", () => {
test.beforeEach(async ({ page }) => {
// Start each auth test without a persisted session.
await page.goto("/");

await page.evaluate(() => {
sessionStorage.clear();
});
});

test("redirects unauthenticated users from profile to login", async ({
page,
}) => {
await page.goto("/profile");

// Protected routes should redirect users without an auth token.
await expect(page).toHaveURL(/\/login$/);

await expect(
page.getByRole("heading", {
name: "Login",
level: 1,
}),
).toBeVisible();

await expect(page.getByLabel("Username")).toBeVisible();
await expect(page.getByLabel("Password")).toBeVisible();
});

test("shows an error when login credentials are invalid", async ({
page,
}) => {
await page.goto("/login");

const loginButton = page.getByRole("button", {
name: "Login",
exact: true,
});

const loginForm = loginButton.locator("xpath=ancestor::form");

const usernameInput = loginForm.getByLabel("Username");
const passwordInput = loginForm.getByLabel("Password");

// Use credentials that should not belong to a real account.
await usernameInput.fill(`invalid-user-${Date.now()}`);
await passwordInput.fill("DefinitelyWrong123!");

await expect(usernameInput).not.toHaveValue("");
await expect(passwordInput).not.toHaveValue("");

await loginButton.click();

// The login attempt should fail without navigating away.
await expect(page).toHaveURL(/\/login$/);

await expect(page.getByRole("alert")).toBeVisible();

// The form should become usable again after the failed request.
await expect(
page.getByRole("button", {
name: "Login",
exact: true,
}),
).toBeEnabled();
});
});
278 changes: 278 additions & 0 deletions frontend/e2e/core-flow.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { test, expect } from "@playwright/test";

test("core user flow", async ({ page }) => {
// The complete flow performs several frontend, backend and database operations.
test.setTimeout(60_000);

// Unique values prevent conflicts when the test is run multiple times.
const runId = Date.now().toString().slice(-8);
const username = `e2e${runId}`;
const password = "Playwright123!";
const galleryTitle = `E2E Gallery ${runId}`;

await page.goto("/");

const artworkResults = page.getByRole("list", {
name: "Artwork results",
});

await expect(artworkResults).toBeVisible();

// Use an artwork that already exists in the database instead of hardcoding a title that might change later.
const firstArtworkHeading = artworkResults
.getByRole("heading", { level: 3 })
.first();

await expect(firstArtworkHeading).toBeVisible();

const artworkTitle = (await firstArtworkHeading.textContent())?.trim();

if (!artworkTitle) {
throw new Error("Could not find an artwork title to use in the E2E test.");
}

// Search for the selected artwork.
const searchInput = page.getByLabel("Search artworks by title or artist");

await searchInput.fill(artworkTitle);
await page.getByRole("button", { name: "Search artworks" }).click();

// Verify that the search is reflected in the URL.
await expect
.poll(() => new URL(page.url()).searchParams.get("search"))
.toBe(artworkTitle);

await expect(
artworkResults.getByRole("heading", {
name: artworkTitle,
exact: true,
}),
).toBeVisible();

// Open the filter panel before accessing the sort controls.
await page.getByRole("button", { name: "Show filters" }).click();

// Open the sort section inside the filter panel.
await page.getByRole("button", { name: "Sort" }).click();

// React Select uses an internal input, so keyboard interaction is more reliable.
const sortSelect = page.getByLabel("Sort by");

await sortSelect.focus();
await sortSelect.press("ArrowDown");

await page.getByRole("option", { name: "Title: A-Z" }).click();

// Verify that the selected sorting option is reflected in the URL.
await expect
.poll(() => new URL(page.url()).searchParams.get("sort"))
.toBe("TITLE_ASC");

// Open the artwork detail page.
await artworkResults
.getByRole("heading", {
name: artworkTitle,
exact: true,
})
.click();

await expect(page).toHaveURL(/\/artworks\/\d+/);

await expect(
page.getByRole("heading", {
name: artworkTitle,
exact: true,
level: 1,
}),
).toBeVisible();

await expect(
page.getByRole("button", {
name: "Add artwork to gallery",
}),
).toBeVisible();

// Go back to the catalogue and create a new account through the UI.
await page.getByRole("link", { name: "← Back" }).click();

// Use the desktop login link specifically because the page contains multiple links with the same accessible name.
await page
.getByLabel("Explore Artex")
.getByRole("link", { name: "Open login" })
.click();

await expect(page).toHaveURL(/\/login$/);

await page.getByRole("link", { name: "Create account" }).click();

await expect(page).toHaveURL(/\/create-account$/);

const createAccountButton = page.getByRole("button", {
name: "Create account",
exact: true,
});

await expect(createAccountButton).toBeEnabled();

// Scope all registration fields to the exact form containing the submit button.
const createAccountForm = createAccountButton.locator("xpath=ancestor::form");

const usernameInput = createAccountForm.getByLabel("Username");
const passwordInput = createAccountForm.getByLabel("Password");

await usernameInput.fill(username);
await passwordInput.fill(password);

// Verify that the values are present in this exact form.
await expect(usernameInput).toHaveValue(username);
await expect(passwordInput).toHaveValue(password);

const formIsValid = await createAccountForm.evaluate(
(form: HTMLFormElement) => form.checkValidity(),
);

expect(formIsValid).toBe(true);

await createAccountButton.click();

// If the submit handler runs, the button should enter its loading state.
await expect(
page.getByRole("button", {
name: "Creating account...",
exact: true,
}),
).toBeVisible({
timeout: 5_000,
});

// Successful registration should redirect to the profile.
// If registration fails, the application should display an alert instead.
await expect(
page
.getByRole("heading", { name: "Your Gallery" })
.or(page.getByRole("alert")),
).toBeVisible({
timeout: 10_000,
});

await expect(page).toHaveURL(/\/profile$/);

await expect(
page.getByRole("heading", {
name: "Your Gallery",
}),
).toBeVisible();

// Create a gallery for the artwork.
await page.getByRole("button", { name: "Create gallery" }).click();

await expect(
page.getByRole("heading", {
name: "Create a new gallery",
}),
).toBeVisible();

await page.getByPlaceholder("Enter gallery title").fill(galleryTitle);

await page
.getByRole("button", {
name: "Create",
exact: true,
})
.click();

// Wait until the newly created gallery has been fetched and rendered.
const galleryLink = page.getByRole("link").filter({
hasText: galleryTitle,
});

await expect(galleryLink).toBeVisible();

// Return to the catalogue while still logged in.
await page.getByRole("link", { name: "← Back" }).click();

await expect(page).toHaveURL(/\/$/);

// Search for the same artwork again.
await page
.getByLabel("Search artworks by title or artist")
.fill(artworkTitle);

await page.getByRole("button", { name: "Search artworks" }).click();

await expect(
page.getByRole("list", { name: "Artwork results" }).getByRole("heading", {
name: artworkTitle,
exact: true,
}),
).toBeVisible();

// Open the artwork again and add it to the newly created gallery.
await page
.getByRole("list", { name: "Artwork results" })
.getByRole("heading", {
name: artworkTitle,
exact: true,
})
.click();

await page
.getByRole("button", {
name: "Add artwork to gallery",
})
.click();

const addToGalleryDialog = page.getByRole("dialog", {
name: "Add to gallery",
});

await expect(addToGalleryDialog).toBeVisible();

await addToGalleryDialog
.getByRole("button", {
name: galleryTitle,
exact: true,
})
.click();

// The modal closes after the artwork has successfully been added.
await expect(addToGalleryDialog).not.toBeVisible();

// Return to the catalogue and open the profile.
await page.getByRole("link", { name: "← Back" }).click();

// Use the desktop profile link specifically because the page contains multiple links with the same accessible name.
await page
.getByLabel("Explore Artex")
.getByRole("link", { name: "Open profile" })
.click();

await expect(page).toHaveURL(/\/profile$/);

// Open the gallery created earlier.
const createdGalleryLink = page.getByRole("link").filter({
hasText: galleryTitle,
});

await expect(createdGalleryLink).toBeVisible();
await createdGalleryLink.click();

await expect(page).toHaveURL(/\/galleries\/.+/);

await expect(
page.getByRole("heading", {
name: galleryTitle,
exact: true,
level: 1,
}),
).toBeVisible();

// Final assertion: the artwork added earlier is persisted in the gallery.
await expect(
page.getByRole("heading", {
name: artworkTitle,
exact: true,
level: 3,
}),
).toBeVisible();
});
Loading