@invisibleloop/pulse 0.20.11 → 0.20.20

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 (266) hide show
  1. package/package.json +6 -5
  2. package/public/.pulse-ui-version +1 -1
  3. package/scripts/build.js +26 -0
  4. package/scripts/release-version.js +17 -8
  5. package/scripts/strip-server.test.js +60 -0
  6. package/src/agent/checklist.md +8 -12
  7. package/src/agent/commands/pulse-start.md +2 -0
  8. package/src/agent/commands/verify.md +30 -8
  9. package/src/agent/guide-components.md +7 -25
  10. package/src/agent/guide-routing.md +0 -12
  11. package/src/agent/guide-server.md +102 -1
  12. package/src/agent/guide-spec.md +8 -1
  13. package/src/agent/guide-styles.md +23 -0
  14. package/src/agent/identity.md +9 -0
  15. package/src/agent/workflow.md +4 -23
  16. package/src/cli/check-bundles.js +113 -0
  17. package/src/cli/check-bundles.test.js +137 -0
  18. package/src/cli/cli.test.js +105 -0
  19. package/src/cli/dev.js +48 -3
  20. package/src/cli/diagnose.js +105 -0
  21. package/src/cli/diagnose.test.js +124 -0
  22. package/src/cli/discover.js +9 -2
  23. package/src/cli/index.js +171 -9
  24. package/src/cli/process-utils.js +57 -0
  25. package/src/cli/process-utils.test.js +114 -0
  26. package/src/cli/review.js +391 -0
  27. package/src/cli/review.test.js +143 -0
  28. package/src/cli/scaffold.js +9 -4
  29. package/src/cli/stamp.js +102 -0
  30. package/src/cli/stamp.test.js +149 -0
  31. package/src/cli/start.js +15 -2
  32. package/src/cli/validate.js +184 -0
  33. package/src/cli/validate.test.js +153 -0
  34. package/src/mcp/mcp.test.js +261 -30
  35. package/src/mcp/server.js +207 -515
  36. package/src/mcp/validate-worker.js +18 -1
  37. package/src/runtime/index.js +144 -11
  38. package/src/runtime/runtime.test.js +199 -0
  39. package/src/runtime/ssr.js +13 -6
  40. package/src/runtime/ssr.test.js +31 -0
  41. package/src/server/index.js +162 -9
  42. package/src/server/server.test.js +225 -2
  43. package/src/spec/schema.js +1 -1
  44. package/src/ui/index.js +0 -5
  45. package/src/ui/ui.test.js +0 -75
  46. package/types/ui.d.ts +0 -21
  47. package/.claude/commands/build-page.md +0 -63
  48. package/.claude/commands/new-doc-page.md +0 -45
  49. package/.claude/commands/pulse-start.md +0 -11
  50. package/.claude/commands/pulse-stop.md +0 -5
  51. package/.claude/commands/verify.md +0 -118
  52. package/.claude/pulse-checklist.md +0 -189
  53. package/.github/workflows/ci.yml +0 -25
  54. package/.github/workflows/publish.yml +0 -45
  55. package/.pulse/load-reports/home/1773432711417.json +0 -22
  56. package/.pulse-verified +0 -1
  57. package/AGENTS.md +0 -697
  58. package/CLAUDE.md +0 -697
  59. package/README.md +0 -95
  60. package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
  61. package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
  62. package/benchmark/public/dist/manifest.json +0 -5
  63. package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
  64. package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
  65. package/benchmark/results/2026-03-27-09-20.json +0 -188
  66. package/benchmark/results/2026-03-27-09-21.json +0 -188
  67. package/benchmark/results/2026-03-27-09-29.json +0 -188
  68. package/benchmark/results/2026-03-27-10-14.json +0 -188
  69. package/benchmark/results/2026-03-27-10-18.json +0 -232
  70. package/benchmark/src/pages/counter.js +0 -37
  71. package/benchmark/src/pages/home.js +0 -21
  72. package/docs/.claude/commands/pulse-build.md +0 -7
  73. package/docs/.claude/commands/pulse-dev.md +0 -7
  74. package/docs/.claude/commands/pulse-start.md +0 -9
  75. package/docs/.claude/commands/pulse-stop.md +0 -5
  76. package/docs/.claude/commands/verify.md +0 -84
  77. package/docs/.claude/coverage-check.js +0 -196
  78. package/docs/.claude/pulse-checklist.md +0 -189
  79. package/docs/public/.pulse-ui-version +0 -1
  80. package/docs/public/andypixel.css +0 -237
  81. package/docs/public/bookworms.css +0 -422
  82. package/docs/public/coutumes.css +0 -561
  83. package/docs/public/docs.css +0 -2388
  84. package/docs/public/favicon.svg +0 -4
  85. package/docs/public/home-brut.css +0 -982
  86. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  87. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  88. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  89. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  90. package/docs/public/images/journey/178.jpg +0 -0
  91. package/docs/public/images/journey/29.jpg +0 -0
  92. package/docs/public/images/retrowheels/hero.jpg +0 -0
  93. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  94. package/docs/public/images/suzukibento/1015.jpg +0 -0
  95. package/docs/public/images/suzukibento/1025.jpg +0 -0
  96. package/docs/public/images/suzukibento/1043.jpg +0 -0
  97. package/docs/public/images/suzukibento/1059.jpg +0 -0
  98. package/docs/public/images/suzukibento/1062.jpg +0 -0
  99. package/docs/public/images/suzukibento/1084.jpg +0 -0
  100. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  101. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  102. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  103. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  104. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  105. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  106. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  107. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  108. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  109. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  110. package/docs/public/img/heic1501a.jpg +0 -0
  111. package/docs/public/img/placeholder-landscape.svg +0 -18
  112. package/docs/public/img/placeholder-square.svg +0 -14
  113. package/docs/public/img/placeholder-tall.svg +0 -22
  114. package/docs/public/journey.css +0 -528
  115. package/docs/public/menu.js +0 -327
  116. package/docs/public/pulse-ui.css +0 -3028
  117. package/docs/public/pulse-ui.js +0 -257
  118. package/docs/public/retrowheels.css +0 -425
  119. package/docs/public/shop.css +0 -269
  120. package/docs/public/sparkys.css +0 -382
  121. package/docs/public/suzukibento.css +0 -517
  122. package/docs/public/swish.css +0 -394
  123. package/docs/public/theme.css +0 -52
  124. package/docs/public/themes/lumio.css +0 -50
  125. package/docs/public/themes/observatory.css +0 -104
  126. package/docs/server.js +0 -161
  127. package/docs/src/lib/component-page.js +0 -49
  128. package/docs/src/lib/highlight.js +0 -2
  129. package/docs/src/lib/layout.js +0 -190
  130. package/docs/src/lib/metrics-store.js +0 -6
  131. package/docs/src/lib/nav.js +0 -171
  132. package/docs/src/lib/stats.js +0 -81
  133. package/docs/src/pages/accessibility.js +0 -157
  134. package/docs/src/pages/actions.js +0 -194
  135. package/docs/src/pages/auth.js +0 -176
  136. package/docs/src/pages/caching.js +0 -95
  137. package/docs/src/pages/components/accordion.js +0 -48
  138. package/docs/src/pages/components/alert.js +0 -35
  139. package/docs/src/pages/components/app-badge.js +0 -41
  140. package/docs/src/pages/components/avatar.js +0 -35
  141. package/docs/src/pages/components/badge.js +0 -36
  142. package/docs/src/pages/components/breadcrumbs.js +0 -94
  143. package/docs/src/pages/components/button.js +0 -84
  144. package/docs/src/pages/components/card.js +0 -225
  145. package/docs/src/pages/components/charts.js +0 -283
  146. package/docs/src/pages/components/checkbox.js +0 -129
  147. package/docs/src/pages/components/cluster.js +0 -47
  148. package/docs/src/pages/components/container.js +0 -40
  149. package/docs/src/pages/components/cta.js +0 -54
  150. package/docs/src/pages/components/divider.js +0 -37
  151. package/docs/src/pages/components/empty.js +0 -36
  152. package/docs/src/pages/components/feature.js +0 -60
  153. package/docs/src/pages/components/fieldset.js +0 -65
  154. package/docs/src/pages/components/fileupload.js +0 -128
  155. package/docs/src/pages/components/footer.js +0 -58
  156. package/docs/src/pages/components/grid.js +0 -169
  157. package/docs/src/pages/components/heading.js +0 -107
  158. package/docs/src/pages/components/hero.js +0 -123
  159. package/docs/src/pages/components/icons.js +0 -410
  160. package/docs/src/pages/components/image.js +0 -71
  161. package/docs/src/pages/components/input.js +0 -51
  162. package/docs/src/pages/components/media.js +0 -51
  163. package/docs/src/pages/components/modal.js +0 -111
  164. package/docs/src/pages/components/nav.js +0 -136
  165. package/docs/src/pages/components/phone-frame.js +0 -74
  166. package/docs/src/pages/components/progress.js +0 -102
  167. package/docs/src/pages/components/prose.js +0 -111
  168. package/docs/src/pages/components/pullquote.js +0 -71
  169. package/docs/src/pages/components/radio.js +0 -194
  170. package/docs/src/pages/components/rating.js +0 -106
  171. package/docs/src/pages/components/search.js +0 -61
  172. package/docs/src/pages/components/section.js +0 -98
  173. package/docs/src/pages/components/segmented.js +0 -121
  174. package/docs/src/pages/components/select.js +0 -45
  175. package/docs/src/pages/components/slider.js +0 -115
  176. package/docs/src/pages/components/spinner.js +0 -73
  177. package/docs/src/pages/components/stack.js +0 -48
  178. package/docs/src/pages/components/stat.js +0 -55
  179. package/docs/src/pages/components/stepper.js +0 -66
  180. package/docs/src/pages/components/table.js +0 -45
  181. package/docs/src/pages/components/textarea.js +0 -31
  182. package/docs/src/pages/components/toggle.js +0 -84
  183. package/docs/src/pages/components/tooltip.js +0 -48
  184. package/docs/src/pages/components.js +0 -200
  185. package/docs/src/pages/config.js +0 -193
  186. package/docs/src/pages/constraints.js +0 -100
  187. package/docs/src/pages/deploy.js +0 -233
  188. package/docs/src/pages/error-pages.js +0 -99
  189. package/docs/src/pages/extending.js +0 -198
  190. package/docs/src/pages/faq.js +0 -99
  191. package/docs/src/pages/forms.js +0 -129
  192. package/docs/src/pages/getting-started.js +0 -147
  193. package/docs/src/pages/guard.js +0 -120
  194. package/docs/src/pages/home.js +0 -350
  195. package/docs/src/pages/how-it-works.js +0 -244
  196. package/docs/src/pages/hydration.js +0 -108
  197. package/docs/src/pages/images.js +0 -120
  198. package/docs/src/pages/markdown.js +0 -156
  199. package/docs/src/pages/meta.js +0 -193
  200. package/docs/src/pages/mutations.js +0 -107
  201. package/docs/src/pages/navigation.js +0 -85
  202. package/docs/src/pages/performance.js +0 -157
  203. package/docs/src/pages/persist.js +0 -89
  204. package/docs/src/pages/project-structure.js +0 -91
  205. package/docs/src/pages/prompt-examples.js +0 -186
  206. package/docs/src/pages/raw-responses.js +0 -122
  207. package/docs/src/pages/routing.js +0 -120
  208. package/docs/src/pages/server-api.js +0 -291
  209. package/docs/src/pages/server-data.js +0 -203
  210. package/docs/src/pages/slash-commands.js +0 -55
  211. package/docs/src/pages/spec.js +0 -207
  212. package/docs/src/pages/state.js +0 -102
  213. package/docs/src/pages/store.js +0 -203
  214. package/docs/src/pages/streaming.js +0 -107
  215. package/docs/src/pages/stripe.js +0 -194
  216. package/docs/src/pages/supabase.js +0 -324
  217. package/docs/src/pages/templates/andypixel.js +0 -260
  218. package/docs/src/pages/templates/blog-post.js +0 -226
  219. package/docs/src/pages/templates/bookworms.js +0 -227
  220. package/docs/src/pages/templates/coutumes.js +0 -237
  221. package/docs/src/pages/templates/journey.js +0 -186
  222. package/docs/src/pages/templates/mobile-app.js +0 -371
  223. package/docs/src/pages/templates/retrowheels.js +0 -355
  224. package/docs/src/pages/templates/shop.js +0 -268
  225. package/docs/src/pages/templates/sparkys.js +0 -264
  226. package/docs/src/pages/templates/suzukibento.js +0 -200
  227. package/docs/src/pages/templates/swish.js +0 -313
  228. package/docs/src/pages/templates.js +0 -534
  229. package/docs/src/pages/testing.js +0 -199
  230. package/docs/src/pages/validation.js +0 -138
  231. package/examples/.claude/commands/pulse-build.md +0 -7
  232. package/examples/.claude/commands/pulse-dev.md +0 -7
  233. package/examples/.claude/commands/pulse-start.md +0 -9
  234. package/examples/.claude/commands/pulse-stop.md +0 -5
  235. package/examples/.claude/commands/verify.md +0 -84
  236. package/examples/.claude/coverage-check.js +0 -196
  237. package/examples/.claude/pulse-checklist.md +0 -189
  238. package/examples/andypixel.css +0 -237
  239. package/examples/andypixel.js +0 -258
  240. package/examples/bookworms.css +0 -422
  241. package/examples/bookworms.js +0 -225
  242. package/examples/contact.js +0 -166
  243. package/examples/counter.js +0 -94
  244. package/examples/dev.server.js +0 -110
  245. package/examples/examples.test.js +0 -394
  246. package/examples/journey.css +0 -528
  247. package/examples/journey.js +0 -186
  248. package/examples/pricing.js +0 -262
  249. package/examples/products.js +0 -190
  250. package/examples/public/.pulse-ui-version +0 -1
  251. package/examples/public/pulse-ui.css +0 -3028
  252. package/examples/public/pulse-ui.js +0 -257
  253. package/examples/quiz.js +0 -208
  254. package/examples/retrowheels.css +0 -425
  255. package/examples/retrowheels.js +0 -353
  256. package/examples/shared.js +0 -84
  257. package/examples/suzukibento.css +0 -513
  258. package/examples/suzukibento.js +0 -200
  259. package/examples/swish.css +0 -394
  260. package/examples/swish.js +0 -311
  261. package/examples/todos.js +0 -162
  262. package/src/ui/fieldset.js +0 -37
  263. package/src/ui/phone-frame.js +0 -40
  264. package/src/ui/search.js +0 -78
  265. package/src/ui/segmented.js +0 -50
  266. package/src/ui/tooltip.js +0 -28
