import { describe, it, expect } from 'vitest' import { generate } from '../generate.js' import { validate } from '../validate.js' import type { ScaffoldComponentInput } from '../types.js' /** * 360 related tabs — the detail page embeds the records of entities IN * RELATION (Client ⟷ Factures / Adresses / Échanges), one tab per relation, * permission-gated, fetching through the FK filter (`{relationFk}: id`). */ const RELATED_TABS = [ { key: 'invoices', displayMode: 'table', relatedEntity: 'Invoice', relationFk: 'clientId', relatedModule: 'crm', relatedSection: 'invoices', targetScreen: 'SCR-CRM-INVOICES-LIST-001', permission: 'crm.invoices.read', }, { key: 'addresses', displayMode: 'cards', relatedEntity: 'Address', // BA-vocabulary alias on purpose — the normaliser maps it to relationFk. relationship: 'clientId', relatedModule: 'crm', relatedSection: 'addresses', }, { key: 'interactions', displayMode: 'summary', relatedEntity: 'Interaction', relationFk: 'clientId', relatedModule: 'support', relatedSection: 'interactions', crossModule: true, }, ] function fixture(overrides: Partial = {}): ScaffoldComponentInput { return { module: 'crm', appCode: 'TestV2', entity: 'Client', section: 'clients', views: ['detail'], fields: [ { name: 'name', type: 'string', required: true }, { name: 'email', type: 'string', required: false }, ], projectPath: '/web', pageSpec: { screenCode: 'SCR-CRM-CLIENTS-DETAIL-001', module: 'crm', appCode: 'TestV2', section: 'clients', entity: 'Client', view: 'detail', filePath: 'src/pages/testv2/crm/clients/ClientDetailPage.tsx', permission: 'crm.clients.read', actions: [], relatedTabs: RELATED_TABS, i18nKeys: { fr: {}, en: {}, it: {}, de: {} }, needsRefinement: false, specHash: 'test', }, relatedTabsData: [ { key: 'invoices', columns: [ { key: 'number', labels: { fr: 'Numéro', en: 'Number', it: 'Numero', de: 'Nummer' } }, { key: 'amount' }, ], displayField: 'number', }, { key: 'addresses', columns: [{ key: 'city' }, { key: 'zipCode' }], }, ], ...overrides, } as ScaffoldComponentInput } const detailOf = (files: Array<{ path: string; content: string }>) => files.find(f => f.path.endsWith('ClientDetailPage.tsx'))! describe('scaffold-component / 360 related tabs — merged strip', () => { const files = generate(fixture()) const page = detailOf(files).content it('synthesises an "info" field tab when only relatedTabs are declared', () => { expect(page).toMatch(/id="tab-info"/) expect(page).toMatch(/t\('client\.detail\.tabs\.info'\)/) }) it('renders one permission-gated trigger per STRIP tab (default = {module}.{section}.read)', () => { // Explicit permission propagates verbatim. expect(page).toMatch(/[\s\S]{0,600}?id="tab-invoices"/) // Omitted permission normalises to the related read permission. expect(page).toMatch(/[\s\S]{0,600}?id="tab-addresses"/) // The summary tab defaults to the BAND — gated cartouche, no strip trigger. expect(page).toMatch(/[\s\S]{0,200}?data-testid="related-band-interactions"/) expect(page).not.toMatch(/id="tab-interactions"/) }) it('delegates the strip to the TabStrip primitive (DEV-UI-027), triggers as children', () => { expect(page).toContain("import { TabStrip } from '@/components/ui/TabStrip'") expect(page).toMatch(//) // The permission-gated triggers stay inline, inside the strip. expect(page).toMatch(/]*>[\s\S]*?[\s\S]*?<\/TabStrip>/) }) it('mounts the child component only while its tab is active, inside the panel guard', () => { expect(page).toMatch(/\{activeTab === 'invoices' && \(\s*[\s\S]*?/) }) }) describe('scaffold-component / 360 related tabs — table mode', () => { const page = detailOf(generate(fixture())).content it('fetches through the FK filter param named exactly relationFk', () => { expect(page).toMatch(/const \{ data, isLoading, error \} = useInvoices\(\{ page, pageSize, clientId: relatedId \}\)/) }) it('renders a server-paged ResponsiveDataTable with the derived columns', () => { expect(page).toMatch(/import \{ ResponsiveDataTable, type ResponsiveColumn \} from '@\/components\/ui\/ResponsiveDataTable'/) expect(page).toMatch(/ResponsiveColumn\[\] = \[/) expect(page).toMatch(/\{ key: 'number', label: t\('client\.detail\.related\.invoices\.columns\.number'\), sortable: false/) expect(page).toMatch(/serverMode/) expect(page).toMatch(/totalCount=\{totalCount\}/) }) it('imports the related hook + ListDto type from the related feature', () => { expect(page).toMatch(/import \{ useInvoices \} from '@\/features\/testv2\/crm\/invoice\/hooks\/useInvoice'/) expect(page).toMatch(/import type \{ InvoiceListDto \} from '@\/features\/testv2\/crm\/invoice\/types'/) }) it('row click navigates to the related detail route', () => { expect(page).toMatch(/onRowClick=\{\(item\) => navigate\(routes\.invoices\.detail\(item\.id\)\)\}/) }) it('the «Créer» button pre-fills the relation via the query string, gated on .create', () => { expect(page).toMatch(//) expect(page).toMatch(/navigate\(`\$\{routes\.invoices\.create\(\)\}\?clientId=\$\{relatedId\}`\)/) }) }) describe('scaffold-component / 360 related tabs — cards + summary modes', () => { const page = detailOf(generate(fixture())).content it('cards mode renders a grid with the title column and a mini-pager', () => { expect(page).toMatch(/function ClientRelatedAddressesTab\(\{ relatedId \}: \{ relatedId: string \}\)/) expect(page).toMatch(/useAddresses\(\{ page, pageSize, clientId: relatedId \}\)/) expect(page).toMatch(/grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4/) expect(page).toMatch(/String\(item\.city \?\? ''\)/) expect(page).toMatch(/ChevronLeft/) }) it('summary mode renders a count cartouche + view-all link, no create button', () => { const summary = page.slice(page.indexOf('function ClientRelatedInteractionsTab')) expect(summary).toMatch(/useInteractions\(\{ page: 1, pageSize: 1, clientId: relatedId \}\)/) expect(summary).toMatch(/\{data\?\.totalCount \?\? 0\}/) expect(summary).toMatch(/t\('client\.detail\.related\.interactions\.viewAll'\)/) expect(summary).not.toMatch(/\.create\(\)/) // The caption is the tab LABEL — in a band row of several cartouches, // identity lives on the card itself (never a generic "Total"). expect(summary).toMatch(/t\('client\.detail\.related\.interactions\.label'\)/) }) it('cross-module tab imports the related module routes registry under an alias', () => { expect(page).toMatch(/import \{ routes as supportRoutes \} from '@\/extensions\/[^']*Routes'/) expect(page).toMatch(/navigate\(supportRoutes\.interactions\.list\(\)\)/) }) }) describe('scaffold-component / 360 related tabs — band placement (§43)', () => { const files = generate(fixture()) const page = detailOf(files).content it('renders the band row with one gated cartouche wrapper per band tab', () => { expect(page).toMatch(/data-testid="related-band"/) expect(page).toMatch(/[\s\S]{0,200}?
[\s\S]{0,120}?/) }) it('band cartouches are ALWAYS mounted — never behind an activeTab conditional', () => { expect(page).not.toMatch(/\{activeTab === 'interactions' &&/) }) it('the band renders between the detail summary band and the TabStrip', () => { const withSummary = fixture() ;(withSummary.pageSpec as { summary?: unknown }).summary = { titleField: 'name' } const p = detailOf(generate(withSummary)).content const iSummary = p.indexOf('data-testid="detail-summary"') const iBand = p.indexOf('data-testid="related-band"') const iStrip = p.indexOf(' { const optBack = fixture() ;(optBack.pageSpec as { relatedTabs: Array> }).relatedTabs = RELATED_TABS.map(t => t.key === 'interactions' ? { ...t, placement: 'tab' } : t, ) const p = detailOf(generate(optBack)).content expect(p).toMatch(/id="tab-interactions"/) expect(p).not.toMatch(/data-testid="related-band/) }) it('a band-only page has NO strip at all — no TabStrip, no useSearchParams, no synthetic info tab', () => { const bandOnly = fixture() ;(bandOnly.pageSpec as { relatedTabs: unknown[] }).relatedTabs = RELATED_TABS.filter(t => t.key === 'interactions') bandOnly.relatedTabsData = undefined const p = detailOf(generate(bandOnly)).content expect(p).not.toContain('TabStrip') expect(p).not.toMatch(/useSearchParams/) expect(p).not.toMatch(/id="tab-info"/) expect(p).toMatch(/data-testid="related-band-interactions"/) // The child component and its cross-module navigation survive untouched. expect(p).toMatch(/function ClientRelatedInteractionsTab/) expect(p).toMatch(/navigate\(supportRoutes\.interactions\.list\(\)\)/) }) it('the stale-URL guard lists only strip keys — a bookmarked band key falls back to the default', () => { expect(page).toMatch(/const activeTab = tabParam !== null && \['info', 'invoices', 'addresses'\]\.includes\(tabParam\) \? tabParam : 'info'/) }) it("fail-loud: placement 'band' on a table tab is rejected by the shared schema", () => { const bad = fixture() ;(bad.pageSpec as { relatedTabs: Array> }).relatedTabs = RELATED_TABS.map(t => t.key === 'invoices' ? { ...t, placement: 'band' } : t, ) expect(() => generate(bad)).toThrow(/invalid pageSpec\.relatedTabs/) }) }) describe('scaffold-component / 360 related tabs — date formatting (display settings)', () => { it('honours columns[].formatHint from relatedTabsData (ba-develop Phase 3a)', () => { const page = detailOf(generate(fixture({ relatedTabsData: [ { key: 'invoices', columns: [ { key: 'number' }, { key: 'issued', formatHint: 'date' }, { key: 'lastReminder', formatHint: 'datetime' }, ], displayField: 'number', }, ], }))).content expect(page).toMatch(/render: \(item\) => formatDate\(item\.issued\)/) expect(page).toMatch(/render: \(item\) => formatDateTime\(item\.lastReminder\)/) expect(page).toMatch(/import \{ Slot, formatDate, formatDateTime \} from '@atlashub\/smartstack'/) }) it('falls back on the key-suffix convention when no hint (…At → date+time, …Date|On|Until → date)', () => { const page = detailOf(generate(fixture({ relatedTabsData: [ { key: 'invoices', columns: [{ key: 'number' }, { key: 'dueDate' }, { key: 'createdAt' }], displayField: 'number', }, ], }))).content expect(page).toMatch(/render: \(item\) => formatDate\(item\.dueDate\)/) expect(page).toMatch(/render: \(item\) => formatDateTime\(item\.createdAt\)/) // Non-date columns keep the verbatim String fallback. expect(page).toMatch(/render: \(item\) => String\(item\.number \?\? ''\)/) }) it('the createdAt fail-open column of a data-less tab renders formatted too', () => { const page = detailOf(generate(fixture({ relatedTabsData: undefined }))).content expect(page).toMatch(/\{ key: 'createdAt', label: t\('client\.detail\.related\.invoices\.columns\.createdAt'\), sortable: false, render: \(item\) => formatDateTime\(item\.createdAt\) \}/) }) }) describe('scaffold-component / 360 related tabs — i18n floor', () => { const files = generate(fixture()) it('seeds the full detail.related family in all 4 locales, with resolved column labels', () => { for (const locale of ['fr', 'en', 'it', 'de']) { const f = files.find(x => x.path.replace(/\\/g, '/').includes(`i18n/locales/${locale}/`))! expect(f, locale).toBeDefined() const json = JSON.parse(f.content) const related = json.client.detail.related expect(related.invoices.label, locale).toBeDefined() expect(related.invoices.empty, locale).toBeDefined() expect(related.invoices.create, locale).toBeDefined() expect(related.interactions.viewAll, locale).toBeDefined() } const fr = JSON.parse(files.find(x => x.path.replace(/\\/g, '/').includes('i18n/locales/fr/'))!.content) expect(fr.client.detail.related.invoices.columns.number).toBe('Numéro') expect(fr.client.detail.tabs.info).toBe('Informations') }) }) describe('scaffold-component / 360 related tabs — fail-loud + fail-open', () => { it('throws on a malformed relatedTabs entry instead of dropping it silently', () => { const bad = fixture() ;(bad.pageSpec as { relatedTabs: unknown[] }).relatedTabs = [ { key: 'invoices', relatedEntity: 'invoice', relationFk: 'clientId', relatedModule: 'crm', relatedSection: 'invoices' }, ] expect(() => generate(bad)).toThrow(/invalid pageSpec\.relatedTabs/) }) it('throws when a related tab key collides with a field tab key', () => { const dup = fixture() ;(dup.pageSpec as { tabs?: unknown[] }).tabs = [{ key: 'invoices', fields: ['name'] }] expect(() => generate(dup)).toThrow(/BOTH tabs\[\] and relatedTabs\[\]/) }) it('a table tab without relatedTabsData falls back to createdAt (and validate warns)', () => { const spec = fixture({ relatedTabsData: undefined }) const page = detailOf(generate(spec)).content expect(page).toMatch(/\{ key: 'createdAt', label: t\('client\.detail\.related\.invoices\.columns\.createdAt'\)/) const res = validate(spec) expect(res.valid).toBe(true) expect(res.warnings.join('\n')).toMatch(/relatedTabs\.invoices.*createdAt/) expect(res.warnings.join('\n')).toMatch(/relatedTabs\.addresses/) // summary mode needs no column data — never warned. expect(res.warnings.join('\n')).not.toMatch(/interactions/) }) }) describe('scaffold-component / 360 related tabs — sub-view routing (relatedRouteFamily)', () => { // The AtlasHub acceptance scenario: Project.detail in module portfolio, // menu section `list` hosting the porteur (Project, family `list`) plus // satellites (ProjectWorkPackage → family `workPackages`, ProjectHistoryEntry // → read-only family `history`). const SUBVIEW_TABS = [ { key: 'lots-de-travail', displayMode: 'table', relatedEntity: 'ProjectWorkPackage', relationFk: 'projectId', relatedModule: 'portfolio', relatedSection: 'list', relatedRouteFamily: 'work-packages', targetScreen: 'SCR-PROJECTS-PORTFOLIO-LIST-006', permission: 'portfolio.list.read', createPermission: 'portfolio.list.work-package.create', withCreate: true, withRowOpen: true, }, { key: 'journal', displayMode: 'table', relatedEntity: 'ProjectHistoryEntry', relationFk: 'projectId', relatedModule: 'portfolio', relatedSection: 'list', relatedRouteFamily: 'history', permission: 'portfolio.list.read', withCreate: false, withRowOpen: false, }, ] // Keyed by `{app}-{module}` — the identity of the generated Routes.ts file, so a page // loading families from several applications cannot see one overwrite another. const PORTFOLIO_FAMILIES = { 'projects-portfolio': { list: ['list', 'detail', 'edit', 'create', 'kanban'], workPackages: ['list', 'detail', 'edit', 'create'], history: ['list'], }, } function subViewFixture(overrides: Partial = {}): ScaffoldComponentInput { return { module: 'portfolio', appCode: 'projects', entity: 'Project', section: 'list', views: ['detail'], fields: [{ name: 'name', type: 'string', required: true }], projectPath: '/web', pageSpec: { screenCode: 'SCR-PROJECTS-PORTFOLIO-LIST-002', module: 'portfolio', appCode: 'projects', section: 'list', entity: 'Project', view: 'detail', filePath: 'src/pages/projects/portfolio/list/ProjectDetailPage.tsx', permission: 'portfolio.list.read', actions: [], relatedTabs: SUBVIEW_TABS, i18nKeys: { fr: {}, en: {}, it: {}, de: {} }, needsRefinement: false, specHash: 'test', }, relatedTabsData: [ { key: 'lots-de-travail', columns: [{ key: 'label' }, { key: 'dueDate' }], displayField: 'label' }, { key: 'journal', columns: [{ key: 'createdAt' }, { key: 'action' }], displayField: 'action' }, ], ...overrides, } as ScaffoldComponentInput } const pageOf = (files: Array<{ path: string; content: string }>) => files.find(f => f.path.endsWith('ProjectDetailPage.tsx'))!.content const componentBlock = (page: string, name: string) => { const start = page.indexOf(`function ${name}`) expect(start, name).toBeGreaterThan(-1) const next = page.indexOf('\nfunction ', start + 1) return next === -1 ? page.slice(start) : page.slice(start, next) } it('acceptance — the satellite tab navigates through ITS family with ITS create permission', () => { const page = pageOf(generate(subViewFixture(), { routesFamilies: PORTFOLIO_FAMILIES })) // Create guard = the target list page's own create permission (4 segments, // resource axis) — never the recomposed portfolio.list.create. expect(page).toContain('') expect(page).not.toContain('portfolio.list.create') // Create + row-open go through routes.workPackages.*, not routes.list.*. expect(page).toMatch(/navigate\(`\$\{routes\.workPackages\.create\(\)\}\?projectId=\$\{relatedId\}`\)/) expect(page).toMatch(/onRowClick=\{\(item\) => navigate\(routes\.workPackages\.detail\(item\.id\)\)\}/) }) it('acceptance — the journal tab is inert: no create, no row click, no orphan navigate', () => { const page = pageOf(generate(subViewFixture(), { routesFamilies: PORTFOLIO_FAMILIES })) const journal = componentBlock(page, 'ProjectRelatedJournalTab') expect(journal).not.toMatch(/\.create\(\)/) expect(journal).not.toMatch(/onRowClick/) expect(journal).not.toMatch(/const navigate = useNavigate\(\)/) }) it('hard-fails when the family does not exist in the target Routes.ts', () => { const families = { 'projects-portfolio': { list: ['list', 'detail', 'edit', 'create'] } } expect(() => generate(subViewFixture(), { routesFamilies: families })) .toThrow(/route family 'workPackages'.*declares no such family/s) }) it('hard-fails on withCreate: true when the family has no create() helper', () => { const families = { 'projects-portfolio': { list: ['list', 'detail', 'edit', 'create'], workPackages: ['list', 'detail'], // no create() history: ['list'], }, } expect(() => generate(subViewFixture(), { routesFamilies: families })) .toThrow(/withCreate: true but no create form resolves/) }) it('falls back to the relatedTabsData signals when no Routes.ts is loaded, and says so', () => { const spec = subViewFixture({ relatedTabsData: [ { key: 'lots-de-travail', columns: [{ key: 'label' }], hasCreateForm: true, hasDetail: true }, { key: 'journal', columns: [{ key: 'createdAt' }], hasCreateForm: false, hasDetail: false }, ], } as Partial) // journal declared withCreate/withRowOpen false explicitly — drop the // explicit flags to exercise the signal-driven resolution. ;(spec.pageSpec as { relatedTabs: Array> }).relatedTabs = SUBVIEW_TABS.map(t => t.key === 'journal' ? { ...t, withCreate: undefined, withRowOpen: undefined } : t, ) const warnings: string[] = [] const page = pageOf(generate(spec, { warnings })) const journal = componentBlock(page, 'ProjectRelatedJournalTab') expect(journal).not.toMatch(/onRowClick/) expect(journal).not.toMatch(/const navigate = useNavigate\(\)/) expect(warnings.join('\n')).toMatch(/Routes\.ts not loaded/) }) it('legacy tabs (no relatedRouteFamily, no signals) keep the historical output untouched', () => { const files = generate(fixture()) const page = detailOf(files).content expect(page).toMatch(/navigate\(`\$\{routes\.invoices\.create\(\)\}\?clientId=\$\{relatedId\}`\)/) expect(page).toMatch(/onRowClick=\{\(item\) => navigate\(routes\.invoices\.detail\(item\.id\)\)\}/) }) }) describe('scaffold-component / 360 related tabs — form FK pre-fill (D7)', () => { it('the create form seeds FK fields from the query string', () => { const files = generate(fixture({ views: ['form'], fields: [ { name: 'name', type: 'string', required: true }, { name: 'clientId', type: 'guid', required: true, fkTo: { entity: 'Client', module: 'crm' } }, ], entity: 'Invoice', section: 'invoices', pageSpec: undefined, relatedTabsData: undefined, } as Partial)) const form = files.find(f => f.path.endsWith('InvoiceFormPage.tsx'))!.content expect(form).toMatch(/import \{ useParams, useNavigate, useSearchParams \} from 'react-router-dom'/) expect(form).toMatch(/const \[searchParams\] = useSearchParams\(\)/) expect(form).toMatch(/clientId: searchParams\.get\('clientId'\) \?\? initialInvoiceFormData\.clientId/) }) it('a form without FK fields keeps the legacy initialiser byte-for-byte', () => { const files = generate(fixture({ views: ['form'], pageSpec: undefined, relatedTabsData: undefined, } as Partial)) const form = files.find(f => f.path.endsWith('ClientFormPage.tsx'))!.content expect(form).not.toMatch(/useSearchParams/) expect(form).toMatch(/useState\(initialClientFormData\)/) }) }) // --------------------------------------------------------------------------- // Tenant-catalogue availability guard (the cross-module 360 guard) // --------------------------------------------------------------------------- describe('related tabs — tenant-catalogue availability guard', () => { const CROSS_APP_TAB = { key: 'factures', displayMode: 'table', relatedEntity: 'Invoice', relationFk: 'clientId', relatedApp: 'facturation', relatedModule: 'factures', relatedSection: 'factures', permission: 'facturation.factures.read', withCreate: false, withRowOpen: false, } const SAME_MODULE_TAB = { key: 'addresses', displayMode: 'cards', relatedEntity: 'Address', relationFk: 'clientId', relatedModule: 'crm', relatedSection: 'addresses', withCreate: false, withRowOpen: false, } const CROSS_MODULE_TAB = { key: 'interactions', displayMode: 'summary', relatedEntity: 'Interaction', relationFk: 'clientId', relatedModule: 'support', relatedSection: 'interactions', crossModule: true, } const pageOfFiles = (files: Array<{ path: string; content: string }>) => files.find((f) => f.path.endsWith('ClientDetailPage.tsx'))!.content function pageWith(tabs: object[]): string { const spec = fixture() ;(spec.pageSpec as Record).relatedTabs = tabs return pageOfFiles(generate(spec)) } it('leaves a same-module tab completely alone', () => { const page = pageWith([SAME_MODULE_TAB]) expect(page).not.toContain('useModuleAvailability') expect(page).not.toContain('moduleAvailability') // the legacy fixed key list, not the render-time filter expect(page).toContain("const activeTab = tabParam !== null && ['info', 'addresses'].includes(tabParam)") }) it('guards a tab pointing at ANOTHER module of the same application', () => { const page = pageWith([CROSS_MODULE_TAB]) expect(page).toContain("import { useModuleAvailability } from '@/components/ui/useModuleAvailability'") expect(page).toContain("const showClientRelatedInteractions = moduleAvailability.hasModule('testv2', 'support')") }) it('guards a tab pointing at another APPLICATION, naming the TARGET application', () => { const page = pageWith([CROSS_APP_TAB]) expect(page).toContain("const showClientRelatedFactures = moduleAvailability.hasModule('facturation', 'factures')") // …and never the page's own application expect(page).not.toContain("hasModule('testv2', 'factures')") }) it('gates the trigger AND the panel — never one without the other', () => { const page = pageWith([CROSS_APP_TAB]) expect(page).toContain('{showClientRelatedFactures && (') expect(page).toContain("{activeTab === 'factures' && showClientRelatedFactures && (") // the permission gate stays: availability and permission are two questions expect(page).toContain('') }) it('keeps the active tab off a hidden trigger', () => { const page = pageWith([CROSS_APP_TAB]) expect(page).toContain('const visibleTabKeys =') expect(page).toContain("(k !== 'factures' || showClientRelatedFactures)") expect(page).toContain('const defaultTabKey = visibleTabKeys[0]') expect(page).toContain('const activeTab = tabParam !== null && visibleTabKeys.includes(tabParam) ? tabParam : defaultTabKey') }) it('does not switch the strip to the dynamic key list when only a BAND cartouche is guarded', () => { const page = pageWith([SAME_MODULE_TAB, CROSS_MODULE_TAB]) expect(page).toContain('const showClientRelatedInteractions =') expect(page).not.toContain('const visibleTabKeys =') }) it('guards a band cartouche without losing its audit anchor', () => { const page = pageWith([CROSS_MODULE_TAB]) expect(page).toContain('{showClientRelatedInteractions && (') expect(page).toContain('data-testid="related-band-interactions"') }) it('honours the availabilityCheck: false opt-out', () => { const page = pageWith([{ ...CROSS_APP_TAB, availabilityCheck: false }]) expect(page).not.toContain('useModuleAvailability') expect(page).toContain('') }) it('imports the hook and the routes of the TARGET application, not the page one', () => { const page = pageWith([CROSS_APP_TAB]) expect(page).toContain("from '@/features/facturation/factures/invoice/hooks/useInvoice'") expect(page).toContain("import { routes as facturationFacturesRoutes } from '@/extensions/facturation-facturesRoutes'") expect(page).not.toContain('@/features/testv2/factures') }) it('never reaches for useLicense — that answers what was bought, not what this tenant has', () => { expect(pageWith([CROSS_APP_TAB])).not.toContain('useLicense') }) })