import { expect, test } from '@playwright/test'; // v0.71.0 — Admin panel e2e: top-bar button → modal; wrong credentials show an error; a session // minted by the /api/test/admin-session hook (FLIGHT_TEST_HOOKS=1) authenticates WITHOUT the real // admin password ever appearing in test code; then the generic DB grid is exercised end-to-end: // table list → edit + save a cell (verified by an independent re-fetch) → add a row → delete it → // logout. v1.1.0 (ISS-107): the whole app now sits behind the sign-in screen — unauthenticated // visitors see IT instead of the shell, and logout drops back to it. const SESSION_KEY = 'flight.session'; // must match lib/authClient.ts async function mintAdminSession(page: import('@playwright/test').Page): Promise { const res = await page.request.post('/api/test/admin-session'); expect(res.status(), 'admin-session hook should be enabled in the CI stack (FLIGHT_TEST_HOOKS=1)').toBe(200); return ((await res.json()) as { sessionId: string }).sessionId; } test('admin panel: sign-in gate, wrong-creds error, session auth, DB grid CRUD, logout', async ({ page }) => { await page.goto('/'); // --- Unauthenticated → the sign-in screen replaces the whole shell (v1.1.0, ISS-107). const signIn = page.getByTestId('sign-in-form'); await expect(signIn).toBeVisible({ timeout: 30_000 }); await expect(page.getByTestId('app-header')).toHaveCount(0); // --- Wrong credentials → error line (the seeded admin's plaintext is never used here). await page.getByTestId('sign-in-username').fill('kamilkrzywda'); await page.getByTestId('sign-in-password').fill('wrong-password-on-purpose'); await page.getByTestId('sign-in-submit').click(); await expect(page.getByTestId('auth-error')).toBeVisible({ timeout: 10_000 }); // --- Authenticate via the test hook + localStorage, then reload into the shell. const sessionId = await mintAdminSession(page); // Both values must be passed as arguments — the function runs in the browser, where Node-side // variables are not in scope. await page.evaluate(([key, sid]) => window.localStorage.setItem(key, sid), [SESSION_KEY, sessionId]); // The independent re-fetch below goes through page.request → needs the header at context level. await page.context().setExtraHTTPHeaders({ 'X-Flight-Session': sessionId }); await page.reload(); const adminBtn = page.getByTestId('admin-btn'); await expect(adminBtn).toBeVisible({ timeout: 30_000 }); // --- Open the modal: stored session is valid → straight to the table list. await adminBtn.click(); // --- Authenticated admin → table list including packages and users. const tables = page.getByTestId('admin-tables'); await expect(tables).toBeVisible({ timeout: 30_000 }); await expect(page.getByTestId('admin-table-packages')).toBeVisible(); await expect(page.getByTestId('admin-table-users')).toBeVisible(); // --- Open packages, edit one cell, Save; verify via an INDEPENDENT re-fetch of the same route. await page.getByTestId('admin-table-packages').click(); const grid = page.getByTestId('admin-rows'); await expect(grid).toBeVisible({ timeout: 30_000 }); const rowidCell = page.getByTestId('admin-cell-0-__rowid'); const originalRowid = Number(await rowidCell.inputValue()); expect(Number.isInteger(originalRowid)).toBeTruthy(); const rewardCell = page.getByTestId('admin-cell-0-reward'); await rewardCell.fill('987654'); await page.getByTestId('admin-save-0').click(); // The grid re-fetches after save — the edited cell shows the new value… await expect(rewardCell).toHaveValue('987654', { timeout: 15_000 }); // …and an independent request (not the grid's) confirms it persisted server-side. const check = await page.request.get('/api/admin/tables/packages/rows?limit=500', { headers: { 'X-Flight-Session': sessionId }, }); expect(check.status()).toBe(200); const fetched = (await check.json()) as { rows: Array> }; const row = fetched.rows.find((r) => Number(r.__rowid) === originalRowid); expect(row, 'edited packages row must still exist').toBeTruthy(); expect(Number(row?.reward)).toBe(987654); // --- Add a row (users table — fewest required columns), then delete it again. await page.getByTestId('admin-back').click(); await expect(tables).toBeVisible(); await page.getByTestId('admin-table-users').click(); const usersGrid = page.getByTestId('admin-rows'); await expect(usersGrid).toBeVisible({ timeout: 30_000 }); const existingRows = await page.getByTestId(/admin-save-\d+/).count(); await page.getByTestId('admin-add-row').click(); const n = existingRows; // the new empty row is appended last await page.getByTestId(`admin-cell-${n}-id`).fill('user-e2e-admin-test'); await page.getByTestId(`admin-cell-${n}-username`).fill('e2e-admin-user'); await page.getByTestId(`admin-cell-${n}-password_hash`).fill('aa$bb'); // any string — dev-tool insert, not a real credential await page.getByTestId(`admin-cell-${n}-created_at`).fill(String(Date.now())); await page.getByTestId(`admin-save-${n}`).click(); // The inserted row is visible after the grid's post-save re-fetch. const bodyRows = usersGrid.locator('tbody tr'); const findUserRow = async (): Promise => { const count = await bodyRows.count(); for (let i = 0; i < count; i++) { if ((await page.getByTestId(`admin-cell-${i}-username`).inputValue()) === 'e2e-admin-user') return i; } return -1; }; await expect.poll(findUserRow, { timeout: 15_000 }).toBeGreaterThanOrEqual(0); const addedIdx = await findUserRow(); // Delete it again — gone after the post-delete re-fetch. await page.getByTestId(`admin-delete-${addedIdx}`).click(); await expect.poll(findUserRow, { timeout: 15_000 }).toBe(-1); // --- Logout drops back to the sign-in screen (v1.1.0, ISS-107 — onLogout unmounts the shell). await page.getByTestId('admin-logout').click(); await expect(page.getByTestId('sign-in-form')).toBeVisible({ timeout: 10_000 }); await expect(tables).toBeHidden(); });