import assert from 'node:assert/strict'; import process from 'node:process'; const playwright = await import(process.env.PLAYWRIGHT_MODULE ?? 'playwright'); const { chromium } = playwright.default ?? playwright; const browser = await chromium.launch({ channel: 'chrome', headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); page.setDefaultTimeout(10000); const errors = []; page.on('pageerror', (error) => errors.push(error.message)); const base = 'http://127.0.0.1:4185/projects/7/tickets/42'; const content = () => page.locator('#main-content'); const visit = async (query = '') => { await page.goto(base + query); await page.getByRole('heading', { level: 1 }).waitFor(); }; const edit = () => page.locator('#ticket-edit > summary'); const tabTo = async (locator) => { await content().focus(); for (let step = 0; step < 45; step++) { await page.keyboard.press('Tab'); if ( await locator.evaluate( (element) => element === document.activeElement, ) ) { assert.equal( await locator.evaluate((element) => element.matches(':focus-visible'), ), true, ); return; } } assert.fail('Ticket control is unreachable by keyboard'); }; try { for (const width of [1440, 768, 390, 320]) { await page.setViewportSize({ width, height: 1000 }); for (const theme of ['light', 'dark']) { await page.emulateMedia({ colorScheme: theme, reducedMotion: 'reduce', }); for (const query of ['', '?empty', '?long']) { await visit(query); assert.equal( await page .locator('html') .evaluate((el) => el.classList.contains('dark')), theme === 'dark', ); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), true, `Page overflow at ${width}/${theme}/${query}`, ); const description = await page .locator('#ticket-description') .boundingBox(); const metadata = await page .locator('#ticket-metadata') .boundingBox(); if (width >= 1280) { assert.ok( metadata.x > description.x + description.width, 'Desktop metadata must be beside the content.', ); } else { assert.ok( metadata.y > description.y + description.height, 'Mobile metadata must be stacked.', ); } await edit().click(); assert.equal( await page.getByLabel('Title', { exact: true }).isVisible(), true, ); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), true, `Editor overflow at ${width}/${theme}/${query}`, ); await edit().click(); } await visit(); await page.screenshot({ path: `/private/tmp/kan30-detail-${width}-${theme}.png`, fullPage: true, }); } } await page.setViewportSize({ width: 390, height: 1000 }); await visit(); await tabTo(edit()); await page.keyboard.press('Enter'); await page.getByLabel('Title', { exact: true }).fill('Unsaved title draft'); await page .locator('#ticket-checklist') .getByRole('checkbox', { name: 'Complete criterion 1: Cache requests safely', exact: true, }) .click(); await page.waitForFunction( () => document.querySelectorAll( '#ticket-checklist input[type=checkbox]', )[1].checked, ); assert.equal( await page.getByLabel('Title', { exact: true }).inputValue(), 'Unsaved title draft', ); await page .getByLabel('Criterion 1', { exact: true }) .fill('A draft checklist item'); assert.equal( await page .locator('#ticket-checklist') .getByRole('checkbox', { name: 'Complete criterion 1: Cache requests safely', exact: true, }) .isDisabled(), true, ); await edit().click(); await edit().click(); assert.equal( await page.getByLabel('Title', { exact: true }).inputValue(), 'Unsaved title draft', ); page.once('dialog', (dialog) => dialog.dismiss()); await content() .getByRole('link', { name: 'Open board', exact: true }) .click(); assert.match(page.url(), /\/tickets\/42/); page.once('dialog', (dialog) => dialog.accept()); await content() .getByRole('link', { name: 'Open board', exact: true }) .click(); await page.waitForURL('**/projects/7/board'); await page.goBack(); await edit().waitFor(); if (!(await page.locator('#ticket-edit').getAttribute('open'))) { await edit().click(); } if (!(await page.getByLabel('Title', { exact: true }).isVisible())) { await edit().click(); } assert.equal( await page.getByLabel('Title', { exact: true }).inputValue(), 'Unsaved title draft', ); assert.equal( await page.getByLabel('Criterion 1', { exact: true }).inputValue(), 'A draft checklist item', ); await visit('?reject'); await edit().click(); await page.getByLabel('Title', { exact: true }).fill('Rejected draft'); await page .getByRole('button', { name: 'Save changes', exact: true }) .click(); await page .getByText('This title is not allowed.', { exact: true }) .waitFor(); assert.equal( await page.getByLabel('Title', { exact: true }).inputValue(), 'Rejected draft', ); assert.equal( await page .getByLabel('Title', { exact: true }) .getAttribute('aria-invalid'), 'true', ); await visit(); await edit().click(); await page .getByLabel('Title', { exact: true }) .fill('Saved updated ticket title'); await page .getByRole('button', { name: 'Save changes', exact: true }) .click(); await page .getByRole('heading', { name: 'Saved updated ticket title', exact: true, }) .waitFor(); await page.getByText('Ticket updated.', { exact: true }).waitFor(); await visit('?rejectToggle'); await page .locator('#ticket-checklist') .getByRole('checkbox', { name: 'Complete criterion 1: Cache requests safely', exact: true, }) .click(); await page .getByRole('alert') .filter({ hasText: 'Checklist status could not be saved' }) .waitFor(); assert.equal( await page .locator('#ticket-checklist') .getByRole('checkbox', { name: 'Complete criterion 1: Cache requests safely', exact: true, }) .isChecked(), false, ); await visit(); const createBranch = page.getByRole('button', { name: 'Create branch', exact: true, }); await tabTo(createBranch); await page.keyboard.press('Enter'); const dialog = page.getByRole('dialog'); await dialog.waitFor(); await page .getByText('Branch name is available.', { exact: true }) .waitFor(); await page.keyboard.press('Escape'); await dialog.waitFor({ state: 'hidden' }); assert.equal( await createBranch.evaluate((el) => el === document.activeElement), true, ); await createBranch.click(); await page .getByText('Branch name is available.', { exact: true }) .waitFor(); await page.getByRole('button', { name: 'Create now', exact: true }).click(); await dialog.waitFor({ state: 'hidden' }); await visit('?slowBranch&serverError'); await page .getByRole('button', { name: 'Create branch', exact: true }) .click(); await page .getByText('Branch name is available.', { exact: true }) .waitFor(); await page .getByLabel('Branch name', { exact: true }) .fill('feature/ENG-42-preserve-this-name'); await page .getByText('Branch name is available.', { exact: true }) .waitFor(); const failedBranchCreation = page.waitForResponse( (response) => response.url().includes('/tickets/42/branches') && response.request().method() === 'POST', ); await page.getByRole('button', { name: 'Create now', exact: true }).click(); await page.getByText('Creating branch…', { exact: true }).waitFor(); assert.equal( await page .locator('button[type="submit"]') .last() .getAttribute('aria-busy'), 'true', ); assert.equal((await failedBranchCreation).status(), 503); await page .getByText('The request could not be completed', { exact: true }) .waitFor(); assert.equal( await page.getByLabel('Branch name', { exact: true }).inputValue(), 'feature/ENG-42-preserve-this-name', ); assert.equal(await page.getByRole('dialog').isVisible(), true); await page.getByLabel('Branch name', { exact: true }).fill('eng-42-cache'); await page .getByText('Branch name is available.', { exact: true }) .waitFor(); await page.keyboard.press('Escape'); await dialog.waitFor({ state: 'hidden' }); await visit(); const linkSaved = page.waitForResponse( (response) => response.url().includes('/tickets/42/links') && response.request().method() === 'POST', ); await page.getByLabel('Linked ticket', { exact: true }).selectOption('41'); await page.getByRole('button', { name: 'Save link', exact: true }).click(); assert.equal((await linkSaved).status(), 303); await page .getByRole('button', { name: 'Remove link to ENG-41', exact: true }) .waitFor(); const linkRemoved = page.waitForResponse( (response) => response.url().includes('/tickets/42/links/3') && response.request().method() === 'DELETE', ); await page .getByRole('button', { name: 'Remove link to ENG-41', exact: true }) .click(); assert.equal((await linkRemoved).status(), 303); await visit('?readonly&hideAuthor&hidePoints'); assert.equal(await content().locator('form').count(), 0); assert.equal( await content().getByText('Sam Chen', { exact: true }).count(), 0, ); assert.equal( await content() .getByText('Estimated story points', { exact: true }) .count(), 0, ); await tabTo( content().getByRole('link', { name: 'ENG-41 Related infrastructure', exact: true, }), ); await page.keyboard.press('Enter'); await page.waitForURL('**/tickets/41'); await page .getByRole('heading', { name: 'Related infrastructure', exact: true }) .waitFor(); assert.equal( await page.getByLabel('Title', { exact: true }).inputValue(), 'Related infrastructure', ); assert.deepEqual(errors, []); console.log( 'Ticket detail browser checks passed: desktop/mobile, both themes, keyboard, drafts, saves, checklist rollback, links, Git dialog and direct navigation.', ); } finally { await browser.close(); }