feat(KAN-27): rebuild responsive project board
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
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:4181/projects/7/board';
|
||||
const content = () => page.locator('#main-content');
|
||||
const visibleColumns = () =>
|
||||
content().locator('[id^="board-column-title-"]:visible');
|
||||
const visit = async (query = '') => {
|
||||
await page.goto(base + query);
|
||||
await page.getByRole('heading', { level: 1 }).waitFor();
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
};
|
||||
const tabTo = async (locator) => {
|
||||
await content().focus();
|
||||
for (let step = 0; step < 40; 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('Board 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, state] of [
|
||||
['', 'filled'],
|
||||
['?empty', 'empty'],
|
||||
['?one&noTickets', 'one-empty'],
|
||||
]) {
|
||||
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,
|
||||
`${state} overflow at ${width}/${theme}`,
|
||||
);
|
||||
assert.equal(
|
||||
await visibleColumns().count(),
|
||||
state === 'empty'
|
||||
? 0
|
||||
: width < 768 || state === 'one-empty'
|
||||
? 1
|
||||
: 8,
|
||||
);
|
||||
if (width < 768 && state !== 'empty') {
|
||||
assert.ok(
|
||||
(await page.getByLabel('Choose a column').boundingBox())
|
||||
.height >= 44,
|
||||
);
|
||||
}
|
||||
if (state === 'filled' && width >= 768) {
|
||||
const region = content().getByRole('region', {
|
||||
name: 'Workflow columns',
|
||||
exact: true,
|
||||
});
|
||||
assert.equal(
|
||||
await region.evaluate(
|
||||
(el) => el.scrollWidth > el.clientWidth,
|
||||
),
|
||||
true,
|
||||
);
|
||||
await region.focus();
|
||||
await page.keyboard.press('End');
|
||||
await content()
|
||||
.locator('#board-column-8')
|
||||
.scrollIntoViewIfNeeded();
|
||||
assert.ok(await region.evaluate((el) => el.scrollLeft > 0));
|
||||
await region.evaluate((el) => {
|
||||
el.scrollLeft = 0;
|
||||
});
|
||||
}
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await page.screenshot({
|
||||
path: `/private/tmp/kan27-board-${state}-${width}-${theme}.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
await page.setViewportSize({ width: 390, height: 1000 });
|
||||
await visit();
|
||||
const chooseColumn = page.getByLabel('Choose a column');
|
||||
await tabTo(chooseColumn);
|
||||
// Native popup selection is handled by macOS outside the browser DOM.
|
||||
await chooseColumn.selectOption('2');
|
||||
assert.equal(await chooseColumn.inputValue(), '2');
|
||||
assert.equal(await visibleColumns().innerText(), 'In development');
|
||||
await chooseColumn.selectOption('8');
|
||||
assert.equal(await visibleColumns().innerText(), 'Column without statuses');
|
||||
await content()
|
||||
.getByText('No statuses configured for this column.')
|
||||
.waitFor();
|
||||
await chooseColumn.selectOption('1');
|
||||
await content()
|
||||
.getByRole('heading', { name: 'Ready · Planned', exact: true })
|
||||
.waitFor();
|
||||
await content()
|
||||
.getByRole('heading', { name: 'Ready · Active', exact: true })
|
||||
.waitFor();
|
||||
const terminalSelect = page.getByLabel('Move ENG-43', { exact: true });
|
||||
assert.equal(await terminalSelect.isDisabled(), true);
|
||||
const moveSelect = page.getByLabel('Move ENG-42', { exact: true });
|
||||
assert.deepEqual(
|
||||
await moveSelect
|
||||
.locator('option')
|
||||
.evaluateAll((options) => options.map((option) => option.value)),
|
||||
['20', '11', '99'],
|
||||
);
|
||||
await tabTo(moveSelect);
|
||||
await moveSelect.selectOption('20');
|
||||
await page.keyboard.press('Tab');
|
||||
assert.equal(await page.locator(':focus').innerText(), 'Update status');
|
||||
await page.keyboard.press('Enter');
|
||||
await page
|
||||
.getByText('ENG-42 moved successfully.', { exact: true })
|
||||
.waitFor();
|
||||
assert.equal(await chooseColumn.inputValue(), '1');
|
||||
assert.match(
|
||||
await chooseColumn.locator('option').first().innerText(),
|
||||
/\(1\)/,
|
||||
);
|
||||
await chooseColumn.selectOption('2');
|
||||
await content()
|
||||
.getByRole('link', {
|
||||
name: 'Open ENG-42: Ship accessible board navigation',
|
||||
exact: true,
|
||||
})
|
||||
.waitFor();
|
||||
|
||||
await visit();
|
||||
await page.getByLabel('Move ENG-42', { exact: true }).selectOption('99');
|
||||
await content()
|
||||
.locator('article')
|
||||
.filter({ hasText: 'ENG-42' })
|
||||
.getByRole('button', { name: 'Update status' })
|
||||
.click();
|
||||
await page
|
||||
.getByText('ENG-42 moved successfully.', { exact: true })
|
||||
.waitFor();
|
||||
assert.equal(
|
||||
await content()
|
||||
.getByRole('link', {
|
||||
name: 'Open ENG-42: Ship accessible board navigation',
|
||||
exact: true,
|
||||
})
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
assert.match(
|
||||
await chooseColumn.locator('option').first().innerText(),
|
||||
/\(1\)/,
|
||||
);
|
||||
|
||||
await visit('?reject');
|
||||
await page.getByLabel('Move ENG-42', { exact: true }).selectOption('20');
|
||||
await content()
|
||||
.locator('article')
|
||||
.filter({ hasText: 'ENG-42' })
|
||||
.getByRole('button', { name: 'Update status' })
|
||||
.click();
|
||||
await page
|
||||
.getByText('This transition is no longer allowed.', { exact: true })
|
||||
.waitFor();
|
||||
assert.equal(
|
||||
await page
|
||||
.getByLabel('Move ENG-42', { exact: true })
|
||||
.getAttribute('aria-invalid'),
|
||||
'true',
|
||||
);
|
||||
assert.equal(
|
||||
await content()
|
||||
.getByText('This transition is no longer allowed.', { exact: true })
|
||||
.count(),
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
await page
|
||||
.getByLabel('Move ENG-43', { exact: true })
|
||||
.getAttribute('aria-invalid'),
|
||||
null,
|
||||
);
|
||||
|
||||
await visit('?readonly');
|
||||
assert.equal(await content().locator('form').count(), 0);
|
||||
const ticketLink = content().getByRole('link', {
|
||||
name: 'Open ENG-42: Ship accessible board navigation',
|
||||
exact: true,
|
||||
});
|
||||
await tabTo(ticketLink);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForURL('**/projects/7/tickets/42');
|
||||
await page
|
||||
.getByRole('heading', { name: 'Destination reached', exact: true })
|
||||
.waitFor();
|
||||
await visit('?empty');
|
||||
await tabTo(
|
||||
content().getByRole('link', { name: 'Open backlog', exact: true }),
|
||||
);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForURL('**/projects/7/backlog');
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(
|
||||
'Board browser checks passed: responsive columns, keyboard, status changes and validation, permissions, empty states and both themes.',
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user