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:4188'; const context = await browser.newContext(); const page = await context.newPage(); page.setDefaultTimeout(10000); const errors = []; page.on('pageerror', (error) => errors.push(error.message)); const editor = page.getByRole('region', { name: 'Status moves', exact: true }); const source = () => editor.getByLabel('From status', { exact: true }); const check = (name) => editor.getByRole('checkbox', { name: new RegExp(name) }); const button = (name) => editor.getByRole('button', { name, exact: true }); const state = (name) => editor.locator('[data-form-state="' + name + '"]'); const result = (value) => context.addCookies([{ name: 'workflow_test_result', value, url: base }]); const visit = async (query = '?reset') => { await result('success'); await page.goto(base + '/projects/1/settings/board' + query); await source().waitFor(); }; const noOverflow = async () => assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), true, ); const save = async (id, targets) => { const sent = page.waitForRequest((request) => request.method() === 'PATCH'); await button('Save moves').click(); const request = await sent; assert.equal( new URL(request.url()).pathname, '/projects/1/settings/board/statuses/' + id + '/transitions', ); assert.deepEqual(request.postDataJSON(), { allowed_ticket_status_ids: targets, }); }; 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 }); await visit(); assert.equal(await check('Ready').isChecked(), true); assert.equal(await check('Building').isChecked(), false); assert.equal(await check('Backlog').count(), 0); assert.equal(await button('Save moves').isDisabled(), true); await check('Building').focus(); await page.keyboard.press('Space'); assert.equal(await check('Building').isChecked(), true); await editor .getByText('Unsaved changes', { exact: true }) .waitFor(); await noOverflow(); await button('Reset changes').click(); assert.equal(await check('Building').isChecked(), false); assert.equal(await check('Ready').isChecked(), true); if ([1440, 390].includes(width)) { await editor.scrollIntoViewIfNeeded(); await page.screenshot({ path: '/private/tmp/KAN-41-' + width + '-' + theme + '.png', }); } } } await visit(); await check('Ready').uncheck(); await check('Review').check(); await result('validation'); await save(1, [4]); await state('validation').waitFor(); await editor .getByText('Choose a destination from this project.', { exact: true }) .waitFor(); assert.equal(await check('Review').isChecked(), true); assert.equal(await check('Ready').isChecked(), false); assert.equal( await editor.getByRole('group').getAttribute('aria-invalid'), 'true', ); await result('server'); await save(1, [4]); await state('server').waitFor(); await result('success'); await button('Try again').click(); await state('success').waitFor(); assert.equal(await button('Save moves').isDisabled(), true); assert.equal(await check('Review').isChecked(), true); await page.reload(); await source().waitFor(); assert.equal(await check('Review').isChecked(), true); assert.equal(await check('Ready').isChecked(), false); await source().selectOption('4'); assert.equal(await check('Backlog').isChecked(), false); assert.equal(await check('Review').count(), 0); await check('Backlog').check(); page.once('dialog', (prompt) => prompt.dismiss()); await source().selectOption('1'); assert.equal(await source().inputValue(), '4'); assert.equal(await check('Backlog').isChecked(), true); page.once('dialog', (prompt) => prompt.accept()); await source().selectOption('1'); assert.equal(await source().inputValue(), '1'); assert.equal(await check('Review').isChecked(), true); await check('Review').uncheck(); await editor.getByText(/With no destinations selected/).waitFor(); await save(1, []); await state('success').waitFor(); await page.reload(); await source().waitFor(); assert.equal(await editor.locator('input:checked').count(), 0); await editor.getByText('Saved destinations: 0', { exact: true }).waitFor(); // A disconnected request retains the draft and supports a single retry. await check('Building').check(); await page.route('**/statuses/1/transitions', (route) => route.abort('failed'), ); await save(1, [3]); await state('network').waitFor(); assert.equal(await check('Building').isChecked(), true); await page.unroute('**/statuses/1/transitions'); await button('Try again').click(); await state('success').waitFor(); // Pending saves disable inputs, source changes and duplicate submits. let release; const blocked = new Promise((resolve) => { release = resolve; }); await page.route('**/statuses/1/transitions', async (route) => { await blocked; await route.continue(); }); await check('Review').check(); await save(1, [3, 4]); await state('loading').waitFor(); assert.equal(await source().isDisabled(), true); assert.equal(await check('Building').isDisabled(), true); assert.equal( await editor.locator('button[type=submit]').isDisabled(), true, ); release(); await state('success').waitFor(); await page.unroute('**/statuses/1/transitions'); await visit('?reset&empty'); await editor .getByText('No other statuses exist yet.', { exact: true }) .waitFor(); assert.equal(await editor.getByRole('checkbox').count(), 0); await noOverflow(); assert.deepEqual(errors, []); console.log( 'Transition browser checks passed: directed rules, persisted selection, clearing all targets, reset, source-change protection, validation/network/server errors, retry, pending saves, keyboard, responsive widths and both themes.', ); } finally { await browser.close(); }