veritas-kanban/e2e/pwa-offline.spec.ts
Brad Groux 98f4b25991
Some checks failed
CI / Lint & Type Check (push) Has been cancelled
CI / Workspace Unit Tests (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
Complete v5 release evidence gates
Backfill v5.0.0 evidence, fix Scheduled QA SQLite full-profile coverage, and stabilize mobile/PWA release gates.
2026-06-09 07:33:46 -05:00

96 lines
3.2 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test';
import { bypassAuth, cleanupRoutes } from './helpers/auth';
async function useMobileDeviceContext(page: Page) {
await page.route('**/api/auth/context', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
role: 'read-only',
isLocalhost: false,
actorType: 'device',
authMethod: 'device-session',
deviceSessionId: 'e2e-pwa-session',
deviceId: 'e2e-pwa-phone',
clientId: 'e2e-pwa-browser',
clientMode: 'mobile-pwa',
capabilities: [
'board:read',
'workspace:read',
'task:read',
'comment:write',
'workflow:read',
'notification:read',
'notification:receive',
],
permissions: [
'workspace:read',
'task:read',
'comment:write',
'workflow:read',
'work_product:read',
'agent:read',
'settings:read',
],
}),
})
);
}
test.describe('mobile PWA offline behavior', () => {
test.beforeEach(async ({ page }) => {
await bypassAuth(page);
await useMobileDeviceContext(page);
});
test.afterEach(async ({ page, context }) => {
await context.setOffline(false);
await cleanupRoutes(page);
});
test('serves install metadata and a static-only service worker', async ({ page, request }) => {
await page.goto('/');
const manifestHref = await page.locator('link[rel="manifest"]').getAttribute('href');
expect(manifestHref).toBeTruthy();
const manifestResponse = await request.get(new URL(manifestHref!, page.url()).toString(), {
timeout: 10_000,
});
expect(manifestResponse.ok()).toBe(true);
const manifest = (await manifestResponse.json()) as {
display: string;
start_url: string;
scope: string;
icons: Array<{ src: string; purpose?: string }>;
};
expect(manifest.display).toBe('standalone');
expect(manifest.start_url).toBe('.');
expect(manifest.scope).toBe('.');
expect(manifest.icons.some((icon) => icon.purpose === 'maskable')).toBe(true);
const serviceWorkerResponse = await request.get(new URL('/sw.js', page.url()).toString(), {
timeout: 10_000,
});
expect(serviceWorkerResponse.ok()).toBe(true);
const serviceWorker = await serviceWorkerResponse.text();
expect(serviceWorker).toContain("request.method !== 'GET'");
expect(serviceWorker).toContain("scopedPath.startsWith('api/')");
expect(serviceWorker).toContain("scopedPath.startsWith('ws/')");
});
test('shows a remote-safe offline warning in the mobile shell', async ({ page, context }) => {
await page.goto('/');
await expect(page.getByRole('navigation', { name: 'Mobile navigation' })).toBeVisible();
await page.waitForLoadState('networkidle');
await context.setOffline(true);
await page.evaluate(() => window.dispatchEvent(new Event('offline')));
await expect(page.getByRole('status', { name: 'Offline' })).toBeVisible();
await expect(page.getByText(/Cached shell only/)).toBeVisible();
await expect(page.getByText(/Task data and changes require the trusted server/)).toBeVisible();
});
});