import assert from 'node:assert/strict'; import process from 'node:process'; const { chromium } = await import( process.env.PLAYWRIGHT_MODULE ?? 'playwright' ); const browser = await chromium.launch({ channel: 'chrome', headless: true }); const base = 'http://127.0.0.1:4179'; const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, }); const page = await context.newPage(); page.setDefaultTimeout(10000); const errors = []; page.on('pageerror', (error) => errors.push(error.message)); page.on('dialog', (dialog) => dialog.accept()); const setResult = (value) => context.addCookies([{ name: 'creation_test_result', value, url: base }]); const content = () => page.locator('#main-content'); const state = (value) => page.locator(`[data-form-state="${value}"]`).waitFor({ state: 'visible' }); const choose = async (value) => { await page.locator(`input[name="creation_mode"][value="${value}"]`).focus(); await page.keyboard.press('Space'); }; const fillDetails = async () => { await page .getByLabel('Project name', { exact: true }) .fill('Frontend workspace'); await page.getByLabel('Project key', { exact: true }).fill('web'); }; const submit = () => page.getByRole('button', { name: 'Create project', exact: true }).click(); const focusWithTab = async (target) => { await content().focus(); for (let step = 0; step < 40; step++) { await page.keyboard.press('Tab'); if ( await target.evaluate( (element) => element === document.activeElement, ) ) { assert.equal( await target.evaluate((element) => element.matches(':focus-visible'), ), true, ); return; } } assert.fail('Form control was not reachable by keyboard'); }; try { for (const width of [1440, 390, 320]) { await page.setViewportSize({ width, height: 1000 }); for (const theme of ['light', 'dark']) { await page.emulateMedia({ colorScheme: theme, reducedMotion: 'reduce', }); await page.goto( `${base}/projects/create?viewport=${width}&theme=${theme}`, ); await page .getByRole('heading', { name: 'Create a project' }) .waitFor(); for (const mode of ['scratch', 'duplicate', 'template']) { await choose(mode); await page.evaluate(() => document.fonts.ready); assert.equal( await page .locator('html') .evaluate((element) => element.classList.contains('dark'), ), theme === 'dark', ); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), true, `${mode} overflows at ${width} in ${theme}`, ); await page.screenshot({ path: `/private/tmp/kan24-${mode}-${width}-${theme}.png`, fullPage: true, }); } await choose('scratch'); const scratch = page.getByRole('radio', { name: 'Start from scratch', exact: true, }); await focusWithTab(scratch); await page.keyboard.press('ArrowRight'); assert.equal( await page .getByRole('radio', { name: 'Duplicate a project', exact: true, }) .isChecked(), true, ); const board = page.getByRole('checkbox', { name: 'Board columns', exact: true, }); await focusWithTab(board); await page.keyboard.press('Space'); assert.equal(await board.isChecked(), true); const source = page.getByLabel('Source project', { exact: true }); await focusWithTab(source); await page.keyboard.press('o'); await page.keyboard.press('Tab'); await page.waitForFunction( () => document.querySelector('#source_project_id').value === '8', ); assert.equal(await source.inputValue(), '8'); } } await page.goto(`${base}/projects/create?empty`); assert.equal( await page .getByRole('radio', { name: 'Start from scratch', exact: true }) .isChecked(), true, ); assert.equal( await page .getByRole('radio', { name: 'Duplicate a project', exact: true }) .isDisabled(), true, ); assert.equal( await page .getByRole('radio', { name: 'Use a template', exact: true }) .isDisabled(), true, ); await fillDetails(); const scratchRequest = page.waitForRequest( (request) => request.method() === 'POST', ); await submit(); assert.deepEqual((await scratchRequest).postDataJSON(), { name: 'Frontend workspace', key: 'web', creation_mode: 'scratch', source_project_id: '', source_template_id: '', copy_areas: [], }); await page.waitForURL('**/projects/99'); await page.goto(`${base}/projects/create?template=4`); assert.equal( await page.getByLabel('Template', { exact: true }).inputValue(), '4', ); await page .getByText('This template uses the default project setup.', { exact: true, }) .waitFor(); await fillDetails(); const templateRequest = page.waitForRequest( (request) => request.method() === 'POST', ); await submit(); const templateData = (await templateRequest).postDataJSON(); assert.equal(templateData.creation_mode, 'template'); assert.equal(templateData.source_template_id, '4'); assert.equal(templateData.source_project_id, ''); assert.deepEqual(templateData.copy_areas, []); await page.waitForURL('**/projects/99'); await page.goto(`${base}/projects/create`); await choose('duplicate'); await fillDetails(); await page.getByLabel('Source project', { exact: true }).selectOption('8'); await page .getByRole('checkbox', { name: 'Board columns', exact: true }) .check(); await page .getByRole('checkbox', { name: 'Custom ticket fields', exact: true }) .check(); await page.getByText('2 areas selected', { exact: true }).waitFor(); await page.getByText('WEB-1', { exact: true }).waitFor(); await choose('template'); await choose('scratch'); await choose('duplicate'); assert.equal( await page.getByLabel('Source project', { exact: true }).inputValue(), '8', ); assert.equal( await page .getByRole('checkbox', { name: 'Board columns', exact: true }) .isChecked(), true, ); assert.equal( await page .getByRole('checkbox', { name: 'Custom ticket fields', exact: true, }) .isChecked(), true, ); await setResult('validation'); const duplicateRequest = page.waitForRequest( (request) => request.method() === 'POST', ); await submit(); const duplicateData = (await duplicateRequest).postDataJSON(); assert.equal(duplicateData.source_project_id, '8'); assert.equal(duplicateData.source_template_id, ''); assert.deepEqual(duplicateData.copy_areas, [ 'board_columns', 'ticket_fields', ]); await state('validation'); assert.equal( await page.getByLabel('Project name', { exact: true }).inputValue(), 'Frontend workspace', ); assert.equal( await page .getByLabel('Project key', { exact: true }) .getAttribute('aria-invalid'), 'true', ); await page .getByText('The key has already been taken.', { exact: true }) .waitFor(); await page .getByText('Select a valid configuration area.', { exact: true }) .waitFor(); assert.equal( await page .getByRole('checkbox', { name: 'Board columns', exact: true }) .getAttribute('aria-invalid'), 'true', ); await choose('scratch'); assert.equal( await page .getByText('Select a valid configuration area.', { exact: true }) .count(), 0, ); assert.equal( await page .getByLabel('Project key', { exact: true }) .getAttribute('aria-invalid'), 'true', ); await choose('duplicate'); assert.equal( await page .getByRole('checkbox', { name: 'Board columns', exact: true }) .isChecked(), true, ); assert.equal( await page .getByRole('checkbox', { name: 'Board columns', exact: true }) .getAttribute('aria-invalid'), 'false', ); await page.route('**/projects', (route) => route.abort('failed')); await submit(); await state('network'); assert.equal( await page.getByLabel('Project name', { exact: true }).inputValue(), 'Frontend workspace', ); await page.unroute('**/projects'); await setResult('server'); await page.getByRole('button', { name: 'Try again' }).click(); await state('server'); await setResult('success'); let pending; let submissions = 0; await page.route('**/projects', (route) => { submissions++; pending = route; }); await page.getByRole('button', { name: 'Try again' }).click(); await state('loading'); assert.equal( await page .getByRole('button', { name: 'Creating project…', exact: true }) .isDisabled(), true, ); assert.equal( await page.getByLabel('Project name', { exact: true }).isDisabled(), true, ); await page.locator('form').evaluate((form) => form.requestSubmit()); assert.equal(submissions, 1); await pending.continue(); await page.waitForURL('**/projects/99'); await page.unroute('**/projects'); await page.goto(`${base}/projects/create`); await choose('duplicate'); await fillDetails(); const emptyCopyRequest = page.waitForRequest( (request) => request.method() === 'POST', ); await submit(); assert.deepEqual((await emptyCopyRequest).postDataJSON().copy_areas, []); await page.waitForURL('**/projects/99'); assert.deepEqual(errors, []); console.log( 'Project creation browser checks passed: all modes, preselection, payloads, retained input, validation, retry, duplicate submission protection, keyboard, 320/390/1440px and both themes.', ); } finally { await browser.close(); }