import assert from 'node:assert/strict'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; import vue from '@vitejs/plugin-vue'; import tailwind from '@tailwindcss/vite'; import { createServer } from 'vite'; import { createSSRApp, h } from 'vue'; import { renderToString } from 'vue/server-renderer'; import { createInertiaApp } from '@inertiajs/vue3'; const serve = process.argv.includes('--serve'); const server = await createServer({ configFile: false, cacheDir: 'node_modules/.vite/project-metadata-settings-fixture', plugins: [vue(), ...(serve ? [tailwind()] : [])], resolve: { alias: { '@': fileURLToPath(new URL('../../resources/js', import.meta.url)), }, }, server: serve ? { host: '127.0.0.1', port: 4189, strictPort: true, hmr: false } : { middlewareMode: true, hmr: false, watch: null }, appType: 'custom', }); function initial() { return { project: { id: 1, key: 'CORE', name: 'Core platform', show_author: true, show_story_points: false, }, status: null, error: false, templateState: null, ticketTypes: [ { id: 1, name: 'Task', position: 1, is_parent: false, tickets_count: 2, }, { id: 2, name: 'Release', position: 2, is_parent: true, tickets_count: 1, }, ], ticketPriorities: [ { id: 3, name: 'Normal', position: 1 }, { id: 4, name: 'Urgent', position: 2 }, ], ticketFieldDefinitions: [ { id: 5, label: 'Reviewer', field_type: 'user', position: 1, ticket_field_values_count: 2, }, { id: 6, label: 'Complexity', field_type: 'number', position: 2, ticket_field_values_count: 0, }, ], ticketLinkTypes: [ { id: 7, label: 'Blocks', counterpart_label: 'Blocked by', position: 1, ticket_links_count: 1, }, ], fieldTypeOptions: [ { label: 'User', value: 'user' }, { label: 'Number', value: 'number' }, ], }; } function makePage( extra = {}, url = '/projects/1/settings/metadata', component = 'Metadata', ) { return { component, url, version: null, clearHistory: false, encryptHistory: false, props: { name: 'KanbanGit', errors: {}, sidebarOpen: true, appearance: 'system', auth: { user: { id: 1, name: 'Alex Morgan', email: 'alex@example.test', avatar: null, avatar_color: '#10B981', avatar_is_generated: true, }, permissions: { globalAdmin: false, projectCreate: false, canManageTeams: false, }, }, navigation: { project: { id: 1, name: 'Core platform', key: 'CORE', canManage: true, }, }, ...initial(), ...extra, }, }; } if (serve) { let data = initial(); let nextId = 100; server.middlewares.use(async (request, response, next) => { if (!request.url.startsWith('/projects')) { return next(); } const url = new URL(request.url, 'http://localhost'); const mutation = request.method !== 'GET'; if (!mutation && url.searchParams.has('reset')) { data = initial(); } let errors = {}; if (mutation) { let body = ''; for await (const chunk of request) { body += chunk; } const submitted = JSON.parse(body || '{}'); const result = request.headers.cookie?.match( /metadata_test_result=(\w+)/, )?.[1] ?? 'success'; if (result === 'server') { response.statusCode = 500; response.end('Unavailable'); return; } const kind = url.pathname.includes('/link-types') ? 'link' : url.pathname.includes('/fields') ? 'field' : url.pathname.includes('/types') ? 'type' : 'priority'; const entries = kind === 'type' ? data.ticketTypes : kind === 'priority' ? data.ticketPriorities : kind === 'field' ? data.ticketFieldDefinitions : data.ticketLinkTypes; const id = Number(url.pathname.split('/').at(-1)); if ( result === 'validation' || ((submitted.name || submitted.label) && entries.some( (entry) => (entry.name ?? entry.label) === (submitted.name ?? submitted.label) && entry.id !== id, )) ) { errors = request.method === 'DELETE' ? { replacement_ticket_type_id: 'Choose a parent-enabled replacement type.', } : kind === 'field' ? { label: 'This field label is already in use.' } : kind === 'link' ? { counterpart_name: 'Enter a distinct counterpart label.', } : { name: 'This name is already in use.' }; } else if (request.method === 'DELETE') { entries.splice( entries.findIndex((item) => item.id === id), 1, ); } else if (url.pathname.endsWith('/metadata')) { Object.assign(data.project, submitted); } else if (request.method === 'POST') { const created = { id: nextId++, position: entries.length + 1, ...(kind === 'type' ? { tickets_count: 0, ...submitted } : kind === 'priority' ? submitted : kind === 'field' ? { label: submitted.label, field_type: submitted.field_type, ticket_field_values_count: 0, } : { label: submitted.name, counterpart_label: submitted.counterpart_name, ticket_links_count: 0, }), }; entries.push(created); } else { const item = entries.find((item) => item.id === id); entries.splice(entries.indexOf(item), 1); Object.assign( item, kind === 'field' ? { label: submitted.label, field_type: submitted.field_type, } : kind === 'link' ? { label: submitted.name, counterpart_label: submitted.counterpart_name, } : submitted, ); entries.splice(submitted.position - 1, 0, item); } entries.forEach((entry, index) => (entry.position = index + 1)); data.status = Object.keys(errors).length ? null : 'Updated'; } if (url.searchParams.has('single')) { data.ticketTypes = data.ticketTypes.slice(0, 1); data.ticketPriorities = data.ticketPriorities.slice(0, 1); } const page = makePage({ ...data, errors }); 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('Metadata settings fixture ready.'); } else { try { const { createLocalization } = await server.ssrLoadModule( '/resources/js/lib/localization.ts', ); const { createAppearance } = await server.ssrLoadModule( '/resources/js/composables/useAppearance.ts', ); const { en } = await server.ssrLoadModule( '/resources/js/locales/en.ts', ); const component = ( await server.ssrLoadModule( '/resources/js/pages/projects/settings/Metadata.vue', ) ).default; const render = async (extra = {}, locale = createLocalization()) => ( await createInertiaApp({ page: makePage(extra), resolve: () => component, render: renderToString, setup: ({ App, props, plugin }) => createSSRApp({ render: () => h(App, props) }) .use(plugin) .use(locale) .use(createAppearance()), }) ).body; const html = await render(); assert.match(html, /Ticket settings/); assert.match(html, /aria-current="page"/); assert.ok( html.indexOf('data-metadata-entry="type-1"') < html.indexOf('data-metadata-entry="type-2"'), ); assert.match(html, /Assigned tickets: 2/); assert.match(html, /Parent type/); assert.match(html, /data-ticket-field-definition="5"/); assert.match(html, /Reviewer/); assert.match(html, /Stored values: 2/); assert.match(html, /data-ticket-link-type="7"/); assert.match(html, /Blocks/); assert.match(html, /Blocked by/); assert.match(html, /Ticket relationships: 1/); assert.match(html, /id="show_author"[^>]*aria-checked="true"/); assert.match(html, /id="show_story_points"[^>]*aria-checked="false"/); const single = await render({ ticketTypes: initial().ticketTypes.slice(0, 1), ticketPriorities: initial().ticketPriorities.slice(0, 1), }); const buttons = single.match(/