///
/**
* UIG-19: what a part draws only behind a prop, and no story passes. Each case
* is one the scan met in Peek or the package:
*
* - a gate on the part itself (SectionHeader's chevron);
* - a prop handed to a smaller part that gates on it (ConversationCard's
* `onReply`, which the strip draws — strict, by Katerina's ruling);
* - a prop handed to a dialog, shut in every story (skipped, her ruling);
* - a story that spreads another story's args (`...Topic.args`);
* - a spread it cannot open (a helper's `{...props}`), which keeps it quiet.
*/
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterAll, describe, expect, it } from 'vitest'
import { buildAppRegistry } from './app'
import { gatedFindings } from './gated'
const made: string[] = []
afterAll(() => {
for (const dir of made) rmSync(dir, { recursive: true, force: true })
})
function app(files: Record): string {
const dir = mkdtempSync(join(tmpdir(), 'uig19-gated-'))
made.push(dir)
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'fixture-app', version: '1.0.0' }))
for (const [path, text] of Object.entries(files)) {
mkdirSync(dirname(join(dir, path)), { recursive: true })
writeFileSync(join(dir, path), text)
}
return dir
}
const find = (dir: string) => gatedFindings(buildAppRegistry({ root: dir, repo: 'fixture' }), dir).map((f) => `${f.part}.${f.prop}`)
const card = `/**
* A card.
* @registry reusable: the fixture says so
* @registry namesake: the fixture says so
*/
export function Card({ title, onReply, onShare }: { title: string; onReply?: () => void; onShare?: () => void }) {
return (
{title}
{onReply && }
)
}
/** The strip under a card. */
export function Strip({ onShare }: { onShare?: () => void }) {
return
{onShare ? : null}
}
/** A dialog, shut until opened. */
export function ShareDialog({ onShare }: { onShare?: () => void }) {
return
{onShare && }
}
`
const story = (body: string) => `import { Card } from './Card'\nconst meta = { title: 'Parts/Card', component: Card, args: { title: 'Hello' } }\nexport default meta\n${body}\n`
describe('what a part draws behind a prop', () => {
it('names a gate on the part, and one it hands to a smaller part, when no story passes either', () => {
const dir = app({ 'src/Card.tsx': card, 'src/Card.stories.tsx': story('export const Default = {}') })
expect(find(dir).sort()).toEqual(['Card.onReply', 'Card.onShare'])
})
it('is satisfied by a story that passes them, in args or through another story’s args', () => {
const dir = app({
'src/Card.tsx': card,
'src/Card.stories.tsx': story("export const Default = { args: { onReply: () => {} } }\nexport const Shared = { args: { ...Default.args, onShare: () => {} } }"),
})
expect(find(dir)).toEqual([])
})
it('skips a prop that reaches only a dialog', () => {
const only = card.replace(' \n', '')
const dir = app({ 'src/Card.tsx': only, 'src/Card.stories.tsx': story("export const Default = { args: { onReply: () => {} } }") })
expect(find(dir)).toEqual([])
})
it('stays quiet when a story passes props it cannot read', () => {
const dir = app({
'src/Card.tsx': card,
'src/Card.stories.tsx': `import { Card } from './Card'\nimport { props } from './fixtures'\nconst meta = { title: 'Parts/Card', component: Card }\nexport default meta\nexport const Default = { render: () => }\n`,
})
expect(find(dir)).toEqual([])
})
it('asks nothing of a part no story draws: the contract says so instead', () => {
const dir = app({ 'src/Card.tsx': card })
expect(find(dir)).toEqual([])
})
})