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: [], ticketLinkTypes: [], fieldTypeOptions: [], }; } 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('/types') ? 'type' : 'priority'; const entries = kind === 'type' ? data.ticketTypes : data.ticketPriorities; const id = Number(url.pathname.split('/').at(-1)); if ( result === 'validation' || (submitted.name && entries.some( (entry) => entry.name === submitted.name && entry.id !== id, )) ) { errors = request.method === 'DELETE' ? { replacement_ticket_type_id: 'Choose a parent-enabled replacement type.', } : { 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') { entries.push({ id: nextId++, position: entries.length + 1, tickets_count: 0, ...submitted, }); } else { const item = entries.find((item) => item.id === id); entries.splice(entries.indexOf(item), 1); Object.assign(item, 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, /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(/