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-templates-fixture', plugins: [vue(), ...(serve ? [tailwind()] : [])], resolve: { alias: { '@': fileURLToPath(new URL('../../resources/js', import.meta.url)), }, }, server: serve ? { host: '127.0.0.1', port: 4191, strictPort: true, hmr: false } : { middlewareMode: true, hmr: false, watch: null }, appType: 'custom', }); const project = { id: 1, name: 'Core platform', key: 'CORE' }; const areas = [ 'project_settings', 'board_columns', 'ticket_statuses', 'status_transitions', 'ticket_types', 'ticket_priorities', 'ticket_fields', 'ticket_link_types', 'repository_connections', 'notification_actions', ]; const template = { id: 2, name: 'Foundation', slug: 'foundation-template', is_published: true, source_project: project, copy_areas: ['project_settings', 'ticket_types'], linked_project_count: 2, }; const initial = () => ({ project, copyAreaOptions: areas.map((value) => ({ value, label: value, description: value, })), template: structuredClone(template), templates: [ structuredClone(template), { ...template, id: 3, name: 'Internal baseline', is_published: false }, ], }); function makePage(component = 'Index', extra = {}, url = '/project-templates') { 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: true, canManageTeams: false, }, }, navigation: { project: null }, ...initial(), ...extra, }, }; } if (serve) { let data = initial(); server.middlewares.use(async (request, response, next) => { if (!request.url.startsWith('/project')) { return next(); } const url = new URL(request.url, 'http://localhost'); let component = url.pathname.endsWith('/edit') ? 'Edit' : url.pathname.includes('/templates/create') ? 'Create' : url.pathname === '/project-templates' ? 'Index' : 'Destination'; if (url.searchParams.has('reset')) { data = initial(); } if (url.searchParams.has('unlinked')) { data.template.linked_project_count = 0; } if (url.searchParams.has('missing')) { data.template.source_project = null; } if (url.searchParams.has('empty')) { data.templates = []; } let errors = {}; if (request.method !== 'GET') { let body = ''; for await (const chunk of request) { body += chunk; } const submitted = JSON.parse(body || '{}'); const result = request.headers.cookie?.match( /template_test_result=(\w+)/, )?.[1] ?? 'success'; if (result === 'server') { response.statusCode = 500; response.end('Unavailable'); return; } if (result === 'validation') { component = request.method === 'POST' ? 'Create' : request.method === 'PATCH' ? 'Edit' : 'Index'; errors = { name: 'This template name is unavailable.', 'copy_areas.0': 'Choose a valid configuration group.', }; } else { if (request.method === 'DELETE') { data.templates = data.templates.filter( (item) => item.id !== 2, ); } else { data.templates = [{ ...data.template, ...submitted }]; } component = 'Index'; } } const page = makePage( component, { ...data, errors, status: request.method !== 'GET' && !Object.keys(errors).length ? 'Saved' : null, }, component === 'Index' ? '/project-templates' : 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('Project template 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 render = async ( name, extra = {}, locale = createLocalization(), ) => { const component = ( await server.ssrLoadModule( '/resources/js/pages/project-templates/' + name + '.vue', ) ).default; return ( await createInertiaApp({ page: makePage(name, extra), resolve: () => component, render: renderToString, setup: ({ App, props, plugin }) => createSSRApp({ render: () => h(App, props) }) .use(plugin) .use(locale) .use(createAppearance()), }) ).body.replace(/ data-page="[^"]*"/, ''); }; const list = await render('Index'); assert.match(list, /Foundation/); assert.doesNotMatch(list, /Internal baseline/); assert.match(list, /2 linked projects/); assert.match(list, /Project settings/); assert.match(list, /href="\/projects\/create\?template=2"/); assert.match( await render('Index', { templates: [] }), /No published templates yet/, ); const create = await render('Create'); assert.match(create, /value="Core platform template"/); assert.equal((create.match(/type="checkbox"/g) ?? []).length, 10); assert.doesNotMatch( create.match(/]*id="slug"[^>]*>/)?.[0] ?? '', / disabled(?:[ =>])/, ); const edit = await render('Edit'); assert.match( edit.match(/]*id="slug"[^>]*>/)?.[0] ?? '', / disabled(?:[ =>])/, ); assert.match(edit, /Linked projects will be affected/); assert.match( edit.match(/