/** * hub-views.test.ts — Locks the contract for non-CRUD pagespec views (Wave 1 * of the URL-convention alignment, 2026-05-27). * * Hub views (app-home / module-home / section-home) are pure Slot containers * on the frontend — scaffold-component emits NO useService hook and NO * api.get/post call. The screen controller therefore emits NO endpoint and * NO TODO marker for them: the absence is by design, not technical debt. * * Kanban / card views read the same data set as `list` (different * visualisation, same projection) — they fall back to the [HttpGet("list")] * endpoint and push a TODO so audit-dev-api can track the pending * dedicated emitter. */ import { describe, it, expect } from 'vitest' import { generate } from '../generate.js' import type { PageSpec, ScaffoldScreenControllerSpec } from '../types.js' function spec(overrides: Partial = {}): ScaffoldScreenControllerSpec { return { section: 'accueil', entity: 'Demande', module: 'affaires', appCode: 'gaf', namespace: 'Gaf', moduleDir: '/ba/affaires', projectPath: '/api', ...overrides, } as ScaffoldScreenControllerSpec } function pagespec(view: string, overrides: Partial = {}): PageSpec { return { screenCode: `SCR-AFFAIRES-DEMANDES-${view.toUpperCase()}`, appCode: 'gaf', module: 'affaires', section: 'accueil', entity: 'Demande', view: view as PageSpec['view'], permission: 'affaires.demandes.read', linkedUseCases: [], linkedBusinessRules: [], columns: [], actions: [], ...overrides, } as PageSpec } function controllerOf(files: { path: string; content: string }[]): { path: string; content: string } | undefined { return files.find((f) => f.path.endsWith('ScreenController.cs')) } describe('scaffold-screen-controller — hub views (no endpoint, no debt)', () => { for (const view of ['app-home', 'module-home', 'section-home'] as const) { it(`view "${view}" emits NO method, NO DTO, NO TODO`, () => { const { files, todos } = generate(spec(), [pagespec(view)]) // The controller scaffolding still emits the class shell (so other // pagespecs of the same entity can attach to it), but the body must // not carry the view's CRUD endpoints. const ctrl = controllerOf(files) expect(ctrl).toBeDefined() expect(ctrl!.content).not.toMatch(/\[HttpGet\("list"\)\]/) expect(ctrl!.content).not.toMatch(/\[HttpGet\("detail/) expect(ctrl!.content).not.toMatch(/\[HttpPost\("form"\)\]/) expect(ctrl!.content).not.toMatch(/\[HttpGet\("dashboard\/consolidated"\)\]/) // No DTO file emitted for hub views. expect(files.find((f) => f.path.includes('ListScreenDto'))).toBeUndefined() expect(files.find((f) => f.path.includes('DetailScreenDto'))).toBeUndefined() // No TODO — the absence of an endpoint is intentional, not debt. expect(todos).toHaveLength(0) }) } it('hub view + sibling list view → ONE controller with the list endpoint only', () => { const { files } = generate(spec(), [ pagespec('section-home'), pagespec('list', { screenCode: 'SCR-AFFAIRES-DEMANDES-LIST' }), ]) const ctrl = controllerOf(files)! expect(ctrl.content).toMatch(/\[HttpGet\("list"\)\]/) // The hub did not inject anything extra. expect(ctrl.content).not.toMatch(/HomeDto/) }) }) describe('scaffold-screen-controller — legacy standalone kanban / card pagespecs fall back to list', () => { for (const view of ['kanban', 'card'] as const) { it(`legacy standalone "${view}" pagespec serves the list endpoint + a fold-and-delete migration TODO`, () => { const { files, todos } = generate(spec(), [pagespec(view)]) const ctrl = controllerOf(files)! expect(ctrl.content).toMatch(/\[HttpGet\("list"\)\]/) expect(ctrl.content).toMatch(/PaginatedResult/) // A list DTO must be emitted because the controller method references it. const dto = files.find((f) => f.path.endsWith('DemandeListScreenDto.cs')) expect(dto).toBeDefined() // The TODO is a MIGRATION signal (fold into the list pagespec), not an // emitter debt — the board/gallery are viewModes served by the list. expect(todos.some((t) => t.includes(view) && t.includes('pre-fold legacy shape'))).toBe(true) expect(todos.some((t) => t.includes('delete this file'))).toBe(true) }) } })