import { addProjectConfiguration, readProjectConfiguration, Tree, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import generator from './vue-admin-crud'; import { Schema } from './schema'; // Mirrors the shape vue-app's own template generates -- vue-admin-crud 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 Admin CRUD Generator', () => { let host: Tree; const baseOptions: Schema = { project: 'test', name: 'regions', resource: 'regions', route: '/regions', fields: [ { key: 'name', label: 'Name' }, { key: 'active', label: 'Active', type: 'checkbox' }, ], }; 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/); }); it('generates the list view with a Create action and per-row Edit, no pagination props', async () => { await generator(host, baseOptions); const view = host .read('apps/test/src/views/RegionsListView.vue') .toString(); expect(view).toContain('

Regions

'); expect(view).toContain("apiFetch('/api/regions')"); expect(view).toContain("import { WorkspaceTable } from '@proj/vue-components';"); expect(view).toContain(' { await generator(host, baseOptions); const view = host .read('apps/test/src/views/RegionsEditView.vue') .toString(); expect(view).toContain("idParam.value === 'new' || idParam.value === ''"); expect(view).toContain("import { GoabInput, GoabCheckbox } from '@proj/vue-components';"); expect(view).toContain("name.trim()"); expect(view).toContain("errors.name = 'Name is required.';"); // Checkbox has no required-validation block. expect(view).not.toContain('errors.active'); expect(view).toContain("method: isNew.value ? 'POST' : 'PUT'"); expect(view).toContain("isNew.value ? '/api/regions'"); expect(view).toContain('apiFetch(`/api/regions/${idParam.value}`'); expect(view).toContain("router.push('/regions')"); expect(view).toContain('Create Regions'); expect(view).toContain('Edit Regions'); }, 30000); it('uses --singularLabel over --heading for Create/Edit headings when both are set', async () => { await generator(host, { ...baseOptions, heading: 'Regions', singularLabel: 'Region', }); const list = host.read('apps/test/src/views/RegionsListView.vue').toString(); const edit = host.read('apps/test/src/views/RegionsEditView.vue').toString(); expect(list).toContain('

Regions

'); expect(list).toContain('Create Region'); expect(edit).toContain('Create Region'); expect(edit).toContain('Edit Region'); }, 30000); it('marks a field as not required with --fields[].required=false', async () => { await generator(host, { ...baseOptions, fields: [{ key: 'name', label: 'Name', required: false }], }); const edit = host.read('apps/test/src/views/RegionsEditView.vue').toString(); expect(edit).not.toContain("errors.name = 'Name is required.'"); expect(edit).not.toContain('requirement="required"'); }, 30000); it('accepts --fields as a JSON string, the form the real CLI produces', async () => { await generator(host, { ...baseOptions, fields: JSON.stringify(baseOptions.fields), }); const view = host .read('apps/test/src/views/RegionsListView.vue') .toString(); expect(view).toContain("{ key: 'name', label: 'Name' }"); }, 30000); it('throws a clear error when --fields is not valid JSON', async () => { await expect( generator(host, { ...baseOptions, fields: '{not json' }), ).rejects.toThrow(); }); it('throws a clear error when --fields parses to an empty array', async () => { await expect( generator(host, { ...baseOptions, fields: '[]' }), ).rejects.toThrow(/non-empty/); }); it('inserts both the list and edit routes, requiring auth by default', async () => { await generator(host, baseOptions); const routerTs = host.read('apps/test/src/router/index.ts').toString(); expect(routerTs).toContain("path: '/regions'"); expect(routerTs).toContain("path: '/regions/:id'"); expect(routerTs).toContain( "component: () => import('../views/RegionsListView.vue')", ); expect(routerTs).toContain( "component: () => import('../views/RegionsEditView.vue')", ); expect( routerTs.split('meta: { requiresAuth: true }').length - 1, ).toBe(2); // The existing route is untouched, not replaced. 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); });