270 lines
10 KiB
JavaScript
270 lines
10 KiB
JavaScript
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 page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
|
page.setDefaultTimeout(10000);
|
|
const base = 'http://127.0.0.1:4185/projects/7/tickets';
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
const field = (label) => page.getByLabel(label, { exact: true });
|
|
const form = () => page.locator('form').first();
|
|
const state = (value) =>
|
|
form().locator(`[data-form-state="${value}"]`).waitFor();
|
|
const button = (label) =>
|
|
form().getByRole('button', { name: label, exact: true });
|
|
async function visit(create, query = '') {
|
|
await page.goto(`${base}/${create ? 'create' : '42'}${query}`);
|
|
await page.getByRole('heading', { level: 1 }).waitFor();
|
|
if (!create) {
|
|
await page.locator('#ticket-edit > summary').click();
|
|
}
|
|
}
|
|
async function fill(create) {
|
|
await field('Title').fill('A complete ticket');
|
|
await field('Description').fill('Description with context.');
|
|
await field('Assignee').selectOption('2');
|
|
await field('Parent ticket').selectOption('40');
|
|
await field('Estimated story points').fill('0');
|
|
await field('Acceptance criteria checklist').fill(
|
|
'[ ] Main criterion\n [x] Nested criterion',
|
|
);
|
|
if (create) {
|
|
await field('Status').selectOption('4');
|
|
} else {
|
|
await field('Author').selectOption('1');
|
|
await field('Blocker comment').fill('Waiting for review');
|
|
await field('Impact score').fill('2.5');
|
|
await field('Reviewer').selectOption('2');
|
|
}
|
|
}
|
|
async function submit(create) {
|
|
const request = page.waitForRequest(
|
|
(r) =>
|
|
r.method() === (create ? 'POST' : 'PATCH') &&
|
|
r.url().split('?')[0] === (create ? base : `${base}/42`),
|
|
);
|
|
await button(create ? 'Create ticket' : 'Save changes').click();
|
|
return (await request).postDataJSON();
|
|
}
|
|
try {
|
|
for (const create of [true, false]) {
|
|
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 visit(create, '?admin');
|
|
assert.equal(
|
|
await page
|
|
.locator('html')
|
|
.evaluate((el) => el.classList.contains('dark')),
|
|
theme === 'dark',
|
|
);
|
|
assert.equal(
|
|
await page.evaluate(
|
|
() =>
|
|
document.documentElement.scrollWidth <= innerWidth,
|
|
),
|
|
true,
|
|
`Overflow ${create}/${width}/${theme}`,
|
|
);
|
|
await field('Title').focus();
|
|
await page.keyboard.press('Tab');
|
|
assert.equal(
|
|
await field('Description').evaluate(
|
|
(el) =>
|
|
el === document.activeElement &&
|
|
el.matches(':focus-visible'),
|
|
),
|
|
true,
|
|
);
|
|
await fill(create);
|
|
await page.screenshot({
|
|
path: `/private/tmp/kan31-${create ? 'create' : 'edit'}-${width}-${theme}.png`,
|
|
fullPage: true,
|
|
});
|
|
page.once('dialog', (dialog) => dialog.dismiss());
|
|
await button('Discard changes').click();
|
|
assert.equal(
|
|
await field('Title').inputValue(),
|
|
'A complete ticket',
|
|
);
|
|
page.once('dialog', (dialog) => dialog.accept());
|
|
await button('Discard changes').click();
|
|
assert.equal(
|
|
await button('Discard changes').isDisabled(),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
await field('Title').inputValue(),
|
|
create
|
|
? ''
|
|
: 'Implement a resilient caching layer for the project API',
|
|
);
|
|
}
|
|
}
|
|
await visit(create, '?admin&reject&allErrors');
|
|
await fill(create);
|
|
const data = await submit(create);
|
|
assert.equal(data.title, 'A complete ticket');
|
|
assert.equal(data.description, 'Description with context.');
|
|
assert.equal(Number(data.ticket_type_id), 1);
|
|
assert.equal(Number(data.ticket_priority_id), 2);
|
|
assert.equal(Number(data.assignee_user_id), 2);
|
|
assert.equal(Number(data.parent_ticket_id), 40);
|
|
assert.equal(Number(data.story_points), 0);
|
|
assert.deepEqual(
|
|
data.acceptance_criteria.map(({ text, checked, indent }) => ({
|
|
text,
|
|
checked,
|
|
indent,
|
|
})),
|
|
[
|
|
{ text: 'Main criterion', checked: false, indent: 0 },
|
|
{ text: 'Nested criterion', checked: true, indent: 1 },
|
|
],
|
|
);
|
|
if (create) {
|
|
assert.equal(Number(data.ticket_status_id), 4);
|
|
assert.equal('author_user_id' in data, false);
|
|
} else {
|
|
assert.equal(Number(data.author_user_id), 1);
|
|
assert.equal(data.is_blocked, true);
|
|
assert.equal(data.blocked_comment, 'Waiting for review');
|
|
assert.equal(Number(data.field_values[0].value_number), 2.5);
|
|
assert.equal(Number(data.field_values[1].value_user_id), 2);
|
|
assert.deepEqual(
|
|
data.acceptance_criteria.map((item) => item.id),
|
|
[10, 11],
|
|
);
|
|
assert.equal('ticket_status_id' in data, false);
|
|
}
|
|
await state('validation');
|
|
const fields = [
|
|
'Title',
|
|
'Description',
|
|
'Type',
|
|
'Priority',
|
|
'Assignee',
|
|
'Parent ticket',
|
|
'Estimated story points',
|
|
'Acceptance criteria checklist',
|
|
...(create
|
|
? ['Status']
|
|
: ['Author', 'Blocker comment', 'Impact score', 'Reviewer']),
|
|
];
|
|
for (const label of fields) {
|
|
const control = field(label);
|
|
assert.equal(
|
|
await control.getAttribute('aria-invalid'),
|
|
'true',
|
|
label,
|
|
);
|
|
const errorId = (await control.getAttribute('aria-describedby'))
|
|
.split(' ')
|
|
.at(-1);
|
|
assert.ok(
|
|
(await page.locator(`[id="${errorId}"]`).innerText()).length,
|
|
`Missing associated error: ${label}`,
|
|
);
|
|
}
|
|
assert.equal(await field('Title').inputValue(), 'A complete ticket');
|
|
await page.route(create ? `${base}` : `${base}/42`, (route) =>
|
|
route.abort('failed'),
|
|
);
|
|
await button(create ? 'Create ticket' : 'Save changes').click();
|
|
await state('network');
|
|
assert.equal(
|
|
await field('Description').inputValue(),
|
|
'Description with context.',
|
|
);
|
|
await page.unrouteAll();
|
|
await visit(create, '?admin&serverError');
|
|
await fill(create);
|
|
await submit(create);
|
|
await state('server');
|
|
assert.equal(await field('Title').inputValue(), 'A complete ticket');
|
|
let pending;
|
|
let requests = 0;
|
|
await page.route(create ? `${base}` : `${base}/42`, (route) => {
|
|
requests++;
|
|
pending = route;
|
|
});
|
|
await button('Try again').click();
|
|
await state('loading');
|
|
assert.equal(await field('Title').isDisabled(), true);
|
|
assert.equal(
|
|
await button(
|
|
create ? 'Creating ticket…' : 'Saving changes…',
|
|
).isDisabled(),
|
|
true,
|
|
);
|
|
await form().evaluate((el) =>
|
|
el.dispatchEvent(
|
|
new Event('submit', { bubbles: true, cancelable: true }),
|
|
),
|
|
);
|
|
assert.equal(requests, 1);
|
|
await pending.abort();
|
|
await state('network');
|
|
await page.unrouteAll();
|
|
await visit(create, '?admin');
|
|
await fill(create);
|
|
page.once('dialog', (dialog) => dialog.dismiss());
|
|
await page
|
|
.locator('#main-content')
|
|
.getByRole('link', {
|
|
name: create ? 'Back to backlog' : 'Open backlog',
|
|
exact: true,
|
|
})
|
|
.click();
|
|
assert.ok(page.url().includes('/tickets/'));
|
|
const successData = await submit(create);
|
|
await page
|
|
.getByRole('heading', { name: successData.title, exact: true })
|
|
.waitFor();
|
|
if (create) {
|
|
await page.goBack();
|
|
await field('Title').waitFor();
|
|
assert.equal(await field('Title').inputValue(), '');
|
|
assert.equal(
|
|
await field('Acceptance criteria checklist').inputValue(),
|
|
'',
|
|
);
|
|
} else {
|
|
await form()
|
|
.getByText('All changes saved', { exact: true })
|
|
.waitFor();
|
|
await field('Title').fill('Another draft');
|
|
page.once('dialog', (dialog) => dialog.accept());
|
|
await button('Discard changes').click();
|
|
assert.equal(
|
|
await field('Title').inputValue(),
|
|
'A complete ticket',
|
|
);
|
|
assert.equal(await field('Impact score').inputValue(), '2.5');
|
|
}
|
|
}
|
|
await visit(false, '?hideAuthor&hidePoints');
|
|
assert.equal(await field('Author').count(), 0);
|
|
assert.equal(await field('Estimated story points').count(), 0);
|
|
await visit(false);
|
|
assert.equal(await field('Author').isDisabled(), true);
|
|
await visit(true, '?hidePoints');
|
|
assert.equal(await field('Estimated story points').count(), 0);
|
|
await visit(true, '?noMetadata');
|
|
assert.equal(await page.locator('form').count(), 0);
|
|
assert.deepEqual(errors, []);
|
|
console.log(
|
|
'Ticket form browser checks passed: create/edit payloads, nested validation, retries, drafts, resets, permissions, keyboard, mobile and both themes.',
|
|
);
|
|
} finally {
|
|
await browser.close();
|
|
}
|