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-general-settings-fixture', plugins: [vue(), ...(serve ? [tailwind()] : [])], resolve: { alias: { '@': fileURLToPath(new URL('../../resources/js', import.meta.url)), }, }, server: serve ? { host: '127.0.0.1', port: 4187, strictPort: true, hmr: false } : { middlewareMode: true, hmr: false, watch: null }, appType: 'custom', }); const area = (value, label) => ({ value, label }); const template = { id: 1, name: 'Team baseline', is_published: true, copy_areas: [], }; const alternative = { ...template, id: 2, name: 'Delivery baseline' }; function initial() { return { project: { id: 1, key: 'CORE', name: 'Core platform', project_template_id: 1, project_template: template, show_author: false, show_story_points: false, }, sourceTemplates: [template, alternative], status: null, templateState: { templateId: 1, templateName: 'Team baseline', inheritsFromTemplate: true, inheritedAreas: [area('board_columns', 'Board columns')], overriddenAreas: [ area('project_settings', 'Project settings'), area('ticket_statuses', 'Ticket statuses'), ], effectiveConfiguration: {}, resetRequirements: { ticket_statuses: { removedStatuses: [ { id: 10, name: 'Local review', ticketCount: 3 }, ], replacementOptions: [{ id: 1, name: 'Ready' }], }, }, }, }; } function makePage( extra = {}, url = '/projects/1/settings/general', component = 'General', ) { 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(); 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'; const result = request.headers.cookie?.match(/general_test_result=(\w+)/)?.[1] ?? 'success'; if (!mutation && url.searchParams.has('reset')) { data = initial(); } let extra = {}; let component = 'General'; let pageUrl = '/projects/1/settings/general'; if (mutation) { if (['server', 'forbidden'].includes(result)) { response.statusCode = result === 'forbidden' ? 403 : 503; response.end('Simulated request failure'); return; } let body = ''; for await (const chunk of request) { body += chunk; } const submitted = JSON.parse(body || '{}'); if (result === 'validation') { extra.errors = url.pathname.endsWith('reset-template') ? { 'areas.0': 'This area could not be reset.' } : { confirmation_key: 'Enter the exact project key.' }; } else if (request.method === 'DELETE') { component = 'Destination'; pageUrl = '/projects'; } else if (url.pathname.endsWith('reset-template')) { const reset = data.templateState.overriddenAreas.find( (item) => item.value === submitted.areas[0], ); data.templateState.overriddenAreas = data.templateState.overriddenAreas.filter( (item) => item.value !== reset.value, ); data.templateState.inheritedAreas.push(reset); if (reset.value === 'project_settings') { data.project.show_author = true; data.project.show_story_points = true; } data.status = 'Project template baseline was reset.'; } else { data.project.project_template = alternative; data.project.project_template_id = 2; data.templateState.templateName = alternative.name; data.templateState.templateId = 2; data.status = 'Project template baseline was switched.'; } } if (url.searchParams.has('noTemplate')) { extra = { project: { ...data.project, project_template: null, project_template_id: null, }, templateState: null, }; } if (url.searchParams.has('baseline')) { extra.templateState = { ...data.templateState, inheritsFromTemplate: false, }; } if (url.searchParams.has('noAlternatives')) { extra.sourceTemplates = [template]; } const page = makePage({ ...data, ...extra }, pageUrl, component); 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 general 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/General.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, /Local override/); assert.match(html, /Inherited/); assert.match(html, /Team baseline/); assert.match(html, /Show author/); assert.match(html, /href="\/projects\/1\/settings\/metadata"/); assert.match(html, /aria-current="page"/); assert.match(html, /Reset Project settings to template/); assert.match(html, /Type CORE exactly/); const buttons = html.match(/]*>[\s\S]*?<\/button>/g); for (const label of ['Switch template', 'Delete project']) { const button = buttons.find((item) => item.includes(label)); assert.match(button, /disabled/); assert.match(button, /type="submit"/); } const standalone = await render({ templateState: null, project: { ...initial().project, project_template: null }, }); assert.match(standalone, /No linked template/); assert.doesNotMatch(standalone, /Reset Project settings to template/); assert.doesNotMatch( await render({ templateState: { ...initial().templateState, inheritsFromTemplate: false, }, }), /Local override/, ); assert.match( await render({ sourceTemplates: [template] }), /No other published templates/, ); const inherited = await render({ templateState: { ...initial().templateState, inheritedAreas: [area('project_settings', 'Project settings')], overriddenAreas: [], }, }); assert.doesNotMatch( inherited, /Local override|Reset Project settings to template/, ); assert.match( await render( {}, createLocalization( { locale: 'de' }, { de: { ...en, 'projectSettings.danger': 'Gefahrenbereich', 'inheritance.area.projectSettings': 'Projekteinstellungen', }, }, ), ), /Gefahrenbereich/, ); console.log('Project general settings rendering checks passed.'); } finally { await server.close(); } }