test: add Playwright E2E test suite (30 tests)
Add browser-level end-to-end tests covering public pages, Keycloak OAuth authentication flow, dashboard interactions, providers config, developer section, organization pages, and sidebar navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -22,3 +22,8 @@ keycloak/*
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
searxng/
|
searxng/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
e2e/.auth/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|||||||
9
bun.lock
9
bun.lock
@@ -8,6 +8,7 @@
|
|||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.52.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -16,6 +17,8 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
||||||
@@ -24,6 +27,12 @@
|
|||||||
|
|
||||||
"daisyui": ["daisyui@5.5.18", "", {}, "sha512-VVzjpOitMGB6DWIBeRSapbjdOevFqyzpk9u5Um6a4tyId3JFrU5pbtF0vgjXDth76mJZbueN/j9Ok03SPrh/og=="],
|
"daisyui": ["daisyui@5.5.18", "", {}, "sha512-VVzjpOitMGB6DWIBeRSapbjdOevFqyzpk9u5Um6a4tyId3JFrU5pbtF0vgjXDth76mJZbueN/j9Ok03SPrh/og=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
|
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
||||||
|
|
||||||
|
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|||||||
24
e2e/auth.setup.ts
Normal file
24
e2e/auth.setup.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { test as setup, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
const AUTH_FILE = "e2e/.auth/user.json";
|
||||||
|
|
||||||
|
setup("authenticate via Keycloak", async ({ page }) => {
|
||||||
|
// Navigate to a protected route to trigger the auth redirect chain:
|
||||||
|
// /dashboard -> /auth (Axum) -> Keycloak login page
|
||||||
|
await page.goto("/dashboard");
|
||||||
|
|
||||||
|
// Wait for Keycloak login form to appear
|
||||||
|
await page.waitForSelector("#username", { timeout: 15_000 });
|
||||||
|
|
||||||
|
// Fill Keycloak credentials
|
||||||
|
await page.fill("#username", process.env.TEST_USER ?? "admin@certifai.local");
|
||||||
|
await page.fill("#password", process.env.TEST_PASSWORD ?? "admin");
|
||||||
|
await page.click("#kc-login");
|
||||||
|
|
||||||
|
// Wait for redirect back to the app dashboard
|
||||||
|
await page.waitForURL("**/dashboard", { timeout: 15_000 });
|
||||||
|
await expect(page.locator(".sidebar")).toBeVisible();
|
||||||
|
|
||||||
|
// Persist authenticated state (cookies + localStorage)
|
||||||
|
await page.context().storageState({ path: AUTH_FILE });
|
||||||
|
});
|
||||||
72
e2e/auth.spec.ts
Normal file
72
e2e/auth.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
// These tests use a fresh browser context (no saved auth state)
|
||||||
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
|
test.describe("Authentication flow", () => {
|
||||||
|
test("unauthenticated visit to /dashboard redirects to Keycloak", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/dashboard");
|
||||||
|
|
||||||
|
// Should end up on Keycloak login page
|
||||||
|
await page.waitForSelector("#username", { timeout: 15_000 });
|
||||||
|
await expect(page.locator("#kc-login")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("valid credentials log in and redirect to dashboard", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/dashboard");
|
||||||
|
await page.waitForSelector("#username", { timeout: 15_000 });
|
||||||
|
|
||||||
|
await page.fill(
|
||||||
|
"#username",
|
||||||
|
process.env.TEST_USER ?? "admin@certifai.local"
|
||||||
|
);
|
||||||
|
await page.fill("#password", process.env.TEST_PASSWORD ?? "admin");
|
||||||
|
await page.click("#kc-login");
|
||||||
|
|
||||||
|
await page.waitForURL("**/dashboard", { timeout: 15_000 });
|
||||||
|
await expect(page.locator(".dashboard-page")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dashboard shows sidebar with user info after login", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/dashboard");
|
||||||
|
await page.waitForSelector("#username", { timeout: 15_000 });
|
||||||
|
|
||||||
|
await page.fill(
|
||||||
|
"#username",
|
||||||
|
process.env.TEST_USER ?? "admin@certifai.local"
|
||||||
|
);
|
||||||
|
await page.fill("#password", process.env.TEST_PASSWORD ?? "admin");
|
||||||
|
await page.click("#kc-login");
|
||||||
|
|
||||||
|
await page.waitForURL("**/dashboard", { timeout: 15_000 });
|
||||||
|
await expect(page.locator(".sidebar-name")).toBeVisible();
|
||||||
|
await expect(page.locator(".sidebar-email")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logout redirects away from dashboard", async ({ page }) => {
|
||||||
|
// First log in
|
||||||
|
await page.goto("/dashboard");
|
||||||
|
await page.waitForSelector("#username", { timeout: 15_000 });
|
||||||
|
|
||||||
|
await page.fill(
|
||||||
|
"#username",
|
||||||
|
process.env.TEST_USER ?? "admin@certifai.local"
|
||||||
|
);
|
||||||
|
await page.fill("#password", process.env.TEST_PASSWORD ?? "admin");
|
||||||
|
await page.click("#kc-login");
|
||||||
|
|
||||||
|
await page.waitForURL("**/dashboard", { timeout: 15_000 });
|
||||||
|
|
||||||
|
// Click logout
|
||||||
|
await page.locator('a.logout-btn, a[href="/logout"]').click();
|
||||||
|
|
||||||
|
// Should no longer be on the dashboard
|
||||||
|
await expect(page).not.toHaveURL(/\/dashboard/);
|
||||||
|
});
|
||||||
|
});
|
||||||
75
e2e/dashboard.spec.ts
Normal file
75
e2e/dashboard.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Dashboard", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto("/dashboard");
|
||||||
|
// Wait for WASM hydration and auth check to complete
|
||||||
|
await page.waitForSelector(".dashboard-page", { timeout: 15_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dashboard page loads with page header", async ({ page }) => {
|
||||||
|
await expect(page.locator(".page-header")).toContainText("Dashboard");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("default topic chips are visible", async ({ page }) => {
|
||||||
|
const topics = ["AI", "Technology", "Science", "Finance", "Writing", "Research"];
|
||||||
|
|
||||||
|
for (const topic of topics) {
|
||||||
|
await expect(
|
||||||
|
page.locator(".filter-tab", { hasText: topic })
|
||||||
|
).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clicking a topic chip triggers search", async ({ page }) => {
|
||||||
|
const chip = page.locator(".filter-tab", { hasText: "AI" });
|
||||||
|
await chip.click();
|
||||||
|
|
||||||
|
// Either a loading state or results should appear
|
||||||
|
const searchingOrResults = page
|
||||||
|
.locator(".dashboard-loading, .news-grid, .dashboard-empty");
|
||||||
|
await expect(searchingOrResults.first()).toBeVisible({ timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("news cards render after search completes", async ({ page }) => {
|
||||||
|
// Click a topic to trigger search
|
||||||
|
await page.locator(".filter-tab", { hasText: "Technology" }).click();
|
||||||
|
|
||||||
|
// Wait for loading to finish
|
||||||
|
await page.waitForSelector(".dashboard-loading", {
|
||||||
|
state: "hidden",
|
||||||
|
timeout: 15_000,
|
||||||
|
}).catch(() => {
|
||||||
|
// Loading may already be done
|
||||||
|
});
|
||||||
|
|
||||||
|
// Either news cards or an empty state message should be visible
|
||||||
|
const content = page.locator(".news-grid .news-card, .dashboard-empty");
|
||||||
|
await expect(content.first()).toBeVisible({ timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clicking a news card opens article detail panel", async ({ page }) => {
|
||||||
|
// Trigger a search and wait for results
|
||||||
|
await page.locator(".filter-tab", { hasText: "AI" }).click();
|
||||||
|
|
||||||
|
await page.waitForSelector(".dashboard-loading", {
|
||||||
|
state: "hidden",
|
||||||
|
timeout: 15_000,
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
const firstCard = page.locator(".news-card").first();
|
||||||
|
// Only test if cards are present (search results depend on live data)
|
||||||
|
if (await firstCard.isVisible().catch(() => false)) {
|
||||||
|
await firstCard.click();
|
||||||
|
await expect(page.locator(".dashboard-right, .dashboard-split")).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("settings toggle opens settings panel", async ({ page }) => {
|
||||||
|
const settingsBtn = page.locator(".settings-toggle");
|
||||||
|
await settingsBtn.click();
|
||||||
|
|
||||||
|
await expect(page.locator(".settings-panel")).toBeVisible();
|
||||||
|
await expect(page.locator(".settings-panel-title")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
33
e2e/developer.spec.ts
Normal file
33
e2e/developer.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Developer section", () => {
|
||||||
|
test("agents page loads with sub-nav tabs", async ({ page }) => {
|
||||||
|
await page.goto("/developer/agents");
|
||||||
|
await page.waitForSelector(".developer-shell", { timeout: 15_000 });
|
||||||
|
|
||||||
|
const nav = page.locator(".sub-nav");
|
||||||
|
await expect(nav.locator("a", { hasText: "Agents" })).toBeVisible();
|
||||||
|
await expect(nav.locator("a", { hasText: "Flow" })).toBeVisible();
|
||||||
|
await expect(nav.locator("a", { hasText: "Analytics" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("agents page shows Coming Soon badge", async ({ page }) => {
|
||||||
|
await page.goto("/developer/agents");
|
||||||
|
await page.waitForSelector(".placeholder-page", { timeout: 15_000 });
|
||||||
|
|
||||||
|
await expect(page.locator(".placeholder-badge")).toContainText(
|
||||||
|
"Coming Soon"
|
||||||
|
);
|
||||||
|
await expect(page.locator("h2")).toContainText("Agent Builder");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("analytics page loads via sub-nav", async ({ page }) => {
|
||||||
|
await page.goto("/developer/analytics");
|
||||||
|
await page.waitForSelector(".placeholder-page", { timeout: 15_000 });
|
||||||
|
|
||||||
|
await expect(page.locator("h2")).toContainText("Analytics");
|
||||||
|
await expect(page.locator(".placeholder-badge")).toContainText(
|
||||||
|
"Coming Soon"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
52
e2e/navigation.spec.ts
Normal file
52
e2e/navigation.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Sidebar navigation", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto("/dashboard");
|
||||||
|
await page.waitForSelector(".sidebar", { timeout: 15_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sidebar links route to correct pages", async ({ page }) => {
|
||||||
|
const navTests = [
|
||||||
|
{ label: "Providers", url: /\/providers/ },
|
||||||
|
{ label: "Developer", url: /\/developer\/agents/ },
|
||||||
|
{ label: "Organization", url: /\/organization\/pricing/ },
|
||||||
|
{ label: "Dashboard", url: /\/dashboard/ },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { label, url } of navTests) {
|
||||||
|
await page.locator(".sidebar-link", { hasText: label }).click();
|
||||||
|
await expect(page).toHaveURL(url, { timeout: 10_000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("browser back/forward navigation works", async ({ page }) => {
|
||||||
|
// Navigate to Providers
|
||||||
|
await page.locator(".sidebar-link", { hasText: "Providers" }).click();
|
||||||
|
await expect(page).toHaveURL(/\/providers/);
|
||||||
|
|
||||||
|
// Navigate to Developer
|
||||||
|
await page.locator(".sidebar-link", { hasText: "Developer" }).click();
|
||||||
|
await expect(page).toHaveURL(/\/developer/);
|
||||||
|
|
||||||
|
// Go back
|
||||||
|
await page.goBack();
|
||||||
|
await expect(page).toHaveURL(/\/providers/);
|
||||||
|
|
||||||
|
// Go forward
|
||||||
|
await page.goForward();
|
||||||
|
await expect(page).toHaveURL(/\/developer/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logo link navigates to dashboard", async ({ page }) => {
|
||||||
|
// Navigate away first
|
||||||
|
await page.locator(".sidebar-link", { hasText: "Providers" }).click();
|
||||||
|
await expect(page).toHaveURL(/\/providers/);
|
||||||
|
|
||||||
|
// Click the logo/brand in sidebar header
|
||||||
|
const logo = page.locator(".sidebar-brand, .sidebar-logo, .sidebar a").first();
|
||||||
|
await logo.click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/dashboard/);
|
||||||
|
});
|
||||||
|
});
|
||||||
41
e2e/organization.spec.ts
Normal file
41
e2e/organization.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Organization section", () => {
|
||||||
|
test("pricing page loads with three pricing cards", async ({ page }) => {
|
||||||
|
await page.goto("/organization/pricing");
|
||||||
|
await page.waitForSelector(".org-shell", { timeout: 15_000 });
|
||||||
|
|
||||||
|
const cards = page.locator(".pricing-card");
|
||||||
|
await expect(cards).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("pricing cards show Starter, Team, Enterprise tiers", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/organization/pricing");
|
||||||
|
await page.waitForSelector(".org-shell", { timeout: 15_000 });
|
||||||
|
|
||||||
|
await expect(page.locator(".pricing-card", { hasText: "Starter" })).toBeVisible();
|
||||||
|
await expect(page.locator(".pricing-card", { hasText: "Team" })).toBeVisible();
|
||||||
|
await expect(page.locator(".pricing-card", { hasText: "Enterprise" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("organization dashboard loads with billing stats", async ({ page }) => {
|
||||||
|
await page.goto("/organization/dashboard");
|
||||||
|
await page.waitForSelector(".org-dashboard-page", { timeout: 15_000 });
|
||||||
|
|
||||||
|
await expect(page.locator(".page-header")).toContainText("Organization");
|
||||||
|
await expect(page.locator(".org-stats-bar")).toBeVisible();
|
||||||
|
await expect(page.locator(".org-stat").first()).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("member table is visible on org dashboard", async ({ page }) => {
|
||||||
|
await page.goto("/organization/dashboard");
|
||||||
|
await page.waitForSelector(".org-dashboard-page", { timeout: 15_000 });
|
||||||
|
|
||||||
|
await expect(page.locator(".org-table")).toBeVisible();
|
||||||
|
await expect(page.locator(".org-table thead")).toContainText("Name");
|
||||||
|
await expect(page.locator(".org-table thead")).toContainText("Email");
|
||||||
|
await expect(page.locator(".org-table thead")).toContainText("Role");
|
||||||
|
});
|
||||||
|
});
|
||||||
55
e2e/providers.spec.ts
Normal file
55
e2e/providers.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Providers page", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto("/providers");
|
||||||
|
await page.waitForSelector(".providers-page", { timeout: 15_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("providers page loads with header", async ({ page }) => {
|
||||||
|
await expect(page.locator(".page-header")).toContainText("Providers");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("provider dropdown has Ollama selected by default", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const providerSelect = page
|
||||||
|
.locator(".form-group")
|
||||||
|
.filter({ hasText: "Provider" })
|
||||||
|
.locator("select");
|
||||||
|
|
||||||
|
await expect(providerSelect).toHaveValue(/ollama/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("changing provider updates the model dropdown", async ({ page }) => {
|
||||||
|
const providerSelect = page
|
||||||
|
.locator(".form-group")
|
||||||
|
.filter({ hasText: "Provider" })
|
||||||
|
.locator("select");
|
||||||
|
|
||||||
|
// Get current model options
|
||||||
|
const modelSelect = page
|
||||||
|
.locator(".form-group")
|
||||||
|
.filter({ hasText: /^Model/ })
|
||||||
|
.locator("select");
|
||||||
|
const initialOptions = await modelSelect.locator("option").allTextContents();
|
||||||
|
|
||||||
|
// Change to a different provider
|
||||||
|
await providerSelect.selectOption({ label: "OpenAI" });
|
||||||
|
|
||||||
|
// Wait for model list to update
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const updatedOptions = await modelSelect.locator("option").allTextContents();
|
||||||
|
|
||||||
|
// Model options should differ between providers
|
||||||
|
expect(updatedOptions).not.toEqual(initialOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save button shows confirmation feedback", async ({ page }) => {
|
||||||
|
const saveBtn = page.locator("button", { hasText: "Save Configuration" });
|
||||||
|
await saveBtn.click();
|
||||||
|
|
||||||
|
await expect(page.locator(".form-success")).toBeVisible({ timeout: 5_000 });
|
||||||
|
await expect(page.locator(".form-success")).toContainText("saved");
|
||||||
|
});
|
||||||
|
});
|
||||||
60
e2e/public.spec.ts
Normal file
60
e2e/public.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Public pages", () => {
|
||||||
|
test("landing page loads with heading and nav links", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await expect(page.locator(".landing-logo").first()).toHaveText("CERTifAI");
|
||||||
|
await expect(page.locator(".landing-nav-links")).toBeVisible();
|
||||||
|
await expect(page.locator('a[href="#features"]')).toBeVisible();
|
||||||
|
await expect(page.locator('a[href="#how-it-works"]')).toBeVisible();
|
||||||
|
await expect(page.locator('a[href="#pricing"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("landing page Log In link navigates to login route", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
const loginLink = page
|
||||||
|
.locator(".landing-nav-actions a, .landing-nav-actions Link")
|
||||||
|
.filter({ hasText: "Log In" });
|
||||||
|
await loginLink.click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/login/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("impressum page loads with legal content", async ({ page }) => {
|
||||||
|
await page.goto("/impressum");
|
||||||
|
|
||||||
|
await expect(page.locator("h1")).toHaveText("Impressum");
|
||||||
|
await expect(
|
||||||
|
page.locator("h2", { hasText: "Information according to" })
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.locator(".legal-content")).toContainText(
|
||||||
|
"CERTifAI GmbH"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("privacy page loads with privacy content", async ({ page }) => {
|
||||||
|
await page.goto("/privacy");
|
||||||
|
|
||||||
|
await expect(page.locator("h1")).toHaveText("Privacy Policy");
|
||||||
|
await expect(
|
||||||
|
page.locator("h2", { hasText: "Introduction" })
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.locator("h2", { hasText: "Your Rights" })
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("footer links are present on landing page", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
const footer = page.locator(".landing-footer");
|
||||||
|
await expect(footer.locator('a:has-text("Impressum")')).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
footer.locator('a:has-text("Privacy Policy")')
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.52.0",
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
40
playwright.config.ts
Normal file
40
playwright.config.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./e2e",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: [["html"], ["list"]],
|
||||||
|
timeout: 30_000,
|
||||||
|
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.BASE_URL ?? "http://localhost:8000",
|
||||||
|
actionTimeout: 10_000,
|
||||||
|
trace: "on-first-retry",
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "setup",
|
||||||
|
testMatch: /auth\.setup\.ts/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "public",
|
||||||
|
testMatch: /public\.spec\.ts/,
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "authenticated",
|
||||||
|
testMatch: /\.spec\.ts$/,
|
||||||
|
testIgnore: /public\.spec\.ts$/,
|
||||||
|
dependencies: ["setup"],
|
||||||
|
use: {
|
||||||
|
...devices["Desktop Chrome"],
|
||||||
|
storageState: "e2e/.auth/user.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user