/** * CROSS-SUITE CONTRACT — every `@/components/ui/X` import a generated page * emits must be a file `scaffold-ui-primitives` actually writes (or an * explicitly allowlisted owner). Before this test the two suites were * independent: ui-primitives guarded its own emissions, generate.test.ts * guarded the page-side imports, and NOTHING related the two — a page could * import a primitive nobody emits and only `validate-page/imports-resolve` * (an err at scaffold time, deep in a client run) would catch it. */ import { describe, it, expect } from 'vitest' import { generate } from '../generate.js' import type { ScaffoldComponentInput, PageSpecMin } from '../types.js' import { generate as generatePrimitives } from '../../../../ui-primitives/cli/scaffold-ui-primitives/generate.js' import { generate as generateAuth } from '../../../../auth/cli/scaffold-frontend-auth/generate.js' /** Owners of @/components/ui/* files that are NOT scaffold-ui-primitives. */ const OTHER_OWNERS = new Set([ 'PageTemplate', // scaffold-layout ]) // A deliberately maximal spec: form (read-first, FK, every control family), // detail (tabs + sections), list (responsive columns + filters) — so the // import surface is as wide as the generator can make it. const spec: ScaffoldComponentInput = { module: 'hr', appCode: 'TestV2', entity: 'Employee', section: 'employees', views: ['list', 'detail', 'form'], fields: [ { name: 'firstName', type: 'string', required: true }, { name: 'bio', type: 'text', required: false }, { name: 'hireDate', type: 'date', required: false }, { name: 'isActive', type: 'bool', required: false }, { name: 'status', type: 'enum', required: true, options: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }] }, { name: 'skills', type: 'multiselect', required: false, options: [{ value: 's1', label: 'S1' }] }, { name: 'departmentId', type: 'guid', required: true, fkTo: { entity: 'Department', module: 'hr' } }, ], pageSpec: { screenCode: 'SCR-HR-EMPLOYEES', module: 'hr', appCode: 'testv2', section: 'employees', entity: 'Employee', view: 'form', filePath: 'src/pages/testv2/hr/employees/EmployeeFormPage.tsx', permission: 'hr.employees.update', actions: [], sections: [{ key: 'identity', fields: ['firstName', 'bio'] }], i18nKeys: { fr: {}, en: {}, it: {}, de: {} }, needsRefinement: false, specHash: 'x', } as unknown as PageSpecMin, projectPath: '/web', } describe('scaffold-component ⊆ scaffold-ui-primitives — import/emission contract', () => { it('every @/components/ui/X import of every generated page resolves to an emitted primitive', () => { const emitted = new Set( generatePrimitives({ projectPath: '/web', appCode: 'hr', force: false }) .map((f) => f.path) .filter((p) => p.startsWith('src/components/ui/')) .map((p) => p.replace(/^src\/components\/ui\//, '').replace(/\.(tsx|ts)$/, '')), ) const files = generate(spec) const importRe = /from '@\/components\/ui\/([A-Za-z0-9]+)'/g let checked = 0 for (const file of files.filter((f) => f.path.endsWith('.tsx'))) { for (const m of file.content.matchAll(importRe)) { const name = m[1]! if (OTHER_OWNERS.has(name)) continue checked++ expect( emitted.has(name), `${file.path} imports @/components/ui/${name} but scaffold-ui-primitives does not emit src/components/ui/${name}.tsx`, ).toBe(true) } } // The net must actually catch something — SectionCard, TabStrip, DataTable… expect(checked).toBeGreaterThan(5) }) }) describe('generated auth imports ⊆ scaffold-frontend-auth — the OTHER alias surface', () => { // The client-run TS2307 family: pages imported @/components/auth/PermissionGuard // and primitives imported @/business/auth/useAuth while the CLI that emits both // was wired into no pipeline phase. This net relates the suites the same way the // @/components/ui/* one above does. const AUTH_IMPORT_RE = /from '@\/((?:business\/auth|components\/auth)\/[A-Za-z0-9]+)'/g const authEmitted = new Set( generateAuth({ projectPath: '/proj', appCode: 'hr', force: false }) .map((f) => f.path.replace(/^web\/hr-web\/src\//, '').replace(/\.(tsx|ts)$/, '')), ) it('scaffold-frontend-auth emits the two canonical seam files — and nothing else', () => { expect(authEmitted).toContain('business/auth/useAuth') expect(authEmitted).toContain('components/auth/PermissionGuard') // REGRESSION: the platform-admin placeholder went away with the // aggregator's fallback block. A component registered on an `administration.*` key // WINS over the SDK's generic ModuleHomePage, so the placeholder blanked every // /administration/ landing page in every generated app. expect(authEmitted).not.toContain('components/AdminModuleFallback') expect(authEmitted.size).toBe(2) }) it('every auth-seam import of every generated page resolves to an emitted file', () => { const files = generate(spec) let checked = 0 for (const file of files.filter((f) => f.path.endsWith('.tsx'))) { for (const m of file.content.matchAll(AUTH_IMPORT_RE)) { checked++ expect( authEmitted.has(m[1]!), `${file.path} imports @/${m[1]} but scaffold-frontend-auth does not emit it`, ).toBe(true) } } expect(checked).toBeGreaterThan(0) // PermissionGuard at minimum }) it('every auth-seam import of every ui primitive resolves too (RowActionsMenu → useAuth)', () => { const primitives = generatePrimitives({ projectPath: '/web', appCode: 'hr', force: false }) let checked = 0 for (const file of primitives) { for (const m of file.content.matchAll(AUTH_IMPORT_RE)) { checked++ expect( authEmitted.has(m[1]!), `${file.path} imports @/${m[1]} but scaffold-frontend-auth does not emit it`, ).toBe(true) } } expect(checked).toBeGreaterThan(0) // RowActionsMenu's useAuth at minimum }) })