@invisibleloop/pulse 0.1.21
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.
- package/.claude/commands/build-page.md +59 -0
- package/.claude/commands/new-doc-page.md +45 -0
- package/.claude/commands/verify.md +52 -0
- package/.claude/pulse-checklist.md +111 -0
- package/.claude/settings.local.json +102 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.pulse/load-reports/home/1773432711417.json +22 -0
- package/CLAUDE.md +383 -0
- package/README.md +95 -0
- package/docs/.claude/pulse-checklist.md +111 -0
- package/docs/public/.pulse-ui-version +1 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
- package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
- package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
- package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
- package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
- package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
- package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
- package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
- package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
- package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
- package/docs/public/dist/docs-8e3d4b5c.css +1 -0
- package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
- package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
- package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
- package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
- package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
- package/docs/public/dist/manifest.json +94 -0
- package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
- package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
- package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
- package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
- package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
- package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
- package/docs/public/dist/runtime-B73WLANC.js +1 -0
- package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
- package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
- package/docs/public/dist/runtime-QFURDKA2.js +5 -0
- package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
- package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
- package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
- package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
- package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
- package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
- package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
- package/docs/public/docs.css +2020 -0
- package/docs/public/menu.js +83 -0
- package/docs/public/pulse-ui.css +2739 -0
- package/docs/public/pulse-ui.js +236 -0
- package/docs/server.js +192 -0
- package/docs/src/lib/component-page.js +47 -0
- package/docs/src/lib/highlight.js +255 -0
- package/docs/src/lib/layout.js +131 -0
- package/docs/src/lib/metrics-store.js +6 -0
- package/docs/src/lib/nav.js +159 -0
- package/docs/src/lib/stats.js +81 -0
- package/docs/src/pages/accessibility.js +157 -0
- package/docs/src/pages/actions.js +191 -0
- package/docs/src/pages/auth.js +177 -0
- package/docs/src/pages/caching.js +95 -0
- package/docs/src/pages/components/accordion.js +48 -0
- package/docs/src/pages/components/alert.js +35 -0
- package/docs/src/pages/components/app-badge.js +41 -0
- package/docs/src/pages/components/avatar.js +35 -0
- package/docs/src/pages/components/badge.js +36 -0
- package/docs/src/pages/components/banner.js +45 -0
- package/docs/src/pages/components/breadcrumbs.js +94 -0
- package/docs/src/pages/components/button.js +84 -0
- package/docs/src/pages/components/card.js +225 -0
- package/docs/src/pages/components/carousel.js +72 -0
- package/docs/src/pages/components/charts.js +278 -0
- package/docs/src/pages/components/checkbox.js +129 -0
- package/docs/src/pages/components/cluster.js +47 -0
- package/docs/src/pages/components/code-window.js +57 -0
- package/docs/src/pages/components/container.js +40 -0
- package/docs/src/pages/components/cta.js +53 -0
- package/docs/src/pages/components/divider.js +37 -0
- package/docs/src/pages/components/empty.js +36 -0
- package/docs/src/pages/components/feature.js +60 -0
- package/docs/src/pages/components/fieldset.js +65 -0
- package/docs/src/pages/components/fileupload.js +127 -0
- package/docs/src/pages/components/footer.js +58 -0
- package/docs/src/pages/components/grid.js +165 -0
- package/docs/src/pages/components/heading.js +107 -0
- package/docs/src/pages/components/hero.js +65 -0
- package/docs/src/pages/components/icons.js +285 -0
- package/docs/src/pages/components/image.js +71 -0
- package/docs/src/pages/components/input.js +51 -0
- package/docs/src/pages/components/list.js +112 -0
- package/docs/src/pages/components/media.js +51 -0
- package/docs/src/pages/components/modal.js +111 -0
- package/docs/src/pages/components/nav.js +86 -0
- package/docs/src/pages/components/pricing.js +68 -0
- package/docs/src/pages/components/progress.js +102 -0
- package/docs/src/pages/components/prose.js +111 -0
- package/docs/src/pages/components/pullquote.js +71 -0
- package/docs/src/pages/components/radio.js +194 -0
- package/docs/src/pages/components/rating.js +106 -0
- package/docs/src/pages/components/search.js +61 -0
- package/docs/src/pages/components/section.js +59 -0
- package/docs/src/pages/components/segmented.js +121 -0
- package/docs/src/pages/components/select.js +45 -0
- package/docs/src/pages/components/slider.js +114 -0
- package/docs/src/pages/components/spinner.js +73 -0
- package/docs/src/pages/components/stack.js +48 -0
- package/docs/src/pages/components/stat.js +55 -0
- package/docs/src/pages/components/stepper.js +66 -0
- package/docs/src/pages/components/table.js +45 -0
- package/docs/src/pages/components/testimonial.js +49 -0
- package/docs/src/pages/components/textarea.js +31 -0
- package/docs/src/pages/components/timeline.js +227 -0
- package/docs/src/pages/components/toggle.js +84 -0
- package/docs/src/pages/components/tooltip.js +48 -0
- package/docs/src/pages/components.js +204 -0
- package/docs/src/pages/config.js +193 -0
- package/docs/src/pages/constraints.js +99 -0
- package/docs/src/pages/deploy.js +233 -0
- package/docs/src/pages/extending.js +198 -0
- package/docs/src/pages/faq.js +96 -0
- package/docs/src/pages/getting-started.js +106 -0
- package/docs/src/pages/guard.js +121 -0
- package/docs/src/pages/home.js +401 -0
- package/docs/src/pages/how-it-works.js +183 -0
- package/docs/src/pages/hydration.js +98 -0
- package/docs/src/pages/images.js +121 -0
- package/docs/src/pages/meta.js +120 -0
- package/docs/src/pages/mutations.js +106 -0
- package/docs/src/pages/navigation.js +85 -0
- package/docs/src/pages/performance.js +157 -0
- package/docs/src/pages/persist.js +88 -0
- package/docs/src/pages/project-structure.js +90 -0
- package/docs/src/pages/prompt-examples.js +186 -0
- package/docs/src/pages/raw-responses.js +124 -0
- package/docs/src/pages/routing.js +99 -0
- package/docs/src/pages/server-api.js +281 -0
- package/docs/src/pages/server-data.js +185 -0
- package/docs/src/pages/slash-commands.js +55 -0
- package/docs/src/pages/spec.js +207 -0
- package/docs/src/pages/state.js +101 -0
- package/docs/src/pages/store.js +181 -0
- package/docs/src/pages/streaming.js +108 -0
- package/docs/src/pages/stripe.js +193 -0
- package/docs/src/pages/supabase.js +323 -0
- package/docs/src/pages/testing.js +198 -0
- package/docs/src/pages/validation.js +138 -0
- package/examples/contact.js +166 -0
- package/examples/counter.js +94 -0
- package/examples/dev.server.js +91 -0
- package/examples/examples.test.js +394 -0
- package/examples/pricing.js +244 -0
- package/examples/products.js +191 -0
- package/examples/quiz.js +208 -0
- package/examples/shared.js +78 -0
- package/examples/todos.js +162 -0
- package/package.json +75 -0
- package/public/.pulse-ui-version +1 -0
- package/public/chippy-bird.css +246 -0
- package/public/examples/contact.css +119 -0
- package/public/examples/counter.css +79 -0
- package/public/examples/pricing.css +132 -0
- package/public/examples/products.css +100 -0
- package/public/examples/quiz.css +200 -0
- package/public/examples/todos.css +137 -0
- package/public/favicon.ico +0 -0
- package/public/log-dashboard.css +383 -0
- package/public/pulse-ui.css +2740 -0
- package/public/pulse-ui.js +236 -0
- package/public/pulse.css +149 -0
- package/scripts/build.js +411 -0
- package/src/agent/checklist.md +111 -0
- package/src/agent/coverage-check.js +66 -0
- package/src/agent/guide-components.md +274 -0
- package/src/agent/guide-examples.md +54 -0
- package/src/agent/guide-routing.md +36 -0
- package/src/agent/guide-server.md +258 -0
- package/src/agent/guide-spec.md +103 -0
- package/src/agent/guide-styles.md +191 -0
- package/src/agent/guide.md +979 -0
- package/src/agent/identity.md +106 -0
- package/src/agent/workflow.md +108 -0
- package/src/cli/cli.test.js +82 -0
- package/src/cli/dev.js +195 -0
- package/src/cli/discover.js +113 -0
- package/src/cli/index.js +361 -0
- package/src/cli/load-report.js +91 -0
- package/src/cli/load-runner.js +121 -0
- package/src/cli/report-server.js +723 -0
- package/src/cli/report.js +116 -0
- package/src/cli/scaffold.archive.js +1371 -0
- package/src/cli/scaffold.js +349 -0
- package/src/cli/start.js +74 -0
- package/src/html.js +19 -0
- package/src/mcp/server.js +884 -0
- package/src/mcp/validate-worker.js +110 -0
- package/src/runtime/image.js +74 -0
- package/src/runtime/image.test.js +111 -0
- package/src/runtime/index.js +621 -0
- package/src/runtime/navigate.js +146 -0
- package/src/runtime/runtime.test.js +773 -0
- package/src/runtime/ssr.js +464 -0
- package/src/runtime/ssr.test.js +421 -0
- package/src/runtime/store.js +92 -0
- package/src/runtime/toast.js +163 -0
- package/src/server/index.js +1386 -0
- package/src/server/server.test.js +1248 -0
- package/src/spec/schema.js +428 -0
- package/src/spec/schema.test.js +291 -0
- package/src/store/index.js +102 -0
- package/src/store/store.test.js +210 -0
- package/src/testing/html.js +283 -0
- package/src/testing/index.js +249 -0
- package/src/testing/testing.test.js +450 -0
- package/src/ui/accordion.js +28 -0
- package/src/ui/alert.js +43 -0
- package/src/ui/app-badge.js +48 -0
- package/src/ui/avatar.js +47 -0
- package/src/ui/badge.js +24 -0
- package/src/ui/banner.js +26 -0
- package/src/ui/breadcrumbs.js +38 -0
- package/src/ui/button.js +66 -0
- package/src/ui/card.js +34 -0
- package/src/ui/carousel.js +59 -0
- package/src/ui/charts.js +321 -0
- package/src/ui/checkbox.js +65 -0
- package/src/ui/cluster.js +44 -0
- package/src/ui/code-window.js +39 -0
- package/src/ui/container.js +24 -0
- package/src/ui/cta.js +37 -0
- package/src/ui/divider.js +29 -0
- package/src/ui/empty.js +33 -0
- package/src/ui/feature.js +33 -0
- package/src/ui/fieldset.js +37 -0
- package/src/ui/fileupload.js +89 -0
- package/src/ui/footer.js +38 -0
- package/src/ui/grid.js +36 -0
- package/src/ui/heading.js +45 -0
- package/src/ui/hero.js +37 -0
- package/src/ui/icons.js +161 -0
- package/src/ui/index.js +89 -0
- package/src/ui/input.js +74 -0
- package/src/ui/list.js +36 -0
- package/src/ui/media.js +44 -0
- package/src/ui/modal.js +80 -0
- package/src/ui/nav.js +61 -0
- package/src/ui/pricing.js +56 -0
- package/src/ui/progress.js +62 -0
- package/src/ui/prose.js +29 -0
- package/src/ui/pullquote.js +34 -0
- package/src/ui/radio.js +102 -0
- package/src/ui/rating.js +93 -0
- package/src/ui/search.js +77 -0
- package/src/ui/section.js +69 -0
- package/src/ui/segmented.js +50 -0
- package/src/ui/select.js +77 -0
- package/src/ui/slider.js +84 -0
- package/src/ui/spinner.js +34 -0
- package/src/ui/stack.js +36 -0
- package/src/ui/stat.js +52 -0
- package/src/ui/stepper.js +46 -0
- package/src/ui/switch.js +57 -0
- package/src/ui/table.js +45 -0
- package/src/ui/testimonial.js +48 -0
- package/src/ui/textarea.js +72 -0
- package/src/ui/timeline.js +72 -0
- package/src/ui/tooltip.js +28 -0
- package/src/ui/ui.test.js +1241 -0
- package/src/ui/uiimage.js +65 -0
- package/tsconfig.json +13 -0
- package/types/html.d.ts +17 -0
- package/types/image.d.ts +70 -0
- package/types/index.d.ts +7 -0
- package/types/navigate.d.ts +38 -0
- package/types/runtime.d.ts +63 -0
- package/types/schema.d.ts +243 -0
- package/types/server.d.ts +145 -0
- package/types/ssr.d.ts +110 -0
- package/types/testing.d.ts +154 -0
- package/types/ui.d.ts +704 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Examples dev server
|
|
3
|
+
*
|
|
4
|
+
* Serves all six standalone example apps.
|
|
5
|
+
* Handles /src/, /examples/, and /public/ as static JS so dev hydration works.
|
|
6
|
+
*
|
|
7
|
+
* Run: node examples/dev.server.js
|
|
8
|
+
*
|
|
9
|
+
* http://localhost:3001/counter — mutations + constraints
|
|
10
|
+
* http://localhost:3001/todos — CRUD + persist + filter
|
|
11
|
+
* http://localhost:3001/contact — server data + validation + action
|
|
12
|
+
* http://localhost:3001/quiz — multi-step state machine
|
|
13
|
+
* http://localhost:3001/products — server data + search/filter/sort
|
|
14
|
+
* http://localhost:3001/pricing — landing page components + billing toggle
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs'
|
|
18
|
+
import path from 'path'
|
|
19
|
+
import { createServer } from '../src/server/index.js'
|
|
20
|
+
import { themeScript } from './shared.js'
|
|
21
|
+
import counter from './counter.js'
|
|
22
|
+
import todos from './todos.js'
|
|
23
|
+
import contact from './contact.js'
|
|
24
|
+
import quiz from './quiz.js'
|
|
25
|
+
import products from './products.js'
|
|
26
|
+
import pricing from './pricing.js'
|
|
27
|
+
|
|
28
|
+
const ROOT = path.resolve(import.meta.dirname, '..')
|
|
29
|
+
|
|
30
|
+
const MIME = {
|
|
31
|
+
'.js': 'application/javascript',
|
|
32
|
+
'.css': 'text/css',
|
|
33
|
+
'.json': 'application/json',
|
|
34
|
+
'.html': 'text/html; charset=utf-8',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// /@pulse/runtime/* → src/runtime/*
|
|
38
|
+
// /@pulse/ui/* → src/ui/*
|
|
39
|
+
// Maps bare-specifier aliases used by the dev-mode bootstrap.
|
|
40
|
+
const PULSE_PREFIX_MAP = {
|
|
41
|
+
'/@pulse/runtime/': path.join(ROOT, 'src/runtime/'),
|
|
42
|
+
'/@pulse/ui/': path.join(ROOT, 'src/ui/'),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Serve /src/, /examples/, and /@pulse/ alias paths so the browser can import
|
|
46
|
+
// spec and runtime files during dev hydration.
|
|
47
|
+
function staticHandler(req, res) {
|
|
48
|
+
const url = new URL(req.url, 'http://localhost')
|
|
49
|
+
const pathname = url.pathname
|
|
50
|
+
|
|
51
|
+
// /@pulse/* prefix aliases → src/
|
|
52
|
+
for (const [prefix, dir] of Object.entries(PULSE_PREFIX_MAP)) {
|
|
53
|
+
if (pathname.startsWith(prefix)) {
|
|
54
|
+
const filePath = path.join(dir, pathname.slice(prefix.length))
|
|
55
|
+
if (!filePath.startsWith(dir)) return
|
|
56
|
+
let stat
|
|
57
|
+
try { stat = fs.statSync(filePath) } catch { return }
|
|
58
|
+
if (!stat.isFile()) return
|
|
59
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
60
|
+
fs.createReadStream(filePath).pipe(res)
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!pathname.startsWith('/examples/') && !pathname.startsWith('/src/')) return
|
|
66
|
+
|
|
67
|
+
const filePath = path.join(ROOT, pathname)
|
|
68
|
+
if (!filePath.startsWith(ROOT + path.sep)) return
|
|
69
|
+
|
|
70
|
+
let stat
|
|
71
|
+
try { stat = fs.statSync(filePath) } catch { return }
|
|
72
|
+
if (!stat.isFile()) return
|
|
73
|
+
|
|
74
|
+
const ext = path.extname(filePath)
|
|
75
|
+
const mime = MIME[ext] || 'text/plain'
|
|
76
|
+
|
|
77
|
+
res.writeHead(200, { 'Content-Type': mime })
|
|
78
|
+
fs.createReadStream(filePath).pipe(res)
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
createServer(
|
|
83
|
+
[counter, todos, contact, quiz, products, pricing],
|
|
84
|
+
{
|
|
85
|
+
port: 3001,
|
|
86
|
+
stream: true,
|
|
87
|
+
staticDir: path.join(ROOT, 'public'),
|
|
88
|
+
onRequest: staticHandler,
|
|
89
|
+
extraBody: themeScript,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
@@ -0,0 +1,394 @@
|
|
|
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('.ui-pricing'), 3)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('pricing: annual view renders 3 plans', () => {
|
|
360
|
+
const r = renderSync(pricing, { state: { billing: 'annual' } })
|
|
361
|
+
assert.equal(r.count('.ui-pricing'), 3)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
test('pricing: monthly Pro price is £12', () => {
|
|
365
|
+
const r = renderSync(pricing, { state: { billing: 'monthly' } })
|
|
366
|
+
const prices = r.findAll('.ui-pricing-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('.ui-pricing-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
|
+
})
|