import { expect, test } from '@playwright/test' test('catalog shell renders the verified built-in slice', async ({ page }) => { await page.goto('/') await expect(page).toHaveTitle(/DevRunbook/) await expect( page.getByRole('heading', { level: 1, name: /Turn intent into a verifiable change contract/i, }), ).toBeVisible() await expect(page.getByText('28 built-ins ยท no Gitea required')).toBeVisible() await expect(page.getByRole('heading', { level: 3 })).toHaveCount(6) await expect(page.locator('[data-nextjs-dialog]')).toHaveCount(0) }) test('catalog API requires a local authenticated workspace', async ({ request, }) => { const response = await request.get('/api/v1/playbooks') expect(response.status()).toBe(401) await expect(response.json()).resolves.toMatchObject({ error: { code: 'authentication_required' }, }) }) test('legacy playbook links preserve the authentication boundary', async ({ page, }) => { await page.goto('/playbooks/root-cause-bugfix') await expect( page.getByRole('heading', { level: 1, name: /continue with your local account/i, }), ).toBeVisible() expect(new URL(page.url()).pathname).toBe('/login') }) test('narrow layout has no horizontal overflow and keyboard focus is visible', async ({ page, }) => { await page.setViewportSize({ width: 390, height: 844 }) await page.goto('/') expect( await page.evaluate( () => document.documentElement.scrollWidth <= document.documentElement.clientWidth, ), ).toBe(true) await page.keyboard.press('Tab') await expect(page.getByRole('link', { name: /DevRunbook/i })).toBeFocused() const outline = await page.evaluate( () => getComputedStyle(document.activeElement as Element).outlineStyle, ) expect(outline).not.toBe('none') }) test('security headers are present on application pages', async ({ request, }) => { const response = await request.get('/') expect(response.headers()['x-content-type-options']).toBe('nosniff') expect(response.headers()['x-frame-options']).toBe('DENY') expect(response.headers()['content-security-policy']).toContain( "frame-ancestors 'none'", ) }) test('tokenless bootstrap is denied through an untrusted proxy boundary', async ({ request, }) => { const response = await request.post('/api/v1/instance/setup', { headers: { 'x-forwarded-for': '127.0.0.1' }, data: { bootstrapToken: '', instanceName: 'Test instance', publicBaseUrl: 'http://127.0.0.1:3000', owner: { email: 'owner@example.test', displayName: 'Owner', password: 'not-a-real-password', }, }, }) expect(response.status()).toBe(403) expect(await response.text()).not.toContain('not-a-real-password') }) test('setup UI exposes the database recovery state without enabling credentials', async ({ page, }) => { test.skip( Boolean(process.env.DEVRUNBOOK_E2E_EMAIL), 'recovery state requires an uninitialized validation database', ) const consoleErrors: string[] = [] const pageErrors: string[] = [] page.on('console', (message) => { if (message.type() === 'error') consoleErrors.push(message.text()) }) page.on('pageerror', (error) => pageErrors.push(error.message)) await page.goto('/setup') await expect( page.getByRole('heading', { level: 1, name: 'Make this instance yours.' }), ).toBeVisible() await expect(page.getByText('recovery required')).toBeVisible({ timeout: 20_000, }) await expect(page.getByLabel('Owner email')).toBeDisabled() await expect(page.getByText(/Gitea is optional/i)).toBeVisible() expect(consoleErrors).toEqual([]) expect(pageErrors).toEqual([]) }) test('local sign-in fails generically and clears credentials when auth is unavailable', async ({ page, }) => { const pageErrors: string[] = [] page.on('pageerror', (error) => pageErrors.push(error.message)) await page.goto('/login?returnTo=https%3A%2F%2Fattacker.example%2Fsteal') await expect( page.getByRole('heading', { level: 1, name: /continue with your local account/i, }), ).toBeVisible() await page.getByLabel('Email address').fill('owner@example.test') const password = page.getByLabel('Password', { exact: true }) await password.fill('not-a-real-password') await page.getByRole('button', { name: 'Sign in' }).click() await expect(page.locator('form').getByRole('alert')).toHaveText( 'Sign-in could not be completed. Check your credentials or try again later.', ) await expect(password).toHaveValue('') expect(new URL(page.url()).pathname).toBe('/login') expect(pageErrors).toEqual([]) }) test('password recovery keeps its token out of HTTP and clears both credentials', async ({ page, }) => { const token = 'a'.repeat(43) const requestedUrls: string[] = [] const pageErrors: string[] = [] page.on('request', (request) => requestedUrls.push(request.url())) page.on('pageerror', (error) => pageErrors.push(error.message)) await page.goto(`/reset-password#token=${token}`) await expect( page.getByRole('heading', { level: 1, name: /restore access/i }), ).toBeVisible() await expect(page).toHaveURL(/\/reset-password$/u) expect(requestedUrls.every((url) => !url.includes(token))).toBe(true) const password = page.getByLabel('New password', { exact: true }) const confirmation = page.getByLabel('Confirm new password') await password.fill('a secure replacement password') await confirmation.fill('a secure replacement password') await page.getByRole('button', { name: 'Update password' }).click() await expect(page.locator('form').getByRole('alert')).toContainText( 'invalid, expired, or already used', ) await expect(password).toHaveValue('') await expect(confirmation).toHaveValue('') expect(pageErrors).toEqual([]) })