test(dashboard): stabilize the e2e suite under parallel runs

Three issues found while running the full B1–B8 suite together:

- The B1 logout test was driving the shared admin storageState
  token, invalidating it for every subsequent test. Switched it to
  a fresh login so its session is disposable.
- Bumped navigationTimeout to 30s and capped local workers at 4 to
  cope with the Vite dev server's first-compile cost under
  parallel load. Local also gets one retry to absorb intermittent
  warmup flakiness.
- Cleared a few lint warnings (unused appId / _adminPage vars) and
  belt-and-braces gitignore for playwright artifacts written to
  the repo root when the CLI is invoked from there by accident.

Suite now: 55/55 passing in ~21s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-28 07:44:07 +02:00
parent cd20ffb580
commit 3e72ddde78
4 changed files with 22 additions and 9 deletions

4
.gitignore vendored
View File

@@ -36,6 +36,10 @@ config.local.toml
/dashboard/playwright-report /dashboard/playwright-report
/dashboard/test-results /dashboard/test-results
/dashboard/.playwright /dashboard/.playwright
# When playwright is invoked from the repo root by accident, these
# also land here.
/playwright-report
/test-results
# Caddy # Caddy
/caddy/data /caddy/data

View File

@@ -15,15 +15,18 @@ export default defineConfig({
outputDir: './tests/e2e/.results', outputDir: './tests/e2e/.results',
fullyParallel: true, fullyParallel: true,
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0, // Local: 1 retry to absorb dev-server warmup flakiness. CI: 2.
workers: process.env.CI ? 2 : undefined, retries: process.env.CI ? 2 : 1,
// Cap at 4 workers locally to keep the shared Vite dev server
// from getting stampeded during cold-start compiles.
workers: process.env.CI ? 2 : 4,
reporter: process.env.CI ? [['html'], ['github']] : 'html', reporter: process.env.CI ? [['html'], ['github']] : 'html',
globalSetup: './tests/e2e/global-setup.ts', globalSetup: './tests/e2e/global-setup.ts',
expect: { timeout: 5_000 }, expect: { timeout: 5_000 },
use: { use: {
baseURL: DASHBOARD_BASE, baseURL: DASHBOARD_BASE,
actionTimeout: 10_000, actionTimeout: 10_000,
navigationTimeout: 15_000, navigationTimeout: 30_000,
trace: 'on-first-retry', trace: 'on-first-retry',
screenshot: 'only-on-failure', screenshot: 'only-on-failure',
video: 'retain-on-failure' video: 'retain-on-failure'

View File

@@ -97,9 +97,16 @@ test.describe('B1 auth — authenticated', () => {
await page.goto('/admin/login'); await page.goto('/admin/login');
await expect(page).toHaveURL(/\/admin\/apps$/); await expect(page).toHaveURL(/\/admin\/apps$/);
}); });
});
test.describe('B1 auth — logout', () => {
// Logout must NOT use the shared storageState token, or it would
// invalidate the session every other test relies on. Each run
// here logs in fresh so its session is disposable.
test.use({ storageState: { cookies: [], origins: [] } });
test('logout clears the session and lands on /login', async ({ page }) => { test('logout clears the session and lands on /login', async ({ page }) => {
await page.goto('/admin/apps'); await loginAsAdmin(page);
await expect(page.getByRole('heading', { name: 'Apps', level: 1 })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Apps', level: 1 })).toBeVisible();
await logout(page); await logout(page);
const token = await page.evaluate(() => localStorage.getItem('picloud.admin.token')); const token = await page.evaluate(() => localStorage.getItem('picloud.admin.token'));

View File

@@ -72,7 +72,7 @@ test.describe('B5 app members', () => {
test('invite a member-role user, then remove them', async ({ page, uniqueSlug, uniqueUsername }) => { test('invite a member-role user, then remove them', async ({ page, uniqueSlug, uniqueUsername }) => {
const slug = uniqueSlug('mem'); const slug = uniqueSlug('mem');
const username = uniqueUsername('inv'); const username = uniqueUsername('inv');
const appId = await createApp(slug); await createApp(slug);
const userId = await createMemberUser(username); const userId = await createMemberUser(username);
cleanup.app(slug); cleanup.app(slug);
cleanup.adminUser(userId); cleanup.adminUser(userId);
@@ -106,7 +106,7 @@ test.describe('B5 app members', () => {
}) => { }) => {
const slug = uniqueSlug('mem'); const slug = uniqueSlug('mem');
const username = uniqueUsername('role'); const username = uniqueUsername('role');
const appId = await createApp(slug); await createApp(slug);
const userId = await createMemberUser(username); const userId = await createMemberUser(username);
cleanup.app(slug); cleanup.app(slug);
cleanup.adminUser(userId); cleanup.adminUser(userId);
@@ -133,14 +133,13 @@ test.describe('B5 app members', () => {
test('non-app-admin viewers do not see the Members tab', async ({ test('non-app-admin viewers do not see the Members tab', async ({
browser, browser,
page: _adminPage,
uniqueSlug, uniqueSlug,
uniqueUsername uniqueUsername
}) => { }) => {
const slug = uniqueSlug('mem'); const slug = uniqueSlug('mem');
const username = uniqueUsername('viewer'); const username = uniqueUsername('viewer');
const password = 'e2e-member-pw'; const password = 'e2e-member-pw';
const appId = await createApp(slug); await createApp(slug);
const userId = await createMemberUser(username); const userId = await createMemberUser(username);
cleanup.app(slug); cleanup.app(slug);
cleanup.adminUser(userId); cleanup.adminUser(userId);
@@ -183,7 +182,7 @@ test.describe('B5 app members adversarial', () => {
}) => { }) => {
const slug = uniqueSlug('mem'); const slug = uniqueSlug('mem');
const username = uniqueUsername('rolelist'); const username = uniqueUsername('rolelist');
const appId = await createApp(slug); await createApp(slug);
const userId = await createMemberUser(username); const userId = await createMemberUser(username);
cleanup.app(slug); cleanup.app(slug);
cleanup.adminUser(userId); cleanup.adminUser(userId);