From 5727c3927c05ad17d4aef97ca3c489baa2bb7029 Mon Sep 17 00:00:00 2001 From: mina Date: Tue, 10 Nov 2026 20:14:36 +0100 Subject: [PATCH 1/9] test: add e2e test for core user flow --- frontend/e2e/core-flow.spec.ts | 318 +++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 frontend/e2e/core-flow.spec.ts diff --git a/frontend/e2e/core-flow.spec.ts b/frontend/e2e/core-flow.spec.ts new file mode 100644 index 0000000..ff96dbc --- /dev/null +++ b/frontend/e2e/core-flow.spec.ts @@ -0,0 +1,318 @@ +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}`; + + // Open the catalogue. + 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(), + ); + + const invalidFields = await createAccountForm.evaluate( + (form: HTMLFormElement) => + Array.from(form.querySelectorAll(":invalid")).map((element) => { + const input = element as HTMLInputElement; + + return { + name: input.name, + value: input.value, + validationMessage: input.validationMessage, + }; + }), + ); + + console.log("Invalid fields:", invalidFields); + + expect(formIsValid).toBe(true); + + // Log browser/runtime errors while debugging registration. + page.on("console", (message) => { + if (message.type() === "error") { + console.log("Browser error:", message.text()); + } + }); + + page.on("pageerror", (error) => { + console.log("Page error:", error.message); + }); + + // Log POST requests to verify that useRegister reaches GraphQL. + page.on("request", (request) => { + if (request.method() === "POST") { + console.log("POST request:", request.url(), request.postData()); + } + }); + + page.on("requestfailed", (request) => { + console.log("Request failed:", request.url(), request.failure()?.errorText); + }); + + 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(); +}); From fba45c98d27d2c1334be8f1ae326a76a80a68b2e Mon Sep 17 00:00:00 2001 From: mina Date: Tue, 10 Nov 2026 20:32:42 +0100 Subject: [PATCH 2/9] test: remove e2e debug logging --- frontend/e2e/core-flow.spec.ts | 44 ++-------------------------------- 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/frontend/e2e/core-flow.spec.ts b/frontend/e2e/core-flow.spec.ts index ff96dbc..d26ddd3 100644 --- a/frontend/e2e/core-flow.spec.ts +++ b/frontend/e2e/core-flow.spec.ts @@ -10,7 +10,6 @@ test("core user flow", async ({ page }) => { const password = "Playwright123!"; const galleryTitle = `E2E Gallery ${runId}`; - // Open the catalogue. await page.goto("/"); const artworkResults = page.getByRole("list", { @@ -19,8 +18,7 @@ test("core user flow", async ({ page }) => { await expect(artworkResults).toBeVisible(); - // Use an artwork that already exists in the database instead of - // hardcoding a title that might change later. + // 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(); @@ -97,8 +95,7 @@ test("core user flow", async ({ page }) => { // 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. + // 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" }) @@ -134,45 +131,8 @@ test("core user flow", async ({ page }) => { (form: HTMLFormElement) => form.checkValidity(), ); - const invalidFields = await createAccountForm.evaluate( - (form: HTMLFormElement) => - Array.from(form.querySelectorAll(":invalid")).map((element) => { - const input = element as HTMLInputElement; - - return { - name: input.name, - value: input.value, - validationMessage: input.validationMessage, - }; - }), - ); - - console.log("Invalid fields:", invalidFields); - expect(formIsValid).toBe(true); - // Log browser/runtime errors while debugging registration. - page.on("console", (message) => { - if (message.type() === "error") { - console.log("Browser error:", message.text()); - } - }); - - page.on("pageerror", (error) => { - console.log("Page error:", error.message); - }); - - // Log POST requests to verify that useRegister reaches GraphQL. - page.on("request", (request) => { - if (request.method() === "POST") { - console.log("POST request:", request.url(), request.postData()); - } - }); - - page.on("requestfailed", (request) => { - console.log("Request failed:", request.url(), request.failure()?.errorText); - }); - await createAccountButton.click(); // If the submit handler runs, the button should enter its loading state. From 21dfe55132ea8841adb9c37354c7f4c2d2526c10 Mon Sep 17 00:00:00 2001 From: mina Date: Tue, 10 Nov 2026 20:33:11 +0100 Subject: [PATCH 3/9] test: configure Playwright e2e testing --- frontend/package-lock.json | 46 +++++++++++++++++++++++++++++++++++ frontend/package.json | 4 ++- frontend/playwright.config.ts | 17 +++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 frontend/playwright.config.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e7be7c5..09630a5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "devDependencies": { "@babel/core": "^7.29.7", "@eslint/js": "^10.0.1", + "@playwright/test": "^1.65.0", "@rolldown/plugin-babel": "^0.2.3", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", @@ -1002,6 +1003,22 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@playwright/test": { + "version": "1.65.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.65.0.tgz", + "integrity": "sha512-iGURLGuKDLakSEl7Pgfv8h7f+XLT+V861Wfhz0zlEl5nyFuKUopdkFBPKqTllTUp06aOwPuH8iuUEb3yp3MpbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.65.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.15", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", @@ -4164,6 +4181,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.65.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.65.0.tgz", + "integrity": "sha512-F3AMWcR44qMBt4kq+6Kb6mX7bjGXshktKFOPULd7reED6ZJWevjbNRqysP7q+AATE9kdPEZBibex1BYsvpVoeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.65.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.65.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.65.0.tgz", + "integrity": "sha512-vNSKgH6YqtN7cdT/L334zo0a+YwkrpicyrHJlWWPIm/Q+Nn2SWInil01oT0mbu96U4GgoRtCYQD9CzJQgju+lA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/postcss": { "version": "8.5.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", diff --git a/frontend/package.json b/frontend/package.json index 22d6202..903dc24 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,8 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "test": "vitest run" + "test": "vitest run", + "test:e2e": "playwright test" }, "dependencies": { "@apollo/client": "^4.3.3", @@ -26,6 +27,7 @@ "devDependencies": { "@babel/core": "^7.29.7", "@eslint/js": "^10.0.1", + "@playwright/test": "^1.65.0", "@rolldown/plugin-babel": "^0.2.3", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..456e933 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + + use: { + baseURL: "http://localhost:5173", + trace: "on-first-retry", + }, + + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}); \ No newline at end of file From 6c216b20b4560ce7782142288d373a820512a18f Mon Sep 17 00:00:00 2001 From: mina Date: Tue, 10 Nov 2026 20:38:11 +0100 Subject: [PATCH 4/9] test: add authentication e2e tests --- frontend/e2e/auth.spec.ts | 69 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 frontend/e2e/auth.spec.ts diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts new file mode 100644 index 0000000..54493a2 --- /dev/null +++ b/frontend/e2e/auth.spec.ts @@ -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(); + }); +}); From 71bd71aa57effb60b230608351b96a81919c5c7a Mon Sep 17 00:00:00 2001 From: mina Date: Tue, 10 Nov 2026 20:45:15 +0100 Subject: [PATCH 5/9] test: add gallery management e2e test --- frontend/e2e/gallery-management.spec.ts | 158 ++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 frontend/e2e/gallery-management.spec.ts diff --git a/frontend/e2e/gallery-management.spec.ts b/frontend/e2e/gallery-management.spec.ts new file mode 100644 index 0000000..835aee4 --- /dev/null +++ b/frontend/e2e/gallery-management.spec.ts @@ -0,0 +1,158 @@ +import { test, expect } from "@playwright/test"; + +test("manages a gallery through rename and delete", async ({ page }) => { + test.setTimeout(45_000); + + // Use unique values so repeated test runs do not conflict. + const runId = Date.now().toString().slice(-8); + const username = `gallery-e2e-${runId}`; + const password = "Playwright123!"; + const originalTitle = `Gallery ${runId}`; + const renamedTitle = `Renamed Gallery ${runId}`; + + // Register a fresh user so the test starts with an isolated account. + await page.goto("/create-account"); + + const createAccountButton = page.getByRole("button", { + name: "Create account", + exact: true, + }); + + 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); + + await expect(usernameInput).toHaveValue(username); + await expect(passwordInput).toHaveValue(password); + + await createAccountButton.click(); + + await expect(page).toHaveURL(/\/profile$/); + + await expect( + page.getByRole("heading", { + name: "Your Gallery", + }), + ).toBeVisible(); + + // Create a new gallery. + 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(originalTitle); + + await page + .getByRole("button", { + name: "Create", + exact: true, + }) + .click(); + + const galleryLink = page.getByRole("link").filter({ + hasText: originalTitle, + }); + + await expect(galleryLink).toBeVisible(); + + await galleryLink.click(); + + await expect(page).toHaveURL(/\/galleries\/.+/); + + await expect( + page.getByRole("heading", { + name: originalTitle, + exact: true, + level: 1, + }), + ).toBeVisible(); + + // Enter edit mode before renaming the gallery. + await page + .getByRole("button", { + name: "Edit gallery", + exact: true, + }) + .click(); + + await page + .getByRole("button", { + name: "Rename gallery", + }) + .click(); + + const galleryNameInput = page.getByLabel("Gallery name"); + + await expect(galleryNameInput).toHaveValue(originalTitle); + + await galleryNameInput.fill(renamedTitle); + + await page + .getByRole("button", { + name: "Save", + exact: true, + }) + .click(); + + // Verify that the renamed title is persisted and rendered. + await expect( + page.getByRole("heading", { + name: renamedTitle, + exact: true, + level: 1, + }), + ).toBeVisible(); + + await expect( + page.getByRole("status").getByText("Gallery name updated."), + ).toBeVisible(); + + // Delete the gallery while still in edit mode. + await page + .getByRole("button", { + name: "Delete gallery", + exact: true, + }) + .click(); + + const deleteDialog = page.getByRole("dialog", { + name: "Delete gallery?", + }); + + await expect(deleteDialog).toBeVisible(); + + await expect( + deleteDialog.getByText(`Delete “${renamedTitle}”? This cannot be undone.`, { + exact: false, + }), + ).toBeVisible(); + + await deleteDialog + .getByRole("button", { + name: "Delete gallery", + exact: true, + }) + .click(); + + // Successful deletion redirects back to the profile page. + await expect(page).toHaveURL(/\/profile$/); + + await expect( + page.getByRole("status").getByText("Gallery deleted."), + ).toBeVisible(); + + // The deleted gallery should no longer be listed on the profile. + await expect( + page.getByRole("link").filter({ + hasText: renamedTitle, + }), + ).toHaveCount(0); +}); From 1fff4a1775e115ef824b38d6c7631cc652b0aa74 Mon Sep 17 00:00:00 2001 From: mina Date: Wed, 11 Nov 2026 15:35:18 +0100 Subject: [PATCH 6/9] test: isolate e2e data in separate database --- backend/src/db.ts | 3 +- backend/src/seedE2eDatabase.ts | 62 ++++++++++++++++++++++++++++++++++ frontend/playwright.config.ts | 17 ++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 backend/src/seedE2eDatabase.ts diff --git a/backend/src/db.ts b/backend/src/db.ts index 9190de1..93b414d 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -9,5 +9,6 @@ export async function connectToDatabase() { console.log("Connected to MongoDB"); - return client.db("artex"); + // Use the normal Artex database unless another database is explicitly configured. + return client.db(process.env.MONGODB_DB ?? "artex"); } diff --git a/backend/src/seedE2eDatabase.ts b/backend/src/seedE2eDatabase.ts new file mode 100644 index 0000000..5fd02ed --- /dev/null +++ b/backend/src/seedE2eDatabase.ts @@ -0,0 +1,62 @@ +import "dotenv/config"; +import { MongoClient } from "mongodb"; + +const mongoUri = process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017"; + +const sourceDatabaseName = process.env.E2E_SOURCE_DB ?? "artex"; +const targetDatabaseName = process.env.MONGODB_DB ?? "artex_e2e_test"; + +const ARTWORK_LIMIT = 50; + +const client = new MongoClient(mongoUri); + +async function seedE2eDatabase() { + if (sourceDatabaseName === targetDatabaseName) { + throw new Error( + "E2E source and target database cannot be the same database.", + ); + } + + try { + await client.connect(); + + const sourceDatabase = client.db(sourceDatabaseName); + const targetDatabase = client.db(targetDatabaseName); + + const sourceArtworks = sourceDatabase.collection("met_artworks"); + const targetArtworks = targetDatabase.collection("met_artworks"); + + // Start every E2E run without data created by previous test runs. + await Promise.all([ + targetDatabase.collection("users").deleteMany({}), + targetDatabase.collection("galleries").deleteMany({}), + targetArtworks.deleteMany({}), + ]); + + // Copy a small deterministic subset of real artwork data for the E2E tests. + const artworks = await sourceArtworks + .find({}, { projection: { _id: 0 } }) + .sort({ id: 1 }) + .limit(ARTWORK_LIMIT) + .toArray(); + + if (artworks.length === 0) { + throw new Error( + `No artworks found in source database "${sourceDatabaseName}".`, + ); + } + + await targetArtworks.insertMany(artworks); + + console.log( + `Seeded "${targetDatabaseName}" with ${artworks.length} artworks from "${sourceDatabaseName}".`, + ); + } finally { + await client.close(); + } +} + +seedE2eDatabase().catch((error: unknown) => { + console.error("Failed to seed E2E database:", error); + process.exitCode = 1; +}); diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 456e933..29b7a87 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -8,6 +8,23 @@ export default defineConfig({ trace: "on-first-retry", }, + webServer: [ + { + command: "MONGODB_URI=mongodb://127.0.0.1:27018 MONGODB_DB=artex_e2e_test npx tsx src/seedE2eDatabase.ts && MONGODB_URI=mongodb://127.0.0.1:27018 MONGODB_DB=artex_e2e_test npm run dev", + cwd: "../backend", + url: "http://127.0.0.1:3001", + reuseExistingServer: false, + timeout: 30_000, + }, + { + command: "npm run dev", + cwd: ".", + url: "http://localhost:5173", + reuseExistingServer: true, + timeout: 30_000, + }, + ], + projects: [ { name: "chromium", From 2f999c7eff562cb51ea5eeec01461bae6d6b7f80 Mon Sep 17 00:00:00 2001 From: mina Date: Wed, 11 Nov 2026 16:14:15 +0100 Subject: [PATCH 7/9] refactor: move e2e tests to project root --- {frontend/e2e => e2e}/auth.spec.ts | 0 {frontend/e2e => e2e}/core-flow.spec.ts | 0 {frontend/e2e => e2e}/gallery-management.spec.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {frontend/e2e => e2e}/auth.spec.ts (100%) rename {frontend/e2e => e2e}/core-flow.spec.ts (100%) rename {frontend/e2e => e2e}/gallery-management.spec.ts (100%) diff --git a/frontend/e2e/auth.spec.ts b/e2e/auth.spec.ts similarity index 100% rename from frontend/e2e/auth.spec.ts rename to e2e/auth.spec.ts diff --git a/frontend/e2e/core-flow.spec.ts b/e2e/core-flow.spec.ts similarity index 100% rename from frontend/e2e/core-flow.spec.ts rename to e2e/core-flow.spec.ts diff --git a/frontend/e2e/gallery-management.spec.ts b/e2e/gallery-management.spec.ts similarity index 100% rename from frontend/e2e/gallery-management.spec.ts rename to e2e/gallery-management.spec.ts From c3d1d0f6a59454cb7b154bfe64163e6a77c8a6e1 Mon Sep 17 00:00:00 2001 From: mina Date: Wed, 11 Nov 2026 16:14:43 +0100 Subject: [PATCH 8/9] refactor: move Playwright config to project root --- frontend/playwright.config.ts => playwright.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename frontend/playwright.config.ts => playwright.config.ts (93%) diff --git a/frontend/playwright.config.ts b/playwright.config.ts similarity index 93% rename from frontend/playwright.config.ts rename to playwright.config.ts index 29b7a87..91dbef1 100644 --- a/frontend/playwright.config.ts +++ b/playwright.config.ts @@ -11,14 +11,14 @@ export default defineConfig({ webServer: [ { command: "MONGODB_URI=mongodb://127.0.0.1:27018 MONGODB_DB=artex_e2e_test npx tsx src/seedE2eDatabase.ts && MONGODB_URI=mongodb://127.0.0.1:27018 MONGODB_DB=artex_e2e_test npm run dev", - cwd: "../backend", + cwd: "./backend", url: "http://127.0.0.1:3001", reuseExistingServer: false, timeout: 30_000, }, { command: "npm run dev", - cwd: ".", + cwd: "./frontend", url: "http://localhost:5173", reuseExistingServer: true, timeout: 30_000, From fb5032daa544a6060425b1dd1ca687656fae3baf Mon Sep 17 00:00:00 2001 From: mina Date: Wed, 11 Nov 2026 16:15:13 +0100 Subject: [PATCH 9/9] chore: move Playwright setup to project root --- frontend/package-lock.json | 46 ------------------------------ frontend/package.json | 4 +-- package-lock.json | 58 ++++++++++++++++++++++++++++++++++++++ package.json | 10 +++++++ 4 files changed, 69 insertions(+), 49 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 09630a5..e7be7c5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,7 +23,6 @@ "devDependencies": { "@babel/core": "^7.29.7", "@eslint/js": "^10.0.1", - "@playwright/test": "^1.65.0", "@rolldown/plugin-babel": "^0.2.3", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", @@ -1003,22 +1002,6 @@ "url": "https://github.com/sponsors/oxc-project" } }, - "node_modules/@playwright/test": { - "version": "1.65.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.65.0.tgz", - "integrity": "sha512-iGURLGuKDLakSEl7Pgfv8h7f+XLT+V861Wfhz0zlEl5nyFuKUopdkFBPKqTllTUp06aOwPuH8iuUEb3yp3MpbQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.65.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.15", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", @@ -4181,35 +4164,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/playwright": { - "version": "1.65.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.65.0.tgz", - "integrity": "sha512-F3AMWcR44qMBt4kq+6Kb6mX7bjGXshktKFOPULd7reED6ZJWevjbNRqysP7q+AATE9kdPEZBibex1BYsvpVoeA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.65.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/playwright-core": { - "version": "1.65.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.65.0.tgz", - "integrity": "sha512-vNSKgH6YqtN7cdT/L334zo0a+YwkrpicyrHJlWWPIm/Q+Nn2SWInil01oT0mbu96U4GgoRtCYQD9CzJQgju+lA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/postcss": { "version": "8.5.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", diff --git a/frontend/package.json b/frontend/package.json index 903dc24..22d6202 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,8 +8,7 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "test": "vitest run", - "test:e2e": "playwright test" + "test": "vitest run" }, "dependencies": { "@apollo/client": "^4.3.3", @@ -27,7 +26,6 @@ "devDependencies": { "@babel/core": "^7.29.7", "@eslint/js": "^10.0.1", - "@playwright/test": "^1.65.0", "@rolldown/plugin-babel": "^0.2.3", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2c683b0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,58 @@ +{ + "name": "t12-project-2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "t12-project-2", + "devDependencies": { + "@playwright/test": "^1.65.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.65.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.65.0.tgz", + "integrity": "sha512-iGURLGuKDLakSEl7Pgfv8h7f+XLT+V861Wfhz0zlEl5nyFuKUopdkFBPKqTllTUp06aOwPuH8iuUEb3yp3MpbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.65.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright": { + "version": "1.65.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.65.0.tgz", + "integrity": "sha512-F3AMWcR44qMBt4kq+6Kb6mX7bjGXshktKFOPULd7reED6ZJWevjbNRqysP7q+AATE9kdPEZBibex1BYsvpVoeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.65.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.65.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.65.0.tgz", + "integrity": "sha512-vNSKgH6YqtN7cdT/L334zo0a+YwkrpicyrHJlWWPIm/Q+Nn2SWInil01oT0mbu96U4GgoRtCYQD9CzJQgju+lA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5899fea --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "t12-project-2", + "private": true, + "scripts": { + "test:e2e": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.65.0" + } +} \ No newline at end of file