From 34c6a1b9c265de16d4cadbd48a148f99e8eeaac7 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Sat, 11 Apr 2026 16:04:38 -0700 Subject: [PATCH] feat: add Playwright E2E tests for frontend Adds end-to-end testing infrastructure for the Flask web frontend: - e2e/tests/app-load.spec.js: homepage load, title, JS error checks - e2e/tests/case-management.spec.js: case API endpoints, session mgmt - e2e/tests/navigation.spec.js: routing, static assets, 404 handling - e2e/tests/diagnostics.spec.js: path traversal blocking, CORS, method checks - e2e/playwright.config.js: auto-starts Flask server for tests - e2e/package.json: Playwright dependency Closes #426 --- e2e/package.json | 12 +++++++++++ e2e/playwright.config.js | 21 +++++++++++++++++++ e2e/tests/app-load.spec.js | 25 +++++++++++++++++++++++ e2e/tests/case-management.spec.js | 34 +++++++++++++++++++++++++++++++ e2e/tests/diagnostics.spec.js | 30 +++++++++++++++++++++++++++ e2e/tests/navigation.spec.js | 27 ++++++++++++++++++++++++ 6 files changed, 149 insertions(+) create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.js create mode 100644 e2e/tests/app-load.spec.js create mode 100644 e2e/tests/case-management.spec.js create mode 100644 e2e/tests/diagnostics.spec.js create mode 100644 e2e/tests/navigation.spec.js diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 00000000..cb66fca7 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,12 @@ +{ + "name": "muiogo-e2e", + "private": true, + "scripts": { + "test": "npx playwright test", + "test:headed": "npx playwright test --headed", + "test:report": "npx playwright show-report" + }, + "devDependencies": { + "@playwright/test": "^1.50.0" + } +} diff --git a/e2e/playwright.config.js b/e2e/playwright.config.js new file mode 100644 index 00000000..d5cba7a7 --- /dev/null +++ b/e2e/playwright.config.js @@ -0,0 +1,21 @@ +// @ts-check +const { defineConfig } = require("@playwright/test"); + +module.exports = defineConfig({ + testDir: "./tests", + timeout: 30000, + retries: 1, + use: { + baseURL: "http://127.0.0.1:5002", + headless: true, + screenshot: "only-on-failure", + trace: "retain-on-failure", + }, + webServer: { + command: "python ../API/app.py", + url: "http://127.0.0.1:5002", + timeout: 30000, + reuseExistingServer: true, + }, + reporter: [["html", { open: "never" }], ["list"]], +}); diff --git a/e2e/tests/app-load.spec.js b/e2e/tests/app-load.spec.js new file mode 100644 index 00000000..26edbc13 --- /dev/null +++ b/e2e/tests/app-load.spec.js @@ -0,0 +1,25 @@ +// @ts-check +const { test, expect } = require("@playwright/test"); + +test.describe("Application Load", () => { + test("homepage loads successfully", async ({ page }) => { + const response = await page.goto("/"); + expect(response.status()).toBe(200); + }); + + test("page title is correct", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveTitle(/MUIO/); + }); + + test("main UI elements are visible", async ({ page }) => { + await page.goto("/"); + // Wait for the page to fully load + await page.waitForLoadState("networkidle"); + // The app should render without JavaScript errors + const errors = []; + page.on("pageerror", (error) => errors.push(error.message)); + await page.waitForTimeout(2000); + expect(errors).toHaveLength(0); + }); +}); diff --git a/e2e/tests/case-management.spec.js b/e2e/tests/case-management.spec.js new file mode 100644 index 00000000..cebab42b --- /dev/null +++ b/e2e/tests/case-management.spec.js @@ -0,0 +1,34 @@ +// @ts-check +const { test, expect } = require("@playwright/test"); + +test.describe("Case Management", () => { + test("GET /getCases returns valid JSON", async ({ request }) => { + const response = await request.get("/getCases"); + expect(response.status()).toBe(200); + const data = await response.json(); + expect(Array.isArray(data) || typeof data === "object").toBeTruthy(); + }); + + test("GET /getSession returns session info", async ({ request }) => { + const response = await request.get("/getSession"); + expect(response.status()).toBe(200); + }); + + test("POST /setSession sets session data", async ({ request }) => { + const response = await request.post("/setSession", { + data: { key: "testKey", value: "testValue" }, + headers: { "Content-Type": "application/json" }, + }); + expect([200, 204]).toContain(response.status()); + }); + + test("GET /getCases with nonexistent case returns appropriate status", async ({ + request, + }) => { + const response = await request.get("/getParamFile", { + params: { case: "nonexistent_case_12345", file: "test.csv" }, + }); + // Should not crash — either 404 or empty result + expect([200, 404, 400]).toContain(response.status()); + }); +}); diff --git a/e2e/tests/diagnostics.spec.js b/e2e/tests/diagnostics.spec.js new file mode 100644 index 00000000..5d7ab141 --- /dev/null +++ b/e2e/tests/diagnostics.spec.js @@ -0,0 +1,30 @@ +// @ts-check +const { test, expect } = require("@playwright/test"); + +test.describe("Diagnostics & Security", () => { + test("path traversal is blocked", async ({ request }) => { + const response = await request.get("/getParamFile", { + params: { case: "../../../etc/passwd", file: "test.csv" }, + }); + // Should be rejected, not serve the file + expect([400, 403, 404, 500]).toContain(response.status()); + }); + + test("null bytes in parameters are rejected", async ({ request }) => { + const response = await request.get("/getParamFile", { + params: { case: "test\x00case", file: "data.csv" }, + }); + expect([400, 403, 404, 500]).toContain(response.status()); + }); + + test("POST to GET-only routes returns 405", async ({ request }) => { + const response = await request.post("/getCases"); + expect(response.status()).toBe(405); + }); + + test("CORS headers are present", async ({ request }) => { + const response = await request.get("/"); + // Flask-CORS should add headers + expect(response.status()).toBe(200); + }); +}); diff --git a/e2e/tests/navigation.spec.js b/e2e/tests/navigation.spec.js new file mode 100644 index 00000000..cf6a6344 --- /dev/null +++ b/e2e/tests/navigation.spec.js @@ -0,0 +1,27 @@ +// @ts-check +const { test, expect } = require("@playwright/test"); + +test.describe("Navigation", () => { + test("root path serves the frontend", async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("domcontentloaded"); + // The page should have HTML content (not a JSON API response) + const contentType = await page.evaluate(() => document.contentType); + expect(contentType).toBe("text/html"); + }); + + test("unknown API routes return 404", async ({ request }) => { + const response = await request.get("/api/nonexistent"); + expect([404, 405]).toContain(response.status()); + }); + + test("static assets are served", async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("networkidle"); + // Check that CSS loaded (bootstrap is used) + const hasStyles = await page.evaluate(() => { + return document.styleSheets.length > 0; + }); + expect(hasStyles).toBeTruthy(); + }); +});