Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
82 changes: 42 additions & 40 deletions dashboard/frontend/e2e/app.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { test, expect } from "@playwright/test";

// Card 4eb58505: no hardcoded external targets.
// - Frontend navigations are relative and resolve against the playwright
Expand All @@ -17,29 +17,28 @@ import { test, expect } from '@playwright/test';
// - E2E_ENV=production flips the docs test to expect 404: the backend
// disables /api/v1/docs when settings.is_production (main.py, PR #147),
// and asserting that response is the point of the test in prod.
const BACKEND_URL = process.env.E2E_API_URL ?? '';
const hasApi = BACKEND_URL !== '';
const BACKEND_URL = process.env.E2E_API_URL ?? "";
const hasApi = BACKEND_URL !== "";

const E2E_USERNAME = process.env.E2E_AUTH_USERNAME ?? '';
const E2E_PASSWORD = process.env.E2E_AUTH_PASSWORD ?? '';
const hasAuth = E2E_USERNAME !== '' && E2E_PASSWORD !== '';
const E2E_USERNAME = process.env.E2E_AUTH_USERNAME ?? "";
const E2E_PASSWORD = process.env.E2E_AUTH_PASSWORD ?? "";
const hasAuth = E2E_USERNAME !== "" && E2E_PASSWORD !== "";
const AUTH_SKIP_REASON =
'E2E_AUTH_USERNAME / E2E_AUTH_PASSWORD not set — configure them (CI secrets or local env) to run login tests against this environment';
"E2E_AUTH_USERNAME / E2E_AUTH_PASSWORD not set — configure them (CI secrets or local env) to run login tests against this environment";

const IS_PRODUCTION = (process.env.E2E_ENV ?? '') === 'production';
const IS_PRODUCTION = (process.env.E2E_ENV ?? "") === "production";

