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:4176'; const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, hasTouch: true, }); const page = await context.newPage(); page.setDefaultTimeout(10000); page.on('dialog', (dialog) => dialog.accept()); const errors = []; page.on('pageerror', (error) => errors.push(error.message)); const checklist = () => page .locator('form') .first() .getByRole('region', { name: 'Acceptance criteria' }); const savedChecklist = () => page.locator('#ticket-checklist'); const field = (number) => page.getByRole('textbox', { name: `Criterion ${number}`, exact: true }); const button = (name) => page.getByRole('button', { name, exact: true }); const setResult = (value) => context.addCookies([{ name: 'feedback_test_result', value, url: base }]); const waitState = (value) => page.locator(`[data-form-state="${value}"]`).first().waitFor(); const save = async () => { await button('Save changes').click(); await waitState('success'); }; try { await page.goto(`${base}/projects/7/tickets/31`); await savedChecklist().waitFor(); const boxes = savedChecklist().getByRole('checkbox'); await setResult('server'); await boxes.first().focus(); await page.keyboard.press('Space'); await page .getByText('Checklist status could not be saved. Please try again.') .waitFor(); assert.equal( await boxes.first().isChecked(), false, 'Failure must restore saved checkbox', ); await setResult('success'); let payload; page.on('request', (request) => { if (request.method() === 'PATCH') { payload = request.postDataJSON(); } }); await boxes.first().tap(); await page.waitForFunction( () => document.querySelectorAll('section[aria-busy="true"]').length === 0, ); await savedChecklist().getByText('3 of 4 complete').waitFor(); assert.deepEqual( payload.acceptance_criteria.map((item) => item.checked), [true, true, true, false], ); assert.deepEqual( payload.acceptance_criteria.map((item) => item.id), [11, 12, 13, 14], ); await page.locator('#ticket-edit > summary').click(); await field(2).fill('Edited child'); await field(2).press('Alt+ArrowLeft'); assert.deepEqual( await checklist() .locator('li') .evaluateAll((items) => items.map((item) => Number(item.dataset.indent)), ), [0, 0, 1, 0], ); await button('Indent criterion 2').tap(); await button('Remove criterion 1').tap(); assert.equal(await field(1).inputValue(), 'Edited child'); assert.equal( await field(1).evaluate((el) => el === document.activeElement), true, ); await button('Add criterion').focus(); await page.keyboard.press('Enter'); assert.equal( await field(4).evaluate((el) => el === document.activeElement), true, ); await field(4).fill('New criterion'); await button('Indent criterion 4').click(); await setResult('validation'); await button('Save changes').click(); await waitState('validation'); assert.equal(await field(1).inputValue(), 'Edited child'); await setResult('success'); await save(); assert.deepEqual( payload.acceptance_criteria.map((item) => item.id), [12, 13, 14, undefined], ); assert.deepEqual( payload.acceptance_criteria.map((item) => item.indent), [0, 1, 0, 1], ); await page.reload(); await page.locator('#ticket-edit > summary').click(); assert.equal(await field(1).inputValue(), 'Edited child'); assert.equal(await field(4).inputValue(), 'New criterion'); // Blank/whitespace text cannot silently delete a persisted criterion. await field(1).fill(' '); assert.equal(await field(1).evaluate((el) => el.checkValidity()), false); const beforeInvalidSave = payload; await button('Save changes').click(); assert.equal(payload, beforeInvalidSave); await button('Discard changes').click(); assert.equal(await field(1).inputValue(), 'Edited child'); assert.equal(await field(1).evaluate((el) => el.checkValidity()), true); for (const width of [1280, 390, 320]) { await page.setViewportSize({ width, height: 900 }); for (const theme of ['light', 'dark']) { await page.emulateMedia({ colorScheme: theme }); await page.evaluate( (theme) => document.documentElement.classList.toggle( 'dark', theme === 'dark', ), theme, ); assert.equal( await checklist().evaluate( (el) => el.scrollWidth <= el.clientWidth, ), true, ); await checklist().screenshot({ path: `/private/tmp/kan32-checklist-${width}-${theme}.png`, }); await button('Outdent criterion 4').tap(); await button('Indent criterion 4').tap(); } } await page.goto(`${base}/projects/7/tickets/31?readonly=1`); await savedChecklist().waitFor(); assert.equal(await savedChecklist().getByRole('textbox').count(), 0); assert.equal(await savedChecklist().getByRole('button').count(), 0); assert.equal( await savedChecklist().getByRole('checkbox').first().isDisabled(), true, ); await page.goto(`${base}/projects/7/tickets/create`); await page.locator('#title').fill('Created with criteria'); for (const text of ['First', 'Second']) { await button('Add criterion').click(); await page.locator('textarea').last().fill(text); } await button('Indent criterion 2').tap(); await checklist().getByRole('checkbox').first().focus(); await page.keyboard.press('Space'); const request = page.waitForRequest( (request) => request.method() === 'POST' && request.url().endsWith('/tickets'), ); await button('Create ticket').click(); const created = (await request).postDataJSON(); assert.deepEqual(created.acceptance_criteria, [ { text: 'First', checked: true, indent: 0 }, { text: 'Second', checked: true, indent: 1 }, ]); await page.getByRole('heading', { name: 'Destination' }).waitFor(); assert.deepEqual(errors, []); console.log( 'Acceptance criteria browser checks passed: persistence, errors, keyboard, touch, readonly, creation, mobile and themes.', ); } finally { await browser.close(); }