707 lines
26 KiB
JavaScript
707 lines
26 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import process from 'node:process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import tailwind from '@tailwindcss/vite';
|
|
import vue from '@vitejs/plugin-vue';
|
|
import { createInertiaApp } from '@inertiajs/vue3';
|
|
import { createServer } from 'vite';
|
|
import { createSSRApp, h, reactive, ref } from 'vue';
|
|
import { renderToString } from 'vue/server-renderer';
|
|
|
|
const serve = process.argv.includes('--serve');
|
|
const server = await createServer({
|
|
configFile: false,
|
|
cacheDir: 'node_modules/.vite/ticket-detail-fixture',
|
|
plugins: [vue(), ...(serve ? [tailwind()] : [])],
|
|
resolve: {
|
|
alias: {
|
|
'@': fileURLToPath(new URL('../../resources/js', import.meta.url)),
|
|
},
|
|
},
|
|
server: serve
|
|
? { host: '127.0.0.1', port: 4185, strictPort: true, hmr: false }
|
|
: { middlewareMode: true, hmr: false, watch: null },
|
|
appType: 'custom',
|
|
});
|
|
const user = {
|
|
id: 1,
|
|
name: 'Alex Morgan',
|
|
email: 'alex@example.test',
|
|
avatar: null,
|
|
};
|
|
const project = {
|
|
id: 7,
|
|
key: 'ENG',
|
|
name: 'Engineering',
|
|
show_author: true,
|
|
show_story_points: true,
|
|
};
|
|
const repository = {
|
|
id: 9,
|
|
provider: 'gitea',
|
|
provider_label: 'Gitea',
|
|
base_url: 'https://git.example.test',
|
|
repository_full_name: 'Engineering/platform',
|
|
repository_url: 'https://git.example.test/Engineering/platform',
|
|
has_personal_access: true,
|
|
};
|
|
const parent = {
|
|
id: 40,
|
|
ticket_key: 'ENG-40',
|
|
title: 'Platform delivery',
|
|
project_id: 7,
|
|
};
|
|
const linked = {
|
|
id: 41,
|
|
ticket_key: 'ENG-41',
|
|
title: 'Related infrastructure',
|
|
project_id: 7,
|
|
};
|
|
const ticket = {
|
|
id: 42,
|
|
project_id: 7,
|
|
sequence_number: 42,
|
|
ticket_key: 'ENG-42',
|
|
title: 'Implement a resilient caching layer for the project API',
|
|
description:
|
|
'Keep every existing ticket action available.\nLongUnbrokenDescriptionThatMustWrapWithoutCausingHorizontalScrollingOnTheSmallestSupportedMobileViewport.',
|
|
acceptance_criteria: [
|
|
{ id: 10, text: 'Cache requests safely', checked: false, indent: 0 },
|
|
{
|
|
id: 11,
|
|
text: 'Invalidate when records change',
|
|
checked: false,
|
|
indent: 1,
|
|
},
|
|
],
|
|
assignee_user_id: 1,
|
|
assignee: user,
|
|
author_user_id: 2,
|
|
author: { ...user, id: 2, name: 'Sam Chen' },
|
|
parent_ticket_id: 40,
|
|
parent_ticket: parent,
|
|
is_blocked: true,
|
|
blocked_comment: 'Waiting for infrastructure approval.',
|
|
story_points: 0,
|
|
ticket_type_id: 1,
|
|
ticket_type: { id: 1, name: 'Custom development task', is_parent: false },
|
|
ticket_priority_id: 2,
|
|
ticket_priority: { id: 2, name: 'Urgent project priority' },
|
|
ticket_status_id: 3,
|
|
ticket_status: {
|
|
id: 3,
|
|
name: 'In progress',
|
|
board_column: { id: 1, name: 'Delivery' },
|
|
},
|
|
field_values: [
|
|
{
|
|
id: 1,
|
|
ticket_field_definition_id: 1,
|
|
value_number: 0,
|
|
value_user_id: null,
|
|
},
|
|
{
|
|
id: 2,
|
|
ticket_field_definition_id: 2,
|
|
value_number: null,
|
|
value_user_id: 1,
|
|
user_value: user,
|
|
},
|
|
],
|
|
worklog_entries: [
|
|
{
|
|
id: 2,
|
|
actor: null,
|
|
actor_id: null,
|
|
entry_type: 'status_changed',
|
|
summary: 'Moved from Backlog to Delivery / In progress.',
|
|
created_at: '2026-09-11T13:00:00Z',
|
|
},
|
|
{
|
|
id: 1,
|
|
actor: user,
|
|
actor_id: user.id,
|
|
entry_type: 'created',
|
|
summary: 'Ticket created in the project backlog.',
|
|
created_at: '2026-09-11T12:00:00Z',
|
|
},
|
|
],
|
|
created_at: '2026-09-10T12:00:00Z',
|
|
updated_at: '2026-09-11T12:00:00Z',
|
|
};
|
|
const fieldDefinitions = [
|
|
{ id: 1, label: 'Impact score', field_type: 'number' },
|
|
{ id: 2, label: 'Reviewer', field_type: 'user' },
|
|
];
|
|
let savedForm = {};
|
|
function makePage(url) {
|
|
const parsed = new URL(url, 'http://localhost');
|
|
const params = parsed.searchParams;
|
|
const creating = parsed.pathname.endsWith('/create');
|
|
const detail =
|
|
!creating && parsed.pathname.startsWith('/projects/7/tickets/');
|
|
const data = structuredClone(ticket);
|
|
if (parsed.pathname.endsWith('/41')) {
|
|
Object.assign(data, linked, {
|
|
parent_ticket_id: null,
|
|
parent_ticket: null,
|
|
title: 'Related infrastructure',
|
|
});
|
|
}
|
|
if (params.has('saved')) {
|
|
Object.assign(data, savedForm, { title: params.get('saved') });
|
|
}
|
|
if (params.has('long')) {
|
|
data.title = 'LongUnbrokenTicketTitle'.repeat(10);
|
|
}
|
|
if (params.has('parent')) {
|
|
data.ticket_type.is_parent = true;
|
|
}
|
|
if (params.has('empty')) {
|
|
Object.assign(data, {
|
|
description: null,
|
|
acceptance_criteria: [],
|
|
assignee: null,
|
|
assignee_user_id: null,
|
|
author: null,
|
|
author_user_id: null,
|
|
parent_ticket: null,
|
|
parent_ticket_id: null,
|
|
is_blocked: false,
|
|
blocked_comment: null,
|
|
story_points: null,
|
|
ticket_status: null,
|
|
field_values: [],
|
|
worklog_entries: [],
|
|
});
|
|
}
|
|
const noAccess = params.has('noAccess');
|
|
const repo = { ...repository, has_personal_access: !noAccess };
|
|
const repositoryError = params.has('gitError')
|
|
? 'Repository temporarily unavailable.'
|
|
: null;
|
|
return {
|
|
component: creating
|
|
? 'TicketCreate'
|
|
: detail
|
|
? 'Ticket'
|
|
: 'Destination',
|
|
url,
|
|
version: null,
|
|
clearHistory: false,
|
|
encryptHistory: false,
|
|
props: {
|
|
name: 'KanbanGit',
|
|
errors: params.has('linkRejected')
|
|
? {
|
|
ticket_link_type_id: 'Invalid relationship type.',
|
|
linked_ticket_id: 'Choose another ticket.',
|
|
}
|
|
: params.has('rejected')
|
|
? {
|
|
title: 'This title is not allowed.',
|
|
...(params.has('allErrors')
|
|
? {
|
|
description: 'Invalid description.',
|
|
ticket_type_id: 'Invalid type.',
|
|
ticket_priority_id: 'Invalid priority.',
|
|
ticket_status_id: 'Invalid status.',
|
|
assignee_user_id: 'Invalid assignee.',
|
|
author_user_id: 'Invalid author.',
|
|
parent_ticket_id: 'Invalid parent.',
|
|
story_points: 'Invalid points.',
|
|
'acceptance_criteria.0.text':
|
|
'Invalid checklist.',
|
|
blocked_comment: 'Invalid blocker.',
|
|
'field_values.0.value_number':
|
|
'Invalid number.',
|
|
'field_values.1.value_user_id':
|
|
'Invalid reviewer.',
|
|
}
|
|
: {}),
|
|
}
|
|
: {},
|
|
sidebarOpen: true,
|
|
auth: {
|
|
user,
|
|
permissions: {
|
|
globalAdmin: false,
|
|
projectCreate: false,
|
|
canManageTeams: false,
|
|
},
|
|
},
|
|
navigation: {
|
|
project: {
|
|
...project,
|
|
canManage: false,
|
|
canCreateTickets: !params.has('readonly'),
|
|
},
|
|
},
|
|
project: {
|
|
...project,
|
|
show_author: !params.has('hideAuthor'),
|
|
show_story_points: !params.has('hidePoints'),
|
|
},
|
|
ticket: data,
|
|
canEditTicket: !params.has('readonly'),
|
|
canManageProject: params.has('admin'),
|
|
status: params.has('saved') ? 'Ticket updated.' : null,
|
|
users: [user, ticket.author],
|
|
ticketTypes: params.has('noMetadata') ? [] : [data.ticket_type],
|
|
source: params.get('source') === 'board' ? 'board' : 'backlog',
|
|
defaultTicketStatusId: 3,
|
|
ticketStatusOptions: [
|
|
{
|
|
id: 3,
|
|
name: 'In progress',
|
|
board_column: { name: 'Delivery' },
|
|
},
|
|
{ id: 4, name: 'Backlog', board_column: null },
|
|
],
|
|
ticketPriorities: [data.ticket_priority],
|
|
ticketFieldDefinitions: params.has('empty') ? [] : fieldDefinitions,
|
|
parentTicketOptions: [parent],
|
|
childTicketTree: params.has('parent')
|
|
? [
|
|
{
|
|
...linked,
|
|
ticket_type_name: 'Epic',
|
|
children: [
|
|
{
|
|
id: 43,
|
|
ticket_key: 'ENG-43',
|
|
title: 'Nested child with a long title that must wrap on mobile',
|
|
ticket_type_name: 'Task',
|
|
children: [],
|
|
},
|
|
],
|
|
children_count: 0,
|
|
},
|
|
]
|
|
: [],
|
|
linkedTickets:
|
|
params.has('empty') || params.has('noLinks')
|
|
? []
|
|
: [
|
|
{
|
|
id: 3,
|
|
source_ticket_id: params.has('incoming')
|
|
? 41
|
|
: 42,
|
|
target_ticket_id: params.has('incoming')
|
|
? 42
|
|
: 41,
|
|
ticket_link_type: {
|
|
id: 1,
|
|
label: params.has('incoming')
|
|
? 'Blocked by'
|
|
: 'Blocks',
|
|
},
|
|
linked_ticket: linked,
|
|
},
|
|
],
|
|
ticketLinkOptions: params.has('noTargets') ? [] : [linked],
|
|
ticketLinkTypes: params.has('noTypes')
|
|
? []
|
|
: [{ id: 1, label: 'Blocks', counterpart_label: 'Blocked by' }],
|
|
repositoryBranches: params.has('empty')
|
|
? []
|
|
: [
|
|
{
|
|
repository_connection: repo,
|
|
branches: [{ name: 'feature/ENG-42-cache' }],
|
|
error: repositoryError,
|
|
},
|
|
],
|
|
repositoryPullRequests: params.has('empty')
|
|
? []
|
|
: [
|
|
{
|
|
repository_connection: repo,
|
|
pull_requests: [
|
|
{
|
|
number: 12,
|
|
status: 'open',
|
|
title: 'Cache implementation',
|
|
source_branch: 'feature/ENG-42-cache',
|
|
html_url:
|
|
'https://git.example.test/Engineering/platform/pulls/12',
|
|
},
|
|
],
|
|
error: repositoryError,
|
|
},
|
|
],
|
|
suggestedBranchName: `${data.ticket_key.toLowerCase()}-cache`,
|
|
},
|
|
};
|
|
}
|
|
if (serve) {
|
|
server.middlewares.use(async (request, response) => {
|
|
if (request.url.includes('/branches/validate')) {
|
|
response.setHeader('Content-Type', 'application/json');
|
|
response.end(
|
|
JSON.stringify({
|
|
is_valid: true,
|
|
message: 'Branch name is available.',
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
if (request.method !== 'GET') {
|
|
let body = '';
|
|
for await (const chunk of request) {
|
|
body += chunk;
|
|
}
|
|
const data = body ? JSON.parse(body) : {};
|
|
const previous = new URL(request.headers.referer);
|
|
const params = previous.searchParams;
|
|
if (request.url.includes('/branches') && params.has('slowBranch')) {
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
}
|
|
if (params.has('serverError')) {
|
|
response.writeHead(503);
|
|
response.end('Simulated failure');
|
|
return;
|
|
}
|
|
if (!request.headers['x-inertia']) {
|
|
assert.equal(request.method, 'PATCH');
|
|
assert.equal(request.url, '/projects/7/tickets/42');
|
|
assert.equal(
|
|
data.title,
|
|
ticket.title,
|
|
'Checklist toggle must not save the title draft.',
|
|
);
|
|
assert.equal(
|
|
data.acceptance_criteria[0].checked,
|
|
data.acceptance_criteria[1].checked,
|
|
'Toggling a parent must update its descendants.',
|
|
);
|
|
response.writeHead(params.has('rejectToggle') ? 422 : 200, {
|
|
'Content-Type': 'application/json',
|
|
});
|
|
response.end('{}');
|
|
return;
|
|
}
|
|
if (request.url.includes('/branches')) {
|
|
assert.equal(data.branch_name, 'eng-42-cache');
|
|
assert.equal(Number(data.repository_connection_id), 9);
|
|
} else if (request.url.includes('/links')) {
|
|
assert.ok(
|
|
request.url.endsWith('/links/3?_method=DELETE') ||
|
|
request.method === 'DELETE' ||
|
|
Number(data.linked_ticket_id) === 41,
|
|
);
|
|
if (params.has('rejectLink')) {
|
|
params.set('linkRejected', '1');
|
|
} else if (request.method === 'DELETE') {
|
|
params.set('noLinks', '1');
|
|
} else {
|
|
params.delete('noLinks');
|
|
params.delete('linkRejected');
|
|
if (data.link_direction === 'counterpart') {
|
|
params.set('incoming', '1');
|
|
} else {
|
|
params.delete('incoming');
|
|
}
|
|
}
|
|
} else {
|
|
assert.ok(
|
|
(request.method === 'POST' || 'field_values' in data) &&
|
|
'acceptance_criteria' in data &&
|
|
'parent_ticket_id' in data,
|
|
);
|
|
if (params.has('reject')) {
|
|
params.set('rejected', '1');
|
|
} else {
|
|
savedForm = {
|
|
...data,
|
|
blocked_comment: data.is_blocked
|
|
? data.blocked_comment
|
|
: null,
|
|
field_values: data.field_values.map((fieldValue) => ({
|
|
...fieldValue,
|
|
user_value:
|
|
Number(fieldValue.value_user_id) === user.id
|
|
? user
|
|
: null,
|
|
})),
|
|
};
|
|
params.set('saved', data.title);
|
|
}
|
|
}
|
|
response.writeHead(303, {
|
|
Location: `${params.has('rejected') && previous.pathname.endsWith('/create') ? previous.pathname : '/projects/7/tickets/42'}?${params}`,
|
|
});
|
|
response.end();
|
|
return;
|
|
}
|
|
const page = makePage(request.url);
|
|
response.setHeader('Vary', 'X-Inertia');
|
|
if (request.headers['x-inertia']) {
|
|
response.writeHead(200, {
|
|
'X-Inertia': 'true',
|
|
'Content-Type': 'application/json',
|
|
});
|
|
response.end(JSON.stringify(page));
|
|
return;
|
|
}
|
|
const escaped = JSON.stringify(page)
|
|
.replaceAll('&', '&')
|
|
.replaceAll('"', '"')
|
|
.replaceAll('<', '<');
|
|
response.setHeader('Content-Type', 'text/html');
|
|
response.end(
|
|
await server.transformIndexHtml(
|
|
request.url,
|
|
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><div id="app" data-page="${escaped}"></div><script type="module" src="/tests/Fixtures/ticket-detail-preview.mjs"></script></body></html>`,
|
|
),
|
|
);
|
|
});
|
|
await server.listen();
|
|
console.log(
|
|
'Ticket detail fixture ready at http://127.0.0.1:4185/projects/7/tickets/42',
|
|
);
|
|
} else {
|
|
try {
|
|
const { useTicketLinkActions } = await server.ssrLoadModule(
|
|
'/resources/js/composables/useTicketLinkActions.ts',
|
|
);
|
|
const linkTypes = ref([
|
|
{ id: 1, label: 'Blocks', counterpart_label: 'Blocked by' },
|
|
{ id: 2, label: 'Relates to', counterpart_label: 'Relates to' },
|
|
]);
|
|
const linkForm = reactive({
|
|
ticket_link_type_id: 1,
|
|
link_direction: 'source',
|
|
linked_ticket_id: 41,
|
|
});
|
|
const { actions, selectedAction, selectedActionLabel } =
|
|
useTicketLinkActions(linkTypes, linkForm);
|
|
assert.deepEqual(
|
|
actions.value.map((action) => action.label),
|
|
['Blocks', 'Blocked by', 'Relates to'],
|
|
);
|
|
assert.equal(selectedActionLabel.value, 'Blocks');
|
|
selectedAction.value = '1:counterpart';
|
|
assert.equal(selectedActionLabel.value, 'Blocked by');
|
|
assert.deepEqual(
|
|
{ ...linkForm },
|
|
{
|
|
ticket_link_type_id: 1,
|
|
link_direction: 'counterpart',
|
|
linked_ticket_id: 41,
|
|
},
|
|
);
|
|
selectedAction.value = '2:source';
|
|
assert.equal(linkForm.ticket_link_type_id, 2);
|
|
assert.equal(linkForm.link_direction, 'source');
|
|
selectedAction.value = 'unknown';
|
|
assert.equal(selectedActionLabel.value, 'Relates to');
|
|
const { createLocalization } = await server.ssrLoadModule(
|
|
'/resources/js/lib/localization.ts',
|
|
);
|
|
const { createAppearance } = await server.ssrLoadModule(
|
|
'/resources/js/composables/useAppearance.ts',
|
|
);
|
|
const { default: Ticket } = await server.ssrLoadModule(
|
|
'/resources/js/pages/projects/Ticket.vue',
|
|
);
|
|
const { default: TicketCreate } = await server.ssrLoadModule(
|
|
'/resources/js/pages/projects/TicketCreate.vue',
|
|
);
|
|
const render = async (query = '', creating = false) => {
|
|
const result = await createInertiaApp({
|
|
page: makePage(
|
|
`/projects/7/tickets/${creating ? 'create' : '42'}${query}`,
|
|
),
|
|
resolve: () => (creating ? TicketCreate : Ticket),
|
|
render: renderToString,
|
|
setup: ({ App, props, plugin }) =>
|
|
createSSRApp({ render: () => h(App, props) })
|
|
.use(plugin)
|
|
.use(createLocalization())
|
|
.use(createAppearance('light')),
|
|
});
|
|
return result.body.split('id="main-content"')[1];
|
|
};
|
|
const html = await render();
|
|
assert.ok(!html.includes('id="link_direction"'));
|
|
assert.ok(html.includes('Blocked by'));
|
|
const linkFields = [
|
|
...html.matchAll(/<fieldset\b[^>]*>[\s\S]*?<\/fieldset>/g),
|
|
].find(([fieldset]) =>
|
|
fieldset.includes('id="ticket_link_type_id"'),
|
|
)?.[0];
|
|
assert.ok(linkFields);
|
|
assert.equal((linkFields.match(/<select\b/g) ?? []).length, 2);
|
|
assert.match(
|
|
linkFields,
|
|
/<option value="1:source"[^>]*>Blocks<\/option>/,
|
|
);
|
|
assert.match(
|
|
linkFields,
|
|
/<option value="1:counterpart"[^>]*>Blocked by<\/option>/,
|
|
);
|
|
assert.doesNotMatch(linkFields, /Blocks \/ Blocked by/);
|
|
for (const text of [
|
|
ticket.title,
|
|
'Keep every existing ticket action available.',
|
|
'Cache requests safely',
|
|
'Invalidate when records change',
|
|
'Urgent project priority',
|
|
'Delivery / In progress',
|
|
'Alex Morgan',
|
|
'Sam Chen',
|
|
'Impact score',
|
|
'Reviewer',
|
|
'Waiting for infrastructure approval.',
|
|
'feature/ENG-42-cache',
|
|
'Cache implementation',
|
|
'Ticket created in the project backlog.',
|
|
'Ticket created',
|
|
'Status changed',
|
|
'System',
|
|
]) {
|
|
assert.ok(
|
|
html.includes(text),
|
|
`Missing ticket information: ${text}`,
|
|
);
|
|
}
|
|
for (const section of [
|
|
'description',
|
|
'checklist',
|
|
'metadata',
|
|
'blocker',
|
|
'relationships',
|
|
'history',
|
|
'git',
|
|
]) {
|
|
assert.ok(html.includes(`href="#ticket-${section}"`));
|
|
assert.ok(html.includes(`id="ticket-${section}"`));
|
|
}
|
|
for (const route of [
|
|
'/projects/7',
|
|
'/projects/7/backlog',
|
|
'/projects/7/board',
|
|
'/projects/7/tickets/40',
|
|
'/projects/7/tickets/41',
|
|
]) {
|
|
assert.ok(html.includes(`href="${route}"`));
|
|
}
|
|
assert.match(html, /<details[^>]+id="ticket-edit"/);
|
|
for (const field of [
|
|
'title',
|
|
'ticket_type_id',
|
|
'ticket_priority_id',
|
|
'assignee_user_id',
|
|
'author_user_id',
|
|
'story_points',
|
|
'parent_ticket_id',
|
|
'description',
|
|
'is_blocked',
|
|
'blocked_comment',
|
|
'field-1',
|
|
'field-2',
|
|
]) {
|
|
assert.ok(
|
|
html.includes(`id="${field}"`),
|
|
`Missing editor: ${field}`,
|
|
);
|
|
}
|
|
assert.match(html, /aria-label="Criterion 1"/);
|
|
assert.match(html, /Add criterion/);
|
|
assert.match(html, /Create branch/);
|
|
assert.match(
|
|
html,
|
|
/<a(?=[^>]*href="https:\/\/git\.example\.test\/Engineering\/platform")(?=[^>]*target="_blank")(?=[^>]*rel="noopener noreferrer")[^>]*>/,
|
|
);
|
|
assert.match(
|
|
html,
|
|
/<a(?=[^>]*href="https:\/\/git\.example\.test\/Engineering\/platform\/pulls\/12")(?=[^>]*target="_blank")(?=[^>]*rel="noopener noreferrer")[^>]*>/,
|
|
);
|
|
assert.match(html, /Save link/);
|
|
assert.doesNotMatch(html, /Outgoing/);
|
|
const incoming = await render('?incoming');
|
|
assert.doesNotMatch(incoming, /Incoming/);
|
|
assert.match(incoming, /Blocked by/);
|
|
assert.match(await render('?noTypes'), /No link types are configured/);
|
|
assert.match(
|
|
await render('?noTargets'),
|
|
/No other tickets are available/,
|
|
);
|
|
assert.match(html, /Remove/);
|
|
const readOnly = await render('?readonly');
|
|
assert.doesNotMatch(
|
|
readOnly,
|
|
/<form|<details|Create branch|Save changes|Save link/,
|
|
);
|
|
assert.match(readOnly, /type="checkbox" disabled/);
|
|
assert.match(readOnly, /Read only/);
|
|
const hidden = await render('?readonly&hideAuthor&hidePoints');
|
|
assert.doesNotMatch(hidden, /Sam Chen|Estimated story points/);
|
|
assert.match(await render('?readonly&hideAuthor&admin'), /Sam Chen/);
|
|
const empty = await render('?empty');
|
|
for (const text of [
|
|
'No description has been added yet.',
|
|
'No acceptance criteria yet.',
|
|
'No status',
|
|
'Unassigned',
|
|
'No author',
|
|
'Not set',
|
|
'No linked repositories',
|
|
'No worklog entries exist',
|
|
]) {
|
|
assert.ok(empty.includes(text), `Missing empty state: ${text}`);
|
|
}
|
|
const hierarchy = await render('?parent');
|
|
assert.match(hierarchy, /Child tickets/);
|
|
assert.match(hierarchy, /Platform delivery/);
|
|
assert.match(hierarchy, /Nested child/);
|
|
assert.match(hierarchy, /Change parent/);
|
|
const noAccess = await render('?noAccess');
|
|
assert.match(noAccess, /Personal Git access required/);
|
|
assert.match(noAccess, /Open Git connections/);
|
|
const gitError = await render('?gitError');
|
|
assert.match(gitError, /Branches unavailable/);
|
|
assert.match(gitError, /Pull requests unavailable/);
|
|
assert.match(gitError, /Repository temporarily unavailable/);
|
|
const create = await render('?source=board', true);
|
|
assert.match(create, /Create ticket/);
|
|
assert.match(create, /Delivery \/ In progress/);
|
|
assert.match(create, /Back to board/);
|
|
assert.match(create, /id="ticket_status_id"/);
|
|
assert.doesNotMatch(
|
|
create,
|
|
/id="author_user_id"|id="blocked_comment"|id="field-1"/,
|
|
);
|
|
assert.doesNotMatch(
|
|
await render('?hidePoints', true),
|
|
/id="story_points"/,
|
|
);
|
|
const unavailable = await render('?noMetadata', true);
|
|
assert.doesNotMatch(unavailable, /<form/);
|
|
assert.match(unavailable, /Ticket creation needs at least one type/);
|
|
for (const creating of [false, true]) {
|
|
const invalid = await render('?rejected&allErrors', creating);
|
|
assert.match(
|
|
invalid,
|
|
/aria-describedby="[^"]+-hint [^"]+-error"|No acceptance criteria yet/,
|
|
);
|
|
for (const id of [
|
|
'title',
|
|
'description',
|
|
'ticket_type_id',
|
|
'ticket_priority_id',
|
|
'assignee_user_id',
|
|
'parent_ticket_id',
|
|
'story_points',
|
|
]) {
|
|
assert.ok(invalid.includes(`id="${id}-error"`));
|
|
}
|
|
}
|
|
console.log('Ticket detail behavior checks passed.');
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|