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 base = 'http://127.0.0.1:4189'; 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)); const requests = []; page.on('request', (request) => { if (['POST', 'PATCH', 'DELETE'].includes(request.method())) { requests.push(request); } }); const button = (name) => page.getByRole('button', { name, exact: true }); const dialog = () => page.getByRole('dialog'); const result = (value) => context.addCookies([{ name: 'metadata_test_result', value, url: base }]); const visit = async () => { await result('success'); await page.goto(base + '/projects/1/settings/metadata?reset'); await page .getByRole('heading', { name: 'Ticket settings', exact: true }) .waitFor(); }; const noOverflow = async () => assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), true, ); const submit = async (label, method, pathname, expected) => { const sent = page.waitForRequest((request) => request.method() === method); await dialog().getByRole('button', { name: label, exact: true }).click(); const request = await sent; assert.equal(new URL(request.url()).pathname, pathname); assert.deepEqual(request.postDataJSON(), expected); }; const closed = () => dialog().waitFor({ state: 'hidden' }); const formState = (state) => dialog() .locator('[data-form-state="' + state + '"]') .waitFor({ state: 'visible' }); 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(); await noOverflow(); await page .getByLabel('Blocks points to Blocked by', { exact: true }) .waitFor(); const trigger = button('Edit Reviewer'); await trigger.focus(); await page.keyboard.press('Enter'); await dialog().waitFor(); assert.equal( await page .getByLabel('Field label', { exact: true }) .inputValue(), 'Reviewer', ); assert.equal( await page .getByLabel('Field type', { exact: true }) .inputValue(), 'user', ); await page.keyboard.press('Tab'); assert.equal( await dialog().evaluate((element) => element.contains(document.activeElement), ), true, ); await noOverflow(); await page.keyboard.press('Escape'); await closed(); assert.equal( await trigger.evaluate( (element) => document.activeElement === element, ), true, ); } } await visit(); await button('Add field').click(); await page.getByLabel('Field label', { exact: true }).fill('Owner'); await page.getByLabel('Field type', { exact: true }).selectOption('user'); await result('validation'); await submit('Add field', 'POST', '/projects/1/settings/metadata/fields', { label: 'Owner', field_type: 'user', }); await formState('validation'); assert.equal( await page .getByLabel('Field label', { exact: true }) .getAttribute('aria-invalid'), 'true', ); await result('server'); await dialog() .getByRole('button', { name: 'Add field', exact: true }) .click(); await formState('server'); await result('success'); await dialog() .getByRole('button', { name: 'Try again', exact: true }) .click(); await closed(); await button('Edit Owner').click(); await page.getByLabel('Field label', { exact: true }).fill('Effort'); await page.getByLabel('Field type', { exact: true }).selectOption('number'); await page.getByLabel('Position', { exact: true }).fill('1'); await submit('Save', 'PATCH', '/projects/1/settings/metadata/fields/100', { label: 'Effort', field_type: 'number', position: 1, }); await closed(); await button('Edit Effort').waitFor(); const beforeFieldDelete = requests.length; await button('Delete Reviewer').click(); await dialog() .getByText(/permanently removes 2 stored ticket values/) .waitFor(); await dialog().getByRole('button', { name: 'Cancel', exact: true }).click(); await closed(); assert.equal(requests.length, beforeFieldDelete); await button('Delete Reviewer').click(); await submit( 'Delete', 'DELETE', '/projects/1/settings/metadata/fields/5', {}, ); await closed(); await button('Add link type').click(); await page.getByLabel('Source label', { exact: true }).fill('Depends on'); await page .getByLabel('Counterpart label', { exact: true }) .fill('Required by'); await submit( 'Add link type', 'POST', '/projects/1/settings/metadata/link-types', { name: 'Depends on', counterpart_name: 'Required by' }, ); await closed(); await button('Edit Depends on').click(); assert.equal( await page .getByLabel('Counterpart label', { exact: true }) .inputValue(), 'Required by', ); await page.getByLabel('Source label', { exact: true }).fill('Requires'); await page .getByLabel('Counterpart label', { exact: true }) .fill('Required for'); await page.getByLabel('Position', { exact: true }).fill('1'); await submit( 'Save', 'PATCH', '/projects/1/settings/metadata/link-types/101', { name: 'Requires', counterpart_name: 'Required for', position: 1 }, ); await closed(); await page .getByLabel('Requires points to Required for', { exact: true }) .waitFor(); const beforeLinkDelete = requests.length; await button('Delete Blocks').click(); await dialog() .getByText(/permanently removes 1 ticket relationships/) .waitFor(); await dialog().getByRole('button', { name: 'Cancel', exact: true }).click(); await closed(); assert.equal(requests.length, beforeLinkDelete); await button('Delete Blocks').click(); await submit( 'Delete', 'DELETE', '/projects/1/settings/metadata/link-types/7', {}, ); await closed(); assert.deepEqual(errors, []); console.log( 'Metadata browser checks passed: field and link-type CRUD payloads, direction labels, deletion impact, validation, retries, keyboard focus, responsive widths and both themes.', ); } finally { await browser.close(); }