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', ), ]; if (params.has('multiple')) { tickets[0].ticket_status.allowed_target_statuses.push(statuses[3]); } 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' || request.method === 'PATCH') { let body = ''; for await (const chunk of request) { body += chunk; } const data = JSON.parse(body); assert.equal( request.url, request.method === 'POST' ? '/projects/7/tickets/42/status?_method=PATCH' : '/projects/7/tickets/42/status', ); assert.deepEqual(Object.keys(data).sort(), [ 'redirect_to', 'ticket_status_id', ]); assert.equal(data.redirect_to, 'board'); assert.ok([11, 20, 21, 99].includes(Number(data.ticket_status_id))); const previous = new URL(request.headers.referer); const reject = previous.searchParams.has('reject'); if (previous.searchParams.has('slow')) { await new Promise((resolve) => setTimeout(resolve, 500)); } 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, '
', ), ); }); 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(/