276 lines
9.7 KiB
JavaScript
276 lines
9.7 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 } from 'vue';
|
|
import { renderToString } from 'vue/server-renderer';
|
|
|
|
const serve = process.argv.includes('--serve');
|
|
const server = await createServer({
|
|
configFile: false,
|
|
cacheDir: 'node_modules/.vite/board-fixture',
|
|
plugins: [vue(), ...(serve ? [tailwind()] : [])],
|
|
resolve: {
|
|
alias: {
|
|
'@': fileURLToPath(new URL('../../resources/js', import.meta.url)),
|
|
},
|
|
},
|
|
server: serve
|
|
? { host: '127.0.0.1', port: 4181, strictPort: true, hmr: false }
|
|
: { middlewareMode: true, hmr: false, watch: null },
|
|
appType: 'custom',
|
|
});
|
|
const project = {
|
|
id: 7,
|
|
key: 'ENG',
|
|
name: 'Engineering · Platform and developer experience',
|
|
};
|
|
const columnNames = [
|
|
'Ready',
|
|
'In development',
|
|
'Review',
|
|
'Testing',
|
|
'Release',
|
|
'Done',
|
|
'Archived',
|
|
'Column without statuses',
|
|
];
|
|
const columns = columnNames.map((name, index) => ({
|
|
id: index + 1,
|
|
name,
|
|
position: index + 1,
|
|
project_id: 7,
|
|
tickets: [],
|
|
ticket_statuses: [],
|
|
}));
|
|
const statuses = columns.slice(0, -1).flatMap((column) =>
|
|
['Planned', 'Active'].map((name, index) => ({
|
|
id: column.id * 10 + index,
|
|
name: `${column.name} · ${name}`,
|
|
position: index + 1,
|
|
project_id: 7,
|
|
board_column_id: column.id,
|
|
board_column: { ...column },
|
|
})),
|
|
);
|
|
const backlog = {
|
|
id: 99,
|
|
name: 'Backlog',
|
|
board_column_id: null,
|
|
board_column: null,
|
|
};
|
|
for (const column of columns) {
|
|
column.ticket_statuses = statuses.filter(
|
|
(status) => status.board_column_id === column.id,
|
|
);
|
|
}
|
|
function ticket(id, status, title) {
|
|
return {
|
|
id,
|
|
title,
|
|
ticket_key: `ENG-${id}`,
|
|
project_id: 7,
|
|
sequence_number: id,
|
|
description:
|
|
'A real ticket card in the isolated board behavior fixture.',
|
|
ticket_status_id: status.id,
|
|
ticket_status: {
|
|
...status,
|
|
allowed_target_statuses:
|
|
id === 42 ? [status, statuses[2], statuses[1], backlog] : [],
|
|
},
|
|
ticket_priority: { id: 1, name: 'Urgent custom priority', position: 1 },
|
|
ticket_type: { id: 1, name: 'Development', is_parent: false },
|
|
assignee: null,
|
|
assignee_user_id: null,
|
|
is_blocked: id === 43,
|
|
blocked_comment: id === 43 ? 'Awaiting the external review.' : null,
|
|
};
|
|
}
|
|
function makePage(url) {
|
|
const parsed = new URL(url, 'http://localhost');
|
|
const params = parsed.searchParams;
|
|
const isBoard = parsed.pathname === '/projects/7/board';
|
|
const boardColumns = structuredClone(
|
|
params.has('empty')
|
|
? []
|
|
: params.has('one')
|
|
? columns.slice(0, 1)
|
|
: columns,
|
|
);
|
|
if (!params.has('noTickets')) {
|
|
const target =
|
|
[...statuses, backlog].find(
|
|
(status) => status.id === Number(params.get('target')),
|
|
) ?? statuses[0];
|
|
const tickets = [
|
|
ticket(42, target, 'Ship accessible board navigation'),
|
|
ticket(
|
|
43,
|
|
statuses[1],
|
|
'InvestigateALongUnbrokenTicketTitleThatMustWrapEvenOnTheSmallestMobileScreenWithoutClipping',
|
|
),
|
|
];
|
|
for (const column of boardColumns) {
|
|
column.tickets = tickets.filter(
|
|
(item) => item.ticket_status.board_column_id === column.id,
|
|
);
|
|
}
|
|
}
|
|
return {
|
|
component: isBoard ? 'Board' : 'Destination',
|
|
url,
|
|
version: null,
|
|
clearHistory: false,
|
|
encryptHistory: false,
|
|
props: {
|
|
name: 'KanbanGit',
|
|
errors: params.has('rejected')
|
|
? {
|
|
'board-ticket-42': {
|
|
ticket_status_id:
|
|
'This transition is no longer allowed.',
|
|
},
|
|
}
|
|
: {},
|
|
sidebarOpen: true,
|
|
auth: {
|
|
user: {
|
|
id: 1,
|
|
name: 'Alex Morgan',
|
|
email: 'alex@example.test',
|
|
avatar: null,
|
|
},
|
|
permissions: {
|
|
globalAdmin: false,
|
|
projectCreate: false,
|
|
canManageTeams: false,
|
|
},
|
|
},
|
|
navigation: {
|
|
project: {
|
|
...project,
|
|
canManage: false,
|
|
canCreateTickets: !params.has('readonly'),
|
|
},
|
|
},
|
|
project,
|
|
boardColumns,
|
|
canCreateTickets: !params.has('readonly'),
|
|
canEditTickets: !params.has('readonly'),
|
|
status: params.has('target') ? 'ENG-42 moved successfully.' : null,
|
|
},
|
|
};
|
|
}
|
|
if (serve) {
|
|
server.middlewares.use(async (request, response) => {
|
|
if (request.method === 'POST') {
|
|
let body = '';
|
|
for await (const chunk of request) {
|
|
body += chunk;
|
|
}
|
|
const data = JSON.parse(body);
|
|
assert.equal(
|
|
request.url,
|
|
'/projects/7/tickets/42/status?_method=PATCH',
|
|
);
|
|
assert.equal(data.redirect_to, 'board');
|
|
assert.ok([11, 20, 99].includes(Number(data.ticket_status_id)));
|
|
const previous = new URL(request.headers.referer);
|
|
const reject = previous.searchParams.has('reject');
|
|
response.writeHead(303, {
|
|
Location: `/projects/7/board?${reject ? 'reject&rejected' : `target=${data.ticket_status_id}`}`,
|
|
});
|
|
response.end();
|
|
return;
|
|
}
|
|
const page = makePage(request.url);
|
|
response.setHeader('Vary', 'X-Inertia');
|
|
if (request.headers['x-inertia']) {
|
|
response.setHeader('X-Inertia', 'true');
|
|
response.setHeader('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/board-preview.mjs"></script></body></html>',
|
|
),
|
|
);
|
|
});
|
|
await server.listen();
|
|
console.log(
|
|
'Board fixture ready at http://127.0.0.1:4181/projects/7/board',
|
|
);
|
|
} else {
|
|
try {
|
|
const { createLocalization } = await server.ssrLoadModule(
|
|
'/resources/js/lib/localization.ts',
|
|
);
|
|
const { createAppearance } = await server.ssrLoadModule(
|
|
'/resources/js/composables/useAppearance.ts',
|
|
);
|
|
const { default: Board } = await server.ssrLoadModule(
|
|
'/resources/js/pages/projects/Board.vue',
|
|
);
|
|
const render = async (query = '') => {
|
|
const result = await createInertiaApp({
|
|
page: makePage(`/projects/7/board${query}`),
|
|
resolve: () => Board,
|
|
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 board = await render();
|
|
assert.equal((board.match(/id="board-column-title-/g) ?? []).length, 8);
|
|
assert.equal((board.match(/id="board-status-/g) ?? []).length, 14);
|
|
assert.match(board, /Ready · Planned/);
|
|
assert.match(board, /Ready · Active/);
|
|
assert.match(board, /No statuses configured for this column/);
|
|
assert.match(board, /No tickets in this column/);
|
|
assert.match(board, /No tickets in this status/);
|
|
assert.match(board, /2 tickets/);
|
|
assert.match(board, /Urgent custom priority/);
|
|
assert.match(board, /href="\/projects\/7\/tickets\/42"/);
|
|
assert.match(
|
|
board,
|
|
/action="\/projects\/7\/tickets\/42\/status\?_method=PATCH"/,
|
|
);
|
|
assert.match(
|
|
board,
|
|
/href="\/projects\/7\/tickets\/create\?source=board"/,
|
|
);
|
|
assert.match(board, /href="\/projects\/7\/backlog"/);
|
|
assert.match(board, /No allowed moves/);
|
|
assert.equal((board.match(/<article/g) ?? []).length, 2);
|
|
const readonly = await render('?readonly');
|
|
assert.doesNotMatch(readonly, /<form|Move ENG-|New ticket/);
|
|
const empty = await render('?empty');
|
|
assert.match(empty, /No workflow columns yet/);
|
|
assert.doesNotMatch(empty, /id="board-column"|<article/);
|
|
const one = await render('?one&noTickets');
|
|
assert.equal((one.match(/id="board-column-title-/g) ?? []).length, 1);
|
|
assert.match(one, /0 tickets/);
|
|
assert.doesNotMatch(one, /<article/);
|
|
console.log('Board behavior checks passed.');
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|