293 lines
10 KiB
JavaScript
293 lines
10 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();
|
|
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.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();
|
|
}
|