@awarebydefault/display-case 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -0
  3. package/display-case.prompt.md +64 -0
  4. package/docs/ai-agents.md +126 -0
  5. package/docs/cli.md +99 -0
  6. package/docs/configuration.md +410 -0
  7. package/docs/documentation-panel.md +50 -0
  8. package/docs/examples/README.md +14 -0
  9. package/docs/examples/multi-variant.case.tsx +30 -0
  10. package/docs/examples/plain.case.tsx +22 -0
  11. package/docs/examples/tweak-control.placard.md +80 -0
  12. package/docs/examples/tweaks.case.tsx +39 -0
  13. package/docs/hierarchy.md +59 -0
  14. package/docs/quick-start.md +78 -0
  15. package/docs/style-engines.md +180 -0
  16. package/docs/testing.md +245 -0
  17. package/docs/theming.md +97 -0
  18. package/docs/tweaks.md +75 -0
  19. package/docs/writing-cases.md +144 -0
  20. package/docs/writing-placard-docs.md +194 -0
  21. package/package.json +113 -0
  22. package/skills/display-case-author-case/README.md +20 -0
  23. package/skills/display-case-author-case/SKILL.md +40 -0
  24. package/skills/display-case-author-placard-doc/README.md +24 -0
  25. package/skills/display-case-author-placard-doc/SKILL.md +65 -0
  26. package/skills/display-case-review/README.md +19 -0
  27. package/skills/display-case-review/SKILL.md +30 -0
  28. package/skills/display-case-snapshot/README.md +20 -0
  29. package/skills/display-case-snapshot/SKILL.md +29 -0
  30. package/src/checks/a11y-scanner.test.ts +240 -0
  31. package/src/checks/a11y-scanner.ts +410 -0
  32. package/src/checks/check-text.test.ts +53 -0
  33. package/src/checks/check-text.ts +78 -0
  34. package/src/checks/check.test.ts +194 -0
  35. package/src/checks/check.ts +473 -0
  36. package/src/checks/providers/pixelmatch-diff.test.ts +79 -0
  37. package/src/checks/providers/pixelmatch-diff.ts +30 -0
  38. package/src/checks/providers/playwright-driver.ts +104 -0
  39. package/src/checks/ssr-check.test.ts +73 -0
  40. package/src/checks/ssr-check.ts +96 -0
  41. package/src/checks/structure-check.cross-package.test.ts +165 -0
  42. package/src/checks/structure-check.test.ts +651 -0
  43. package/src/checks/structure-check.ts +988 -0
  44. package/src/checks/tokens-check.test.ts +159 -0
  45. package/src/checks/tokens-check.ts +162 -0
  46. package/src/cli.ts +218 -0
  47. package/src/commands/agents.test.ts +24 -0
  48. package/src/commands/agents.ts +28 -0
  49. package/src/commands/init-run.test.ts +123 -0
  50. package/src/commands/init.test.ts +63 -0
  51. package/src/commands/init.ts +412 -0
  52. package/src/commands/publish.test.ts +210 -0
  53. package/src/commands/publish.ts +292 -0
  54. package/src/core/affected.test.ts +99 -0
  55. package/src/core/affected.ts +144 -0
  56. package/src/core/catalog.test.ts +152 -0
  57. package/src/core/catalog.ts +92 -0
  58. package/src/core/discovery.test.ts +184 -0
  59. package/src/core/discovery.ts +250 -0
  60. package/src/core/manifest.ts +41 -0
  61. package/src/core/mdx-lite/__fixtures__/box-stub.tsx +7 -0
  62. package/src/core/mdx-lite/index.ts +393 -0
  63. package/src/core/mdx-lite/mdx-lite.test.ts +345 -0
  64. package/src/core/mdx-plugin.test.ts +60 -0
  65. package/src/core/mdx-plugin.ts +30 -0
  66. package/src/flow-step.test-d.ts +39 -0
  67. package/src/index.test.ts +100 -0
  68. package/src/index.ts +564 -0
  69. package/src/render/collect-styles.emotion.test.tsx +114 -0
  70. package/src/render/collect-styles.test.tsx +72 -0
  71. package/src/render/collect-styles.ts +33 -0
  72. package/src/render/documents.test.ts +184 -0
  73. package/src/render/documents.ts +88 -0
  74. package/src/render/render-node.test.tsx +160 -0
  75. package/src/render/render-node.tsx +133 -0
  76. package/src/render/ssr-primer.test.tsx +25 -0
  77. package/src/render/ssr-primer.tsx +54 -0
  78. package/src/render/ssr-render.test.tsx +142 -0
  79. package/src/render/ssr-render.tsx +63 -0
  80. package/src/render/ssr-shell.test.tsx +57 -0
  81. package/src/render/ssr-shell.tsx +54 -0
  82. package/src/server/prod-server.ts +237 -0
  83. package/src/server/server.test.ts +117 -0
  84. package/src/server/server.ts +1039 -0
  85. package/src/style-engine.test-d.ts +37 -0
  86. package/src/testing/test-helpers.ts +27 -0
  87. package/src/types/pixelmatch.d.ts +12 -0
  88. package/src/ui/browser-entry.tsx +51 -0
  89. package/src/ui/chrome.css +485 -0
  90. package/src/ui/design-system/README.md +88 -0
  91. package/src/ui/design-system/components/controls/Button.case.tsx +52 -0
  92. package/src/ui/design-system/components/controls/Button.css +89 -0
  93. package/src/ui/design-system/components/controls/Button.placard.md +14 -0
  94. package/src/ui/design-system/components/controls/Button.test.tsx +45 -0
  95. package/src/ui/design-system/components/controls/Button.tsx +41 -0
  96. package/src/ui/design-system/components/controls/IconButton.case.tsx +52 -0
  97. package/src/ui/design-system/components/controls/IconButton.css +67 -0
  98. package/src/ui/design-system/components/controls/IconButton.placard.md +13 -0
  99. package/src/ui/design-system/components/controls/IconButton.test.tsx +39 -0
  100. package/src/ui/design-system/components/controls/IconButton.tsx +47 -0
  101. package/src/ui/design-system/components/controls/Input.case.tsx +50 -0
  102. package/src/ui/design-system/components/controls/Input.css +52 -0
  103. package/src/ui/design-system/components/controls/Input.placard.md +12 -0
  104. package/src/ui/design-system/components/controls/Input.test.tsx +43 -0
  105. package/src/ui/design-system/components/controls/Input.tsx +45 -0
  106. package/src/ui/design-system/components/controls/Select.case.tsx +48 -0
  107. package/src/ui/design-system/components/controls/Select.css +44 -0
  108. package/src/ui/design-system/components/controls/Select.placard.md +15 -0
  109. package/src/ui/design-system/components/controls/Select.test.tsx +57 -0
  110. package/src/ui/design-system/components/controls/Select.tsx +58 -0
  111. package/src/ui/design-system/components/controls/SelectMenu.case.tsx +100 -0
  112. package/src/ui/design-system/components/controls/SelectMenu.css +72 -0
  113. package/src/ui/design-system/components/controls/SelectMenu.placard.md +18 -0
  114. package/src/ui/design-system/components/controls/SelectMenu.test.tsx +66 -0
  115. package/src/ui/design-system/components/controls/SelectMenu.tsx +377 -0
  116. package/src/ui/design-system/components/index.ts +66 -0
  117. package/src/ui/design-system/components/primer-specimen/ColorRamp.case.tsx +44 -0
  118. package/src/ui/design-system/components/primer-specimen/ColorRamp.placard.md +15 -0
  119. package/src/ui/design-system/components/primer-specimen/ColorRamp.tsx +51 -0
  120. package/src/ui/design-system/components/primer-specimen/DefinitionList.case.tsx +38 -0
  121. package/src/ui/design-system/components/primer-specimen/DefinitionList.placard.md +15 -0
  122. package/src/ui/design-system/components/primer-specimen/DefinitionList.tsx +41 -0
  123. package/src/ui/design-system/components/primer-specimen/FontFamilies.case.tsx +24 -0
  124. package/src/ui/design-system/components/primer-specimen/FontFamilies.placard.md +12 -0
  125. package/src/ui/design-system/components/primer-specimen/FontFamilies.tsx +41 -0
  126. package/src/ui/design-system/components/primer-specimen/GlyphGrid.case.tsx +27 -0
  127. package/src/ui/design-system/components/primer-specimen/GlyphGrid.placard.md +13 -0
  128. package/src/ui/design-system/components/primer-specimen/GlyphGrid.tsx +34 -0
  129. package/src/ui/design-system/components/primer-specimen/LayoutMock.case.tsx +36 -0
  130. package/src/ui/design-system/components/primer-specimen/LayoutMock.placard.md +7 -0
  131. package/src/ui/design-system/components/primer-specimen/LayoutMock.tsx +36 -0
  132. package/src/ui/design-system/components/primer-specimen/SpacingScale.case.tsx +20 -0
  133. package/src/ui/design-system/components/primer-specimen/SpacingScale.placard.md +12 -0
  134. package/src/ui/design-system/components/primer-specimen/SpacingScale.tsx +33 -0
  135. package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.case.tsx +56 -0
  136. package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.placard.md +17 -0
  137. package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.tsx +45 -0
  138. package/src/ui/design-system/components/primer-specimen/StatusList.case.tsx +17 -0
  139. package/src/ui/design-system/components/primer-specimen/StatusList.placard.md +16 -0
  140. package/src/ui/design-system/components/primer-specimen/StatusList.tsx +39 -0
  141. package/src/ui/design-system/components/primer-specimen/SwatchGrid.case.tsx +26 -0
  142. package/src/ui/design-system/components/primer-specimen/SwatchGrid.placard.md +15 -0
  143. package/src/ui/design-system/components/primer-specimen/SwatchGrid.tsx +42 -0
  144. package/src/ui/design-system/components/primer-specimen/TypeScale.case.tsx +23 -0
  145. package/src/ui/design-system/components/primer-specimen/TypeScale.placard.md +14 -0
  146. package/src/ui/design-system/components/primer-specimen/TypeScale.tsx +34 -0
  147. package/src/ui/design-system/components/primer-specimen/WeightSpecimen.case.tsx +28 -0
  148. package/src/ui/design-system/components/primer-specimen/WeightSpecimen.placard.md +15 -0
  149. package/src/ui/design-system/components/primer-specimen/WeightSpecimen.tsx +46 -0
  150. package/src/ui/design-system/components/primer-specimen/index.ts +31 -0
  151. package/src/ui/design-system/components/primer-specimen/styles.css +476 -0
  152. package/src/ui/design-system/components/shell/A11yPage.case.tsx +237 -0
  153. package/src/ui/design-system/components/shell/A11yPage.placard.md +15 -0
  154. package/src/ui/design-system/components/shell/CaseTemplate.case.tsx +32 -0
  155. package/src/ui/design-system/components/shell/CaseTemplate.placard.md +5 -0
  156. package/src/ui/design-system/components/shell/CasesPage.case.tsx +141 -0
  157. package/src/ui/design-system/components/shell/CasesPage.placard.md +12 -0
  158. package/src/ui/design-system/components/shell/PrimerPage.case.tsx +22 -0
  159. package/src/ui/design-system/components/shell/PrimerPage.placard.md +3 -0
  160. package/src/ui/design-system/components/shell/PrimerTemplate.case.tsx +22 -0
  161. package/src/ui/design-system/components/shell/PrimerTemplate.placard.md +5 -0
  162. package/src/ui/design-system/components/shell/ShellView.case.tsx +57 -0
  163. package/src/ui/design-system/components/shell/ShellView.placard.md +5 -0
  164. package/src/ui/design-system/components/shell/ShellView.tsx +678 -0
  165. package/src/ui/design-system/components/shell/shell-fixtures.tsx +727 -0
  166. package/src/ui/design-system/components/showcase/A11yBadge.case.tsx +46 -0
  167. package/src/ui/design-system/components/showcase/A11yBadge.css +27 -0
  168. package/src/ui/design-system/components/showcase/A11yBadge.placard.md +11 -0
  169. package/src/ui/design-system/components/showcase/A11yBadge.test.tsx +31 -0
  170. package/src/ui/design-system/components/showcase/A11yBadge.tsx +41 -0
  171. package/src/ui/design-system/components/showcase/A11yPanel.case.tsx +121 -0
  172. package/src/ui/design-system/components/showcase/A11yPanel.css +198 -0
  173. package/src/ui/design-system/components/showcase/A11yPanel.placard.md +19 -0
  174. package/src/ui/design-system/components/showcase/A11yPanel.test.tsx +81 -0
  175. package/src/ui/design-system/components/showcase/A11yPanel.tsx +144 -0
  176. package/src/ui/design-system/components/showcase/Chip.case.tsx +48 -0
  177. package/src/ui/design-system/components/showcase/Chip.css +51 -0
  178. package/src/ui/design-system/components/showcase/Chip.placard.md +13 -0
  179. package/src/ui/design-system/components/showcase/Chip.test.tsx +46 -0
  180. package/src/ui/design-system/components/showcase/Chip.tsx +54 -0
  181. package/src/ui/design-system/components/showcase/Eyebrow.case.tsx +30 -0
  182. package/src/ui/design-system/components/showcase/Eyebrow.css +16 -0
  183. package/src/ui/design-system/components/showcase/Eyebrow.placard.md +10 -0
  184. package/src/ui/design-system/components/showcase/Eyebrow.test.tsx +38 -0
  185. package/src/ui/design-system/components/showcase/Eyebrow.tsx +29 -0
  186. package/src/ui/design-system/components/showcase/FlowNav.case.tsx +35 -0
  187. package/src/ui/design-system/components/showcase/FlowNav.css +29 -0
  188. package/src/ui/design-system/components/showcase/FlowNav.placard.md +13 -0
  189. package/src/ui/design-system/components/showcase/FlowNav.test.tsx +48 -0
  190. package/src/ui/design-system/components/showcase/FlowNav.tsx +58 -0
  191. package/src/ui/design-system/components/showcase/ImpactTag.case.tsx +19 -0
  192. package/src/ui/design-system/components/showcase/ImpactTag.css +36 -0
  193. package/src/ui/design-system/components/showcase/ImpactTag.placard.md +14 -0
  194. package/src/ui/design-system/components/showcase/ImpactTag.test.tsx +40 -0
  195. package/src/ui/design-system/components/showcase/ImpactTag.tsx +35 -0
  196. package/src/ui/design-system/components/showcase/NavItem.case.tsx +86 -0
  197. package/src/ui/design-system/components/showcase/NavItem.css +111 -0
  198. package/src/ui/design-system/components/showcase/NavItem.placard.md +13 -0
  199. package/src/ui/design-system/components/showcase/NavItem.test.tsx +65 -0
  200. package/src/ui/design-system/components/showcase/NavItem.tsx +95 -0
  201. package/src/ui/design-system/components/showcase/RenderAddress.case.tsx +21 -0
  202. package/src/ui/design-system/components/showcase/RenderAddress.css +35 -0
  203. package/src/ui/design-system/components/showcase/RenderAddress.placard.md +7 -0
  204. package/src/ui/design-system/components/showcase/RenderAddress.test.tsx +26 -0
  205. package/src/ui/design-system/components/showcase/RenderAddress.tsx +43 -0
  206. package/src/ui/design-system/components/showcase/SegmentedToggle.case.tsx +84 -0
  207. package/src/ui/design-system/components/showcase/SegmentedToggle.css +61 -0
  208. package/src/ui/design-system/components/showcase/SegmentedToggle.placard.md +21 -0
  209. package/src/ui/design-system/components/showcase/SegmentedToggle.test.tsx +81 -0
  210. package/src/ui/design-system/components/showcase/SegmentedToggle.tsx +75 -0
  211. package/src/ui/design-system/components/showcase/Sidebar.case.tsx +67 -0
  212. package/src/ui/design-system/components/showcase/Sidebar.css +6 -0
  213. package/src/ui/design-system/components/showcase/Sidebar.placard.md +14 -0
  214. package/src/ui/design-system/components/showcase/Sidebar.test.tsx +32 -0
  215. package/src/ui/design-system/components/showcase/Sidebar.tsx +30 -0
  216. package/src/ui/design-system/components/showcase/Stage.case.tsx +51 -0
  217. package/src/ui/design-system/components/showcase/Stage.css +91 -0
  218. package/src/ui/design-system/components/showcase/Stage.placard.md +15 -0
  219. package/src/ui/design-system/components/showcase/Stage.test.tsx +84 -0
  220. package/src/ui/design-system/components/showcase/Stage.tsx +97 -0
  221. package/src/ui/design-system/components/showcase/TweaksPanel.case.tsx +81 -0
  222. package/src/ui/design-system/components/showcase/TweaksPanel.css +169 -0
  223. package/src/ui/design-system/components/showcase/TweaksPanel.placard.md +20 -0
  224. package/src/ui/design-system/components/showcase/TweaksPanel.tsx +230 -0
  225. package/src/ui/design-system/components/showcase/Wordmark.case.tsx +42 -0
  226. package/src/ui/design-system/components/showcase/Wordmark.css +31 -0
  227. package/src/ui/design-system/components/showcase/Wordmark.placard.md +10 -0
  228. package/src/ui/design-system/components/showcase/Wordmark.test.tsx +22 -0
  229. package/src/ui/design-system/components/showcase/Wordmark.tsx +22 -0
  230. package/src/ui/design-system/primer-specimens/brand.tsx +26 -0
  231. package/src/ui/design-system/primer-specimens/colors.tsx +83 -0
  232. package/src/ui/design-system/primer-specimens/components.tsx +308 -0
  233. package/src/ui/design-system/primer-specimens/foundations.tsx +71 -0
  234. package/src/ui/design-system/primer-specimens/index.ts +25 -0
  235. package/src/ui/design-system/primer-specimens/showcase.tsx +68 -0
  236. package/src/ui/design-system/primer-specimens/spacing.tsx +101 -0
  237. package/src/ui/design-system/primer-specimens/type.tsx +75 -0
  238. package/src/ui/design-system/primer.mdx +236 -0
  239. package/src/ui/design-system/styles.css +14 -0
  240. package/src/ui/design-system/tokens/colors.css +172 -0
  241. package/src/ui/design-system/tokens/fonts.css +18 -0
  242. package/src/ui/design-system/tokens/spacing.css +48 -0
  243. package/src/ui/design-system/tokens/typography.css +49 -0
  244. package/src/ui/markdown.test.tsx +54 -0
  245. package/src/ui/markdown.tsx +19 -0
  246. package/src/ui/primer-mount.tsx +76 -0
  247. package/src/ui/primer.css +175 -0
  248. package/src/ui/primer.tsx +277 -0
  249. package/src/ui/render-mount.tsx +284 -0
  250. package/src/ui/shell-core.test.ts +340 -0
  251. package/src/ui/shell-core.ts +295 -0
  252. package/src/ui/shell.tsx +60 -0
  253. package/src/ui/test-ids.ts +53 -0
  254. package/src/ui/use-shell.ts +1230 -0
