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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user