Files
KanbanGit/tests/Fixtures/ticket-detail-browser.mjs
T
Eric 38438b4700
CI/CD / preconditions (pull_request) Successful in 1m1s
CI/CD / test_and_check_app (pull_request) Failing after 3m49s
CI/CD / Publish immutable image (pull_request) Skipped
feat(KAN-34): rework blocker and ticket history
2026-09-14 16:20:09 +02:00

399 lines
14 KiB
JavaScript

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 page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
page.setDefaultTimeout(10000);
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
const base = 'http://127.0.0.1:4185/projects/7/tickets/42';
const content = () => page.locator('#main-content');
const visit = async (query = '') => {
await page.goto(base + query);
await page.getByRole('heading', { level: 1 }).waitFor();
};
const edit = () => page.locator('#ticket-edit > summary');
const tabTo = async (locator) => {
await content().focus();
for (let step = 0; step < 45; step++) {
await page.keyboard.press('Tab');
if (
await locator.evaluate(
(element) => element === document.activeElement,
)
) {
assert.equal(
await locator.evaluate((element) =>
element.matches(':focus-visible'),
),
true,
);
return;
}
}
assert.fail('Ticket control is unreachable by keyboard');
};
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,
reducedMotion: 'reduce',
});
for (const query of ['', '?empty', '?long']) {
await visit(query);
assert.equal(
await page
.locator('html')
.evaluate((el) => el.classList.contains('dark')),
theme === 'dark',
);
assert.equal(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
window.innerWidth,
),
true,
`Page overflow at ${width}/${theme}/${query}`,
);
const description = await page
.locator('#ticket-description')
.boundingBox();
const metadata = await page
.locator('#ticket-metadata')
.boundingBox();
if (width >= 1280) {
assert.ok(
metadata.x > description.x + description.width,
'Desktop metadata must be beside the content.',
);
} else {
assert.ok(
metadata.y > description.y + description.height,
'Mobile metadata must be stacked.',
);
}
await edit().click();
assert.equal(
await page.getByLabel('Title', { exact: true }).isVisible(),
true,
);
assert.equal(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
window.innerWidth,
),
true,
`Editor overflow at ${width}/${theme}/${query}`,
);
await edit().click();
}
await visit();
await page.screenshot({
path: `/private/tmp/kan30-detail-${width}-${theme}.png`,
fullPage: true,
});
}
}
await page.setViewportSize({ width: 390, height: 1000 });
await visit();
const blocker = content().locator('#ticket-blocker');
const blockerToggle = blocker.getByRole('checkbox', {
name: 'Mark ticket as blocked',
exact: true,
});
const blockerComment = blocker.getByLabel('Blocker comment', {
exact: true,
});
assert.equal(await blockerToggle.isChecked(), true);
assert.equal(
await blocker.getByText('Alex Morgan', { exact: true }).count(),
0,
);
assert.equal(
await content()
.locator('#ticket-history')
.getByText('Alex Morgan', { exact: true })
.count(),
1,
);
assert.equal(
await content()
.locator('#ticket-history')
.getByText('Status changed', { exact: true })
.count(),
1,
);
await blockerComment.fill('');
assert.equal(
await blockerComment.evaluate(
(element) => element.validity.valueMissing,
),
true,
);
await blockerComment.fill('Waiting for a production decision.');
await blocker.getByRole('button', { name: 'Save blocker' }).click();
await page.getByText('Ticket updated.', { exact: true }).waitFor();
assert.equal(
await blockerComment.inputValue(),
'Waiting for a production decision.',
);
for (const label of ['Assignee', 'Reviewer']) {
const field = content()
.locator('#ticket-metadata dl > div')
.filter({ has: page.getByText(label, { exact: true }) });
assert.match(await field.locator('dd').innerText(), /Alex Morgan/);
}
await blockerToggle.uncheck();
await blocker.getByRole('button', { name: 'Unblock ticket' }).click();
await blocker
.getByText('This ticket currently has no active blocker.', {
exact: true,
})
.waitFor();
assert.equal(await blocker.getByLabel('Blocker comment').count(), 0);
await content().getByRole('button', { name: 'Change parent' }).click();
await page.waitForFunction(
() => document.activeElement?.id === 'parent_ticket_id',
);
const saveTicket = content().getByRole('button', {
name: 'Save changes',
exact: true,
});
assert.equal(
await content()
.getByRole('button', { name: 'Discard changes', exact: true })
.isDisabled(),
true,
'Saving the blocker must also update the ticket form defaults.',
);
await page
.getByLabel('Title', { exact: true })
.fill('Updated title after unblocking');
const ticketUpdate = page.waitForRequest(
(request) =>
request.method() === 'PATCH' &&
new URL(request.url()).pathname === '/projects/7/tickets/42',
);
await saveTicket.click();
const savedTicket = (await ticketUpdate).postDataJSON();
assert.equal(savedTicket.is_blocked, false);
assert.equal(savedTicket.blocked_comment, '');
assert.equal(Number(savedTicket.parent_ticket_id), 40);
assert.deepEqual(savedTicket.acceptance_criteria, [
{ id: 10, text: 'Cache requests safely', checked: false, indent: 0 },
{
id: 11,
text: 'Invalidate when records change',
checked: false,
indent: 1,
},
]);
await page
.getByRole('heading', { name: 'Updated title after unblocking' })
.waitFor();
await visit();
await tabTo(edit());
await page.keyboard.press('Enter');
await page.getByLabel('Title', { exact: true }).fill('Unsaved title draft');
await page
.locator('#ticket-checklist')
.getByRole('checkbox', {
name: 'Complete criterion 1: Cache requests safely',
exact: true,
})
.click();
await page.waitForFunction(
() =>
document.querySelectorAll(
'#ticket-checklist input[type=checkbox]',
)[1].checked,
);
assert.equal(
await page.getByLabel('Title', { exact: true }).inputValue(),
'Unsaved title draft',
);
await page
.getByLabel('Criterion 1', { exact: true })
.fill('A draft checklist item');
assert.equal(
await page
.locator('#ticket-checklist')
.getByRole('checkbox', {
name: 'Complete criterion 1: Cache requests safely',
exact: true,
})
.isDisabled(),
true,
);
await edit().click();
await edit().click();
assert.equal(
await page.getByLabel('Title', { exact: true }).inputValue(),
'Unsaved title draft',
);
page.once('dialog', (dialog) => dialog.dismiss());
await content()
.getByRole('link', { name: 'Open board', exact: true })
.click();
assert.match(page.url(), /\/tickets\/42/);
page.once('dialog', (dialog) => dialog.accept());
await content()
.getByRole('link', { name: 'Open board', exact: true })
.click();
await page.waitForURL('**/projects/7/board');
await page.goBack();
await edit().waitFor();
if (!(await page.locator('#ticket-edit').getAttribute('open'))) {
await edit().click();
}
if (!(await page.getByLabel('Title', { exact: true }).isVisible())) {
await edit().click();
}
assert.equal(
await page.getByLabel('Title', { exact: true }).inputValue(),
'Unsaved title draft',
);
assert.equal(
await page.getByLabel('Criterion 1', { exact: true }).inputValue(),
'A draft checklist item',
);
await visit('?reject');
await edit().click();
await page.getByLabel('Title', { exact: true }).fill('Rejected draft');
await page
.getByRole('button', { name: 'Save changes', exact: true })
.click();
await page
.getByText('This title is not allowed.', { exact: true })
.waitFor();
assert.equal(
await page.getByLabel('Title', { exact: true }).inputValue(),
'Rejected draft',
);
assert.equal(
await page
.getByLabel('Title', { exact: true })
.getAttribute('aria-invalid'),
'true',
);
await visit();
await edit().click();
await page
.getByLabel('Title', { exact: true })
.fill('Saved updated ticket title');
await page
.getByRole('button', { name: 'Save changes', exact: true })
.click();
await page
.getByRole('heading', {
name: 'Saved updated ticket title',
exact: true,
})
.waitFor();
await page.getByText('Ticket updated.', { exact: true }).waitFor();
await visit('?rejectToggle');
await page
.locator('#ticket-checklist')
.getByRole('checkbox', {
name: 'Complete criterion 1: Cache requests safely',
exact: true,
})
.click();
await page
.getByRole('alert')
.filter({ hasText: 'Checklist status could not be saved' })
.waitFor();
assert.equal(
await page
.locator('#ticket-checklist')
.getByRole('checkbox', {
name: 'Complete criterion 1: Cache requests safely',
exact: true,
})
.isChecked(),
false,
);
await visit();
const createBranch = page.getByRole('button', {
name: 'Create branch',
exact: true,
});
await tabTo(createBranch);
await page.keyboard.press('Enter');
const dialog = page.getByRole('dialog');
await dialog.waitFor();
await page
.getByText('Branch name is available.', { exact: true })
.waitFor();
await page.keyboard.press('Escape');
await dialog.waitFor({ state: 'hidden' });
assert.equal(
await createBranch.evaluate((el) => el === document.activeElement),
true,
);
await createBranch.click();
await page
.getByText('Branch name is available.', { exact: true })
.waitFor();
await page.getByRole('button', { name: 'Create now', exact: true }).click();
await dialog.waitFor({ state: 'hidden' });
const linkSaved = page.waitForResponse(
(response) =>
response.url().includes('/tickets/42/links') &&
response.request().method() === 'POST',
);
await page.getByLabel('Linked ticket', { exact: true }).selectOption('41');
await page.getByRole('button', { name: 'Save link', exact: true }).click();
assert.equal((await linkSaved).status(), 303);
await page
.getByRole('button', { name: 'Remove link to ENG-41', exact: true })
.waitFor();
const linkRemoved = page.waitForResponse(
(response) =>
response.url().includes('/tickets/42/links/3') &&
response.request().method() === 'DELETE',
);
await page
.getByRole('button', { name: 'Remove link to ENG-41', exact: true })
.click();
assert.equal((await linkRemoved).status(), 303);
await visit('?readonly&hideAuthor&hidePoints');
assert.equal(await content().locator('form').count(), 0);
assert.equal(
await content().getByText('Sam Chen', { exact: true }).count(),
0,
);
assert.equal(
await content()
.getByText('Estimated story points', { exact: true })
.count(),
0,
);
await tabTo(
content().getByRole('link', {
name: 'ENG-41 Related infrastructure',
exact: true,
}),
);
await page.keyboard.press('Enter');
await page.waitForURL('**/tickets/41');
await page
.getByRole('heading', { name: 'Related infrastructure', exact: true })
.waitFor();
assert.equal(
await page.getByLabel('Title', { exact: true }).inputValue(),
'Related infrastructure',
);
assert.deepEqual(errors, []);
console.log(
'Ticket detail browser checks passed: desktop/mobile, both themes, keyboard, drafts, saves, checklist rollback, links, Git dialog and direct navigation.',
);
} finally {
await browser.close();
}