@@ -0,0 +1,651 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { rm } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import type { StructureRuleId } from '../index'
5
+ import { makeTempDir, writeFiles } from '../testing/test-helpers'
6
+ import { checkStructure, type StructureFinding } from './structure-check'
7
+
8
+ // Case fixtures import the real authoring helpers by absolute path so they load
9
+ // from a temp dir outside the workspace's module resolution.
10
+ const DC = join(import.meta.dir, '..', 'index.ts')
11
+ const caseFile = (body: string) =>
12
+ `import { defineCases, defineFlow, tweak } from '${DC}'\nexport default ${body}\n`
13
+
14
+ const ALL_RULES: StructureRuleId[] = [
15
+ 'case-placard-coverage',
16
+ 'no-orphaned-placard-doc',
17
+ 'primer-present-and-used',
18
+ 'setup-present',
19
+ 'config-paths-exist',
20
+ 'levels-classified',
21
+ 'cases-load',
22
+ 'flow-transitions-resolve',
23
+ 'flow-multi-step',
24
+ 'unique-slugs',
25
+ 'tweak-defaults-valid',
26
+ 'interactive-cases-keyed',
27
+ 'atom-purity',
28
+ 'no-downward-dependency',
29
+ 'composes-lower-level',
30
+ 'level-fit',
31
+ ]
32
+
33
+ // Build a config that enables only the named rules (others disabled), so each
34
+ // test sees findings from its rule alone. `enabled[id]`: true ⇒ default severity,
35
+ // or a severity string. `extra` splices extra config fields (leading comma).
36
+ function config(
37
+ enabled: Partial<Record<StructureRuleId, true | 'warn' | 'error'>>,
38
+ extra = '',
39
+ ): string {
40
+ const rules = ALL_RULES.map((id) => {
41
+ if (!(id in enabled)) return `'${id}': false`
42
+ const v = enabled[id]
43
+ return `'${id}': ${v === true ? '{}' : `'${v}'`}`
44
+ }).join(', ')
45
+ return `export default { title: 'F', roots: ['**/*.case.tsx'], check: { structure: { rules: { ${rules} } } }${extra} }\n`
46
+ }
47
+
48
+ describe('checkStructure', () => {
49
+ const dirs: string[] = []
50
+ const setup = async (files: Record<string, string>) => {
51
+ const dir = await makeTempDir()
52
+ dirs.push(dir)
53
+ await writeFiles(dir, files)
54
+ return dir
55
+ }
56
+ const run = async (dir: string, opts = {}) =>
57
+ (await checkStructure(dir, opts)).findings
58
+ const only = (findings: StructureFinding[], rule: StructureRuleId) =>
59
+ findings.filter((f) => f.rule === rule)
60
+
61
+ afterEach(async () => {
62
+ while (dirs.length)
63
+ await rm(dirs.pop() as string, { recursive: true, force: true })
64
+ })
65
+
66
+ // ── case-placard-coverage ──────────────────────────────────────────────────
67
+
68
+ test('coverage: flags a component missing its placard doc', async () => {
69
+ const dir = await setup({
70
+ 'display-case.config.ts': config({ 'case-placard-coverage': true }),
71
+ 'Button.tsx': 'export const Button = () => null\n',
72
+ 'Button.case.tsx': caseFile(
73
+ `defineCases('Button', {}, { level: 'atom' })`,
74
+ ),
75
+ })
76
+ const f = only(await run(dir), 'case-placard-coverage')
77
+ expect(f).toHaveLength(1)
78
+ expect(f[0].message).toContain('*.placard.md')
79
+ })
80
+
81
+ test('coverage: flags a component missing its case', async () => {
82
+ const dir = await setup({
83
+ 'display-case.config.ts': config({ 'case-placard-coverage': true }),
84
+ 'Widget.tsx': 'export const Widget = () => null\n',
85
+ 'Widget.placard.md': '# Widget\n',
86
+ })
87
+ const f = only(await run(dir), 'case-placard-coverage')
88
+ expect(f).toHaveLength(1)
89
+ expect(f[0].message).toContain('*.case.tsx')
90
+ })
91
+
92
+ test('coverage: passes when case and prompt both present', async () => {
93
+ const dir = await setup({
94
+ 'display-case.config.ts': config({ 'case-placard-coverage': true }),
95
+ 'Button.tsx': 'export const Button = () => null\n',
96
+ 'Button.case.tsx': caseFile(
97
+ `defineCases('Button', {}, { level: 'atom' })`,
98
+ ),
99
+ 'Button.placard.md': '# Button\n',
100
+ })
101
+ expect(only(await run(dir), 'case-placard-coverage')).toHaveLength(0)
102
+ })
103
+
104
+ test('coverage: a no-placard marker exempts the component', async () => {
105
+ const dir = await setup({
106
+ 'display-case.config.ts': config({ 'case-placard-coverage': true }),
107
+ 'Button.tsx':
108
+ '// display-case: no-placard internal\nexport const Button = () => null\n',
109
+ 'Button.case.tsx': caseFile(
110
+ `defineCases('Button', {}, { level: 'atom' })`,
111
+ ),
112
+ })
113
+ expect(only(await run(dir), 'case-placard-coverage')).toHaveLength(0)
114
+ })
115
+
116
+ test('coverage: a no-case marker fully exempts a non-component module', async () => {
117
+ const dir = await setup({
118
+ 'display-case.config.ts': config({ 'case-placard-coverage': true }),
119
+ 'fixtures.tsx':
120
+ '// display-case: no-case shared fixtures\nexport const data = 1\n',
121
+ })
122
+ expect(only(await run(dir), 'case-placard-coverage')).toHaveLength(0)
123
+ })
124
+
125
+ test('coverage: config ignore exempts matching paths', async () => {
126
+ const dir = await setup({
127
+ 'display-case.config.ts': config({
128
+ 'case-placard-coverage': true,
129
+ }).replace(
130
+ `'case-placard-coverage': {}`,
131
+ `'case-placard-coverage': { ignore: ['Button.tsx'] }`,
132
+ ),
133
+ 'Button.tsx': 'export const Button = () => null\n',
134
+ 'Button.case.tsx': caseFile(
135
+ `defineCases('Button', {}, { level: 'atom' })`,
136
+ ),
137
+ })
138
+ expect(only(await run(dir), 'case-placard-coverage')).toHaveLength(0)
139
+ })
140
+
141
+ // ── no-orphaned-placard-doc ────────────────────────────────────────────────
142
+
143
+ test('orphan: flags a placard doc with no case', async () => {
144
+ const dir = await setup({
145
+ 'display-case.config.ts': config({ 'no-orphaned-placard-doc': true }),
146
+ 'Lonely.placard.md': '# Lonely\n',
147
+ })
148
+ expect(only(await run(dir), 'no-orphaned-placard-doc')).toHaveLength(1)
149
+ })
150
+
151
+ test('orphan: passes with a sibling case (no component module needed)', async () => {
152
+ const dir = await setup({
153
+ 'display-case.config.ts': config({ 'no-orphaned-placard-doc': true }),
154
+ 'Lonely.placard.md': '# Lonely\n',
155
+ 'Lonely.case.tsx': caseFile(
156
+ `defineCases('Lonely', {}, { level: 'atom' })`,
157
+ ),
158
+ })
159
+ expect(only(await run(dir), 'no-orphaned-placard-doc')).toHaveLength(0)
160
+ })
161
+
162
+ test('orphan: an allow-orphan marker exempts the doc', async () => {
163
+ const dir = await setup({
164
+ 'display-case.config.ts': config({ 'no-orphaned-placard-doc': true }),
165
+ 'Lonely.placard.md':
166
+ '<!-- display-case: allow-orphan legacy -->\n# Lonely\n',
167
+ })
168
+ expect(only(await run(dir), 'no-orphaned-placard-doc')).toHaveLength(0)
169
+ })
170
+
171
+ // ── config-paths-exist ────────────────────────────────────────────────────
172
+
173
+ test('config-paths: flags a missing globalStyles entry', async () => {
174
+ const dir = await setup({
175
+ 'display-case.config.ts': config(
176
+ { 'config-paths-exist': true },
177
+ `, globalStyles: ['missing.css']`,
178
+ ),
179
+ })
180
+ expect(only(await run(dir), 'config-paths-exist')).toHaveLength(1)
181
+ })
182
+
183
+ test('config-paths: passes when the file exists', async () => {
184
+ const dir = await setup({
185
+ 'display-case.config.ts': config(
186
+ { 'config-paths-exist': true },
187
+ `, globalStyles: ['tokens.css']`,
188
+ ),
189
+ 'tokens.css': ':root{}',
190
+ })
191
+ expect(only(await run(dir), 'config-paths-exist')).toHaveLength(0)
192
+ })
193
+
194
+ test('config-paths: flags a baselineDir that points at a file', async () => {
195
+ const dir = await setup({
196
+ 'display-case.config.ts': config(
197
+ { 'config-paths-exist': true },
198
+ `, baselineDir: 'baselines'`,
199
+ ),
200
+ // `baselines` is a file, not the expected directory.
201
+ baselines: 'oops, a file\n',
202
+ })
203
+ const f = only(await run(dir), 'config-paths-exist')
204
+ expect(f).toHaveLength(1)
205
+ expect(f[0].message).toContain('not a directory')
206
+ })
207
+
208
+ test('config-paths: a not-yet-created baselineDir is fine', async () => {
209
+ const dir = await setup({
210
+ // Nothing recorded yet ⇒ the dir is absent; that is not a violation.
211
+ 'display-case.config.ts': config(
212
+ { 'config-paths-exist': true },
213
+ `, baselineDir: 'baselines'`,
214
+ ),
215
+ })
216
+ expect(only(await run(dir), 'config-paths-exist')).toHaveLength(0)
217
+ })
218
+
219
+ // ── setup-present ──────────────────────────────────────────────────────────
220
+
221
+ test('setup: flags a missing default toolchain', async () => {
222
+ const dir = await setup({
223
+ 'display-case.config.ts': config({ 'setup-present': true }),
224
+ })
225
+ // Point the tooling probe at the toolchain-free fixture too, so neither
226
+ // location resolves it — the genuinely-missing case.
227
+ expect(
228
+ only(await run(dir, { toolingDir: dir }), 'setup-present'),
229
+ ).toHaveLength(1)
230
+ })
231
+
232
+ test('setup: passes when the toolchain resolves via the display-case tooling', async () => {
233
+ const dir = await setup({
234
+ 'display-case.config.ts': config({ 'setup-present': true }),
235
+ })
236
+ // Default toolingDir is the display-case package, which carries the toolchain
237
+ // (as the visual backend resolves it). The consumer dir has none, yet the
238
+ // setup is present — proving the second probe location.
239
+ expect(only(await run(dir), 'setup-present')).toHaveLength(0)
240
+ })
241
+
242
+ test('setup: passes when a custom provider is configured', async () => {
243
+ const dir = await setup({
244
+ 'display-case.config.ts': config(
245
+ { 'setup-present': true },
246
+ `, providers: { diff: () => ({ changed: false }) }`,
247
+ ),
248
+ })
249
+ expect(
250
+ only(await run(dir, { toolingDir: dir }), 'setup-present'),
251
+ ).toHaveLength(0)
252
+ })
253
+
254
+ // ── primer-present-and-used ──────────────────────────────────────────────
255
+
256
+ test('primer: flags an unconfigured primer', async () => {
257
+ const dir = await setup({
258
+ 'display-case.config.ts': config({ 'primer-present-and-used': true }),
259
+ })
260
+ const f = only(await run(dir), 'primer-present-and-used')
261
+ expect(f).toHaveLength(1)
262
+ expect(f[0].message).toContain('no primer')
263
+ })
264
+
265
+ test('primer: flags a primer with no Display specimen', async () => {
266
+ const dir = await setup({
267
+ 'display-case.config.ts': config(
268
+ { 'primer-present-and-used': true },
269
+ `, primer: './primer.mdx'`,
270
+ ),
271
+ 'primer.mdx': '# Wall text\n\nJust prose, no specimens.\n',
272
+ })
273
+ const f = only(await run(dir), 'primer-present-and-used')
274
+ expect(f).toHaveLength(1)
275
+ expect(f[0].message).toContain('<Display>')
276
+ })
277
+
278
+ test('primer: passes with prose and a Display specimen', async () => {
279
+ const dir = await setup({
280
+ 'display-case.config.ts': config(
281
+ { 'primer-present-and-used': true },
282
+ `, primer: './primer.mdx'`,
283
+ ),
284
+ 'primer.mdx':
285
+ '# Wall text\n\nSome prose here.\n\n<Display title="x"><Button /></Display>\n',
286
+ })
287
+ expect(only(await run(dir), 'primer-present-and-used')).toHaveLength(0)
288
+ })
289
+
290
+ // ── levels-classified ─────────────────────────────────────────────────────
291
+
292
+ test('levels: flags an unclassified component', async () => {
293
+ const dir = await setup({
294
+ 'display-case.config.ts': config({ 'levels-classified': true }),
295
+ 'X.case.tsx': caseFile(`defineCases('X', { Default: () => null })`),
296
+ })
297
+ expect(only(await run(dir), 'levels-classified')).toHaveLength(1)
298
+ })
299
+
300
+ test('levels: passes when a level is declared', async () => {
301
+ const dir = await setup({
302
+ 'display-case.config.ts': config({ 'levels-classified': true }),
303
+ 'X.case.tsx': caseFile(
304
+ `defineCases('X', { Default: () => null }, { level: 'atom' })`,
305
+ ),
306
+ })
307
+ expect(only(await run(dir), 'levels-classified')).toHaveLength(0)
308
+ })
309
+
310
+ test('levels: an unclassified marker exempts the case', async () => {
311
+ const dir = await setup({
312
+ 'display-case.config.ts': config({ 'levels-classified': true }),
313
+ 'X.case.tsx': `// display-case: unclassified wip\n${caseFile(`defineCases('X', { Default: () => null })`)}`,
314
+ })
315
+ expect(only(await run(dir), 'levels-classified')).toHaveLength(0)
316
+ })
317
+
318
+ // ── cases-load ─────────────────────────────────────────────────────────────
319
+
320
+ test('cases-load: flags a malformed case file', async () => {
321
+ const dir = await setup({
322
+ 'display-case.config.ts': config({ 'cases-load': true }),
323
+ 'Bad.case.tsx': `export default { not: 'a case module' }\n`,
324
+ })
325
+ expect(only(await run(dir), 'cases-load')).toHaveLength(1)
326
+ })
327
+
328
+ // ── flow rules ─────────────────────────────────────────────────────────────
329
+
330
+ test('flow-transitions: flags a transition to a missing step', async () => {
331
+ const dir = await setup({
332
+ 'display-case.config.ts': config({ 'flow-transitions-resolve': true }),
333
+ 'F.case.tsx': caseFile(
334
+ `defineFlow('F', { steps: { A: { transitions: ['Nope'], render: () => null }, B: { render: () => null } } })`,
335
+ ),
336
+ })
337
+ expect(only(await run(dir), 'flow-transitions-resolve')).toHaveLength(1)
338
+ })
339
+
340
+ test('flow-transitions: passes when targets exist', async () => {
341
+ const dir = await setup({
342
+ 'display-case.config.ts': config({ 'flow-transitions-resolve': true }),
343
+ 'F.case.tsx': caseFile(
344
+ `defineFlow('F', { steps: { A: { transitions: ['B'], render: () => null }, B: { render: () => null } } })`,
345
+ ),
346
+ })
347
+ expect(only(await run(dir), 'flow-transitions-resolve')).toHaveLength(0)
348
+ })
349
+
350
+ test('flow-multi-step: flags a single-step flow', async () => {
351
+ const dir = await setup({
352
+ 'display-case.config.ts': config({ 'flow-multi-step': true }),
353
+ 'F.case.tsx': caseFile(
354
+ `defineFlow('F', { steps: { Only: { render: () => null } } })`,
355
+ ),
356
+ })
357
+ expect(only(await run(dir), 'flow-multi-step')).toHaveLength(1)
358
+ })
359
+
360
+ // ── unique-slugs ───────────────────────────────────────────────────────────
361
+
362
+ test('unique-slugs: flags two components colliding on slug', async () => {
363
+ const dir = await setup({
364
+ 'display-case.config.ts': config({ 'unique-slugs': true }),
365
+ 'A.case.tsx': caseFile(`defineCases('Sign in', {}, { level: 'page' })`),
366
+ 'B.case.tsx': caseFile(`defineCases('Sign-in', {}, { level: 'page' })`),
367
+ })
368
+ expect(only(await run(dir), 'unique-slugs')).toHaveLength(1)
369
+ })
370
+
371
+ test('unique-slugs: flags cases colliding within a component', async () => {
372
+ const dir = await setup({
373
+ 'display-case.config.ts': config({ 'unique-slugs': true }),
374
+ 'A.case.tsx': caseFile(
375
+ `defineCases('A', { 'Sign in': () => null, 'Sign-in': () => null }, { level: 'atom' })`,
376
+ ),
377
+ })
378
+ expect(only(await run(dir), 'unique-slugs')).toHaveLength(1)
379
+ })
380
+
381
+ // ── tweak-defaults-valid ──────────────────────────────────────────────────
382
+
383
+ test('tweak-defaults: flags a choice default outside its options', async () => {
384
+ const dir = await setup({
385
+ 'display-case.config.ts': config({ 'tweak-defaults-valid': true }),
386
+ 'A.case.tsx': caseFile(
387
+ `defineCases('A', { Default: { tweaks: { size: tweak.choice(['sm','lg'], 'sm') }, render: () => null } }, { level: 'atom' })`,
388
+ ).replace(`'sm') }`, `'xl' as 'sm') }`),
389
+ })
390
+ expect(only(await run(dir), 'tweak-defaults-valid')).toHaveLength(1)
391
+ })
392
+
393
+ test('tweak-defaults: passes for a valid choice default', async () => {
394
+ const dir = await setup({
395
+ 'display-case.config.ts': config({ 'tweak-defaults-valid': true }),
396
+ 'A.case.tsx': caseFile(
397
+ `defineCases('A', { Default: { tweaks: { size: tweak.choice(['sm','lg'], 'lg') }, render: () => null } }, { level: 'atom' })`,
398
+ ),
399
+ })
400
+ expect(only(await run(dir), 'tweak-defaults-valid')).toHaveLength(0)
401
+ })
402
+
403
+ // ── interactive-cases-keyed ───────────────────────────────────────────────
404
+
405
+ // A case file with a stateful `Demo` wrapper defined above the export, then
406
+ // `cases` rendered from a record of `Name: jsx` entries. `import 'react'` may
407
+ // not resolve in the temp dir, but the rule reads source text (not the loaded
408
+ // module), so a load failure is irrelevant here.
409
+ const statefulCase = (cases: string, header = '') =>
410
+ `${header}import { defineCases } from '${DC}'\n` +
411
+ `import { useState } from 'react'\n` +
412
+ `function Demo({ initial }: { initial: string }) {\n` +
413
+ ` const [v, setV] = useState(initial)\n` +
414
+ ` return <button type="button" onClick={() => setV('x')}>{v}</button>\n` +
415
+ `}\n` +
416
+ `export default defineCases('Seg', { ${cases} }, { level: 'atom' })\n`
417
+
418
+ test('interactive-keyed: flags a stateful specimen reused across cases without keys', async () => {
419
+ const dir = await setup({
420
+ 'display-case.config.ts': config({ 'interactive-cases-keyed': true }),
421
+ 'Seg.tsx': 'export const Seg = () => null\n',
422
+ 'Seg.case.tsx': statefulCase(
423
+ `A: () => <Demo initial="a" />, B: () => <Demo initial="b" />`,
424
+ ),
425
+ })
426
+ const f = only(await run(dir), 'interactive-cases-keyed')
427
+ expect(f).toHaveLength(1)
428
+ expect(f[0].message).toContain('<Demo>')
429
+ })
430
+
431
+ test('interactive-keyed: passes when every usage carries a key', async () => {
432
+ const dir = await setup({
433
+ 'display-case.config.ts': config({ 'interactive-cases-keyed': true }),
434
+ 'Seg.tsx': 'export const Seg = () => null\n',
435
+ 'Seg.case.tsx': statefulCase(
436
+ `A: () => <Demo key="a" initial="a" />, B: () => <Demo key="b" initial="b" />`,
437
+ ),
438
+ })
439
+ expect(only(await run(dir), 'interactive-cases-keyed')).toHaveLength(0)
440
+ })
441
+
442
+ test('interactive-keyed: a single-use stateful specimen is safe (always remounts)', async () => {
443
+ const dir = await setup({
444
+ 'display-case.config.ts': config({ 'interactive-cases-keyed': true }),
445
+ 'Seg.tsx': 'export const Seg = () => null\n',
446
+ 'Seg.case.tsx': statefulCase(
447
+ `A: () => <Demo initial="a" />, B: () => <span>static</span>`,
448
+ ),
449
+ })
450
+ expect(only(await run(dir), 'interactive-cases-keyed')).toHaveLength(0)
451
+ })
452
+
453
+ test('interactive-keyed: ignores a pure (stateless) component reused across cases', async () => {
454
+ const dir = await setup({
455
+ 'display-case.config.ts': config({ 'interactive-cases-keyed': true }),
456
+ 'Seg.tsx': 'export const Seg = () => null\n',
457
+ 'Seg.case.tsx':
458
+ `import { defineCases } from '${DC}'\n` +
459
+ `function Pure({ label }: { label: string }) { return <span>{label}</span> }\n` +
460
+ `export default defineCases('Seg', { A: () => <Pure label="a" />, B: () => <Pure label="b" /> }, { level: 'atom' })\n`,
461
+ })
462
+ expect(only(await run(dir), 'interactive-cases-keyed')).toHaveLength(0)
463
+ })
464
+
465
+ test('interactive-keyed: an allow marker waives the file', async () => {
466
+ const dir = await setup({
467
+ 'display-case.config.ts': config({ 'interactive-cases-keyed': true }),
468
+ 'Seg.tsx': 'export const Seg = () => null\n',
469
+ 'Seg.case.tsx': statefulCase(
470
+ `A: () => <Demo initial="a" />, B: () => <Demo initial="b" />`,
471
+ '// display-case: allow-interactive-cases-keyed shared on purpose\n',
472
+ ),
473
+ })
474
+ expect(only(await run(dir), 'interactive-cases-keyed')).toHaveLength(0)
475
+ })
476
+
477
+ // ── composition rules ──────────────────────────────────────────────────────
478
+
479
+ test('atom-purity: flags an atom importing another component', async () => {
480
+ const dir = await setup({
481
+ 'display-case.config.ts': config({ 'atom-purity': true }),
482
+ 'Button.tsx': `import { Icon } from './icon'\nexport const Button = () => Icon\n`,
483
+ 'Button.case.tsx': caseFile(
484
+ `defineCases('Button', {}, { level: 'atom' })`,
485
+ ),
486
+ 'icon.tsx': 'export const Icon = null\n',
487
+ 'icon.case.tsx': caseFile(`defineCases('Icon', {}, { level: 'atom' })`),
488
+ })
489
+ expect(only(await run(dir), 'atom-purity')).toHaveLength(1)
490
+ })
491
+
492
+ test('atom-purity: an unresolvable bare import is not a violation', async () => {
493
+ const dir = await setup({
494
+ 'display-case.config.ts': config({ 'atom-purity': true }),
495
+ 'Button.tsx': `import { useState } from 'react'\nexport const Button = () => useState\n`,
496
+ 'Button.case.tsx': caseFile(
497
+ `defineCases('Button', {}, { level: 'atom' })`,
498
+ ),
499
+ })
500
+ expect(only(await run(dir), 'atom-purity')).toHaveLength(0)
501
+ })
502
+
503
+ test('atom-purity: a commented-out or string-literal import is not a dependency', async () => {
504
+ const dir = await setup({
505
+ 'display-case.config.ts': config({ 'atom-purity': true }),
506
+ // The scanner ignores these; the old regex would have treated them as a
507
+ // real import of `Icon` and falsely flagged the atom.
508
+ 'Button.tsx':
509
+ `// import { Icon } from './icon'\n` +
510
+ `const sample = "import { Icon } from './icon'"\n` +
511
+ `export const Button = () => sample\n`,
512
+ 'Button.case.tsx': caseFile(
513
+ `defineCases('Button', {}, { level: 'atom' })`,
514
+ ),
515
+ 'icon.tsx': 'export const Icon = null\n',
516
+ 'icon.case.tsx': caseFile(`defineCases('Icon', {}, { level: 'atom' })`),
517
+ })
518
+ expect(only(await run(dir), 'atom-purity')).toHaveLength(0)
519
+ })
520
+
521
+ test('no-downward-dependency: flags a molecule importing an organism', async () => {
522
+ const dir = await setup({
523
+ 'display-case.config.ts': config({ 'no-downward-dependency': true }),
524
+ 'Mol.tsx': `import { Org } from './org'\nexport const Mol = () => Org\n`,
525
+ 'Mol.case.tsx': caseFile(`defineCases('Mol', {}, { level: 'molecule' })`),
526
+ 'org.case.tsx': caseFile(`defineCases('Org', {}, { level: 'organism' })`),
527
+ })
528
+ expect(only(await run(dir), 'no-downward-dependency')).toHaveLength(1)
529
+ })
530
+
531
+ test('no-downward-dependency: same-level import is allowed', async () => {
532
+ const dir = await setup({
533
+ 'display-case.config.ts': config({ 'no-downward-dependency': true }),
534
+ 'Org1.tsx': `import { Org2 } from './org2'\nexport const Org1 = () => Org2\n`,
535
+ 'Org1.case.tsx': caseFile(
536
+ `defineCases('Org1', {}, { level: 'organism' })`,
537
+ ),
538
+ 'org2.case.tsx': caseFile(
539
+ `defineCases('Org2', {}, { level: 'organism' })`,
540
+ ),
541
+ })
542
+ expect(only(await run(dir), 'no-downward-dependency')).toHaveLength(0)
543
+ })
544
+
545
+ test('composes-lower-level: an organism of only atoms passes', async () => {
546
+ const dir = await setup({
547
+ 'display-case.config.ts': config({ 'composes-lower-level': true }),
548
+ 'Org.tsx': `import { A } from './a'\nexport const Org = () => A\n`,
549
+ 'Org.case.tsx': caseFile(`defineCases('Org', {}, { level: 'organism' })`),
550
+ 'a.case.tsx': caseFile(`defineCases('A', {}, { level: 'atom' })`),
551
+ })
552
+ expect(only(await run(dir), 'composes-lower-level')).toHaveLength(0)
553
+ })
554
+
555
+ test('composes-lower-level: flags a molecule composing nothing lower', async () => {
556
+ const dir = await setup({
557
+ 'display-case.config.ts': config({ 'composes-lower-level': true }),
558
+ 'Mol.tsx': `import { useState } from 'react'\nexport const Mol = () => useState\n`,
559
+ 'Mol.case.tsx': caseFile(`defineCases('Mol', {}, { level: 'molecule' })`),
560
+ })
561
+ expect(only(await run(dir), 'composes-lower-level')).toHaveLength(1)
562
+ })
563
+
564
+ test('composition: a generic allow-<rule-id> marker exempts the file', async () => {
565
+ const dir = await setup({
566
+ 'display-case.config.ts': config({ 'no-downward-dependency': true }),
567
+ // The marker waives this rule for this component only.
568
+ 'Mol.tsx': `// display-case: allow-no-downward-dependency intentional\nimport { Org } from './org'\nexport const Mol = () => Org\n`,
569
+ 'Mol.case.tsx': caseFile(`defineCases('Mol', {}, { level: 'molecule' })`),
570
+ 'org.case.tsx': caseFile(`defineCases('Org', {}, { level: 'organism' })`),
571
+ })
572
+ expect(only(await run(dir), 'no-downward-dependency')).toHaveLength(0)
573
+ })
574
+
575
+ // ── level-fit (advisory) ─────────────────────────────────────────────────
576
+
577
+ test('level-fit: flags a component over its level threshold', async () => {
578
+ const rules = ALL_RULES.map((id) =>
579
+ id === 'level-fit'
580
+ ? `'level-fit': { thresholds: { molecule: 1 } }`
581
+ : `'${id}': false`,
582
+ ).join(', ')
583
+ const dir = await setup({
584
+ 'display-case.config.ts': `export default { title:'F', roots:['**/*.case.tsx'], check:{ structure:{ rules:{ ${rules} } } } }\n`,
585
+ // A molecule composing two atoms exceeds the threshold of 1.
586
+ 'Mol.tsx': `import { A } from './a'\nimport { B } from './b'\nexport const Mol = () => [A, B]\n`,
587
+ 'Mol.case.tsx': caseFile(`defineCases('Mol', {}, { level: 'molecule' })`),
588
+ 'a.case.tsx': caseFile(`defineCases('A', {}, { level: 'atom' })`),
589
+ 'b.case.tsx': caseFile(`defineCases('B', {}, { level: 'atom' })`),
590
+ })
591
+ const f = only(await run(dir), 'level-fit')
592
+ expect(f).toHaveLength(1)
593
+ expect(f[0].severity).toBe('warn')
594
+ expect(f[0].message).toContain('promoting')
595
+ })
596
+
597
+ test('level-fit: off by default', async () => {
598
+ const dir = await setup({
599
+ // No check config ⇒ defaults apply; level-fit is opt-in (off).
600
+ 'display-case.config.ts': `export default { title:'F', roots:['**/*.case.tsx'] }\n`,
601
+ 'Mol.tsx': `import { A } from './a'\nimport { B } from './b'\nexport const Mol = () => [A, B]\n`,
602
+ 'Mol.case.tsx': caseFile(`defineCases('Mol', {}, { level: 'molecule' })`),
603
+ 'a.case.tsx': caseFile(`defineCases('A', {}, { level: 'atom' })`),
604
+ 'b.case.tsx': caseFile(`defineCases('B', {}, { level: 'atom' })`),
605
+ })
606
+ expect(only(await run(dir), 'level-fit')).toHaveLength(0)
607
+ })
608
+
609
+ // ── severity ───────────────────────────────────────────────────────────────
610
+
611
+ test('severity: composes-lower-level defaults to warn', async () => {
612
+ const dir = await setup({
613
+ 'display-case.config.ts': config({ 'composes-lower-level': true }),
614
+ 'Mol.tsx': `export const Mol = () => null\n`,
615
+ 'Mol.case.tsx': caseFile(`defineCases('Mol', {}, { level: 'molecule' })`),
616
+ })
617
+ const f = only(await run(dir), 'composes-lower-level')
618
+ expect(f).toHaveLength(1)
619
+ expect(f[0].severity).toBe('warn')
620
+ })
621
+
622
+ test('severity: a per-rule override flips warn to error', async () => {
623
+ const dir = await setup({
624
+ 'display-case.config.ts': config({ 'composes-lower-level': 'error' }),
625
+ 'Mol.tsx': `export const Mol = () => null\n`,
626
+ 'Mol.case.tsx': caseFile(`defineCases('Mol', {}, { level: 'molecule' })`),
627
+ })
628
+ expect(only(await run(dir), 'composes-lower-level')[0].severity).toBe(
629
+ 'error',
630
+ )
631
+ })
632
+
633
+ test('severity: strict mode escalates warnings to errors', async () => {
634
+ const dir = await setup({
635
+ 'display-case.config.ts': config({ 'composes-lower-level': true }),
636
+ 'Mol.tsx': `export const Mol = () => null\n`,
637
+ 'Mol.case.tsx': caseFile(`defineCases('Mol', {}, { level: 'molecule' })`),
638
+ })
639
+ const f = only(await run(dir, { strict: true }), 'composes-lower-level')
640
+ expect(f[0].severity).toBe('error')
641
+ })
642
+
643
+ test('a disabled rule contributes no findings', async () => {
644
+ const dir = await setup({
645
+ // Every rule disabled.
646
+ 'display-case.config.ts': config({}),
647
+ 'Widget.tsx': 'export const Widget = () => null\n',
648
+ })
649
+ expect(await run(dir)).toHaveLength(0)
650
+ })
651
+ })