import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { RuleTester } from 'eslint'
import { parser } from 'typescript-eslint'
import { describe, expect, it } from 'vitest'
import { noNativeTitle, PARTS_WITH_A_TITLE } from './no-native-title'
RuleTester.describe = describe
RuleTester.it = it
RuleTester.itOnly = it.only
const tester = new RuleTester({
languageOptions: { parser, parserOptions: { ecmaFeatures: { jsx: true } } },
linterOptions: { reportUnusedDisableDirectives: 'off' },
})
/** An app on disk, so a wrapper of the app's own is followed to its part, as Peek's are. */
const root = mkdtempSync(join(tmpdir(), 'no-native-title-'))
const write = (path: string, text: string) => {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), text)
}
write('app/package.json', '{ "name": "app" }')
write('app/src/components/ui/IconButton.tsx', "export { IconButton } from '@estiva-app/ui'\n")
write('app/src/components/ui/Button.tsx', "import { Button as UiButton, type ButtonProps } from '@estiva-app/ui'\nexport function Button(props: ButtonProps) {\n return \n}\n")
write('app/src/components/Banner.tsx', 'export function NewTopicBanner({ title }: { title: string }) {\n return
{title}
\n}\n')
const app = join(root, 'app/src/Probe.tsx')
const component = (body: string, imports = '') => `${imports}export function Probe({ at, name }: { at: string; name: string }) {\n return (\n${body}\n )\n}\n`
const native = [{ messageId: 'native' }]
tester.run('no-native-title', noNativeTitle, {
valid: [
{ name: 'the part: WithTooltip', code: component('', "import { WithTooltip } from '@estiva-app/ui'\n") },
{ name: "IconButton's own tooltip", code: component('x', "import { IconButton } from '@estiva-app/ui'\n") },
{ name: 'a inside an ') },
...PARTS_WITH_A_TITLE.map((part) => ({ name: `${part}'s title is its own heading`, code: component(`<${part} title="Files" />`, `import { ${part} } from '@estiva-app/ui'\n`) })),
{ name: "the app's own component with a title of its own (Peek's NewTopicBanner)", filename: app, code: component('', "import { NewTopicBanner } from '@/components/Banner'\n") },
{ name: "a story's title is the Storybook's, an object key", code: "const meta = { title: 'Primitives/Tooltip', component: Tooltip }\nexport default meta\n" },
{ name: 'a prop of another name', code: component('{name}') },
{ name: 'an escape with its reason', code: component('<>\n{/* @estiva-escape(no-native-title): the print view has no pointer, and the address must survive a copy */}\n{at}\n>') },
],
invalid: [
{
name: 'the file conversation timestamp at peek 3dc663b~1 (ForeignConversationView.tsx:106)',
code: component(''),
errors: native,
},
{ name: "a reference chip's cut-short title (Reference.tsx at UIG-1's commits)", code: component('{name}'), errors: native },
{ name: 'a title that is only there sometimes (MentionChip.tsx)', code: component('{name}'), errors: native },
{ name: 'a string', code: component(''), errors: native },
{ name: "a part that hands it on to its element: the apps' status probe", code: component('', "import { Button } from '@estiva-app/ui'\n"), errors: native },
{ name: "through the app's re-export", filename: app, code: component('x', "import { IconButton } from '@/components/ui/IconButton'\n"), errors: native },
{ name: "through the app's wrapper that hands its props on", filename: app, code: component('', "import { Button } from '@/components/ui/Button'\n"), errors: native },
{ name: 'an escape with no reason is not an escape', code: component('<>\n{/* @estiva-escape(no-native-title) */}\n{at}\n>'), errors: [{ messageId: 'escapeWithoutReason' }, ...native] },
],
})
describe('PARTS_WITH_A_TITLE', () => {
it('is every part the catalogue says takes a title of its own, and no other', () => {
const registry = JSON.parse(readFileSync(new URL('../../registry.json', import.meta.url), 'utf8')) as { entries: { name: string; kind: string; props?: { name: string }[] | null }[] }
// A part is a component: a helper whose argument has a `title` field (`loadingShapeOf`) is not drawn with one.
const declared = registry.entries.filter((entry) => entry.kind === 'component' && entry.props?.some((prop) => prop.name === 'title')).map((entry) => entry.name).sort()
expect([...PARTS_WITH_A_TITLE].sort()).toEqual(declared)
})
})