@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,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec schema tests — run with: node src/spec/schema.test.js
|
|
3
|
+
*/
|
|
4
|
+
import { validateSpec, getStreamOrder, getViewSegments } from './schema.js'
|
|
5
|
+
|
|
6
|
+
let passed = 0
|
|
7
|
+
let failed = 0
|
|
8
|
+
|
|
9
|
+
function test(label, fn) {
|
|
10
|
+
try {
|
|
11
|
+
fn()
|
|
12
|
+
console.log(` ✓ ${label}`)
|
|
13
|
+
passed++
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.log(` ✗ ${label}`)
|
|
16
|
+
console.log(` ${e.message}`)
|
|
17
|
+
failed++
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function assert(condition, msg) {
|
|
22
|
+
if (!condition) throw new Error(msg || 'Assertion failed')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertErrors(spec, ...expectedFragments) {
|
|
26
|
+
const { valid, errors } = validateSpec(spec)
|
|
27
|
+
assert(!valid, 'Expected spec to be invalid but it passed')
|
|
28
|
+
for (const fragment of expectedFragments) {
|
|
29
|
+
const found = errors.some(e => e.includes(fragment))
|
|
30
|
+
assert(found, `Expected error containing "${fragment}" but got:\n ${errors.join('\n ')}`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertValid(spec) {
|
|
35
|
+
const { valid, errors } = validateSpec(spec)
|
|
36
|
+
assert(valid, `Expected spec to be valid but got errors:\n ${errors.join('\n ')}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
console.log('\nSpec validation\n')
|
|
42
|
+
|
|
43
|
+
test('rejects non-object', () => {
|
|
44
|
+
assertErrors(null, 'must be a plain object')
|
|
45
|
+
assertErrors('string', 'must be a plain object')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('route is optional (derived server-side by convention)', () => {
|
|
49
|
+
const { valid } = validateSpec({ state: {}, view: () => '' })
|
|
50
|
+
assert(valid, 'spec without route should be valid for client-side use')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('rejects route that does not start with /', () => {
|
|
54
|
+
assertErrors({ route: 'contact', state: {}, view: () => '' }, 'must start with "/"')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('requires state', () => {
|
|
58
|
+
assertErrors({ route: '/contact', view: () => '' }, 'spec.state is required')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('requires view', () => {
|
|
62
|
+
assertErrors({ route: '/contact', state: {} }, 'spec.view is required')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('accepts function view', () => {
|
|
66
|
+
assertValid({ route: '/contact', state: {}, view: () => '<div/>' })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('accepts segmented view', () => {
|
|
70
|
+
assertValid({
|
|
71
|
+
route: '/contact',
|
|
72
|
+
state: {},
|
|
73
|
+
view: {
|
|
74
|
+
header: () => '<header/>',
|
|
75
|
+
form: () => '<form/>'
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('rejects view segment that is not a function', () => {
|
|
81
|
+
assertErrors(
|
|
82
|
+
{ route: '/contact', state: {}, view: { header: '<header/>' } },
|
|
83
|
+
'spec.view.header must be a function'
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('rejects stream referencing undefined segment', () => {
|
|
88
|
+
assertErrors(
|
|
89
|
+
{
|
|
90
|
+
route: '/contact',
|
|
91
|
+
state: {},
|
|
92
|
+
view: { header: () => '' },
|
|
93
|
+
stream: { shell: ['header', 'missing'] }
|
|
94
|
+
},
|
|
95
|
+
'spec.stream references "missing"'
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('rejects stream.shell that is not an array', () => {
|
|
100
|
+
assertErrors(
|
|
101
|
+
{ route: '/contact', state: {}, view: () => '', stream: { shell: 'header' } },
|
|
102
|
+
'spec.stream.shell must be an array'
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('rejects mutations with non-function values', () => {
|
|
107
|
+
assertErrors(
|
|
108
|
+
{ route: '/x', state: {}, view: () => '', mutations: { inc: 'not a function' } },
|
|
109
|
+
'spec.mutations.inc must be a function'
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('rejects action missing run', () => {
|
|
114
|
+
assertErrors(
|
|
115
|
+
{
|
|
116
|
+
route: '/x', state: {}, view: () => '',
|
|
117
|
+
actions: { send: { onSuccess: () => {}, onError: () => {} } }
|
|
118
|
+
},
|
|
119
|
+
'spec.actions.send.run must be'
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('rejects action missing onSuccess', () => {
|
|
124
|
+
assertErrors(
|
|
125
|
+
{
|
|
126
|
+
route: '/x', state: {}, view: () => '',
|
|
127
|
+
actions: { send: { run: async () => {}, onError: () => {} } }
|
|
128
|
+
},
|
|
129
|
+
'spec.actions.send.onSuccess must be'
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('rejects unknown validation rule', () => {
|
|
134
|
+
assertErrors(
|
|
135
|
+
{
|
|
136
|
+
route: '/x', state: {}, view: () => '',
|
|
137
|
+
validation: { 'fields.email': { required: true, unknown: true } }
|
|
138
|
+
},
|
|
139
|
+
'unknown is not a recognised rule'
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('accepts full valid spec', () => {
|
|
144
|
+
assertValid({
|
|
145
|
+
route: '/contact',
|
|
146
|
+
stream: {
|
|
147
|
+
shell: ['header', 'form'],
|
|
148
|
+
deferred: ['recent']
|
|
149
|
+
},
|
|
150
|
+
server: {
|
|
151
|
+
recent: async () => []
|
|
152
|
+
},
|
|
153
|
+
state: {
|
|
154
|
+
fields: { name: '', email: '', message: '' },
|
|
155
|
+
status: 'idle'
|
|
156
|
+
},
|
|
157
|
+
validation: {
|
|
158
|
+
'fields.name': { required: true, minLength: 2 },
|
|
159
|
+
'fields.email': { required: true, format: 'email' },
|
|
160
|
+
'fields.message': { required: true, minLength: 10, maxLength: 1000 }
|
|
161
|
+
},
|
|
162
|
+
view: {
|
|
163
|
+
header: () => '<header/>',
|
|
164
|
+
form: (state) => `<form>${state.status}</form>`,
|
|
165
|
+
recent: (state, server) => `<aside>${server.recent.length}</aside>`
|
|
166
|
+
},
|
|
167
|
+
mutations: {
|
|
168
|
+
updateField: (state, e) => ({ fields: { ...state.fields, [e.target.name]: e.target.value } })
|
|
169
|
+
},
|
|
170
|
+
actions: {
|
|
171
|
+
sendMessage: {
|
|
172
|
+
validate: true,
|
|
173
|
+
run: async (state) => {},
|
|
174
|
+
onSuccess: () => ({ status: 'success' }),
|
|
175
|
+
onError: () => ({ status: 'error' })
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
meta: { title: 'Contact Us' }
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
console.log('\nRaw content specs (contentType)\n')
|
|
185
|
+
|
|
186
|
+
test('accepts a valid raw content spec', () => {
|
|
187
|
+
assertValid({
|
|
188
|
+
route: '/feed.xml',
|
|
189
|
+
contentType: 'application/rss+xml; charset=utf-8',
|
|
190
|
+
render: (ctx, server) => `<rss/>`
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('raw spec does not require state or view', () => {
|
|
195
|
+
const { valid } = validateSpec({
|
|
196
|
+
route: '/sitemap.xml',
|
|
197
|
+
contentType: 'application/xml',
|
|
198
|
+
render: () => `<?xml?>`
|
|
199
|
+
})
|
|
200
|
+
assert(valid, 'raw spec without state/view should be valid')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('raw spec accepts server data', () => {
|
|
204
|
+
assertValid({
|
|
205
|
+
route: '/feed.json',
|
|
206
|
+
contentType: 'application/json',
|
|
207
|
+
server: { posts: async () => [] },
|
|
208
|
+
render: (ctx, server) => JSON.stringify(server.posts)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('rejects raw spec missing render', () => {
|
|
213
|
+
assertErrors(
|
|
214
|
+
{ route: '/feed.xml', contentType: 'application/rss+xml' },
|
|
215
|
+
'spec.render is required'
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('rejects raw spec with empty contentType', () => {
|
|
220
|
+
assertErrors(
|
|
221
|
+
{ route: '/feed.xml', contentType: '', render: () => '' },
|
|
222
|
+
'spec.contentType must be a non-empty string'
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
console.log('\ngetViewSegments\n')
|
|
229
|
+
|
|
230
|
+
test('function view returns ["default"]', () => {
|
|
231
|
+
const segments = getViewSegments({ view: () => '' })
|
|
232
|
+
assert(segments.length === 1 && segments[0] === 'default', `Got ${segments}`)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('segmented view returns keys', () => {
|
|
236
|
+
const segments = getViewSegments({ view: { header: () => '', form: () => '' } })
|
|
237
|
+
assert(segments.includes('header') && segments.includes('form'), `Got ${segments}`)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
console.log('\ngetStreamOrder\n')
|
|
243
|
+
|
|
244
|
+
test('no stream config — all segments in shell', () => {
|
|
245
|
+
const order = getStreamOrder({ view: { header: () => '', form: () => '' } })
|
|
246
|
+
assert(order.shell.includes('header'), 'header should be in shell')
|
|
247
|
+
assert(order.deferred.length === 0, 'deferred should be empty')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('stream config is respected', () => {
|
|
251
|
+
const order = getStreamOrder({
|
|
252
|
+
view: { header: () => '', form: () => '', recent: () => '' },
|
|
253
|
+
stream: { shell: ['header', 'form'], deferred: ['recent'] }
|
|
254
|
+
})
|
|
255
|
+
assert(order.shell.includes('header'), 'header in shell')
|
|
256
|
+
assert(order.shell.includes('form'), 'form in shell')
|
|
257
|
+
assert(order.deferred.includes('recent'), 'recent in deferred')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// guard
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
test('accepts a valid guard function', () => {
|
|
265
|
+
const { valid, errors } = validateSpec({
|
|
266
|
+
route: '/admin',
|
|
267
|
+
state: {},
|
|
268
|
+
view: () => '<main id="main-content">Admin</main>',
|
|
269
|
+
guard: async (ctx) => { if (!ctx.cookies.session) return { redirect: '/login' } },
|
|
270
|
+
})
|
|
271
|
+
assert(valid, `Expected valid spec, got errors: ${errors.join(', ')}`)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('rejects guard that is not a function', () => {
|
|
275
|
+
assertErrors(
|
|
276
|
+
{ route: '/admin', state: {}, view: () => '', guard: '/login' },
|
|
277
|
+
'spec.guard must be a function'
|
|
278
|
+
)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('rejects guard that is an object', () => {
|
|
282
|
+
assertErrors(
|
|
283
|
+
{ route: '/admin', state: {}, view: () => '', guard: { redirect: '/login' } },
|
|
284
|
+
'spec.guard must be a function'
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
|
|
291
|
+
if (failed > 0) process.exit(1)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Global Store
|
|
3
|
+
*
|
|
4
|
+
* The store is a single shared data layer declared in pulse.store.js.
|
|
5
|
+
* Its server fetchers run once per request and the results are available to
|
|
6
|
+
* any page that declares which keys it uses via spec.store.
|
|
7
|
+
*
|
|
8
|
+
* Store mutations (store.mutations) allow synchronous client-side updates that
|
|
9
|
+
* broadcast to all subscribed pages without a server round-trip.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate a store definition.
|
|
14
|
+
* Returns { valid: true } or { valid: false, errors: string[] }
|
|
15
|
+
*
|
|
16
|
+
* @param {unknown} store
|
|
17
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
18
|
+
*/
|
|
19
|
+
export function validateStore(store) {
|
|
20
|
+
const errors = []
|
|
21
|
+
|
|
22
|
+
if (!store || typeof store !== 'object' || Array.isArray(store)) {
|
|
23
|
+
return { valid: false, errors: ['Store must be a plain object'] }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (store.state !== undefined) {
|
|
27
|
+
if (typeof store.state !== 'object' || Array.isArray(store.state)) {
|
|
28
|
+
errors.push('store.state must be a plain object')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (store.server !== undefined) {
|
|
33
|
+
if (typeof store.server !== 'object' || Array.isArray(store.server)) {
|
|
34
|
+
errors.push('store.server must be a plain object of async functions')
|
|
35
|
+
} else {
|
|
36
|
+
for (const [key, fn] of Object.entries(store.server)) {
|
|
37
|
+
if (typeof fn !== 'function') {
|
|
38
|
+
errors.push(`store.server.${key} must be a function`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (store.hydrate !== undefined) {
|
|
45
|
+
if (typeof store.hydrate !== 'string' || !store.hydrate.startsWith('/')) {
|
|
46
|
+
errors.push('store.hydrate must be a string starting with "/"')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (store.mutations !== undefined) {
|
|
51
|
+
if (typeof store.mutations !== 'object' || Array.isArray(store.mutations)) {
|
|
52
|
+
errors.push('store.mutations must be a plain object of functions')
|
|
53
|
+
} else {
|
|
54
|
+
for (const [key, fn] of Object.entries(store.mutations)) {
|
|
55
|
+
if (typeof fn !== 'function') {
|
|
56
|
+
errors.push(`store.mutations.${key} must be a function`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { valid: errors.length === 0, errors }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve all store server fetchers in parallel.
|
|
67
|
+
* Falls back to store.state values for keys not covered by server fetchers.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} store - The store definition
|
|
70
|
+
* @param {Object} ctx - The request context (params, query, cookies, etc.)
|
|
71
|
+
* @returns {Promise<Object>}
|
|
72
|
+
*/
|
|
73
|
+
export async function resolveStoreState(store, ctx) {
|
|
74
|
+
const base = store.state ? { ...store.state } : {}
|
|
75
|
+
|
|
76
|
+
if (!store.server) return base
|
|
77
|
+
|
|
78
|
+
const timeout = ctx.fetcherTimeout ?? null
|
|
79
|
+
|
|
80
|
+
const entries = await Promise.all(
|
|
81
|
+
Object.entries(store.server).map(async ([key, fn]) => {
|
|
82
|
+
const value = timeout
|
|
83
|
+
? await withTimeout(fn(ctx), timeout, `store.${key}`)
|
|
84
|
+
: await fn(ctx)
|
|
85
|
+
return [key, value]
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return { ...base, ...Object.fromEntries(entries) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function withTimeout(promise, ms, name) {
|
|
93
|
+
return Promise.race([
|
|
94
|
+
promise,
|
|
95
|
+
new Promise((_, reject) =>
|
|
96
|
+
setTimeout(
|
|
97
|
+
() => reject(new Error(`Store fetcher "${name}" timed out after ${ms}ms`)),
|
|
98
|
+
ms
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
])
|
|
102
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Store tests
|
|
3
|
+
* run: node src/store/store.test.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { validateStore, resolveStoreState } from './index.js'
|
|
7
|
+
|
|
8
|
+
let passed = 0
|
|
9
|
+
let failed = 0
|
|
10
|
+
|
|
11
|
+
async function test(label, fn) {
|
|
12
|
+
try {
|
|
13
|
+
await fn()
|
|
14
|
+
console.log(` ✓ ${label}`)
|
|
15
|
+
passed++
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.log(` ✗ ${label}`)
|
|
18
|
+
console.log(` ${e.message}`)
|
|
19
|
+
failed++
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assert(condition, msg) {
|
|
24
|
+
if (!condition) throw new Error(msg || 'Assertion failed')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assertEqual(a, b, msg) {
|
|
28
|
+
if (a !== b) throw new Error(msg || `Expected ${JSON.stringify(a)} === ${JSON.stringify(b)}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
console.log('\nvalidateStore\n')
|
|
34
|
+
|
|
35
|
+
await test('accepts a valid store with state and server', () => {
|
|
36
|
+
const { valid } = validateStore({
|
|
37
|
+
state: { user: null },
|
|
38
|
+
server: { user: async () => null },
|
|
39
|
+
})
|
|
40
|
+
assert(valid, 'Expected valid store')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
await test('accepts a store with only state', () => {
|
|
44
|
+
const { valid } = validateStore({ state: { theme: 'dark' } })
|
|
45
|
+
assert(valid)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await test('accepts a store with only server', () => {
|
|
49
|
+
const { valid } = validateStore({ server: { user: async () => null } })
|
|
50
|
+
assert(valid)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
await test('accepts a store with mutations', () => {
|
|
54
|
+
const { valid } = validateStore({
|
|
55
|
+
state: { count: 0 },
|
|
56
|
+
mutations: { increment: (s) => ({ count: s.count + 1 }) },
|
|
57
|
+
})
|
|
58
|
+
assert(valid)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await test('accepts a store with hydrate', () => {
|
|
62
|
+
const { valid } = validateStore({ state: {}, hydrate: '/pulse.store.js' })
|
|
63
|
+
assert(valid)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
await test('rejects null', () => {
|
|
67
|
+
const { valid, errors } = validateStore(null)
|
|
68
|
+
assert(!valid)
|
|
69
|
+
assert(errors.length > 0)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await test('rejects a non-object', () => {
|
|
73
|
+
const { valid } = validateStore('not an object')
|
|
74
|
+
assert(!valid)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
await test('rejects an array', () => {
|
|
78
|
+
const { valid } = validateStore([])
|
|
79
|
+
assert(!valid)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
await test('rejects state that is an array', () => {
|
|
83
|
+
const { valid, errors } = validateStore({ state: [] })
|
|
84
|
+
assert(!valid)
|
|
85
|
+
assert(errors.some(e => e.includes('state')), `Expected state error in: ${errors}`)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await test('rejects state that is a string', () => {
|
|
89
|
+
const { valid } = validateStore({ state: 'bad' })
|
|
90
|
+
assert(!valid)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
await test('rejects server that is an array', () => {
|
|
94
|
+
const { valid, errors } = validateStore({ server: ['fn'] })
|
|
95
|
+
assert(!valid)
|
|
96
|
+
assert(errors.some(e => e.includes('server')), `Expected server error in: ${errors}`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
await test('rejects a server entry that is not a function', () => {
|
|
100
|
+
const { valid, errors } = validateStore({ server: { user: 42 } })
|
|
101
|
+
assert(!valid)
|
|
102
|
+
assert(errors.some(e => e.includes('store.server.user')), `Expected named error in: ${errors}`)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await test('rejects mutations that is an array', () => {
|
|
106
|
+
const { valid, errors } = validateStore({ mutations: [() => {}] })
|
|
107
|
+
assert(!valid)
|
|
108
|
+
assert(errors.some(e => e.includes('mutations')), `Expected mutations error in: ${errors}`)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await test('rejects a mutation entry that is not a function', () => {
|
|
112
|
+
const { valid, errors } = validateStore({ mutations: { toggle: 'bad' } })
|
|
113
|
+
assert(!valid)
|
|
114
|
+
assert(errors.some(e => e.includes('store.mutations.toggle')), `Expected named error in: ${errors}`)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await test('rejects hydrate that does not start with /', () => {
|
|
118
|
+
const { valid, errors } = validateStore({ hydrate: 'pulse.store.js' })
|
|
119
|
+
assert(!valid)
|
|
120
|
+
assert(errors.some(e => e.includes('hydrate')), `Expected hydrate error in: ${errors}`)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
await test('rejects hydrate that is not a string', () => {
|
|
124
|
+
const { valid } = validateStore({ hydrate: 42 })
|
|
125
|
+
assert(!valid)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await test('returns all errors for multiple violations', () => {
|
|
129
|
+
const { valid, errors } = validateStore({ state: [], server: { x: 'not-a-fn' } })
|
|
130
|
+
assert(!valid)
|
|
131
|
+
assert(errors.length >= 2, `Expected >= 2 errors, got ${errors.length}: ${errors.join(', ')}`)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
console.log('\nresolveStoreState\n')
|
|
137
|
+
|
|
138
|
+
await test('returns state defaults when no server fetchers', async () => {
|
|
139
|
+
const result = await resolveStoreState({ state: { user: null, theme: 'dark' } }, {})
|
|
140
|
+
assertEqual(result.theme, 'dark')
|
|
141
|
+
assert(result.user === null)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await test('resolves server fetchers and returns their results', async () => {
|
|
145
|
+
const result = await resolveStoreState({
|
|
146
|
+
state: { user: null, settings: {} },
|
|
147
|
+
server: {
|
|
148
|
+
user: async () => ({ id: 1, name: 'Alice' }),
|
|
149
|
+
settings: async () => ({ theme: 'light' }),
|
|
150
|
+
},
|
|
151
|
+
}, {})
|
|
152
|
+
assertEqual(result.user.name, 'Alice')
|
|
153
|
+
assertEqual(result.settings.theme, 'light')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
await test('server values override state defaults', async () => {
|
|
157
|
+
const result = await resolveStoreState({
|
|
158
|
+
state: { user: 'default' },
|
|
159
|
+
server: { user: async () => 'from-server' },
|
|
160
|
+
}, {})
|
|
161
|
+
assertEqual(result.user, 'from-server')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
await test('state defaults are preserved for keys not covered by server', async () => {
|
|
165
|
+
const result = await resolveStoreState({
|
|
166
|
+
state: { user: null, nav: [1, 2, 3] },
|
|
167
|
+
server: { user: async () => ({ id: 1 }) },
|
|
168
|
+
}, {})
|
|
169
|
+
assertEqual(result.nav.length, 3)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
await test('passes ctx to server fetchers', async () => {
|
|
173
|
+
let received
|
|
174
|
+
await resolveStoreState({
|
|
175
|
+
server: { data: async (ctx) => { received = ctx; return null } },
|
|
176
|
+
}, { cookies: { session: 'abc' } })
|
|
177
|
+
assertEqual(received.cookies.session, 'abc')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
await test('returns empty object when store has no state and no server', async () => {
|
|
181
|
+
const result = await resolveStoreState({}, {})
|
|
182
|
+
assert(typeof result === 'object' && result !== null)
|
|
183
|
+
assertEqual(Object.keys(result).length, 0)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
await test('timeout rejects if a fetcher exceeds fetcherTimeout', async () => {
|
|
187
|
+
let threw = false
|
|
188
|
+
try {
|
|
189
|
+
await resolveStoreState({
|
|
190
|
+
server: { slow: async () => new Promise(r => setTimeout(r, 200)) },
|
|
191
|
+
}, { fetcherTimeout: 20 })
|
|
192
|
+
} catch (e) {
|
|
193
|
+
threw = true
|
|
194
|
+
assert(e.message.includes('slow'), `Expected fetcher name in error: ${e.message}`)
|
|
195
|
+
assert(e.message.toLowerCase().includes('timed out'), `Expected timeout message: ${e.message}`)
|
|
196
|
+
}
|
|
197
|
+
assert(threw, 'Expected timeout to throw')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
await test('no timeout when fetcherTimeout is not set', async () => {
|
|
201
|
+
const result = await resolveStoreState({
|
|
202
|
+
server: { fast: async () => 'ok' },
|
|
203
|
+
}, {})
|
|
204
|
+
assertEqual(result.fast, 'ok')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
console.log(`\n${passed} passed, ${failed} failed\n`)
|
|
210
|
+
if (failed > 0) process.exit(1)
|