Publish DevRunbook source
Managed validation / full (push) Successful in 3m18s

This commit is contained in:
DevRunbook release export
2026-09-03 04:09:17 +02:00
commit cfd2804e27
928 changed files with 161642 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
import { request, type FullConfig } from '@playwright/test'
import { mkdir } from 'node:fs/promises'
export default async function globalSetup(config: FullConfig) {
const email = process.env.DEVRUNBOOK_E2E_EMAIL
const password = process.env.DEVRUNBOOK_E2E_PASSWORD
if (!email || !password) return
const baseURL = config.projects[0]?.use.baseURL
if (typeof baseURL !== 'string') {
throw new Error('PLAYWRIGHT_BASE_URL is required for authenticated setup')
}
const context = await request.newContext({
baseURL,
extraHTTPHeaders: { origin: new URL(baseURL).origin },
})
try {
const response = await context.post('/api/auth/sign-in/email', {
data: { email, password },
})
if (!response.ok()) {
throw new Error(`Browser authentication failed with ${response.status()}`)
}
await mkdir('test-results/.auth', { recursive: true })
await context.storageState({ path: 'test-results/.auth/m2.json' })
} finally {
await context.dispose()
}
}
+305
View File
@@ -0,0 +1,305 @@
import { expect, test, type APIRequestContext } from '@playwright/test'
const email = process.env.DEVRUNBOOK_E2E_EMAIL
const password = process.env.DEVRUNBOOK_E2E_PASSWORD
function profile(name: string) {
return {
apiVersion: 'devrunbook.io/v1alpha1',
kind: 'RepositoryProfile',
metadata: { name, revision: 1, source: 'manual' },
spec: {
repositoryType: 'single-app',
defaultBranch: 'main',
stack: {
languages: ['TypeScript'],
frameworks: ['Next.js'],
packageManagers: ['pnpm'],
databases: ['PostgreSQL'],
deploymentTypes: ['Docker'],
testFrameworks: ['Vitest', 'Playwright'],
},
commands: [
{
id: 'verify',
role: 'unit-test',
command: 'pnpm test',
workingDirectory: '.',
platform: 'any',
shell: 'auto',
source: 'manual',
confirmed: true,
safeForAgentSuggestion: false,
},
],
paths: {
applicationRoots: ['apps/web'],
testRoots: ['tests'],
documentationRoots: ['docs'],
generated: ['dist'],
protected: ['runtime/secrets'],
excluded: ['node_modules'],
},
policies: {
preserveBackwardCompatibility: true,
newDependencies: 'justify',
gitWrite: 'none',
migrations: 'plan-only',
documentationRequired: true,
networkAccess: 'forbidden',
productionDataAccess: 'forbidden',
requiredValidationRoles: ['unit-test'],
},
},
}
}
function origin(testInfo: { project: { use: { baseURL?: string } } }) {
return new URL(String(testInfo.project.use.baseURL)).origin
}
async function createRepository(
request: APIRequestContext,
requestOrigin: string,
name: string,
) {
const response = await request.post('/api/v1/repositories', {
headers: { origin: requestOrigin },
data: { displayName: name, initialProfile: profile(name) },
})
expect(response.status()).toBe(201)
return {
body: (await response.json()) as {
repository: { id: string; currentProfileRevision: number | null }
currentProfile: {
revision: number
profile: ReturnType<typeof profile> & {
metadata: { contentDigest: string }
}
}
},
etag: response.headers().etag,
}
}
test.describe('authenticated Milestone 3 repository profiles', () => {
test.skip(!email || !password, 'live local-account credentials are required')
test.use({ storageState: 'test-results/.auth/m2.json' })
test('immutable HTTP lifecycle, conflicts, exports and re-imports are governed', async ({
request,
}, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'mutation is covered once')
const requestOrigin = origin(testInfo)
const name = `API evidence ${Date.now()}`
const created = await createRepository(request, requestOrigin, name)
const repositoryId = created.body.repository.id
expect(created.etag).toMatch(/^"profile:1:[0-9a-f]{64}"$/u)
expect(created.body.currentProfile.revision).toBe(1)
const listed = await request.get('/api/v1/repositories?source=manual')
expect(listed.status()).toBe(200)
expect(
(
(await listed.json()) as {
items: { id: string; currentProfileRevision: number | null }[]
}
).items,
).toContainEqual(
expect.objectContaining({ id: repositoryId, currentProfileRevision: 1 }),
)
const current = await request.get(
`/api/v1/repositories/${repositoryId}/profile`,
)
expect(current.status()).toBe(200)
expect(current.headers().etag).toBe(created.etag)
const currentBody = (await current.json()) as {
revision: number
profile: ReturnType<typeof profile>
}
const noOp = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{
headers: { origin: requestOrigin, 'if-match': created.etag },
data: currentBody.profile,
},
)
expect(noOp.status()).toBe(200)
expect((await noOp.json()).revision).toBe(1)
const changedProfile = {
...currentBody.profile,
spec: {
...currentBody.profile.spec,
paths: {
...currentBody.profile.spec.paths,
protected: ['runtime/secrets', '.github/workflows'],
},
},
}
const revised = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{
headers: { origin: requestOrigin, 'if-match': created.etag },
data: changedProfile,
},
)
expect(revised.status()).toBe(201)
expect((await revised.json()).revision).toBe(2)
const revisedEtag = revised.headers().etag
expect(revisedEtag).toMatch(/^"profile:2:[0-9a-f]{64}"$/u)
const stale = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{
headers: { origin: requestOrigin, 'if-match': created.etag },
data: changedProfile,
},
)
expect(stale.status()).toBe(409)
await expect(stale.json()).resolves.toMatchObject({
error: {
code: 'repository_profile_conflict',
details: [
expect.objectContaining({
currentRevision: 2,
currentEtag: revisedEtag,
recovery: 'reload-and-review',
}),
],
},
})
const missingPrecondition = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{ headers: { origin: requestOrigin }, data: changedProfile },
)
expect(missingPrecondition.status()).toBe(428)
for (const format of ['json', 'yaml'] as const) {
const exported = await request.get(
`/api/v1/repositories/${repositoryId}/profile/export?format=${format}`,
)
expect(exported.status()).toBe(200)
expect(exported.headers().etag).toBe(revisedEtag)
const reimported = await request.post('/api/v1/repositories', {
headers: {
origin: requestOrigin,
'content-type':
format === 'json' ? 'application/json' : 'application/yaml',
},
data: await exported.body(),
})
expect(reimported.status()).toBe(201)
}
const invalid = {
...changedProfile,
spec: {
...changedProfile.spec,
paths: { ...changedProfile.spec.paths, protected: ['../outside'] },
},
}
const rejected = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{
headers: { origin: requestOrigin, 'if-match': revisedEtag },
data: invalid,
},
)
expect(rejected.status()).toBe(422)
expect(await rejected.text()).toContain('/spec/paths/protected/0')
const crossOrigin = await request.post('/api/v1/repositories', {
headers: { origin: 'https://attacker.example' },
data: { displayName: 'Rejected', initialProfile: profile('Rejected') },
})
expect(crossOrigin.status()).toBe(403)
})
test('manual UI creation, immutable editing and composer context remain usable', async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'mutation is covered once')
const errors: string[] = []
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text())
})
page.on('pageerror', (error) => errors.push(error.message))
const name = `Browser evidence ${Date.now()}`
await page.goto('/repositories/new')
await expect(
page.getByRole('heading', {
level: 1,
name: 'Create a repository profile',
}),
).toBeVisible()
await expect(
page.getByText(/Nothing entered here is executed/u),
).toBeVisible()
await page.getByLabel('Display name').fill(name)
await page.getByRole('button', { name: 'Create repository' }).click()
await expect(page).toHaveURL(/\/repositories\/[0-9a-f-]+$/u)
await expect(page.getByRole('heading', { level: 1, name })).toBeVisible()
await expect(page.getByText('Immutable profile revision 1')).toBeVisible()
await expect(
page.getByText(/does not run repository commands/u),
).toBeVisible()
await page.getByRole('link', { name: 'Edit profile' }).click()
await page
.getByLabel('Protected paths')
.fill('runtime/secrets, .github/workflows')
await page.getByRole('button', { name: 'Save immutable revision' }).click()
await expect(page.getByRole('status')).toContainText('Revision 2 saved')
await page.getByRole('link', { name: 'Back to repository' }).click()
await expect(page.getByText('Immutable profile revision 2')).toBeVisible()
await expect(page.getByText('.github/workflows')).toBeVisible()
await page.getByRole('link', { name: 'Create task' }).click()
await expect(page).toHaveURL(/\/composer\/new\?repository=/u)
await expect(page.getByText('Repository context retained')).toBeVisible()
await expect(page.getByText('.github/workflows')).toBeVisible()
expect(errors).toEqual([])
})
test('narrow keyboard, theme and reduced-motion behavior stay operable', async ({
page,
}) => {
const errors: string[] = []
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text())
})
page.on('pageerror', (error) => errors.push(error.message))
await page.emulateMedia({ reducedMotion: 'reduce' })
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/repositories')
await expect(
page.getByRole('heading', { level: 1, name: 'Repositories' }),
).toBeVisible()
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
expect(
await page.evaluate(
() => matchMedia('(prefers-reduced-motion: reduce)').matches,
),
).toBe(true)
await page.keyboard.press('Control+K')
const palette = page.getByRole('dialog', { name: 'Command palette' })
await expect(palette).toBeVisible()
await palette.getByRole('combobox').fill('repositories')
await page.keyboard.press('Enter')
await expect(page).toHaveURL(/\/repositories$/u)
await page.getByRole('button', { name: 'Use light theme' }).click()
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
expect(errors).toEqual([])
})
})
+119
View File
@@ -0,0 +1,119 @@
import { expect, test } from '@playwright/test'
const email = process.env.DEVRUNBOOK_E2E_EMAIL
const password = process.env.DEVRUNBOOK_E2E_PASSWORD
test.describe('authenticated Milestone 2 library', () => {
test.skip(!email || !password, 'live local-account credentials are required')
test.use({ storageState: 'test-results/.auth/m2.json' })
test.beforeEach(async ({ page }) => {
await page.goto('/library')
await expect(page).toHaveURL(/\/library(?:\?.*)?$/u)
})
test('search, filter, view and refresh state remain URL-backed', async ({
page,
}) => {
const errors: string[] = []
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text())
})
page.on('pageerror', (error) => errors.push(error.message))
await expect(
page.getByRole('heading', { level: 1, name: 'Library' }),
).toBeVisible()
await expect(page.locator('[data-playbook-slug]')).toHaveCount(28)
await page.getByLabel('Search playbooks').fill('root cause')
await page.getByRole('button', { name: 'Search', exact: true }).click()
await expect(page).toHaveURL(/q=root(?:\+|%20)cause/u)
await expect(
page.locator('[data-playbook-slug="root-cause-bugfix"]'),
).toBeVisible()
await page.getByRole('link', { name: 'Dense' }).click()
await expect(page).toHaveURL(/view=dense/u)
await page.reload()
await expect(page.locator('[data-view="dense"]')).toBeVisible()
await expect(page.getByLabel('Search playbooks')).toHaveValue('root cause')
expect(errors).toEqual([])
})
test('favorites persist and can be recovered through the saved filter', async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'mutation is covered once')
const card = page.locator('[data-playbook-slug="root-cause-bugfix"]')
const favorite = card.getByRole('button', { name: /favorites/i })
const initiallySaved =
(await favorite.getAttribute('aria-pressed')) === 'true'
if (!initiallySaved) await favorite.click()
await expect(favorite).toHaveAttribute('aria-pressed', 'true')
await page.getByLabel('Saved playbooks only').check()
await page.getByRole('button', { name: 'Apply filters' }).click()
await expect(page).toHaveURL(/favorites=true/u)
await expect(card).toBeVisible()
await card.getByRole('button', { name: /favorites/i }).click()
await expect(
card.getByRole('button', { name: /favorites/i }),
).toHaveAttribute('aria-pressed', 'false')
})
test('detail exposes governance panels and version-bound composer handoff', async ({
page,
}) => {
await page.goto('/library/root-cause-bugfix')
await expect(
page.getByRole('heading', { level: 1, name: /root.?cause/i }),
).toBeVisible()
for (const panel of [
'Intent and outcome',
'Inputs and defaults',
'Repository compatibility',
'Guardrails',
'Workflow',
'Validation requirements',
'Completion criteria',
'Quality and limitations',
'Package files',
'Version history',
]) {
await expect(
page.getByRole('heading', { level: 2, name: panel }),
).toBeVisible()
}
await page.getByRole('link', { name: /Compose with this version/i }).click()
await expect(page).toHaveURL(
/\/composer\/new\?playbook=root-cause-bugfix&version=1\.0\.0/u,
)
await expect(page.getByText('root-cause-bugfix@1.0.0')).toBeVisible()
})
test('command palette, theme and narrow keyboard layout are operable', async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/library')
await page.keyboard.press('Control+K')
await expect(
page.getByRole('dialog', { name: 'Command palette' }),
).toBeVisible()
await page
.getByRole('dialog', { name: 'Command palette' })
.getByRole('combobox')
.fill('root cause')
await page.keyboard.press('Enter')
await expect(page).toHaveURL(/\/library\/root-cause-bugfix$/u)
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
await page.getByRole('button', { name: 'Use light theme' }).click()
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
await page.reload()
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
})
})
+172
View File
@@ -0,0 +1,172 @@
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([])
})
@@ -0,0 +1,132 @@
import AxeBuilder from '@axe-core/playwright'
import { expect, test } from '@playwright/test'
const email = process.env.DEVRUNBOOK_E2E_EMAIL
const password = process.env.DEVRUNBOOK_E2E_PASSWORD
const criticalRoutes = [
'/start',
'/composer/new',
'/repositories',
'/runs',
'/account',
'/account/security',
'/repositories/new',
'/management',
] as const
test.describe('Phase 14 accessibility regression', () => {
test.skip(!email || !password, 'live local-account credentials are required')
test.use({
storageState: 'test-results/.auth/m2.json',
reducedMotion: 'reduce',
})
for (const route of criticalRoutes) {
test(`${route} has one main and no serious accessibility violations`, async ({
page,
}) => {
await page.goto(route)
await expect(page.locator('main')).toHaveCount(1)
await expect(page.locator('[id]')).toHaveCount(
await page
.locator('[id]')
.evaluateAll(
(elements) => new Set(elements.map((element) => element.id)).size,
),
)
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()
expect(
results.violations.filter(
({ impact }) => impact === 'critical' || impact === 'serious',
),
).toEqual([])
})
}
test('Start remains keyboard complete, reflows and exposes touch targets', async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/start')
await page.keyboard.press('Tab')
await expect(page.locator(':focus-visible')).toHaveCount(1)
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
const undersizedPrimaryTargets = await page
.locator('main button, main a[href="/settings/integrations/gitea/new"]')
.evaluateAll((elements) =>
elements
.filter((element) => {
const box = element.getBoundingClientRect()
const style = getComputedStyle(element)
return (
style.display !== 'none' &&
style.visibility !== 'hidden' &&
box.width > 0 &&
box.height > 0 &&
(box.width < 44 || box.height < 44)
)
})
.map(
(element) =>
element.getAttribute('aria-label') ?? element.textContent,
),
)
expect(undersizedPrimaryTargets).toEqual([])
})
test('critical pages reflow at a 200 percent equivalent CSS viewport', async ({
page,
}) => {
await page.setViewportSize({ width: 640, height: 720 })
for (const route of criticalRoutes) {
await page.goto(route)
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
}
})
for (const locale of ['en', 'nl'] as const) {
for (const mode of ['simple', 'expert'] as const) {
test(`${locale}/${mode} presentation stays accessible`, async ({
page,
context,
baseURL,
}) => {
const origin = new URL(baseURL ?? 'http://127.0.0.1:3000')
await context.addCookies([
{ name: 'devrunbook_locale', value: locale, url: origin.origin },
{ name: 'devrunbook_mode', value: mode, url: origin.origin },
])
await page.goto('/start')
await expect(
page.getByRole('heading', {
name:
locale === 'nl' ? 'Wat wil je doen?' : 'What do you want to do?',
}),
).toBeVisible()
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()
expect(
results.violations.filter(
({ impact }) => impact === 'critical' || impact === 'serious',
),
).toEqual([])
})
}
}
})
+102
View File
@@ -0,0 +1,102 @@
import { expect, test } from '@playwright/test'
const hasCredentials = Boolean(
process.env.DEVRUNBOOK_E2E_EMAIL && process.env.DEVRUNBOOK_E2E_PASSWORD,
)
test.describe('Usability recovery', () => {
test.skip(!hasCredentials, 'live local-account credentials are required')
test.use({ storageState: 'test-results/.auth/m2.json' })
test.beforeEach(async ({ context, baseURL }) => {
if (!baseURL) throw new Error('A browser base URL is required')
await context.addCookies([
{
name: 'devrunbook_locale',
value: 'nl',
url: baseURL,
sameSite: 'Lax',
},
])
})
test('Task library is compact and understandable on mobile', async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/library')
await expect(page).toHaveTitle('Taakbibliotheek · DevRunbook')
await expect(
page.getByRole('heading', { level: 1, name: 'Kies een bewezen taak' }),
).toBeVisible()
await expect(
page.locator('.drb-library-filters details'),
).not.toHaveAttribute('open', '')
await expect(page.locator('.drb-playbook-card')).toHaveCount(8)
await expect(page.getByRole('button', { name: /Meer tonen/ })).toBeVisible()
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
const undersizedNavigationTargets = await page
.locator(
'.drb-app-shell__header button, .drb-app-shell__header a, .drb-app-shell__mobile-tabs a',
)
.evaluateAll((elements) =>
elements
.filter((element) => {
const box = element.getBoundingClientRect()
const style = getComputedStyle(element)
return (
style.display !== 'none' &&
style.visibility !== 'hidden' &&
box.width > 0 &&
box.height > 0 &&
(box.width < 44 || box.height < 44)
)
})
.map((element) => element.textContent?.trim()),
)
expect(undersizedNavigationTargets).toEqual([])
})
test('Dutch shell, account and role context use ordinary language', async ({
page,
}) => {
await page.goto('/account')
await expect(
page.getByRole('link', { name: 'DevRunbook startpagina' }),
).toBeVisible()
await expect(page.getByLabel('Actieve werkruimte')).toBeVisible()
await expect(
page.getByRole('heading', { level: 1, name: 'Jouw account' }),
).toBeVisible()
await expect(page.getByText('Rol in deze werkruimte')).toBeVisible()
await page.goto('/account/security')
await expect(
page.getByRole('heading', { level: 1, name: 'Wachtwoord en sessies' }),
).toBeVisible()
await expect(
page.getByRole('link', { name: 'Wachtwoord veilig herstellen' }),
).toBeVisible()
})
test('Start action does not cover mobile content', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/start')
await expect(page).toHaveTitle('Nieuwe taak · DevRunbook')
expect(
await page.locator('main').evaluate((main) => {
const stickyElements = [...main.querySelectorAll('*')].filter(
(element) => getComputedStyle(element).position === 'sticky',
)
return stickyElements.length
}),
).toBe(0)
})
})