feat(KAN-30): rebuild responsive ticket detail view
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
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();
|
||||
await tabTo(edit());
|
||||
await page.keyboard.press('Enter');
|
||||
await page.getByLabel('Title', { exact: true }).fill('Unsaved title draft');
|
||||
await page
|
||||
.getByRole('checkbox', { name: 'Cache requests safely', exact: true })
|
||||
.check();
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector('#acceptance-criteria-11').checked,
|
||||
);
|
||||
assert.equal(
|
||||
await page.getByLabel('Title', { exact: true }).inputValue(),
|
||||
'Unsaved title draft',
|
||||
);
|
||||
await page
|
||||
.getByLabel('Acceptance criteria checklist', { exact: true })
|
||||
.fill('[ ] A draft checklist item');
|
||||
assert.equal(
|
||||
await page
|
||||
.getByRole('checkbox', {
|
||||
name: '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('Acceptance criteria checklist', { 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
|
||||
.getByRole('checkbox', { name: 'Cache requests safely', exact: true })
|
||||
.check();
|
||||
await page
|
||||
.getByRole('alert')
|
||||
.filter({ hasText: 'Checklist status could not be saved' })
|
||||
.waitFor();
|
||||
assert.equal(
|
||||
await page
|
||||
.getByRole('checkbox', {
|
||||
name: '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.getByRole('button', { name: 'Save link', exact: true }).click();
|
||||
assert.equal((await linkSaved).status(), 303);
|
||||
await page.getByRole('button', { name: 'Remove', exact: true }).waitFor();
|
||||
const linkRemoved = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/tickets/42/links/3?_method=DELETE') &&
|
||||
response.request().method() === 'POST',
|
||||
);
|
||||
await page.getByRole('button', { name: 'Remove', 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', 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();
|
||||
}
|
||||
Reference in New Issue
Block a user