254 lines
9.0 KiB
JavaScript
254 lines
9.0 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();
|
|
page.setDefaultTimeout(10000);
|
|
const base = 'http://127.0.0.1:4185/projects/7/tickets/42';
|
|
const section = () => page.locator('#ticket-relationships');
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
async function visit(query = '') {
|
|
await page.goto(base + query);
|
|
await section().waitFor();
|
|
}
|
|
const control = (label) => section().getByLabel(label, { exact: true });
|
|
const save = () =>
|
|
section().getByRole('button', { name: 'Save link', exact: true });
|
|
const remove = () =>
|
|
section().getByRole('button', {
|
|
name: 'Remove link to ENG-41',
|
|
exact: true,
|
|
});
|
|
try {
|
|
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('?parent&incoming');
|
|
assert.equal(
|
|
await page
|
|
.locator('html')
|
|
.evaluate((el) => el.classList.contains('dark')),
|
|
theme === 'dark',
|
|
);
|
|
for (const text of [
|
|
'Platform delivery',
|
|
'Child tickets',
|
|
'Nested child with a long title that must wrap on mobile',
|
|
]) {
|
|
assert.equal(
|
|
await section()
|
|
.getByText(text, { exact: true })
|
|
.isVisible(),
|
|
true,
|
|
text,
|
|
);
|
|
}
|
|
assert.equal(
|
|
await section()
|
|
.locator('[data-ticket-link]')
|
|
.getByText('Blocked by', { exact: true })
|
|
.isVisible(),
|
|
true,
|
|
);
|
|
const collapse = section().getByRole('button', {
|
|
name: 'Collapse children of ENG-41',
|
|
});
|
|
await collapse.focus();
|
|
await page.keyboard.press('Enter');
|
|
assert.equal(
|
|
await section()
|
|
.getByText(
|
|
'Nested child with a long title that must wrap on mobile',
|
|
{ exact: true },
|
|
)
|
|
.count(),
|
|
0,
|
|
);
|
|
await page.keyboard.press('Space');
|
|
assert.equal(await collapse.getAttribute('aria-expanded'), 'true');
|
|
await control('Action').focus();
|
|
await page.keyboard.press('Tab');
|
|
assert.equal(
|
|
await control('Linked ticket').evaluate(
|
|
(el) =>
|
|
el === document.activeElement &&
|
|
el.matches(':focus-visible'),
|
|
),
|
|
true,
|
|
);
|
|
await control('Linked ticket').selectOption('41');
|
|
await section()
|
|
.locator('form')
|
|
.last()
|
|
.getByText('ENG-42', { exact: true })
|
|
.waitFor();
|
|
assert.equal(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= innerWidth,
|
|
),
|
|
true,
|
|
`Overflow ${width}/${theme}`,
|
|
);
|
|
await section().screenshot({
|
|
path: `/private/tmp/kan33-relationships-${width}-${theme}.png`,
|
|
});
|
|
await section()
|
|
.getByRole('button', { name: 'Change parent', exact: true })
|
|
.click();
|
|
assert.equal(
|
|
await page
|
|
.locator('#parent_ticket_id')
|
|
.evaluate((el) => el === document.activeElement),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
await page.locator('#ticket-edit').evaluate((el) => el.open),
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
await visit('?noLinks');
|
|
assert.equal(await save().isDisabled(), true);
|
|
await control('Linked ticket').selectOption('41');
|
|
let request = page.waitForRequest(
|
|
(r) => r.method() === 'POST' && r.url().endsWith('/links'),
|
|
);
|
|
await save().click();
|
|
assert.deepEqual((await request).postDataJSON(), {
|
|
ticket_link_type_id: 1,
|
|
link_direction: 'source',
|
|
linked_ticket_id: 41,
|
|
});
|
|
await section()
|
|
.locator('[data-ticket-link]')
|
|
.getByText('Blocks', { exact: true })
|
|
.waitFor();
|
|
await page.waitForFunction(
|
|
() => document.querySelector('#linked_ticket_id')?.value === '',
|
|
);
|
|
await page.waitForFunction(() =>
|
|
document.activeElement?.textContent.includes('Linked tickets'),
|
|
);
|
|
request = page.waitForRequest(
|
|
(r) => r.method() === 'DELETE' && r.url().endsWith('/links/3'),
|
|
);
|
|
await remove().click();
|
|
await request;
|
|
await section()
|
|
.getByText('No ticket links are configured for this ticket yet.', {
|
|
exact: true,
|
|
})
|
|
.waitFor();
|
|
await page.waitForFunction(() =>
|
|
document.activeElement?.textContent.includes('Linked tickets'),
|
|
);
|
|
|
|
await visit('?noLinks');
|
|
await control('Action').selectOption('1:counterpart');
|
|
await control('Linked ticket').selectOption('41');
|
|
await section()
|
|
.locator('p[aria-live="polite"]')
|
|
.getByText('Blocked by', { exact: true })
|
|
.waitFor();
|
|
request = page.waitForRequest(
|
|
(r) => r.method() === 'POST' && r.url().endsWith('/links'),
|
|
);
|
|
await save().click();
|
|
assert.deepEqual((await request).postDataJSON(), {
|
|
ticket_link_type_id: 1,
|
|
link_direction: 'counterpart',
|
|
linked_ticket_id: 41,
|
|
});
|
|
await section()
|
|
.locator('[data-ticket-link]')
|
|
.getByText('Blocked by', { exact: true })
|
|
.waitFor();
|
|
|
|
await visit('?rejectLink');
|
|
await control('Linked ticket').selectOption('41');
|
|
await save().click();
|
|
await section()
|
|
.getByText('Choose another ticket.', { exact: true })
|
|
.waitFor();
|
|
assert.equal(await control('Linked ticket').inputValue(), '41');
|
|
for (const label of ['Action', 'Linked ticket']) {
|
|
assert.equal(await control(label).getAttribute('aria-invalid'), 'true');
|
|
}
|
|
await page.route('**/tickets/42/links', (route) => route.abort('failed'));
|
|
await save().click();
|
|
await section().locator('[data-form-state="network"]').waitFor();
|
|
assert.equal(await control('Linked ticket').inputValue(), '41');
|
|
await page.unrouteAll();
|
|
|
|
await visit('?serverError');
|
|
await remove().click();
|
|
await section().locator('[data-form-state="server"]').waitFor();
|
|
assert.equal(await section().locator('[data-ticket-link]').count(), 1);
|
|
let pending;
|
|
await page.route('**/tickets/42/links/3', (route) => {
|
|
pending = route;
|
|
});
|
|
await section()
|
|
.getByRole('button', { name: 'Try again', exact: true })
|
|
.click();
|
|
await section().locator('[data-form-state="loading"]').waitFor();
|
|
assert.equal(await remove().isDisabled(), true);
|
|
await pending.abort();
|
|
await section().locator('[data-form-state="network"]').waitFor();
|
|
await page.unrouteAll();
|
|
|
|
await visit();
|
|
await section()
|
|
.getByRole('button', { name: 'Change parent', exact: true })
|
|
.click();
|
|
await page.getByLabel('Parent ticket', { exact: true }).selectOption('');
|
|
request = page.waitForRequest(
|
|
(r) => r.method() === 'PATCH' && r.url().endsWith('/tickets/42'),
|
|
);
|
|
await page
|
|
.getByRole('button', { name: 'Save changes', exact: true })
|
|
.click();
|
|
assert.equal((await request).postDataJSON().parent_ticket_id, '');
|
|
await page.getByText('Ticket updated.', { exact: true }).waitFor();
|
|
|
|
await visit('?readonly&parent&incoming');
|
|
assert.equal(await section().locator('form').count(), 0);
|
|
assert.equal(
|
|
await section()
|
|
.getByRole('button', { name: 'Change parent', exact: true })
|
|
.count(),
|
|
0,
|
|
);
|
|
assert.equal(
|
|
await section().getByText('Blocked by', { exact: true }).isVisible(),
|
|
true,
|
|
);
|
|
for (const [query, message] of [
|
|
['?noTypes', 'No link types are configured for this project.'],
|
|
[
|
|
'?noTargets',
|
|
'No other tickets are available to link in this project.',
|
|
],
|
|
]) {
|
|
await visit(query);
|
|
assert.equal(
|
|
await section().getByText(message, { exact: true }).isVisible(),
|
|
true,
|
|
);
|
|
assert.equal(await save().count(), 0);
|
|
}
|
|
assert.deepEqual(errors, []);
|
|
console.log(
|
|
'Ticket relationships browser checks passed: hierarchy, direction, add/remove, validation, retries, permissions, keyboard, mobile and themes.',
|
|
);
|
|
} finally {
|
|
await browser.close();
|
|
}
|