test.describe('QA-FRAMEWORK E2E Tests', () => {

test('Backend health check', async ({ request }) => {
test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend');
test.describe("QA-FRAMEWORK E2E Tests", () => {
test("Backend health check", async ({ request }) => {
test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend");
const response = await request.get(`${BACKEND_URL}/health`);
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.status).toBe('healthy');
expect(data.status).toBe("healthy");
});

test('Backend API docs accessible', async ({ request }) => {
test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend');
test("Backend API docs accessible", async ({ request }) => {
test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend");
const response = await request.get(`${BACKEND_URL}/api/v1/docs`);
if (IS_PRODUCTION) {
// Production deliberately disables the docs endpoints
Expand All @@ -50,60 +49,63 @@ test.describe('QA-FRAMEWORK E2E Tests', () => {
}
});

test('Login API works', async ({ request }) => {
test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend');
test("Login API works", async ({ request }) => {
test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend");
test.skip(!hasAuth, AUTH_SKIP_REASON);
const response = await request.post(`${BACKEND_URL}/api/v1/auth/login`, {
headers: { 'Content-Type': 'application/json' },
data: { username: E2E_USERNAME, password: E2E_PASSWORD }
headers: { "Content-Type": "application/json" },
data: { username: E2E_USERNAME, password: E2E_PASSWORD },
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.access_token).toBeDefined();
expect(data.token_type).toBe('bearer');
expect(data.token_type).toBe("bearer");
});

test('Get user info with token', async ({ request }) => {
test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend');
test("Get user info with token", async ({ request }) => {
test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend");
test.skip(!hasAuth, AUTH_SKIP_REASON);
// First login
const loginResponse = await request.post(`${BACKEND_URL}/api/v1/auth/login`, {
headers: { 'Content-Type': 'application/json' },
data: { username: E2E_USERNAME, password: E2E_PASSWORD }
});
const loginResponse = await request.post(
`${BACKEND_URL}/api/v1/auth/login`,
{
headers: { "Content-Type": "application/json" },
data: { username: E2E_USERNAME, password: E2E_PASSWORD },
},
);
const loginData = await loginResponse.json();
const token = loginData.access_token;

// Get user info
const meResponse = await request.get(`${BACKEND_URL}/api/v1/me`, {
headers: { 'Authorization': `Bearer ${token}` }
headers: { Authorization: `Bearer ${token}` },
});
expect(meResponse.ok()).toBeTruthy();
const userData = await meResponse.json();
expect(userData.username).toBe(E2E_USERNAME);
});

test('Frontend loads', async ({ page }) => {
await page.goto('/');
await expect(page.locator('body')).toBeVisible();
test("Frontend loads", async ({ page }) => {
await page.goto("/");
await expect(page.locator("body")).toBeVisible();
});

test('Login page displays correctly', async ({ page }) => {
await page.goto('/login');
test("Login page displays correctly", async ({ page }) => {
await page.goto("/login");
// Wait for page to load
await page.waitForLoadState('networkidle');
await page.waitForLoadState("networkidle");
// Check for login form elements
const usernameInput = page.locator('input').first();
const usernameInput = page.locator("input").first();
await expect(usernameInput).toBeVisible();
});

test('Full login flow', async ({ page }) => {
test("Full login flow", async ({ page }) => {
test.skip(!hasAuth, AUTH_SKIP_REASON);
await page.goto('/login');
await page.waitForLoadState('networkidle');
await page.goto("/login");
await page.waitForLoadState("networkidle");

// Fill login form using placeholder or type
const inputs = page.locator('input');
const inputs = page.locator("input");
await inputs.nth(0).fill(E2E_USERNAME);
await inputs.nth(1).fill(E2E_PASSWORD);

Expand All @@ -119,8 +121,8 @@ test.describe('QA-FRAMEWORK E2E Tests', () => {
expect(url).toBeDefined();
});

test('Billing plans accessible without auth', async ({ request }) => {
test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend');
test("Billing plans accessible without auth", async ({ request }) => {
test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend");
const response = await request.get(`${BACKEND_URL}/api/v1/billing/plans`);
expect(response.ok()).toBeTruthy();
const data = await response.json();
Expand Down
92 changes: 58 additions & 34 deletions dashboard/frontend/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { test, expect } from "@playwright/test";

/**
* Regression smoke suite (card 4eb58505, from QA's PR #212 local smoke).
Expand All @@ -8,60 +8,84 @@ import { test, expect } from '@playwright/test';
* Catches runtime breakage of the majors migration in a real browser:
* uncaught exceptions, broken public routes, broken auth redirect.
*/
test.describe('Smoke regression (post-migration)', () => {
test('Landing renders with content', async ({ page }) => {
test.describe("Smoke regression (post-migration)", () => {
test("Landing renders with content", async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', (e) => errors.push(String(e)));
await page.goto('/');
await expect(page.locator('body')).toBeVisible();
await expect(page.locator('main, [role="main"], h1, h2').first()).toBeVisible();
expect(errors, `uncaught JS errors on /: ${errors.join(' | ')}`).toEqual([]);
page.on("pageerror", (e) => errors.push(String(e)));
await page.goto("/");
await expect(page.locator("body")).toBeVisible();
await expect(
page.locator('main, [role="main"], h1, h2').first(),
).toBeVisible();
expect(errors, `uncaught JS errors on /: ${errors.join(" | ")}`).toEqual(
[],
);
});

test('Login renders form (2 inputs + submit)', async ({ page }) => {
test("Login renders form (2 inputs + submit)", async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', (e) => errors.push(String(e)));
await page.goto('/login');
await expect(page.locator('input').first()).toBeVisible();
const inputs = page.locator('input');
page.on("pageerror", (e) => errors.push(String(e)));
await page.goto("/login");
await expect(page.locator("input").first()).toBeVisible();
const inputs = page.locator("input");
expect(await inputs.count()).toBeGreaterThanOrEqual(2);
await expect(page.locator('button[type="submit"], button:has-text("Login"), button:has-text("Sign")').first()).toBeVisible();
expect(errors, `uncaught JS errors on /login: ${errors.join(' | ')}`).toEqual([]);
await expect(
page
.locator(
'button[type="submit"], button:has-text("Login"), button:has-text("Sign")',
)
.first(),
).toBeVisible();
expect(
errors,
`uncaught JS errors on /login: ${errors.join(" | ")}`,
).toEqual([]);
});

test('Register renders form', async ({ page }) => {
test("Register renders form", async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', (e) => errors.push(String(e)));
await page.goto('/register');
await expect(page.locator('input').first()).toBeVisible();
expect(await page.locator('input').count()).toBeGreaterThanOrEqual(2);
expect(errors, `uncaught JS errors on /register: ${errors.join(' | ')}`).toEqual([]);
page.on("pageerror", (e) => errors.push(String(e)));
await page.goto("/register");
await expect(page.locator("input").first()).toBeVisible();
expect(await page.locator("input").count()).toBeGreaterThanOrEqual(2);
expect(
errors,
`uncaught JS errors on /register: ${errors.join(" | ")}`,
).toEqual([]);
});

test('Unauthenticated /dashboard redirects to /login (RR7)', async ({ page }) => {
await page.goto('/dashboard');
test("Unauthenticated /dashboard redirects to /login (RR7)", async ({
page,
}) => {
await page.goto("/dashboard");
await page.waitForURL(/\/login/, { timeout: 10_000 });
expect(page.url()).toContain('/login');
expect(page.url()).toContain("/login");
});

test('No 404/500 on unknown route (NotFound page)', async ({ page }) => {
test("No 404/500 on unknown route (NotFound page)", async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', (e) => errors.push(String(e)));
const resp = await page.goto('/definitely-not-a-route');
page.on("pageerror", (e) => errors.push(String(e)));
const resp = await page.goto("/definitely-not-a-route");
expect(resp?.status()).toBeLessThan(500);
expect(errors).toEqual([]);
});

// Visual evidence for the codemod that removed margin="normal" from
// TextField (PR #212, reviewer 9a74c5b6): full-page screenshots are
// uploaded as CI artifacts from every run.
test('Visual spot-check: Login & Register screenshots', async ({ page }) => {
await page.goto('/login');
await expect(page.locator('input').first()).toBeVisible();
await page.screenshot({ path: 'test-results/spotcheck-login.png', fullPage: true });
test("Visual spot-check: Login & Register screenshots", async ({ page }) => {
await page.goto("/login");
await expect(page.locator("input").first()).toBeVisible();
await page.screenshot({
path: "test-results/spotcheck-login.png",
fullPage: true,
});

await page.goto('/register');
await expect(page.locator('input').first()).toBeVisible();
await page.screenshot({ path: 'test-results/spotcheck-register.png', fullPage: true });
await page.goto("/register");
await expect(page.locator("input").first()).toBeVisible();
await page.screenshot({
path: "test-results/spotcheck-register.png",
fullPage: true,
});
});
});
20 changes: 10 additions & 10 deletions dashboard/frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineConfig, devices } from '@playwright/test';
import { defineConfig, devices } from "@playwright/test";

// Card 4eb58505: E2E targets are env-driven — zero hardcoded external stacks.
// - CI (PR): pr-deploy-coolify.yml injects E2E_BASE_URL pointing at the
Expand All @@ -10,30 +10,30 @@ import { defineConfig, devices } from '@playwright/test';
const E2E_BASE_URL = process.env.E2E_BASE_URL;

export default defineConfig({
testDir: './e2e',
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
reporter: "html",
use: {
baseURL: E2E_BASE_URL ?? 'http://localhost:4173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
baseURL: E2E_BASE_URL ?? "http://localhost:4173",
trace: "on-first-retry",
screenshot: "only-on-failure",
// no `video`: requires the ffmpeg playwright binary — not worth the CI
// dependency for this suite (screenshots + trace carry the evidence).
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: E2E_BASE_URL
? undefined
: {
command: 'npm run build && npm run preview -- --port 4173 --strictPort',
url: 'http://localhost:4173',
command: "npm run build && npm run preview -- --port 4173 --strictPort",
url: "http://localhost:4173",
reuseExistingServer: !process.env.CI,
timeout: 180_000,
},
Expand Down
Loading
Loading