import { addProjectConfiguration, readProjectConfiguration, Tree, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import generator from './vue-workspace-view'; import { Schema } from './schema'; // Mirrors the shape vue-app's own template generates -- vue-workspace-view // retrofits into this file, so the fixture must match what it actually looks for. const ROUTER_FIXTURE = `import { createRouter, createWebHistory } from 'vue-router'; import HomeView from '../views/HomeView.vue'; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', component: HomeView }, ], }); export default router; `; describe('Vue Workspace View Generator', () => { let host: Tree; const baseOptions: Schema = { project: 'test', name: 'applications', resource: 'applications', route: '/applications', columns: [ { key: 'status', label: 'Status', type: 'badge' }, { key: 'lastSaved', label: 'Last saved', type: 'date', sortable: true }, { key: 'requestTotal', label: 'Request total', type: 'currency' }, { key: 'serviceModel', label: 'Service Model' }, ], }; beforeEach(() => { host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); addProjectConfiguration(host, 'test', { root: 'apps/test' }); host.write('apps/test/src/router/index.ts', ROUTER_FIXTURE); }); it('throws when --project does not exist', async () => { await expect( generator(host, { ...baseOptions, project: 'no-such-app' }), ).rejects.toThrow(); }); it("throws a clear error when the project isn't a vue-app (no router/index.ts)", async () => { addProjectConfiguration(host, 'not-vue', { root: 'apps/not-vue' }); await expect( generator(host, { ...baseOptions, project: 'not-vue' }), ).rejects.toThrow(/router\/index\.ts/); }); // Regression guard: Nx's own CLI option coercion for `"type": "array"` splits // on commas, not JSON -- --columns is `"type": "string"` in schema.json // specifically so the real CLI's JSON string survives unmangled. Assert the // string form the CLI actually produces, not just the array form direct // (unit-test) callers use. it('accepts --columns as a JSON string, the form the real CLI produces', async () => { await generator(host, { ...baseOptions, columns: JSON.stringify(baseOptions.columns), }); const view = host .read('apps/test/src/views/ApplicationsListView.vue') .toString(); expect(view).toContain( "{ key: 'status', label: 'Status', sortable: false }", ); }, 30000); it('throws a clear error when --columns is not valid JSON', async () => { await expect( generator(host, { ...baseOptions, columns: '{not json' }), ).rejects.toThrow(); }); it('throws a clear error when --columns parses to an empty array', async () => { await expect( generator(host, { ...baseOptions, columns: '[]' }), ).rejects.toThrow(/non-empty/); }); it('generates the view with columns, sort, and per-type cell slots', async () => { await generator(host, baseOptions); const view = host .read('apps/test/src/views/ApplicationsListView.vue') .toString(); expect(view).toContain('

Applications

'); expect(view).toContain( "{ key: 'status', label: 'Status', sortable: false }", ); expect(view).toContain( "{ key: 'lastSaved', label: 'Last saved', sortable: true }", ); expect(view).toContain('apiFetch(`/api/applications?${params.toString()}`)'); expect(view).toContain( "", ); expect(view).toContain("formatDate(row['lastSaved'])"); expect(view).toContain("formatCurrency(row['requestTotal'])"); // Uses the shared table shell, not hand-rolled loading/pagination markup. expect(view).toContain("import { WorkspaceTable } from '@proj/vue-components';"); expect(view).toContain(' { await generator(host, { ...baseOptions, filterable: false }); const view = host .read('apps/test/src/views/ApplicationsListView.vue') .toString(); expect(view).not.toContain('type="search"'); expect(view).not.toContain('searchDebounce'); }, 30000); it('generates a View action linking to --detailRoute when set', async () => { await generator(host, { ...baseOptions, detailRoute: '/applications' }); const view = host .read('apps/test/src/views/ApplicationsListView.vue') .toString(); expect(view).toContain('#actions="{ row }"'); expect(view).toContain(':to="`/applications/${row.id}`"'); }, 30000); it('omits the actions slot entirely when --detailRoute is not set', async () => { await generator(host, baseOptions); const view = host .read('apps/test/src/views/ApplicationsListView.vue') .toString(); expect(view).not.toContain('#actions'); }, 30000); it('respects an explicit --heading and --pageSize', async () => { await generator(host, { ...baseOptions, heading: 'Grant Applications', pageSize: 50 }); const view = host .read('apps/test/src/views/ApplicationsListView.vue') .toString(); expect(view).toContain('

Grant Applications

'); expect(view).toContain('PAGE_SIZE = 50'); expect(view).toContain(':per-page-count="50"'); }, 30000); it('inserts the route into router/index.ts, requiring auth by default', async () => { await generator(host, baseOptions); const routerTs = host.read('apps/test/src/router/index.ts').toString(); expect(routerTs).toContain("path: '/applications'"); expect(routerTs).toContain( "component: () => import('../views/ApplicationsListView.vue')", ); expect(routerTs).toContain('meta: { requiresAuth: true }'); expect(routerTs).toContain("{ path: '/', component: HomeView }"); }, 30000); it('omits the requiresAuth meta when --requiresAuth=false', async () => { await generator(host, { ...baseOptions, requiresAuth: false }); const routerTs = host.read('apps/test/src/router/index.ts').toString(); expect(routerTs).not.toContain('requiresAuth'); }, 30000); it('ensures the shared WorkspaceTable pattern component exists', async () => { await generator(host, baseOptions); expect( host.exists('libs/vue-components/src/lib/patterns/WorkspaceTable.vue'), ).toBeTruthy(); }, 30000); it('does not touch the target project configuration', async () => { const before = readProjectConfiguration(host, 'test'); await generator(host, baseOptions); const after = readProjectConfiguration(host, 'test'); expect(after).toEqual(before); }, 30000); });