@@ -1,394 +0,0 @@
1
- /**
2
- * Pulse — Examples test suite
3
- *
4
- * View tests (renderSync) for all six example specs.
5
- * Unit tests for extracted pure logic functions.
6
- *
7
- * Run: node examples/examples.test.js
8
- */
9
-
10
- import { test } from 'node:test'
11
- import assert from 'node:assert/strict'
12
- import { renderSync, render } from '../src/testing/index.js'
13
-
14
- import counter from './counter.js'
15
- import todos from './todos.js'
16
- import contact from './contact.js'
17
- import quiz, { QUESTIONS, scoreLabel } from './quiz.js'
18
- import products, { applyFilters, CATEGORIES } from './products.js'
19
- import pricing, { FAQ } from './pricing.js'
20
-
21
- import { filterTodos, countByStatus } from './todos.js'
22
-
23
- // ─── Counter ─────────────────────────────────────────────────────────────────
24
-
25
- test('counter: renders initial value 0', () => {
26
- const r = renderSync(counter, { state: { count: 0, step: 1 } })
27
- const countSpan = r.find('.u-text-accent')
28
- assert(countSpan, 'count span should exist')
29
- assert.equal(countSpan.text, '0')
30
- })
31
-
32
- test('counter: shows progress bar', () => {
33
- const r = renderSync(counter, { state: { count: 10, step: 1 } })
34
- const fill = r.find('.ui-progress-fill')
35
- assert(fill, 'track fill should exist')
36
- assert(fill.attr('style')?.includes('50%'), 'fill width should be 50% at count 10')
37
- })
38
-
39
- test('counter: decrement disabled at min', () => {
40
- const r = renderSync(counter, { state: { count: 0, step: 1 } })
41
- const dec = r.find('[aria-label="Decrement"]')
42
- assert(dec, 'decrement button should exist')
43
- assert.equal(dec.attr('disabled'), '', 'decrement should be disabled at 0')
44
- })
45
-
46
- test('counter: increment disabled at max', () => {
47
- const r = renderSync(counter, { state: { count: 20, step: 1 } })
48
- const inc = r.find('[aria-label="Increment"]')
49
- assert(inc, 'increment button should exist')
50
- assert.equal(inc.attr('disabled'), '', 'increment should be disabled at 20')
51
- })
52
-
53
- test('counter: active step is checked in segmented control', () => {
54
- const r = renderSync(counter, { state: { count: 5, step: 2 } })
55
- const checked = r.find('input[value="2"]')
56
- assert(checked, 'step 2 input should exist')
57
- assert.equal(checked.attr('checked'), '', 'step 2 should be checked when step is 2')
58
- })
59
-
60
- test('counter: has main landmark', () => {
61
- const r = renderSync(counter, { state: { count: 0, step: 1 } })
62
- assert(r.has('#main-content'), 'main landmark must exist')
63
- })
64
-
65
- // ─── Todos — unit tests ───────────────────────────────────────────────────────
66
-
67
- test('filterTodos: all returns all', () => {
68
- const todos = [{ id: 1, text: 'a', done: false }, { id: 2, text: 'b', done: true }]
69
- assert.equal(filterTodos(todos, 'all').length, 2)
70
- })
71
-
72
- test('filterTodos: active returns only undone', () => {
73
- const todos = [{ id: 1, text: 'a', done: false }, { id: 2, text: 'b', done: true }]
74
- const result = filterTodos(todos, 'active')
75
- assert.equal(result.length, 1)
76
- assert.equal(result[0].id, 1)
77
- })
78
-
79
- test('filterTodos: done returns only completed', () => {
80
- const todos = [{ id: 1, text: 'a', done: false }, { id: 2, text: 'b', done: true }]
81
- const result = filterTodos(todos, 'done')
82
- assert.equal(result.length, 1)
83
- assert.equal(result[0].id, 2)
84
- })
85
-
86
- test('countByStatus: counts correctly', () => {
87
- const todos = [
88
- { id: 1, text: 'a', done: false },
89
- { id: 2, text: 'b', done: true },
90
- { id: 3, text: 'c', done: false },
91
- ]
92
- const { active, done, total } = countByStatus(todos)
93
- assert.equal(active, 2)
94
- assert.equal(done, 1)
95
- assert.equal(total, 3)
96
- })
97
-
98
- test('countByStatus: empty array', () => {
99
- const { active, done, total } = countByStatus([])
100
- assert.equal(active, 0)
101
- assert.equal(done, 0)
102
- assert.equal(total, 0)
103
- })
104
-
105
- // ─── Todos — view tests ───────────────────────────────────────────────────────
106
-
107
- test('todos: empty state shows empty component', () => {
108
- const r = renderSync(todos, { state: { todos: [], filter: 'all', nextId: 1 } })
109
- assert(r.has('.ui-empty'), 'empty state component should render')
110
- })
111
-
112
- test('todos: renders todo items', () => {
113
- const state = {
114
- todos: [{ id: 1, text: 'Buy milk', done: false }, { id: 2, text: 'Walk dog', done: true }],
115
- filter: 'all',
116
- nextId: 3,
117
- }
118
- const r = renderSync(todos, { state })
119
- assert.equal(r.count('[data-id]'), 2)
120
- })
121
-
122
- test('todos: done item has muted text span', () => {
123
- const state = {
124
- todos: [{ id: 1, text: 'Done task', done: true }],
125
- filter: 'all',
126
- nextId: 2,
127
- }
128
- const r = renderSync(todos, { state })
129
- assert(r.has('.u-text-muted'), 'done item text should be muted')
130
- const cb = r.find('input[type="checkbox"]')
131
- assert.equal(cb?.attr('checked'), '', 'done item checkbox should be checked')
132
- })
133
-
134
- test('todos: active filter hides done items', () => {
135
- const state = {
136
- todos: [{ id: 1, text: 'Active', done: false }, { id: 2, text: 'Done', done: true }],
137
- filter: 'active',
138
- nextId: 3,
139
- }
140
- const r = renderSync(todos, { state })
141
- assert.equal(r.count('[data-id]'), 1)
142
- })
143
-
144
- test('todos: at limit shows limit message', () => {
145
- const state = {
146
- todos: Array.from({ length: 20 }, (_, i) => ({ id: i + 1, text: `Task ${i + 1}`, done: false })),
147
- filter: 'all',
148
- nextId: 21,
149
- }
150
- const r = renderSync(todos, { state })
151
- assert(r.has('[role="alert"]'), 'limit alert should appear at 20 todos')
152
- })
153
-
154
- test('todos: has main landmark', () => {
155
- const r = renderSync(todos, { state: { todos: [], filter: 'all', nextId: 1 } })
156
- assert(r.has('#main-content'))
157
- })
158
-
159
- // ─── Contact — view tests ─────────────────────────────────────────────────────
160
-
161
- test('contact: renders form in idle state', async () => {
162
- const r = await render(contact, { server: { info: { email: 'hi@example.com', phone: '+44 20 0000 0000', address: '1 Street', hours: '9–5' } } })
163
- assert(r.has('form[data-action="submit"]'), 'form should be visible in idle state')
164
- })
165
-
166
- test('contact: success state shows success content, hides form', async () => {
167
- const r = await render(contact, {
168
- state: { status: 'success', errors: [] },
169
- server: { info: { email: 'hi@example.com', phone: '+44 20 0000 0000', address: '1 Street', hours: '9–5' } },
170
- })
171
- assert(!r.has('form'), 'form should be hidden on success')
172
- const h2s = r.findAll('h2')
173
- assert(h2s.some(h => h.text === 'Message sent!'), 'success heading should appear')
174
- })
175
-
176
- test('contact: has main landmark', async () => {
177
- const r = await render(contact, {
178
- server: { info: { email: 'hi@example.com', phone: '+44 20 0000 0000', address: '1 Street', hours: '9–5' } },
179
- })
180
- assert(r.has('#main-content'))
181
- })
182
-
183
- // ─── Quiz — unit tests ────────────────────────────────────────────────────────
184
-
185
- test('scoreLabel: perfect score', () => {
186
- const { label, variant } = scoreLabel(5, 5)
187
- assert.equal(label, 'Perfect score!')
188
- assert.equal(variant, 'success')
189
- })
190
-
191
- test('scoreLabel: 80% is excellent', () => {
192
- const { label, variant } = scoreLabel(4, 5)
193
- assert.equal(label, 'Excellent!')
194
- assert.equal(variant, 'success')
195
- })
196
-
197
- test('scoreLabel: 60% is good', () => {
198
- const { label, variant } = scoreLabel(3, 5)
199
- assert.equal(label, 'Good effort!')
200
- assert.equal(variant, 'info')
201
- })
202
-
203
- test('scoreLabel: 40% keeps practising', () => {
204
- const { label, variant } = scoreLabel(2, 5)
205
- assert.equal(label, 'Keep practising!')
206
- assert.equal(variant, 'warning')
207
- })
208
-
209
- test('scoreLabel: below 40%', () => {
210
- const { label, variant } = scoreLabel(1, 5)
211
- assert.equal(label, 'Better luck next time.')
212
- assert.equal(variant, 'error')
213
- })
214
-
215
- test('QUESTIONS has 5 items', () => {
216
- assert.equal(QUESTIONS.length, 5)
217
- })
218
-
219
- test('each question has a valid answer index', () => {
220
- for (const q of QUESTIONS) {
221
- assert(q.answer >= 0 && q.answer < q.options.length,
222
- `Question ${q.id} answer index ${q.answer} out of range`)
223
- }
224
- })
225
-
226
- // ─── Quiz — view tests ────────────────────────────────────────────────────────
227
-
228
- test('quiz: intro phase renders start button', () => {
229
- const r = renderSync(quiz, { state: { phase: 'intro', current: 0, answers: [], score: 0 } })
230
- assert(r.has('[data-event="start"]'), 'start button should exist on intro')
231
- assert(r.has('#main-content'))
232
- })
233
-
234
- test('quiz: question phase renders options', () => {
235
- const r = renderSync(quiz, { state: { phase: 'question', current: 0, answers: [], score: 0 } })
236
- assert.equal(r.count('[data-option]'), 4, 'first question has 4 options')
237
- })
238
-
239
- test('quiz: question phase shows progress', () => {
240
- const r = renderSync(quiz, { state: { phase: 'question', current: 2, answers: [1, 0], score: 1 } })
241
- assert(r.has('[role="progressbar"]'), 'progress bar should exist')
242
- const progressText = r.find('.u-text-muted')
243
- assert(progressText?.text.includes('3 of 5'), 'progress should show question 3 of 5')
244
- })
245
-
246
- test('quiz: results phase shows score', () => {
247
- const r = renderSync(quiz, { state: { phase: 'results', current: 4, answers: [1, 2, 0, 3, 1], score: 5 } })
248
- const scoreSpan = r.find('.u-text-accent')
249
- assert(scoreSpan, 'score span should exist')
250
- assert.equal(scoreSpan.text, '5')
251
- assert.equal(r.count('.u-flex.u-items-start'), 5, 'one row per question')
252
- })
253
-
254
- test('quiz: results shows correct/wrong rows', () => {
255
- // answer correctly for q1 (answer=1), wrong for q2 (answer=2 but we choose 0)
256
- const r = renderSync(quiz, {
257
- state: { phase: 'results', current: 4, answers: [1, 0, 0, 3, 1], score: 4 },
258
- })
259
- assert(r.has('.u-text-green'), 'correct row icon should be green')
260
- assert(r.has('.u-text-red'), 'wrong row icon should be red')
261
- })
262
-
263
- test('quiz: results has restart button', () => {
264
- const r = renderSync(quiz, { state: { phase: 'results', current: 4, answers: [1, 2, 0, 3, 1], score: 3 } })
265
- assert(r.has('[data-event="restart"]'))
266
- })
267
-
268
- // ─── Products — unit tests ────────────────────────────────────────────────────
269
-
270
- const MOCK_PRODUCTS = [
271
- { id: 1, name: 'Arc Lamp', category: 'Lighting', price: 89, rating: 4.8, reviews: 100, featured: true },
272
- { id: 2, name: 'Walnut Tray', category: 'Storage', price: 45, rating: 4.5, reviews: 50, featured: false },
273
- { id: 3, name: 'Desk Pad', category: 'Lighting', price: 32, rating: 4.2, reviews: 200, featured: false },
274
- ]
275
-
276
- test('applyFilters: no filters returns all', () => {
277
- assert.equal(applyFilters(MOCK_PRODUCTS, '', 'All', 'featured').length, 3)
278
- })
279
-
280
- test('applyFilters: category filter', () => {
281
- const r = applyFilters(MOCK_PRODUCTS, '', 'Lighting', 'featured')
282
- assert.equal(r.length, 2)
283
- assert(r.every(p => p.category === 'Lighting'))
284
- })
285
-
286
- test('applyFilters: search filter', () => {
287
- const r = applyFilters(MOCK_PRODUCTS, 'lamp', 'All', 'featured')
288
- assert.equal(r.length, 1)
289
- assert.equal(r[0].name, 'Arc Lamp')
290
- })
291
-
292
- test('applyFilters: search is case-insensitive', () => {
293
- const r = applyFilters(MOCK_PRODUCTS, 'WALNUT', 'All', 'featured')
294
- assert.equal(r.length, 1)
295
- })
296
-
297
- test('applyFilters: price-asc sort', () => {
298
- const r = applyFilters(MOCK_PRODUCTS, '', 'All', 'price-asc')
299
- assert.equal(r[0].price, 32)
300
- assert.equal(r[2].price, 89)
301
- })
302
-
303
- test('applyFilters: price-desc sort', () => {
304
- const r = applyFilters(MOCK_PRODUCTS, '', 'All', 'price-desc')
305
- assert.equal(r[0].price, 89)
306
- })
307
-
308
- test('applyFilters: rating sort', () => {
309
- const r = applyFilters(MOCK_PRODUCTS, '', 'All', 'rating')
310
- assert.equal(r[0].rating, 4.8)
311
- })
312
-
313
- test('applyFilters: no results when nothing matches', () => {
314
- const r = applyFilters(MOCK_PRODUCTS, 'zzznotfound', 'All', 'featured')
315
- assert.equal(r.length, 0)
316
- })
317
-
318
- test('CATEGORIES includes All and is sorted', () => {
319
- assert.equal(CATEGORIES[0], 'All')
320
- })
321
-
322
- // ─── Products — view tests ────────────────────────────────────────────────────
323
-
324
- const PRODUCTS_SERVER = { products: MOCK_PRODUCTS }
325
-
326
- test('products: renders product cards', () => {
327
- const r = renderSync(products, {
328
- state: { search: '', category: 'All', sort: 'featured' },
329
- server: PRODUCTS_SERVER,
330
- })
331
- assert.equal(r.count('h2'), 3)
332
- })
333
-
334
- test('products: has main landmark', () => {
335
- const r = renderSync(products, {
336
- state: { search: '', category: 'All', sort: 'featured' },
337
- server: PRODUCTS_SERVER,
338
- })
339
- assert(r.has('#main-content'))
340
- })
341
-
342
- // ─── Pricing — unit tests ─────────────────────────────────────────────────────
343
-
344
- test('FAQ has entries', () => {
345
- assert(FAQ.length > 0, 'FAQ should have at least one item')
346
- for (const item of FAQ) {
347
- assert(item.question, 'each FAQ item needs a question')
348
- assert(item.answer, 'each FAQ item needs an answer')
349
- }
350
- })
351
-
352
- // ─── Pricing — view tests ─────────────────────────────────────────────────────
353
-
354
- test('pricing: monthly view renders 3 plans', () => {
355
- const r = renderSync(pricing, { state: { billing: 'monthly' } })
356
- assert.equal(r.count('.pr-plan'), 3)
357
- })
358
-
359
- test('pricing: annual view renders 3 plans', () => {
360
- const r = renderSync(pricing, { state: { billing: 'annual' } })
361
- assert.equal(r.count('.pr-plan'), 3)
362
- })
363
-
364
- test('pricing: monthly Pro price is £12', () => {
365
- const r = renderSync(pricing, { state: { billing: 'monthly' } })
366
- const prices = r.findAll('.pr-plan-amount')
367
- assert(prices.some(p => p.text === '£12'), 'monthly Pro should be £12')
368
- })
369
-
370
- test('pricing: annual Pro price is £9', () => {
371
- const r = renderSync(pricing, { state: { billing: 'annual' } })
372
- const prices = r.findAll('.pr-plan-amount')
373
- assert(prices.some(p => p.text === '£9'), 'annual Pro should be £9')
374
- })
375
-
376
- test('pricing: annual note appears when billing is annual', () => {
377
- const r = renderSync(pricing, { state: { billing: 'annual' } })
378
- assert(r.has('.pr-annual-note'), 'annual note should appear')
379
- })
380
-
381
- test('pricing: annual note absent when billing is monthly', () => {
382
- const r = renderSync(pricing, { state: { billing: 'monthly' } })
383
- assert(!r.has('.pr-annual-note'), 'annual note should not appear on monthly')
384
- })
385
-
386
- test('pricing: FAQ accordion renders', () => {
387
- const r = renderSync(pricing, { state: { billing: 'monthly' } })
388
- assert(r.has('.ui-accordion'), 'accordion should render')
389
- })
390
-
391
- test('pricing: has main landmark', () => {
392
- const r = renderSync(pricing, { state: { billing: 'monthly' } })
393
- assert(r.has('#main-content'))
394
- })