@crvouga/atlas 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +178 -604
- package/dist/bun/src/driver.d.ts +41 -7
- package/dist/bun/src/driver.js +47 -6
- package/dist/bun/src/index.d.ts +8 -0
- package/dist/bun/src/index.js +8 -0
- package/dist/core/src/cli.d.ts +14 -1
- package/dist/core/src/cli.js +30 -1
- package/dist/core/src/config.d.ts +70 -4
- package/dist/core/src/config.js +62 -3
- package/dist/core/src/events/cloudevents.d.ts +54 -9
- package/dist/core/src/events/cloudevents.js +60 -9
- package/dist/core/src/graph.d.ts +21 -0
- package/dist/core/src/graph.js +26 -0
- package/dist/core/src/index.d.ts +11 -0
- package/dist/core/src/index.js +11 -0
- package/dist/core/src/lint.d.ts +30 -0
- package/dist/core/src/lint.js +13 -0
- package/dist/core/src/plan.d.ts +39 -1
- package/dist/core/src/plan.js +28 -1
- package/dist/core/src/privacy.d.ts +29 -2
- package/dist/core/src/privacy.js +19 -2
- package/dist/core/src/report/formats.d.ts +34 -4
- package/dist/core/src/report/formats.js +24 -4
- package/dist/core/src/report/markdown.d.ts +15 -0
- package/dist/core/src/report/markdown.js +8 -0
- package/dist/core/src/run/clients.d.ts +44 -5
- package/dist/core/src/run/clients.js +46 -2
- package/dist/core/src/run/execute.d.ts +69 -16
- package/dist/core/src/run/execute.js +67 -16
- package/dist/core/src/run/frames.d.ts +13 -4
- package/dist/core/src/run/frames.js +19 -4
- package/dist/core/src/run/http.d.ts +34 -1
- package/dist/core/src/run/http.js +14 -1
- package/dist/core/src/run/types.d.ts +76 -3
- package/dist/core/src/run/types.js +19 -3
- package/dist/core/src/signals.d.ts +49 -5
- package/dist/core/src/signals.js +22 -5
- package/dist/core/src/spec/compose.d.ts +23 -1
- package/dist/core/src/spec/compose.js +10 -1
- package/dist/core/src/spec/index.d.ts +9 -1
- package/dist/core/src/spec/index.js +9 -1
- package/dist/core/src/spec/load.d.ts +22 -1
- package/dist/core/src/spec/load.js +31 -1
- package/dist/core/src/spec/parse.d.ts +16 -2
- package/dist/core/src/spec/parse.js +25 -2
- package/dist/core/src/spec/scxml.d.ts +25 -4
- package/dist/core/src/spec/scxml.js +36 -4
- package/dist/core/src/spec/types.d.ts +52 -1
- package/dist/core/src/spec/types.js +24 -0
- package/dist/detox/src/index.d.ts +54 -11
- package/dist/detox/src/index.js +45 -7
- package/dist/playwright/src/driver.d.ts +39 -3
- package/dist/playwright/src/driver.js +27 -3
- package/dist/playwright/src/index.d.ts +10 -0
- package/dist/playwright/src/index.js +10 -0
- package/dist/playwright/src/media.d.ts +50 -4
- package/dist/playwright/src/media.js +55 -4
- package/dist/playwright/src/signals.d.ts +44 -5
- package/dist/playwright/src/signals.js +29 -5
- package/dist/reports/src/attachment-files.d.ts +44 -0
- package/dist/reports/src/attachment-files.js +121 -0
- package/dist/reports/src/attachment-response.d.ts +31 -0
- package/dist/reports/src/attachment-response.js +55 -0
- package/dist/reports/src/aws-credentials.d.ts +48 -0
- package/dist/reports/src/aws-credentials.js +221 -0
- package/dist/reports/src/browser-navigation.d.ts +14 -0
- package/dist/reports/src/browser-navigation.js +7 -0
- package/dist/reports/src/bundle.d.ts +34 -2
- package/dist/reports/src/bundle.js +38 -2
- package/dist/reports/src/catalog.d.ts +34 -3
- package/dist/reports/src/catalog.js +17 -3
- package/dist/reports/src/cli-stores.d.ts +36 -0
- package/dist/reports/src/cli-stores.js +73 -0
- package/dist/reports/src/clock.d.ts +7 -2
- package/dist/reports/src/clock.js +5 -2
- package/dist/reports/src/completion-order.d.ts +18 -5
- package/dist/reports/src/completion-order.js +18 -5
- package/dist/reports/src/compose.d.ts +80 -0
- package/dist/reports/src/compose.js +117 -0
- package/dist/reports/src/copy-store.d.ts +29 -0
- package/dist/reports/src/copy-store.js +52 -0
- package/dist/reports/src/directory-outline.d.ts +38 -0
- package/dist/reports/src/directory-outline.js +103 -0
- package/dist/reports/src/directory-store.d.ts +20 -0
- package/dist/reports/src/directory-store.js +166 -0
- package/dist/reports/src/doctor.d.ts +29 -0
- package/dist/reports/src/doctor.js +83 -0
- package/dist/reports/src/effective-delta.d.ts +34 -0
- package/dist/reports/src/effective-delta.js +25 -0
- package/dist/reports/src/evidence-detail.d.ts +64 -0
- package/dist/reports/src/evidence-detail.js +92 -0
- package/dist/reports/src/evidence-live.d.ts +40 -4
- package/dist/reports/src/evidence-live.js +52 -4
- package/dist/reports/src/evidence-types.d.ts +70 -1
- package/dist/reports/src/evidence-types.js +5 -1
- package/dist/reports/src/evidence-view.d.ts +58 -6
- package/dist/reports/src/evidence-view.js +44 -5
- package/dist/reports/src/explorer-types.d.ts +40 -3
- package/dist/reports/src/explorer-types.js +8 -0
- package/dist/reports/src/explorer-versions.d.ts +9 -1
- package/dist/reports/src/explorer-versions.js +18 -1
- package/dist/reports/src/export-evidence.d.ts +16 -1
- package/dist/reports/src/export-evidence.js +52 -1
- package/dist/reports/src/fetcher.d.ts +27 -3
- package/dist/reports/src/fetcher.js +11 -3
- package/dist/reports/src/format-date.d.ts +6 -0
- package/dist/reports/src/format-date.js +6 -0
- package/dist/reports/src/format.d.ts +16 -0
- package/dist/reports/src/format.js +10 -0
- package/dist/reports/src/frames.d.ts +36 -3
- package/dist/reports/src/frames.js +22 -3
- package/dist/reports/src/hash.d.ts +18 -0
- package/dist/reports/src/hash.js +13 -0
- package/dist/reports/src/host.d.ts +29 -0
- package/dist/reports/src/host.js +64 -0
- package/dist/reports/src/html-frame.d.ts +5 -6
- package/dist/reports/src/html-frame.js +12 -1
- package/dist/reports/src/http-store.d.ts +52 -0
- package/dist/reports/src/http-store.js +145 -0
- package/dist/reports/src/http.d.ts +7 -0
- package/dist/reports/src/http.js +6 -0
- package/dist/reports/src/ingest.d.ts +54 -0
- package/dist/reports/src/ingest.js +210 -0
- package/dist/reports/src/init.d.ts +40 -0
- package/dist/reports/src/init.js +364 -0
- package/dist/reports/src/junit.d.ts +33 -6
- package/dist/reports/src/junit.js +87 -21
- package/dist/reports/src/keyboard.d.ts +20 -0
- package/dist/reports/src/keyboard.js +11 -0
- package/dist/reports/src/keys.d.ts +21 -3
- package/dist/reports/src/keys.js +21 -3
- package/dist/reports/src/layer-axes.d.ts +48 -0
- package/dist/reports/src/layer-axes.js +71 -0
- package/dist/reports/src/layer-catalog.d.ts +28 -1
- package/dist/reports/src/layer-catalog.js +19 -2
- package/dist/reports/src/layer-merge.d.ts +98 -9
- package/dist/reports/src/layer-merge.js +154 -33
- package/dist/reports/src/layer-source.d.ts +18 -0
- package/dist/reports/src/layer-source.js +14 -0
- package/dist/reports/src/layer-store.d.ts +42 -0
- package/dist/reports/src/layer-store.js +63 -5
- package/dist/reports/src/layer-types.d.ts +34 -1
- package/dist/reports/src/layers-route.d.ts +47 -4
- package/dist/reports/src/layers-route.js +88 -12
- package/dist/reports/src/media-types.d.ts +11 -0
- package/dist/reports/src/media-types.js +22 -0
- package/dist/reports/src/namespace-guard.d.ts +10 -1
- package/dist/reports/src/namespace-guard.js +10 -1
- package/dist/reports/src/navigation.d.ts +12 -0
- package/dist/reports/src/navigation.js +7 -0
- package/dist/reports/src/object-io.d.ts +11 -4
- package/dist/reports/src/object-io.js +4 -4
- package/dist/reports/src/object-store-conformance.d.ts +27 -0
- package/dist/reports/src/object-store-conformance.js +87 -0
- package/dist/reports/src/object-store.d.ts +33 -24
- package/dist/reports/src/object-store.js +17 -24
- package/dist/reports/src/parse-with.d.ts +16 -0
- package/dist/reports/src/parse-with.js +14 -0
- package/dist/reports/src/performance-part.d.ts +25 -1
- package/dist/reports/src/performance-part.js +15 -1
- package/dist/reports/src/performance.d.ts +22 -2
- package/dist/reports/src/performance.js +12 -2
- package/dist/reports/src/persist.d.ts +12 -3
- package/dist/reports/src/persist.js +12 -3
- package/dist/reports/src/phone.d.ts +18 -1
- package/dist/reports/src/phone.js +18 -1
- package/dist/reports/src/protocol.d.ts +30 -4
- package/dist/reports/src/protocol.js +15 -4
- package/dist/reports/src/publication.d.ts +21 -1
- package/dist/reports/src/publication.js +21 -1
- package/dist/reports/src/publish-checks.d.ts +35 -0
- package/dist/reports/src/publish-checks.js +122 -0
- package/dist/reports/src/publish.d.ts +44 -9
- package/dist/reports/src/publish.js +40 -7
- package/dist/reports/src/read.d.ts +46 -16
- package/dist/reports/src/read.js +38 -14
- package/dist/reports/src/recording-bundle.d.ts +13 -2
- package/dist/reports/src/recording-bundle.js +4 -2
- package/dist/reports/src/recording-keys.d.ts +15 -1
- package/dist/reports/src/recording-keys.js +15 -1
- package/dist/reports/src/remote-report.d.ts +28 -7
- package/dist/reports/src/remote-report.js +15 -7
- package/dist/reports/src/report-cli.d.ts +51 -0
- package/dist/reports/src/report-cli.js +485 -0
- package/dist/reports/src/report-index.d.ts +99 -0
- package/dist/reports/src/report-index.js +337 -0
- package/dist/reports/src/report-loader.d.ts +10 -0
- package/dist/reports/src/report-loader.js +6 -0
- package/dist/reports/src/report-parts.d.ts +29 -3
- package/dist/reports/src/report-parts.js +40 -3
- package/dist/reports/src/report-status.d.ts +48 -0
- package/dist/reports/src/report-status.js +156 -0
- package/dist/reports/src/report-types.d.ts +73 -0
- package/dist/reports/src/report-types.js +3 -0
- package/dist/reports/src/reporter.d.ts +67 -0
- package/dist/reports/src/reporter.js +128 -0
- package/dist/reports/src/reports-explorer-route.d.ts +541 -0
- package/dist/reports/src/reports-explorer-route.js +11 -0
- package/dist/reports/src/reports-list-route.d.ts +9 -0
- package/dist/reports/src/reports-list-route.js +9 -0
- package/dist/reports/src/reports-route.d.ts +561 -22
- package/dist/reports/src/reports-route.js +11 -3
- package/dist/reports/src/reports-store.d.ts +19 -2
- package/dist/reports/src/reports-store.js +10 -2
- package/dist/reports/src/resilient-cache.d.ts +14 -1
- package/dist/reports/src/resilient-cache.js +8 -1
- package/dist/reports/src/retention.d.ts +17 -1
- package/dist/reports/src/retention.js +5 -1
- package/dist/reports/src/run-bundle.d.ts +63 -0
- package/dist/reports/src/run-bundle.js +159 -0
- package/dist/reports/src/s3-store.d.ts +79 -0
- package/dist/reports/src/s3-store.js +295 -0
- package/dist/reports/src/scenario.d.ts +14 -4
- package/dist/reports/src/scenario.js +5 -4
- package/dist/reports/src/sigv4.d.ts +72 -0
- package/dist/reports/src/sigv4.js +96 -0
- package/dist/reports/src/snapshot-bundle.d.ts +24 -1
- package/dist/reports/src/snapshot-bundle.js +27 -2
- package/dist/reports/src/snapshot-surface.d.ts +20 -9
- package/dist/reports/src/snapshot-surface.js +32 -19
- package/dist/reports/src/source-content.d.ts +23 -0
- package/dist/reports/src/source-content.js +30 -1
- package/dist/reports/src/source-entries.d.ts +13 -0
- package/dist/reports/src/source-entries.js +6 -0
- package/dist/reports/src/source-part.d.ts +15 -0
- package/dist/reports/src/source-part.js +8 -0
- package/dist/reports/src/source-reader.d.ts +11 -0
- package/dist/reports/src/source-reader.js +5 -0
- package/dist/reports/src/spec-evidence.d.ts +12 -2
- package/dist/reports/src/spec-evidence.js +6 -2
- package/dist/reports/src/spec-inventory.d.ts +15 -1
- package/dist/reports/src/spec-inventory.js +22 -1
- package/dist/reports/src/spec-link.d.ts +28 -0
- package/dist/reports/src/spec-link.js +70 -0
- package/dist/reports/src/spec-set.d.ts +63 -0
- package/dist/reports/src/spec-set.js +177 -0
- package/dist/reports/src/status.d.ts +10 -0
- package/dist/reports/src/status.js +5 -0
- package/dist/reports/src/store-prefix.d.ts +28 -0
- package/dist/reports/src/store-prefix.js +22 -0
- package/dist/reports/src/store-url.d.ts +89 -0
- package/dist/reports/src/store-url.js +160 -0
- package/dist/reports/src/test-catalog-entity.d.ts +11 -0
- package/dist/reports/src/test-catalog-entity.js +6 -0
- package/dist/reports/src/types.d.ts +6 -1
- package/dist/reports/src/use-report-nav.d.ts +35 -0
- package/dist/reports/src/use-report-nav.js +12 -0
- package/dist/schema/src/api.d.ts +799 -0
- package/dist/schema/src/api.js +201 -0
- package/dist/schema/src/cli-results.d.ts +210 -0
- package/dist/schema/src/cli-results.js +124 -0
- package/dist/schema/src/common.d.ts +53 -0
- package/dist/schema/src/common.js +42 -0
- package/dist/schema/src/index.d.ts +14 -0
- package/dist/schema/src/index.js +14 -0
- package/dist/schema/src/layer.d.ts +2273 -0
- package/dist/schema/src/layer.js +462 -0
- package/dist/schema/src/manifest.d.ts +95 -0
- package/dist/schema/src/manifest.js +84 -0
- package/dist/schema/src/report-sources.d.ts +28 -0
- package/dist/schema/src/report-sources.js +25 -0
- package/dist/schema/src/runs-index.d.ts +16 -0
- package/dist/schema/src/runs-index.js +15 -0
- package/dist/schema/src/server-config.d.ts +93 -0
- package/dist/schema/src/server-config.js +73 -0
- package/dist/schema/src/spec.d.ts +69 -0
- package/dist/schema/src/spec.js +58 -0
- package/dist/server/src/app.d.ts +62 -0
- package/dist/server/src/app.js +419 -0
- package/dist/server/src/auth.d.ts +41 -0
- package/dist/server/src/auth.js +59 -0
- package/dist/server/src/config.d.ts +83 -0
- package/dist/server/src/config.js +187 -0
- package/dist/server/src/dev.d.ts +12 -0
- package/dist/server/src/dev.js +80 -0
- package/dist/server/src/events.d.ts +29 -0
- package/dist/server/src/events.js +101 -0
- package/dist/server/src/host.d.ts +30 -0
- package/dist/server/src/host.js +115 -0
- package/dist/server/src/index.d.ts +16 -0
- package/dist/server/src/index.js +16 -0
- package/dist/server/src/llms.d.ts +19 -0
- package/dist/server/src/llms.js +63 -0
- package/dist/server/src/main.d.ts +30 -0
- package/dist/server/src/main.js +112 -0
- package/dist/server/src/openapi.d.ts +59 -0
- package/dist/server/src/openapi.js +374 -0
- package/dist/server/src/problem.d.ts +8 -0
- package/dist/server/src/problem.js +39 -0
- package/dist/server/src/site.d.ts +13 -0
- package/dist/server/src/site.js +43 -0
- package/dist/server/src/store-api.d.ts +28 -0
- package/dist/server/src/store-api.js +159 -0
- package/dist/server/src/validate.d.ts +71 -0
- package/dist/server/src/validate.js +197 -0
- package/dist/vitest/src/index.d.ts +27 -5
- package/dist/vitest/src/index.js +18 -5
- package/dist/webdriver/src/driver.d.ts +63 -11
- package/dist/webdriver/src/driver.js +57 -6
- package/dist/webdriver/src/index.d.ts +8 -0
- package/dist/webdriver/src/index.js +1 -0
- package/dist/webdriver/src/protocol.d.ts +75 -6
- package/dist/webdriver/src/protocol.js +79 -6
- package/docs/agents.md +137 -0
- package/docs/deploying.md +345 -0
- package/docs/dogfooding.md +175 -0
- package/docs/extending.md +242 -0
- package/docs/integrating.md +485 -0
- package/docs/reference/bun.md +141 -0
- package/docs/reference/cli.md +146 -0
- package/docs/reference/core.md +2426 -0
- package/docs/reference/detox.md +150 -0
- package/docs/reference/http-api.md +303 -0
- package/docs/reference/playwright.md +303 -0
- package/docs/reference/reports.md +7098 -0
- package/docs/reference/schema.md +1651 -0
- package/docs/reference/server.md +685 -0
- package/docs/reference/vitest.md +45 -0
- package/docs/reference/webdriver.md +330 -0
- package/docs/report-backends.md +144 -0
- package/docs/report-contract.md +400 -0
- package/docs/reporting-issues.md +160 -0
- package/docs/server.md +285 -0
- package/docs/statecharts.md +641 -0
- package/llms-full.txt +3506 -0
- package/llms.txt +47 -0
- package/package.json +33 -5
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +69 -2
- package/packages/reports/STORAGE.md +5 -1
- package/packages/schema/README.md +14 -0
- package/packages/schema/schemas/check-detail.schema.json +1032 -0
- package/packages/schema/schemas/checks-result.schema.json +26 -0
- package/packages/schema/schemas/command-error.schema.json +35 -0
- package/packages/schema/schemas/doctor-result.schema.json +60 -0
- package/packages/schema/schemas/effective-delta.schema.json +490 -0
- package/packages/schema/schemas/effective-evidence.schema.json +471 -0
- package/packages/schema/schemas/evidence-page.schema.json +271 -0
- package/packages/schema/schemas/hello-event.schema.json +22 -0
- package/packages/schema/schemas/init-plan.schema.json +347 -0
- package/packages/schema/schemas/layer-rows.schema.json +199 -0
- package/packages/schema/schemas/layer.schema.json +2888 -0
- package/packages/schema/schemas/merge-result.schema.json +40 -0
- package/packages/schema/schemas/outline.schema.json +62 -0
- package/packages/schema/schemas/problem.schema.json +37 -0
- package/packages/schema/schemas/publish-checks.schema.json +1413 -0
- package/packages/schema/schemas/publish-result.schema.json +168 -0
- package/packages/schema/schemas/ready.schema.json +68 -0
- package/packages/schema/schemas/report-event.schema.json +100 -0
- package/packages/schema/schemas/report-status.schema.json +370 -0
- package/packages/schema/schemas/server-config.schema.json +194 -0
- package/packages/schema/schemas/server-info.schema.json +114 -0
- package/packages/schema/schemas/spec-set.schema.json +103 -0
- package/packages/schema/schemas/store-listing.schema.json +29 -0
- package/packages/schema/schemas/store-write-result.schema.json +28 -0
- package/packages/schema/schemas/stream-result.schema.json +59 -0
- package/packages/server/README.md +43 -0
- package/packages/server/openapi.json +6699 -0
- package/packages/visualizer/README.md +27 -5
- package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/scripts/make-fixtures.ts +2 -0
- package/packages/visualizer/scripts/publish-data.ts +1 -2
- package/packages/visualizer/src/app/AppShell.tsx +9 -2
- package/packages/visualizer/src/app/report-sources.tsx +6 -4
- package/packages/visualizer/src/app/router.tsx +3 -3
- package/packages/visualizer/src/components/Header.module.css +35 -0
- package/packages/visualizer/src/components/Header.tsx +41 -4
- package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
- package/packages/visualizer/src/data/adapters/index.ts +10 -1
- package/packages/visualizer/src/evidence/api.ts +14 -2
- package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
- package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +38 -4
- package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
- package/packages/visualizer/src/evidence/evidence.module.css +20 -0
- package/packages/visualizer/src/evidence/marks.tsx +2 -0
- package/packages/visualizer/src/evidence/overview.tsx +3 -3
- package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
- package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
- package/packages/visualizer/src/find/Finder.tsx +1 -7
- package/packages/visualizer/src/find/open.ts +4 -0
- package/packages/visualizer/src/live/live-stream.ts +204 -0
- package/packages/visualizer/src/runtime.ts +19 -0
- package/packages/visualizer/src/state/ui-store.ts +6 -2
- package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
- package/packages/visualizer/vite/example-reports.ts +475 -0
- package/packages/visualizer/vite.config.ts +18 -2
|
@@ -0,0 +1,2426 @@
|
|
|
1
|
+
# Reference: core
|
|
2
|
+
|
|
3
|
+
Executable statecharts: loading, composing, linting, planning and running specs against real apps, and the `atlas` command. Import path `@crvouga/atlas`.
|
|
4
|
+
|
|
5
|
+
> Generated from the doc comments of `packages/core/src` by `bun run docs`. Do not edit: change the comment next to the code.
|
|
6
|
+
|
|
7
|
+
### The `atlas` command
|
|
8
|
+
|
|
9
|
+
Source: `packages/core/src/cli.ts`. Import: `@crvouga/atlas/cli`
|
|
10
|
+
|
|
11
|
+
Dispatches `atlas <command>`: the statechart commands (`lint`, `plan`, `run`, `merge`, `export`) and, loaded on demand so
|
|
12
|
+
they cost nothing otherwise, `serve`, `dev`, `init` and `report`. Statechart configuration is `atlas.config.ts` (see
|
|
13
|
+
`defineConfig`). A command that is wrong prints a message and exits 2; a plan, lint or run that finds problems exits 1.
|
|
14
|
+
|
|
15
|
+
See docs/statecharts.md
|
|
16
|
+
See docs/reference/cli.md
|
|
17
|
+
|
|
18
|
+
#### `USAGE`
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
export const USAGE = …
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The text `atlas --help` prints: every command, and the flags of the statechart commands.
|
|
25
|
+
|
|
26
|
+
#### `main`
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export async function main(argv = process.argv.slice(2))
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Runs `atlas` with `argv` (default: the process arguments) and returns the exit code. Throws for a command that is wrong, which the `atlas` binary reports as a message and exit code 2.
|
|
33
|
+
|
|
34
|
+
### Config and programmatic API
|
|
35
|
+
|
|
36
|
+
Source: `packages/core/src/config.ts`
|
|
37
|
+
|
|
38
|
+
The user-facing entry point: `defineConfig` types a project's `atlas.config.ts`, and `prepare`,
|
|
39
|
+
`runConfig` and `mergeConfigRuns` are the programmatic equivalents of `atlas lint`/`plan`, `atlas
|
|
40
|
+
run` and `atlas merge`. They wire the pieces together (load specs, lint, scope, plan, run) and add
|
|
41
|
+
no behaviour of their own beyond validating the config against the spec. Relative paths in a config
|
|
42
|
+
resolve against the `baseDirectory` argument (the config file's directory when called by the CLI).
|
|
43
|
+
|
|
44
|
+
See docs/statecharts.md
|
|
45
|
+
|
|
46
|
+
#### `AtlasConfig`
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export type AtlasConfig<C = unknown> = {
|
|
50
|
+
/** The directory holding the charts (`*.scxml`, `*.machine.{json,yaml}`) and `journeys.yaml`. */
|
|
51
|
+
specs: string;
|
|
52
|
+
/** The chart to run; the root chart when omitted. */
|
|
53
|
+
chart?: string;
|
|
54
|
+
/** Where generated paths start (an XState state value); the initial state when omitted. */
|
|
55
|
+
start?: StateValue;
|
|
56
|
+
/** A phrase describing `start` in reports; `The start of the chart` when omitted. */
|
|
57
|
+
startLabel?: string;
|
|
58
|
+
/** Only these events enter the chart from outside it. */
|
|
59
|
+
entryEvents?: string[];
|
|
60
|
+
/** Run only this state and what is inside it, with the transitions into and out of it. */
|
|
61
|
+
state?: string;
|
|
62
|
+
/** Path attempts that run at once (capped by the driver's `concurrency`); one per CPU when omitted. */
|
|
63
|
+
workers?: number;
|
|
64
|
+
/** Runs once before any path: prepare what every seed relies on (reference data, config). */
|
|
65
|
+
beforeRun?: () => Promise<void> | void;
|
|
66
|
+
/** The driver, or a function that creates it (called once per run, after `beforeRun`). */
|
|
67
|
+
driver: Driver<C> | (() => Driver<C> | Promise<Driver<C>>);
|
|
68
|
+
/** How to make each event happen and recognise each state. */
|
|
69
|
+
implementation: Implementation<C>;
|
|
70
|
+
/** Where runs are written; `atlas-runs` beside the config when omitted. */
|
|
71
|
+
output?: string;
|
|
72
|
+
/** Creates the business event sources for a run (called once per run; the run closes them). */
|
|
73
|
+
eventSources?: () => EventSource[] | Promise<EventSource[]>;
|
|
74
|
+
/** How each business event name from `meta.events` is recognised; by default a CloudEvent whose `type` equals the name. */
|
|
75
|
+
eventMatchers?: Record<string, EventMatcher>;
|
|
76
|
+
/** Extra privacy rules on top of the defaults. */
|
|
77
|
+
privacy?: PrivacyRules;
|
|
78
|
+
/** URLs the run will reach; checked against `privacy.allowHosts` (loopback by default). */
|
|
79
|
+
targets?: string[];
|
|
80
|
+
/** Recorded in the manifest as the run's environment (`run.environment`), beside the driver name. */
|
|
81
|
+
environment?: Record<string, unknown>;
|
|
82
|
+
/** How long to wait for a state to be recognised after an event, in milliseconds; 45 000 when omitted. */
|
|
83
|
+
stepTimeoutMs?: number;
|
|
84
|
+
/** Meta fields every state must carry (default: description). */
|
|
85
|
+
requiredMeta?: string[];
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Everything a project says about running its specs. `C` is the type of the driver's context
|
|
90
|
+
(a Playwright `Page`, a model, ...), which events and states receive.
|
|
91
|
+
|
|
92
|
+
#### `defineConfig`
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
export function defineConfig<C>(config: AtlasConfig<C>)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Declares an Atlas config with types, returning it unchanged. Put it as the default export of
|
|
99
|
+
`atlas.config.ts`; the CLI loads the file with `tsx` and defaults to `atlas.config.ts` in the
|
|
100
|
+
current directory.
|
|
101
|
+
|
|
102
|
+
See /statecharts.md
|
|
103
|
+
|
|
104
|
+
**Example**
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
export default defineConfig({
|
|
108
|
+
specs: './specs',
|
|
109
|
+
driver: () => functionDriver(() => new TodoModel()),
|
|
110
|
+
implementation: {
|
|
111
|
+
events: { 'Adds a todo': { kind: 'user', how: 'Calls add().', run: async (model) => model.add('x') } },
|
|
112
|
+
states: { 'No todos': { recognize: async (model) => ({ matched: model.count === 0, signals: [] }) } }
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### `PrepareOptions`
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
export type PrepareOptions = {
|
|
121
|
+
/** Scope the run to this state instead of the config's. */
|
|
122
|
+
state?: string;
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Overrides of a config that the command line can give.
|
|
127
|
+
|
|
128
|
+
#### `prepare`
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
export function prepare<C>(config: AtlasConfig<C>, baseDirectory = process.cwd(), options: PrepareOptions = {})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Loads, lints and plans a config without running anything (what `atlas lint` and `atlas plan` do).
|
|
135
|
+
|
|
136
|
+
- `baseDirectory`: what a relative `config.specs` resolves against.
|
|
137
|
+
|
|
138
|
+
**Returns** the loaded `bundle`; `lint` (findings, composition and graph, including `implemented`
|
|
139
|
+
findings for what the implementation lacks); the run's `scope`; the `plan` (paths, journeys, seeds,
|
|
140
|
+
`unreachable`); and the resolved `specs` directory.
|
|
141
|
+
|
|
142
|
+
**Throws** Error when the specs cannot be loaded or composed, when a seed's `shows` is not active where
|
|
143
|
+
it starts, or when a seed's `for` names an event no state handles. Lint findings are returned,
|
|
144
|
+
not thrown.
|
|
145
|
+
|
|
146
|
+
#### `RunCommandOptions`
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
export type RunCommandOptions = PrepareOptions & {
|
|
150
|
+
/** `fast` when omitted. */
|
|
151
|
+
mode?: RunMode;
|
|
152
|
+
/** Only these paths, by id. */
|
|
153
|
+
paths?: string[];
|
|
154
|
+
/** Only paths that start from these seeds ("start" for paths that `setup` starts). */
|
|
155
|
+
seeds?: string[];
|
|
156
|
+
/** Whether a failed path runs again to tell flaky from broken; true unless `false`. */
|
|
157
|
+
retry?: boolean;
|
|
158
|
+
/** Where the run is written; the config's `output`, else `atlas-runs`. */
|
|
159
|
+
output?: string;
|
|
160
|
+
/** Path attempts at once; the config's `workers`, else one per CPU, always capped by the driver. */
|
|
161
|
+
workers?: number;
|
|
162
|
+
/** Run one shard of the plan; `atlas merge` composes the shards. */
|
|
163
|
+
shard?: Shard;
|
|
164
|
+
/** Keep the passed paths of this run (or the newest run, with "latest") and run the rest. */
|
|
165
|
+
reuse?: string;
|
|
166
|
+
/** Receives progress lines; stdout when omitted. */
|
|
167
|
+
log?: (line: string) => void;
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The options of `atlas run` that `runConfig` accepts; each overrides the config's own setting.
|
|
172
|
+
|
|
173
|
+
#### `runConfig`
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
export async function runConfig<C>(config: AtlasConfig<C>, options: RunCommandOptions = {}, baseDirectory = process.cwd())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Runs a config end to end and writes the run (manifest, reports, media) under its output
|
|
180
|
+
directory, like `atlas run`.
|
|
181
|
+
|
|
182
|
+
Refuses to start (throws) when the spec has any structural lint finding (everything but missing
|
|
183
|
+
implementations, which just block paths) or when a `targets` URL is not on the privacy allow-list.
|
|
184
|
+
Then it calls `beforeRun`, creates the driver and runs the planned paths (filtered by
|
|
185
|
+
`options.paths`/`seeds`/`shard`, reusing earlier passes when `options.reuse` is set).
|
|
186
|
+
|
|
187
|
+
**Returns** what `runPaths` returns: the run directory, the manifest and `ok` (false if any path
|
|
188
|
+
failed or the privacy scan found something).
|
|
189
|
+
|
|
190
|
+
**Throws** Error also when `reuse` names a run with no outcomes or there is no earlier run for `latest`.
|
|
191
|
+
|
|
192
|
+
See /statecharts.md
|
|
193
|
+
|
|
194
|
+
#### `mergeConfigRuns`
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
export function mergeConfigRuns<C>(config: AtlasConfig<C>, runDirs: string[], options: PrepareOptions & { output?: string } = {}, baseDirectory = process.cwd())
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Composes runs of a config (its shards, or a rerun and the run before it) into one new run
|
|
201
|
+
under the output directory, like `atlas merge`. The plan is recomputed from the config, so the runs
|
|
202
|
+
must come from the same specs, implementation and seeds.
|
|
203
|
+
|
|
204
|
+
- `runDirs`: the run directories to compose; later ones win when two ran the same path.
|
|
205
|
+
|
|
206
|
+
**Returns** what `mergeRuns` returns: the new run directory, its manifest and `ok`.
|
|
207
|
+
|
|
208
|
+
**Throws** Error when a run directory has no `outcomes.json`.
|
|
209
|
+
|
|
210
|
+
### Business event sources
|
|
211
|
+
|
|
212
|
+
Source: `packages/core/src/events/cloudevents.ts`
|
|
213
|
+
|
|
214
|
+
Where a run learns which business events actually happened: an HTTP endpoint that accepts
|
|
215
|
+
CloudEvents (`cloudEventsHttpSource`), or the lines a log file gains (`logFileSource`), plus the pure
|
|
216
|
+
functions that compare what a state expects (`meta.events`) with what was collected. Every source
|
|
217
|
+
implements `EventSource`: `mark()` records a position before a step and `collect(mark)` returns only
|
|
218
|
+
what arrived after it. An event with no way to be observed is reported as "no signal", never as
|
|
219
|
+
missing, so absence of evidence is not read as failure.
|
|
220
|
+
|
|
221
|
+
See docs/statecharts.md
|
|
222
|
+
|
|
223
|
+
#### `CloudEvent`
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
export type CloudEvent = {
|
|
227
|
+
specversion: '1.0';
|
|
228
|
+
id: string;
|
|
229
|
+
source: string;
|
|
230
|
+
type: string;
|
|
231
|
+
time?: string;
|
|
232
|
+
subject?: string;
|
|
233
|
+
datacontenttype?: string;
|
|
234
|
+
data?: unknown;
|
|
235
|
+
[extension: string]: unknown;
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
A CloudEvents 1.0 event in the JSON format (https://cloudevents.io).
|
|
240
|
+
|
|
241
|
+
#### `cloudEventsFromHttp`
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
export function cloudEventsFromHttp(headers: Record<string, string | string[] | undefined>, body: string): CloudEvent[]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Parses an HTTP delivery in any CloudEvents content mode: structured (`application/cloudevents+json`),
|
|
248
|
+
batch (`application/cloudevents-batch+json`) or binary (`ce-*` headers, with the body as `data`).
|
|
249
|
+
A request with none of those (no `ce-type` header and a different content type) yields no events.
|
|
250
|
+
In binary mode a missing `ce-id` becomes a random UUID and a missing `ce-source` becomes `unknown`;
|
|
251
|
+
other `ce-*` headers become extension attributes; the body is parsed as JSON when the content type
|
|
252
|
+
says JSON and it parses, else kept as text.
|
|
253
|
+
|
|
254
|
+
- `headers`: lower-cased header names, as Node's `IncomingMessage.headers`.
|
|
255
|
+
|
|
256
|
+
**Throws** SyntaxError when a structured or batch body is not valid JSON.
|
|
257
|
+
|
|
258
|
+
#### `cloudEventsHttpSource`
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
export async function cloudEventsHttpSource(options: { port?: number; host?: string } = {}): Promise<EventSource & { url: string }>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Starts an HTTP endpoint the system under test sends CloudEvents to (any content mode). Point the
|
|
265
|
+
product's event bus, webhook relay or OpenTelemetry collector's CloudEvents exporter at its `url`.
|
|
266
|
+
|
|
267
|
+
It answers every valid POST with 202 and an unparseable delivery with 400, and keeps events in
|
|
268
|
+
memory. It does no authentication and listens on `127.0.0.1` unless `host` says otherwise, so
|
|
269
|
+
only the local machine can reach it by default. `mark()` is the number of events received so far.
|
|
270
|
+
Call `close()` to stop the server; `runPaths` does that for the sources it is given.
|
|
271
|
+
|
|
272
|
+
- `options.port`: the port to listen on; an unused port when omitted (read the real one from `url`).
|
|
273
|
+
|
|
274
|
+
- `options.host`: the interface to listen on; `127.0.0.1` when omitted.
|
|
275
|
+
|
|
276
|
+
**Example**
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
eventSources: async () => [await cloudEventsHttpSource({ port: 4318 })]
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### `logFileSource`
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
export function logFileSource(file: string, options: { settleMs?: number } = {}): EventSource
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
An event source for products whose only event stream is a log file: `collect` returns the text
|
|
289
|
+
appended to the file since `mark` (match it with an `{ pattern }` matcher), and no CloudEvents.
|
|
290
|
+
|
|
291
|
+
`mark()` is the file's size in bytes (0 when it does not exist). `collect` first waits `settleMs`
|
|
292
|
+
for the product to write, then reads from the mark to the end of the file; a missing or
|
|
293
|
+
unreadable file gives empty text, and a file that was truncated or rotated since the mark is not
|
|
294
|
+
detected. The source's name starts with `log:`, which is how the runner tells it from a CloudEvents source.
|
|
295
|
+
|
|
296
|
+
- `file`: the log file path.
|
|
297
|
+
|
|
298
|
+
- `options.settleMs`: how long `collect` waits before reading, in milliseconds; 1000 when omitted.
|
|
299
|
+
|
|
300
|
+
#### `BusinessEventResult`
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
export type BusinessEventResult = {
|
|
304
|
+
/** The names listed in the state's `meta.events`. */
|
|
305
|
+
expected: string[];
|
|
306
|
+
/** Expected names that a matcher (or a CloudEvent of that type) confirmed. */
|
|
307
|
+
observed: string[];
|
|
308
|
+
/** Expected names that could be observed but were not. */
|
|
309
|
+
missing: string[];
|
|
310
|
+
/** Expected names with no way to be observed (no matcher and no CloudEvents source). Not a failure. */
|
|
311
|
+
noSignal: string[];
|
|
312
|
+
/** The CloudEvents collected for this step. */
|
|
313
|
+
events: CloudEvent[];
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The outcome of comparing a state's expected business events with what the sources collected.
|
|
318
|
+
|
|
319
|
+
#### `correlated`
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
export function correlated(collected: { events: CloudEvent[]; text: string }, ids: string[])
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Keeps what belongs to one attempt when attempts run side by side: events (whose JSON mentions an
|
|
326
|
+
id) and log lines that contain one of its correlation ids. Empty ids are ignored; with no ids,
|
|
327
|
+
everything is kept.
|
|
328
|
+
|
|
329
|
+
#### `compareBusinessEvents`
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
export function compareBusinessEvents(
|
|
333
|
+
expected: string[],
|
|
334
|
+
collected: { events: CloudEvent[]; text: string },
|
|
335
|
+
matchers: Record<string, EventMatcher>,
|
|
336
|
+
hasCloudEvents: boolean
|
|
337
|
+
): BusinessEventResult
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Compares the business events a state expects with what the sources saw, sorting each expected
|
|
341
|
+
name into `observed`, `missing` or `noSignal`. A business event is observed when its matcher
|
|
342
|
+
matches (a function gets the events and log text; `{ type }` compares CloudEvent types;
|
|
343
|
+
`{ pattern }` tests the log text); with no matcher, when a CloudEvent's `type` equals the name.
|
|
344
|
+
With neither a matcher nor any CloudEvents source (`hasCloudEvents` false) it is "no signal",
|
|
345
|
+
never "missing". Pure.
|
|
346
|
+
|
|
347
|
+
- `matchers`: matchers by business event name.
|
|
348
|
+
|
|
349
|
+
- `hasCloudEvents`: whether any non-log source was configured.
|
|
350
|
+
|
|
351
|
+
### Chart graph simulator
|
|
352
|
+
|
|
353
|
+
Source: `packages/core/src/graph.ts`
|
|
354
|
+
|
|
355
|
+
A pure, deterministic simulator over a composed machine (see `composeCharts`): given a
|
|
356
|
+
configuration and an event it computes the next configuration and names the transitions that
|
|
357
|
+
fired. The planner, linter and runner all use it to reason about a chart without running
|
|
358
|
+
the product. It never executes guards or actions (the linter rejects charts that have them), and
|
|
359
|
+
it identifies states by name alone, so names must be unique across the composed product.
|
|
360
|
+
|
|
361
|
+
See docs/statecharts.md
|
|
362
|
+
|
|
363
|
+
#### `StateValue`
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
export type StateValue = string | { [key: string]: StateValue };
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
An XState state value: a leaf state's name, or an object mapping each active child to its own value (parallel regions have several keys).
|
|
370
|
+
|
|
371
|
+
#### `Step`
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
export type Step = {
|
|
375
|
+
event: string;
|
|
376
|
+
/** Ids of the transitions that fired (see `transitionId`). */
|
|
377
|
+
transitions: string[];
|
|
378
|
+
/** True when the event was inserted automatically because a child chart's final state became active. */
|
|
379
|
+
handOff: boolean;
|
|
380
|
+
};
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
One event taken during a replay.
|
|
384
|
+
|
|
385
|
+
#### `Config`
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
export type Config = {
|
|
389
|
+
value: StateValue;
|
|
390
|
+
/** Names of every active state, ancestors included (see `activeKeys`). */
|
|
391
|
+
active: string[];
|
|
392
|
+
};
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
A configuration of the chart: where it is right now.
|
|
396
|
+
|
|
397
|
+
#### `configKey`
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
export function configKey(value: StateValue): string
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
A configuration's identity: its state value with keys sorted, so equal configurations compare equal.
|
|
404
|
+
|
|
405
|
+
#### `activeKeys`
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
export function activeKeys(value: StateValue): string[]
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Names of every state in a state value, each parent before its children.
|
|
412
|
+
|
|
413
|
+
#### `ChartGraph`
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
export class ChartGraph
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
A pure simulator over a composed machine: XState computes the next configuration, and this
|
|
420
|
+
names which transitions fired, so coverage and the report can talk about transitions.
|
|
421
|
+
|
|
422
|
+
- `readonly machine;` — The XState machine built from the composed config, used for transition semantics.
|
|
423
|
+
- `readonly handOffStates: Map<string, string>;` — Hand-off state name to the event it passes to the parent chart.
|
|
424
|
+
- `node(name: string)` — The config of the state called `name`, or `undefined` when there is none.
|
|
425
|
+
- `target(source: string, event: string)` — The state a transition leads to, by name.
|
|
426
|
+
- `isLeaf(name: string)` — Whether the state has no child states.
|
|
427
|
+
- `parent(name: string)` — The name of the enclosing state, or `null` for a top-level state (or an unknown name).
|
|
428
|
+
- `stateNames()` — Every state name in the composed chart, parents before children.
|
|
429
|
+
- `regionsOf(state: string): string[]` — The parallel regions a state sits in, outermost first: each the child of a parallel ancestor.
|
|
430
|
+
- `actsIn(event: string, regions: string[])` — Whether an event can matter to what happens in `regions`: some state that handles it sits in those regions or outside them (never only in a sibling region). Regions of a statechart without guards are independent, so exploring one region with the others held still loses nothing and keeps the search from multiplying across regions.
|
|
431
|
+
- `regionSets()` — Every distinct set of regions a state sits in, the empty set (outside every parallel) first.
|
|
432
|
+
- `initial(): Config` — The configuration the chart starts in.
|
|
433
|
+
- `valueOf(state: string): StateValue` — The state value that names `state`: its ancestors down to it, defaults filled in by `resolve`.
|
|
434
|
+
- `resolve(value: StateValue): Config` — Fills a partial state value in with each parent's initial child, returning a complete configuration.
|
|
435
|
+
- `enabledEvents(config: Config)` — Names of the events some active state handles (a state's own or an ancestor's), without duplicates. Whether the event moves anything is for `step` to say.
|
|
436
|
+
- `firedBy(config: Config, event: string)` — The transitions `event` fires from `config`: in each region, the deepest active state that handles it.
|
|
437
|
+
- `step(config: Config, event: string): { config: Config; fired: string[] } | null` — Takes `event` from `config`: the next configuration and the transition ids that fired, or `null` when no active state handles the event.
|
|
438
|
+
- `pendingHandOffs(config: Config)` — Hand-off states active in `config` (a child's former final states), with the event each passes on.
|
|
439
|
+
- `replay(start: Config, events: readonly string[])` — Replay member- and system-sent events, inserting hand-off events whenever a hand-off state becomes active, as the composed spec says the parent receives them automatically.
|
|
440
|
+
- `explore(start: Config | Config[], allowed: (event: string) => boolean, maxConfigs = 20_000)` — Breadth-first from one or several starts at once: every configuration reachable using only `allowed` events, each with the shortest event path from its nearest start (`origin` is that start's index; ties go to the earlier start). `firstFired` holds, for every transition, the shortest way to take it: the configuration it fires from and how that configuration was reached. Exploration order is fixed by the chart, so the result is deterministic.
|
|
441
|
+
|
|
442
|
+
### Atlas core
|
|
443
|
+
|
|
444
|
+
Source: `packages/core/src/index.ts`. Import: `@crvouga/atlas`
|
|
445
|
+
|
|
446
|
+
The public entry point of `@crvouga/atlas`: define a config (`defineConfig`), then load, lint, plan
|
|
447
|
+
and run executable statecharts against a real product through a driver, and write a manifest of the
|
|
448
|
+
results. This file only re-exports, grouped by stage: spec (types, load, SCXML, composition), graph,
|
|
449
|
+
lint, plan, run (drivers, implementations, execution), business events, privacy, report formats and
|
|
450
|
+
config. It does no work at import time. Browser-safe parts are also available from `./spec`.
|
|
451
|
+
|
|
452
|
+
See docs/statecharts.md
|
|
453
|
+
|
|
454
|
+
### Spec linter
|
|
455
|
+
|
|
456
|
+
Source: `packages/core/src/lint.ts`
|
|
457
|
+
|
|
458
|
+
Decides whether a spec bundle is executable before anything runs: pure structure, reachable states,
|
|
459
|
+
no silent dead ends, journeys that replay, and (optionally) an implementation for everything in scope.
|
|
460
|
+
It reports problems as findings rather than throwing, except when the charts cannot be composed at
|
|
461
|
+
all. The `rule` of a finding is a stable short name (`pure`, `naming`, `meta`, `reachable`,
|
|
462
|
+
`dead-end`, `journey`, `implemented`). `runConfig` refuses to run when any finding other than
|
|
463
|
+
`implemented` exists.
|
|
464
|
+
|
|
465
|
+
See docs/statecharts.md
|
|
466
|
+
|
|
467
|
+
#### `LintFinding`
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
export type LintFinding = {
|
|
471
|
+
/** Which check found it: `pure`, `naming`, `meta`, `reachable`, `dead-end`, `journey` or `implemented`. */
|
|
472
|
+
rule: string;
|
|
473
|
+
/** A sentence for a person, naming the state, event or journey concerned. */
|
|
474
|
+
message: string;
|
|
475
|
+
};
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
One problem found by `lintBundle`.
|
|
479
|
+
|
|
480
|
+
#### `LintOptions`
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
export type LintOptions = {
|
|
484
|
+
/** Meta fields every state must carry; `description` by default. */
|
|
485
|
+
requiredMeta?: readonly string[];
|
|
486
|
+
/** Only these states and events need implementations (a chart being brought up). */
|
|
487
|
+
scope?: { states: ReadonlySet<string>; events: ReadonlySet<string> };
|
|
488
|
+
/** The event and state names the implementation provides. When given, anything in scope without one is an `implemented` finding. */
|
|
489
|
+
implementations?: { events: ReadonlySet<string>; states: ReadonlySet<string> };
|
|
490
|
+
};
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Options for `lintBundle`.
|
|
494
|
+
|
|
495
|
+
#### `lintBundle`
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
export function lintBundle(bundle: SpecBundle, options: LintOptions = {})
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Checks a spec bundle is executable: pure (no guards, actions, context or delays), every state
|
|
502
|
+
reachable, no dead ends without `meta.deadEnd`, every journey replays to its `endsIn` states, and
|
|
503
|
+
(when implementations are given) every event and state in scope has one.
|
|
504
|
+
|
|
505
|
+
Findings are returned, never thrown. Each leaf state counts as having an exit when it or any
|
|
506
|
+
ancestor handles an event; final states are exempt from the dead-end rule. `requiredMeta` applies
|
|
507
|
+
to states, not to the chart's root. Only leaf states need recognisers.
|
|
508
|
+
|
|
509
|
+
**Returns** `findings`; the `composition` and `graph` it built (reuse them rather than composing
|
|
510
|
+
again); each journey with its `replay`; and the names in `unimplemented`.
|
|
511
|
+
|
|
512
|
+
**Throws** Error when the charts cannot be composed (a missing or cyclic child chart, a child final
|
|
513
|
+
with nowhere to go); the message lists the findings gathered so far.
|
|
514
|
+
|
|
515
|
+
**Example**
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
const { findings } = lintBundle(loadSpecDirectory('./specs'));
|
|
519
|
+
if (findings.length) throw new Error(findings.map((f) => f.message).join('\n'));
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Run planner
|
|
523
|
+
|
|
524
|
+
Source: `packages/core/src/plan.ts`
|
|
525
|
+
|
|
526
|
+
Decides, from the charts alone, which paths a run executes: the curated journeys (cut at seeded
|
|
527
|
+
configurations), plus the shortest generated path to every transition the journeys leave uncovered.
|
|
528
|
+
It also computes the scope of a run (all of the product, one chart, or one state). Pure and
|
|
529
|
+
deterministic: the same bundle, scope, seeds and blocked events give the same paths, ids and keys on
|
|
530
|
+
any machine, which is what lets shards and reruns be composed. It never runs the product.
|
|
531
|
+
|
|
532
|
+
See docs/statecharts.md
|
|
533
|
+
|
|
534
|
+
#### `ChartScope`
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
export type ChartScope = {
|
|
538
|
+
/** The id of the chart being run. */
|
|
539
|
+
chart: string;
|
|
540
|
+
/** Set when the run covers one state of the chart and what is inside it. */
|
|
541
|
+
state?: string;
|
|
542
|
+
/** The configuration each generated path starts from. */
|
|
543
|
+
start: StateValue;
|
|
544
|
+
/** A phrase describing the start for reports. */
|
|
545
|
+
startLabel: string;
|
|
546
|
+
/** Names of the states in scope. */
|
|
547
|
+
states: Set<string>;
|
|
548
|
+
/** Ids of the transitions in scope (see `transitionId`): the ones the run must cover. */
|
|
549
|
+
transitions: Set<string>;
|
|
550
|
+
/** Names of the events of those transitions. */
|
|
551
|
+
events: Set<string>;
|
|
552
|
+
/** Transitions that enter the chart from outside it. */
|
|
553
|
+
entries: Set<string>;
|
|
554
|
+
};
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
The part of the composed product one run covers, as computed by `chartScope`.
|
|
558
|
+
|
|
559
|
+
#### `PlannedStep`
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
export type PlannedStep = {
|
|
563
|
+
event: string;
|
|
564
|
+
/** Ids of the transitions the event fires (see `transitionId`). */
|
|
565
|
+
transitions: string[];
|
|
566
|
+
handOff: boolean;
|
|
567
|
+
/** Names of the active states before the step. */
|
|
568
|
+
from: string[];
|
|
569
|
+
/** Names of the active states after the step. */
|
|
570
|
+
to: string[];
|
|
571
|
+
};
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
One event of a planned path, with the transitions it fires and the active states before and after. A hand-off step is inserted automatically and is not driven by the implementation's events.
|
|
575
|
+
|
|
576
|
+
#### `PlannedPath`
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
export type PlannedPath = {
|
|
580
|
+
/** `journey-<n>` or `generated-<n>`, numbered in plan order. Stable for a given plan; use `key` to match across plans. */
|
|
581
|
+
id: string;
|
|
582
|
+
/** The same start and events give the same key on any machine and in any run. */
|
|
583
|
+
key: string;
|
|
584
|
+
/** The journey's name, or `Covers: <state> → <event>` for a generated path. */
|
|
585
|
+
name: string;
|
|
586
|
+
kind: 'journey' | 'generated';
|
|
587
|
+
description: string;
|
|
588
|
+
steps: PlannedStep[];
|
|
589
|
+
/** Active states the path starts in, for the implementation's setup. */
|
|
590
|
+
start: string[];
|
|
591
|
+
/** The seed that puts the system in the start configuration; `setup` does it when absent. */
|
|
592
|
+
seed?: string;
|
|
593
|
+
/** The journeys this stretch belongs to: a stretch several journeys share is run once. */
|
|
594
|
+
journeys?: string[];
|
|
595
|
+
/** Why the path cannot fully run: `Start: <reason>` when its start cannot be set up, then the blocked events on it. Empty when it can run in full. */
|
|
596
|
+
blockedBy: string[];
|
|
597
|
+
/** A blocked path whose runnable part other paths already cover: reported, not run. */
|
|
598
|
+
skipRun?: boolean;
|
|
599
|
+
};
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
One sequence of events to run against the product from a start point.
|
|
603
|
+
|
|
604
|
+
#### `ScopeOptions`
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
export type ScopeOptions = {
|
|
608
|
+
/** The chart to run; the root chart (the whole product) when omitted. */
|
|
609
|
+
chart?: string;
|
|
610
|
+
/** Where generated paths start; the root's initial configuration when omitted. */
|
|
611
|
+
start?: StateValue;
|
|
612
|
+
/** A phrase describing the start for reports; `The start of the chart` when omitted. */
|
|
613
|
+
startLabel?: string;
|
|
614
|
+
/** Only these events enter the chart from outside it; every entering transition when omitted. */
|
|
615
|
+
entryEvents?: string[];
|
|
616
|
+
/**
|
|
617
|
+
* Cover only this state and what is inside it, with the transitions into and out of it: one
|
|
618
|
+
* area of a chart that holds the whole product.
|
|
619
|
+
*/
|
|
620
|
+
state?: string;
|
|
621
|
+
};
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
What to narrow a run to; all fields are optional and combine in `chartScope`.
|
|
625
|
+
|
|
626
|
+
#### `chartScope`
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
export function chartScope(bundle: SpecBundle, composition: Composition, graph: ChartGraph, options: ScopeOptions = {}): ChartScope
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
The part of the composed product a run covers: every state of `chart`, the transitions that
|
|
633
|
+
enter it from outside, and the hand-offs back to its parent. With `options.state`, the scope is
|
|
634
|
+
that state, everything inside it, and the transitions into and out of it.
|
|
635
|
+
|
|
636
|
+
**Throws** Error when `options.state` names a state that does not exist.
|
|
637
|
+
|
|
638
|
+
#### `SeedPoint`
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
export type SeedPoint = {
|
|
642
|
+
name: string;
|
|
643
|
+
/** The configuration the seed produces, as a state name or a full state value. */
|
|
644
|
+
at: StateValue;
|
|
645
|
+
/** The reason the seed cannot be used yet. A blocked seed starts no path. */
|
|
646
|
+
blocked?: string;
|
|
647
|
+
/** Events this seed is a variant for: it starts only paths that take one of them, and they start only from it. */
|
|
648
|
+
for?: string[];
|
|
649
|
+
/** The state the seed leaves on screen; in a parallel state it picks the region the first event must act in. */
|
|
650
|
+
shows?: string;
|
|
651
|
+
};
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
A named seed: a configuration the implementation can put the system in directly.
|
|
655
|
+
|
|
656
|
+
#### `PlannedJourney`
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
export type PlannedJourney = {
|
|
660
|
+
name: string;
|
|
661
|
+
description: string;
|
|
662
|
+
/** Ids of the paths the journey is cut into, in order. */
|
|
663
|
+
paths: string[];
|
|
664
|
+
};
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
A journey as the paths it is cut into, one per seeded stretch, in order.
|
|
668
|
+
|
|
669
|
+
#### `PlanOptions`
|
|
670
|
+
|
|
671
|
+
```ts
|
|
672
|
+
export type PlanOptions = {
|
|
673
|
+
/** Events that cannot run yet; paths stop before them. */
|
|
674
|
+
isBlocked?: (event: string) => boolean;
|
|
675
|
+
/** Whether the implementation can set a path up in this starting configuration. */
|
|
676
|
+
canStart?: (active: string[]) => true | string;
|
|
677
|
+
/**
|
|
678
|
+
* Configurations the implementation can seed directly. Paths start from the nearest one, and
|
|
679
|
+
* journeys are cut wherever they pass through one, so every stretch runs on its own.
|
|
680
|
+
*/
|
|
681
|
+
seeds?: SeedPoint[];
|
|
682
|
+
/** Whether `setup` can start a path at the scope's start; true when omitted. */
|
|
683
|
+
hasSetup?: boolean;
|
|
684
|
+
};
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Inputs to `planChart` that come from the implementation rather than the charts.
|
|
688
|
+
|
|
689
|
+
#### `runnableSteps`
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
export function runnableSteps(steps: PlannedStep[], isBlocked: (event: string) => boolean)
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
The steps a path can actually run: everything before its first blocked event.
|
|
696
|
+
|
|
697
|
+
#### `pathKey`
|
|
698
|
+
|
|
699
|
+
```ts
|
|
700
|
+
export function pathKey(seed: string | undefined, start: Config, events: string[])
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
A 16-hex-character key for a path: a SHA-256 of its seed name, start configuration and event names.
|
|
704
|
+
Identical on every machine and run; it is how reruns, merges and layers match a path across runs.
|
|
705
|
+
|
|
706
|
+
#### `planChart`
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
export function planChart(bundle: SpecBundle, graph: ChartGraph, scope: ChartScope, options: PlanOptions = {})
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Paths for one chart. Every path starts at a start point: the scope's start (through `setup`)
|
|
713
|
+
or a seed. Each curated journey is replayed from the chart's initial state and cut into
|
|
714
|
+
stretches wherever it passes through a seeded configuration; identical stretches of different
|
|
715
|
+
journeys become one path. Then the shortest extra path from the nearest start point is added
|
|
716
|
+
for every transition still uncovered, and extra paths that are a prefix of another path are
|
|
717
|
+
dropped. Everything is ordered by the chart and the journeys, so the plan is deterministic.
|
|
718
|
+
|
|
719
|
+
Paths stop before blocked events. A path whose start cannot be set up gets a `Start:` entry in
|
|
720
|
+
`blockedBy`, and a blocked path whose runnable steps add no coverage gets `skipRun`. The generated
|
|
721
|
+
paths are searched first avoiding blocked events, then (for what remains) allowing them.
|
|
722
|
+
|
|
723
|
+
**Returns** `paths` (journey paths first, then generated, with ids assigned), `unreachable` (ids of
|
|
724
|
+
in-scope transitions no startable path takes), `journeys` (the paths each journey is cut into),
|
|
725
|
+
and `seeds` (each seed's name and active states, with `blocked` when it cannot be seeded yet).
|
|
726
|
+
|
|
727
|
+
See /statecharts.md
|
|
728
|
+
|
|
729
|
+
### Privacy scanning
|
|
730
|
+
|
|
731
|
+
Source: `packages/core/src/privacy.ts`
|
|
732
|
+
|
|
733
|
+
Keeps real personal data out of the media and manifests a run produces and shares: patterns for
|
|
734
|
+
data that must never appear, an allow-list for synthetic emails, and an allow-list of hosts a run may
|
|
735
|
+
target. Findings describe what matched without repeating it. These are pure functions of their input;
|
|
736
|
+
the runner applies them to the text a driver reports after each step and to the finished manifest.
|
|
737
|
+
|
|
738
|
+
See docs/statecharts.md
|
|
739
|
+
|
|
740
|
+
#### `DEFAULT_PRIVACY`
|
|
741
|
+
|
|
742
|
+
```ts
|
|
743
|
+
export const DEFAULT_PRIVACY: Required<PrivacyRules>
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Defaults that fit any product: no real-looking emails, SSNs or card numbers in media or manifests.
|
|
747
|
+
|
|
748
|
+
#### `mergePrivacy`
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
export function mergePrivacy(rules: PrivacyRules = {}): Required<PrivacyRules>
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
Combines a project's `PrivacyRules` with `DEFAULT_PRIVACY`: its `deny` and `allowEmails` are added
|
|
755
|
+
to the defaults, while its `allowHosts`, when given, replaces the default hosts (so list loopback
|
|
756
|
+
again if you still need it).
|
|
757
|
+
|
|
758
|
+
#### `privacyFindings`
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
export function privacyFindings(text: string, where: string, rules: Required<PrivacyRules>)
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Scans `text` for real-looking personal data and returns one description per finding, each
|
|
765
|
+
prefixed by `where` and never containing the matched text (an email is shortened to its first
|
|
766
|
+
letter and domain). Flags emails not matching `rules.allowEmails` and anything matching a
|
|
767
|
+
`rules.deny` pattern. Returns an empty array when the text is clean.
|
|
768
|
+
|
|
769
|
+
- `rules`: the merged rules (`mergePrivacy`).
|
|
770
|
+
|
|
771
|
+
#### `assertAllowedTarget`
|
|
772
|
+
|
|
773
|
+
```ts
|
|
774
|
+
export function assertAllowedTarget(urls: string[], rules: Required<PrivacyRules>)
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
Throws unless every URL's hostname is in `rules.allowHosts` (loopback by default), so a run
|
|
778
|
+
cannot touch a real environment by accident.
|
|
779
|
+
|
|
780
|
+
**Throws** Error naming the first refused host, or a `TypeError` for a URL that does not parse.
|
|
781
|
+
|
|
782
|
+
### Run report formats
|
|
783
|
+
|
|
784
|
+
Source: `packages/core/src/report/formats.ts`
|
|
785
|
+
|
|
786
|
+
Pure converters from a run's manifest (or a chart) to the formats other tools read: JUnit XML, CTRF
|
|
787
|
+
JSON, WebVTT captions and a Mermaid state diagram. They take plain data, do no I/O and are
|
|
788
|
+
deterministic. The manifest is read through a minimal structural type, so only the fields named
|
|
789
|
+
there are required of it.
|
|
790
|
+
|
|
791
|
+
See docs/statecharts.md
|
|
792
|
+
|
|
793
|
+
#### `toJUnit`
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
export function toJUnit(manifest: ManifestLike)
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Renders the manifest as JUnit XML (the format every CI system reads): one test case per path,
|
|
800
|
+
with `classname` the path kind (`journey` or `generated`). A failed path is a `failure` carrying
|
|
801
|
+
the last attempt's error, a `not-reached` path is `skipped` (with the blocking reason when there is
|
|
802
|
+
one), and a flaky path passes with a note in `system-out`. The suite time is the run duration in seconds.
|
|
803
|
+
|
|
804
|
+
#### `toCtrf`
|
|
805
|
+
|
|
806
|
+
```ts
|
|
807
|
+
export function toCtrf(manifest: ManifestLike)
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
Renders the manifest as CTRF (Common Test Report Format, https://ctrf.io) JSON: one test per
|
|
811
|
+
path, its steps as `steps`. `flaky` paths are `passed` with `flaky: true`, `not-reached` is
|
|
812
|
+
`skipped`, and the failed path's trace (if any) is attached. Per-test `duration` is always 0:
|
|
813
|
+
only the run's start and stop times are recorded.
|
|
814
|
+
|
|
815
|
+
#### `toWebVtt`
|
|
816
|
+
|
|
817
|
+
```ts
|
|
818
|
+
export function toWebVtt(timeline: TimelineEntry[], durationMs: number)
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
Renders WebVTT captions for a clip, one cue per timeline entry. A cue lasts until the next entry
|
|
822
|
+
(or the clip's end), at least 500 ms and at most 4 s.
|
|
823
|
+
|
|
824
|
+
- `timeline`: the step's entries, with `atMs` relative to the start of the clip.
|
|
825
|
+
|
|
826
|
+
- `durationMs`: the clip's length in milliseconds.
|
|
827
|
+
|
|
828
|
+
#### `toMermaid`
|
|
829
|
+
|
|
830
|
+
```ts
|
|
831
|
+
export function toMermaid(machine: MachineConfig)
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
Renders a machine as a Mermaid `stateDiagram-v2` with nested and parallel states. States get
|
|
835
|
+
short ids (`s0`, `s1`, ... in order of first use) and keep their names as labels; `:` and `;` in event
|
|
836
|
+
names are replaced by spaces because Mermaid reads them as syntax. Written as `chart.mmd` in every run.
|
|
837
|
+
|
|
838
|
+
### Markdown chart map
|
|
839
|
+
|
|
840
|
+
Source: `packages/core/src/report/markdown.ts`
|
|
841
|
+
|
|
842
|
+
Renders a chart (usually the composed product) as a Markdown document for the people who own the
|
|
843
|
+
product, not for developers: plain Markdown with internal links, so it renders on any code host
|
|
844
|
+
and diffs with the chart. Pure and deterministic.
|
|
845
|
+
|
|
846
|
+
#### `toMarkdown`
|
|
847
|
+
|
|
848
|
+
```ts
|
|
849
|
+
export function toMarkdown(machine: MachineConfig, journeys: Journey[] = [])
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
A readable map of a chart for the people who own the product: one section per area (parallel
|
|
853
|
+
regions apart), each state with what it means, what the user sees, and every event that can
|
|
854
|
+
happen there and where it leads, then the named journeys. Plain Markdown, so it renders on any
|
|
855
|
+
code host and diffs with the chart.
|
|
856
|
+
|
|
857
|
+
Top-level states become sections, and each region of a top-level parallel state gets its own. A
|
|
858
|
+
`snapshot` is shown only when it contains whitespace (so an identifier-like snapshot is left out),
|
|
859
|
+
and a state with `confidence: 'assumed'` is marked as unconfirmed. Links use GitHub's heading anchors.
|
|
860
|
+
|
|
861
|
+
- `machine`: the chart, composed if child charts should be inlined.
|
|
862
|
+
|
|
863
|
+
- `journeys`: the named journeys to list at the end; none by default.
|
|
864
|
+
|
|
865
|
+
See /statecharts.md
|
|
866
|
+
|
|
867
|
+
### Multi-client composition
|
|
868
|
+
|
|
869
|
+
Source: `packages/core/src/run/clients.ts`
|
|
870
|
+
|
|
871
|
+
Lets one run drive several clients at once (a phone, a desktop browser, an API) by combining their
|
|
872
|
+
drivers into one driver whose context is a record keyed by client name, and their implementations
|
|
873
|
+
into one implementation. Each event and state records which client it acts on, which decides whose
|
|
874
|
+
screen is photographed and recorded. Owns no driver behaviour of its own; it only routes to the
|
|
875
|
+
drivers it is given.
|
|
876
|
+
|
|
877
|
+
See docs/statecharts.md
|
|
878
|
+
|
|
879
|
+
#### `CombineDriversOptions`
|
|
880
|
+
|
|
881
|
+
```ts
|
|
882
|
+
export type CombineDriversOptions = {
|
|
883
|
+
/**
|
|
884
|
+
* `focused` (default) screenshots the client a state is recognised on; `all` also screenshots
|
|
885
|
+
* every other client at the same moment, kept in the image's `also`.
|
|
886
|
+
*/
|
|
887
|
+
screens?: 'focused' | 'all';
|
|
888
|
+
};
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
Options for `combineDrivers`.
|
|
892
|
+
|
|
893
|
+
#### `combineDrivers`
|
|
894
|
+
|
|
895
|
+
```ts
|
|
896
|
+
export function combineDrivers<M extends Record<string, unknown>>(
|
|
897
|
+
drivers: { [K in keyof M]: Driver<M[K]> },
|
|
898
|
+
options: CombineDriversOptions = {}
|
|
899
|
+
): Driver<M>
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
Run several drivers as the clients of one run: a member on an iOS simulator (WebDriver/Appium
|
|
903
|
+
or Detox), an operator on a desktop browser (Playwright, Bun WebView), a partner system over
|
|
904
|
+
HTTP. Each client gets its own context, keyed by name; events and states say which client they
|
|
905
|
+
act on (`onClient`, `combineImplementations`), so screenshots and clips come from that client's
|
|
906
|
+
screen. Timeline entries are tagged with the client that made them.
|
|
907
|
+
|
|
908
|
+
The combined driver's `concurrency` is the smallest of the drivers' (1 for any that give none),
|
|
909
|
+
its `pacing` is the first driver's that has one, and its name lists `<client>:<driver name>`.
|
|
910
|
+
If any client fails to open, the ones that did open are closed and the open throws naming that client.
|
|
911
|
+
Closing returns the first client's trace as `trace` and every client's in `traces`. Without
|
|
912
|
+
a `focus`, `screenshot` and `record` use the first driver that supports them and `text` joins every
|
|
913
|
+
client's text.
|
|
914
|
+
|
|
915
|
+
**Throws** Error when `drivers` is empty.
|
|
916
|
+
|
|
917
|
+
See /statecharts.md
|
|
918
|
+
|
|
919
|
+
**Example**
|
|
920
|
+
|
|
921
|
+
```ts
|
|
922
|
+
const driver = combineDrivers<{ member: Page; api: HttpClient }>({
|
|
923
|
+
member: playwrightDriver(),
|
|
924
|
+
api: httpDriver('http://127.0.0.1:4000')
|
|
925
|
+
});
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
#### `onClient`
|
|
929
|
+
|
|
930
|
+
```ts
|
|
931
|
+
export function onClient<K extends string, C>(
|
|
932
|
+
client: K,
|
|
933
|
+
impl: StateImplementation<C> | EventImplementation<C>
|
|
934
|
+
): StateImplementation<Record<K, C>> | EventImplementation<Record<K, C>>
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
Lifts a state or an event implementation to a client of a multi-client context (see the overloads above); a value with `recognize` is a state.
|
|
938
|
+
|
|
939
|
+
#### `ImplementationPart`
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
export type ImplementationPart<C> = Partial<Implementation<C>>;
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
Part of an implementation (any subset of its fields): what one client does and shows, or what spans clients.
|
|
946
|
+
|
|
947
|
+
#### `combineImplementations`
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
export function combineImplementations<M extends Record<string, unknown>>(
|
|
951
|
+
parts: { [K in keyof M]?: ImplementationPart<M[K]> },
|
|
952
|
+
shared: ImplementationPart<M> = {}
|
|
953
|
+
): Implementation<M>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
Merge per-client implementations into one. Each client's part is written against that client's
|
|
957
|
+
own context (a Playwright page, a WebDriver session, an HTTP client), so the helpers of every
|
|
958
|
+
driver work unchanged; `shared` holds what needs several clients at once (read an id on one
|
|
959
|
+
screen, act on it on another). `shared.setup` runs first (seed data), then each client's setup
|
|
960
|
+
in the order the clients are listed. An event or state implemented twice is an error.
|
|
961
|
+
|
|
962
|
+
A seed with the same name in several parts runs `shared`'s first, then each client's; the combined
|
|
963
|
+
seed's `at` is the first one's and its `how` joins all of them. Correlation ids are pooled,
|
|
964
|
+
and `canStart` returns the first reason any part gives.
|
|
965
|
+
|
|
966
|
+
- `parts`: the implementation of each client, against that client's own context.
|
|
967
|
+
|
|
968
|
+
- `shared`: events, states, seeds and setup that need several clients at once, against the combined context.
|
|
969
|
+
|
|
970
|
+
**Throws** Error when an event or state is implemented by more than one part (including `shared`).
|
|
971
|
+
|
|
972
|
+
See /statecharts.md
|
|
973
|
+
|
|
974
|
+
**Example**
|
|
975
|
+
|
|
976
|
+
```ts
|
|
977
|
+
const implementation = combineImplementations<{ member: SignalContext; api: HttpClient }>(
|
|
978
|
+
{ member: { events: {}, states: {} }, api: { events: {}, states: {} } },
|
|
979
|
+
{ setup: async ({ api }) => void (await api.post('/test/reset')) }
|
|
980
|
+
);
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
#### `mergeImplementations`
|
|
984
|
+
|
|
985
|
+
```ts
|
|
986
|
+
export function mergeImplementations<C>(...parts: ImplementationPart<C>[]): Implementation<C>
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
Merge parts of one implementation written against the same context, such as one file per area
|
|
990
|
+
of a large chart (sign-up, checkout, results). Events, states and seeds must each be
|
|
991
|
+
implemented once; setups run in order, `canStart` returns the first reason any part gives, and
|
|
992
|
+
correlation ids are pooled without duplicates.
|
|
993
|
+
|
|
994
|
+
**Throws** Error when an event, state or seed appears in more than one part.
|
|
995
|
+
|
|
996
|
+
### Path runner and manifest
|
|
997
|
+
|
|
998
|
+
Source: `packages/core/src/run/execute.ts`
|
|
999
|
+
|
|
1000
|
+
Runs planned paths against the product through a driver and turns what happened into a run: a
|
|
1001
|
+
directory holding `manifest.json`, `junit.xml`, `ctrf.json`, `chart.mmd`, `outcomes.json` and `media/`.
|
|
1002
|
+
The invariants: the manifest is a pure fold of the plan and the per-path outcomes in plan order (so
|
|
1003
|
+
worker scheduling, sharding and reruns never change the report); it is rewritten atomically after
|
|
1004
|
+
every path so a crash keeps what ran; a failed path is retried once, and passing the second time is
|
|
1005
|
+
`flaky`. This module is the only one that touches the driver and the file system for a run. Its
|
|
1006
|
+
outputs follow the manifest schema in `packages/schema` (`schemaVersion: 1`).
|
|
1007
|
+
|
|
1008
|
+
See docs/statecharts.md
|
|
1009
|
+
|
|
1010
|
+
#### `PlannedSeed`
|
|
1011
|
+
|
|
1012
|
+
```ts
|
|
1013
|
+
export type PlannedSeed = { name: string; active: string[]; blocked?: string };
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
A seed as the plan sees it: where it starts, and whether it can run yet.
|
|
1017
|
+
|
|
1018
|
+
#### `Shard`
|
|
1019
|
+
|
|
1020
|
+
```ts
|
|
1021
|
+
export type Shard = { index: number; count: number };
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
One shard of a run split across machines: `index` is 1-based.
|
|
1025
|
+
|
|
1026
|
+
#### `RunContext`
|
|
1027
|
+
|
|
1028
|
+
```ts
|
|
1029
|
+
export type RunContext<C> = {
|
|
1030
|
+
/** The loaded charts. Their files are re-read (to hash them into `specVersion`) when the manifest is built. */
|
|
1031
|
+
bundle: SpecBundle;
|
|
1032
|
+
composition: Composition;
|
|
1033
|
+
graph: ChartGraph;
|
|
1034
|
+
scope: ChartScope;
|
|
1035
|
+
implementation: Implementation<C>;
|
|
1036
|
+
/** The journeys of the plan, as the paths they are cut into; journeys are reported from them. */
|
|
1037
|
+
journeys?: PlannedJourney[];
|
|
1038
|
+
/** The plan's seeds, reported in the manifest with the paths that start from and verify each. */
|
|
1039
|
+
seeds?: PlannedSeed[];
|
|
1040
|
+
/** How a business event name is recognised, by name; see `EventMatcher`. */
|
|
1041
|
+
eventMatchers?: Record<string, EventMatcher>;
|
|
1042
|
+
privacy?: PrivacyRules;
|
|
1043
|
+
/** Recorded in the manifest as the run's environment. */
|
|
1044
|
+
environment?: Record<string, unknown>;
|
|
1045
|
+
};
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
What a run reports about: the spec, the scope, and the implementation's metadata.
|
|
1049
|
+
|
|
1050
|
+
#### `RunOptions`
|
|
1051
|
+
|
|
1052
|
+
```ts
|
|
1053
|
+
export type RunOptions<C> = RunContext<C> & {
|
|
1054
|
+
/** The whole plan, in plan order. Paths marked `skipRun` are reported but not run. The manifest covers all of them. */
|
|
1055
|
+
paths: PlannedPath[];
|
|
1056
|
+
driver: Driver<C>;
|
|
1057
|
+
mode: RunMode;
|
|
1058
|
+
/** Runs land in `<outputRoot>/<run-id>/`. */
|
|
1059
|
+
outputRoot: string;
|
|
1060
|
+
/** Whether a failed path runs a second time to tell flaky from broken; true unless `false`. */
|
|
1061
|
+
retry?: boolean;
|
|
1062
|
+
/** How long to wait for a state to be recognised after an event, in milliseconds; 45 000 when omitted. */
|
|
1063
|
+
stepTimeoutMs?: number;
|
|
1064
|
+
/** Where business events come from. Sources named with a `log:` prefix are treated as log files, not CloudEvents sources. */
|
|
1065
|
+
eventSources?: EventSource[];
|
|
1066
|
+
/** Path attempts that run at once, capped by the driver's `concurrency`; 1 when omitted. */
|
|
1067
|
+
workers?: number;
|
|
1068
|
+
/** Reported in the manifest when the paths are one shard of a larger run. */
|
|
1069
|
+
shard?: Shard;
|
|
1070
|
+
/** Passed outcomes of an earlier run to keep instead of running those paths again. */
|
|
1071
|
+
reuse?: { runDir: string; outcomes: Record<string, PathOutcome> };
|
|
1072
|
+
/** Receives progress lines; writes to stdout when omitted. */
|
|
1073
|
+
log?: (line: string) => void;
|
|
1074
|
+
};
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
Everything `runPaths` needs: the `RunContext`, the paths to run and how to run them.
|
|
1078
|
+
|
|
1079
|
+
#### `PathOutcome`
|
|
1080
|
+
|
|
1081
|
+
```ts
|
|
1082
|
+
export type PathOutcome = {
|
|
1083
|
+
id: string;
|
|
1084
|
+
/** The path's `key`; outcomes are matched across runs by it, not by `id`. */
|
|
1085
|
+
key: string;
|
|
1086
|
+
/** `failed` when the last attempt failed, `flaky` when the first failed and the retry passed, `not-reached` when the path stopped at a blocked event, else `passed`. */
|
|
1087
|
+
status: Status;
|
|
1088
|
+
attempts: Attempt[];
|
|
1089
|
+
steps: StepResult[];
|
|
1090
|
+
stoppedAt: string | null;
|
|
1091
|
+
observed: Observed[];
|
|
1092
|
+
/** Where and why the last attempt failed; `null` when it did not. */
|
|
1093
|
+
failure: { state: string; recognizer: RecognizerResult | null; looksLike: string[]; shot: Image | null } | null;
|
|
1094
|
+
/** Privacy findings from the text the driver reported; they fail the run. */
|
|
1095
|
+
leaks: string[];
|
|
1096
|
+
/** Wall-clock milliseconds for all attempts of the path. */
|
|
1097
|
+
durationMs: number;
|
|
1098
|
+
/** How long the last attempt took to seed (or set up) and recognise its start. */
|
|
1099
|
+
setupMs?: number;
|
|
1100
|
+
/** The run the outcome was taken from, when it was reused rather than run again. */
|
|
1101
|
+
reusedFrom?: string;
|
|
1102
|
+
};
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
Everything one path's run produced, with media paths relative to the run directory. A run's
|
|
1106
|
+
manifest is a pure function of its plan and these outcomes, folded in plan order, so outcomes
|
|
1107
|
+
from parallel workers, shards and earlier runs compose into the same report.
|
|
1108
|
+
|
|
1109
|
+
#### `estimateMs`
|
|
1110
|
+
|
|
1111
|
+
```ts
|
|
1112
|
+
export const estimateMs = …
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
A rough cost in milliseconds of a path that has never run (5 s to set up, 2.5 s a step); used to order and shard paths.
|
|
1116
|
+
|
|
1117
|
+
#### `shardPaths`
|
|
1118
|
+
|
|
1119
|
+
```ts
|
|
1120
|
+
export function shardPaths(paths: PlannedPath[], shard: Shard)
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
The paths one shard runs: longest first onto the least loaded shard, by the plan alone, so
|
|
1124
|
+
every machine computes the same split. Paths reported without running go to the first shard.
|
|
1125
|
+
|
|
1126
|
+
#### `readOutcomes`
|
|
1127
|
+
|
|
1128
|
+
```ts
|
|
1129
|
+
export function readOutcomes(runDir: string): { runId: string; outcomes: Record<string, PathOutcome> } | null
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
Reads the `outcomes.json` of a run directory: the run id and each finished path's outcome by key.
|
|
1133
|
+
Returns `null` when the file is missing or unreadable, never throws.
|
|
1134
|
+
|
|
1135
|
+
#### `durationHistory`
|
|
1136
|
+
|
|
1137
|
+
```ts
|
|
1138
|
+
export function durationHistory(outputRoot: string)
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
Milliseconds each path took, by path key, taken from the newest of the last 10 runs under
|
|
1142
|
+
`outputRoot` that ran it (outcomes reused from elsewhere are ignored). Empty when the directory
|
|
1143
|
+
does not exist. `runPaths` uses it to start the slowest paths first.
|
|
1144
|
+
|
|
1145
|
+
#### `latestRun`
|
|
1146
|
+
|
|
1147
|
+
```ts
|
|
1148
|
+
export function latestRun(outputRoot: string)
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
The directory of the newest run under `outputRoot` that wrote `outcomes.json` (run ids sort by time), or `null`.
|
|
1152
|
+
|
|
1153
|
+
#### `buildManifest`
|
|
1154
|
+
|
|
1155
|
+
```ts
|
|
1156
|
+
export function buildManifest<C>(
|
|
1157
|
+
context: RunContext<C>,
|
|
1158
|
+
paths: PlannedPath[],
|
|
1159
|
+
outcomes: Map<string, PathOutcome>,
|
|
1160
|
+
run: { id: string; startedAt: Date; finished: boolean; mode: RunMode; driver: { name: string; clients?: Record<string, string> }; activePaths?: string[]; extra?: Record<string, unknown> }
|
|
1161
|
+
)
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
Folds path outcomes into the run's manifest object, in plan order: the same plan and outcomes
|
|
1165
|
+
always give the same manifest, however many workers ran them, in whatever order they finished.
|
|
1166
|
+
|
|
1167
|
+
A state or transition is `passed` when a path reached it, `flaky` when only a flaky path did,
|
|
1168
|
+
`failed` when any failure touched it (a failed check fails its state), else `not-reached`. Reaching a
|
|
1169
|
+
state also marks its ancestors reached. The whole manifest is scanned for privacy findings, and any
|
|
1170
|
+
finding sets `privacy.passed` to false. Reads the chart files (for `specVersion`) and runs `git`
|
|
1171
|
+
in the current directory (for `commit` and `branch`, `null` when that fails). Apart from the
|
|
1172
|
+
wall-clock fields (`finishedAt`, `updatedAt`, `durationMs`) and those that depend on the working
|
|
1173
|
+
directory (`commit`, `branch`, `specDirectory`), two folds of the same outcomes are identical.
|
|
1174
|
+
|
|
1175
|
+
- `outcomes`: finished or in-progress outcomes by path key; a path with none is `queued` or `running`.
|
|
1176
|
+
|
|
1177
|
+
- `run.activePaths`: ids of the paths in flight; they are reported `running` and not counted complete.
|
|
1178
|
+
|
|
1179
|
+
- `run.extra`: extra fields merged into `run` of the manifest (workers, shard, reused, mergedFrom).
|
|
1180
|
+
|
|
1181
|
+
#### `Manifest`
|
|
1182
|
+
|
|
1183
|
+
```ts
|
|
1184
|
+
export type Manifest = ReturnType<typeof buildManifest>;
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
The manifest object `buildManifest` returns; it is what `manifest.json` contains.
|
|
1188
|
+
|
|
1189
|
+
#### `writeRun`
|
|
1190
|
+
|
|
1191
|
+
```ts
|
|
1192
|
+
export function writeRun<C>(
|
|
1193
|
+
runDir: string,
|
|
1194
|
+
context: RunContext<C>,
|
|
1195
|
+
paths: PlannedPath[],
|
|
1196
|
+
outcomes: Map<string, PathOutcome>,
|
|
1197
|
+
run: Parameters<typeof buildManifest>[3]
|
|
1198
|
+
)
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
Writes a run's `junit.xml`, `ctrf.json`, `chart.mmd`, `outcomes.json` and `manifest.json` into
|
|
1202
|
+
`runDir` (each through a temporary file and a rename, so a reader never sees half a file) and returns
|
|
1203
|
+
the manifest. `outcomes.json` holds only finished paths, not those in `run.activePaths`.
|
|
1204
|
+
|
|
1205
|
+
#### `mergeRuns`
|
|
1206
|
+
|
|
1207
|
+
```ts
|
|
1208
|
+
export function mergeRuns<C>(
|
|
1209
|
+
context: RunContext<C>,
|
|
1210
|
+
paths: PlannedPath[],
|
|
1211
|
+
runDirs: string[],
|
|
1212
|
+
options: { outputRoot: string; mode?: RunMode; driver?: { name: string; clients?: Record<string, string> } }
|
|
1213
|
+
)
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
Composes the outcomes of several runs of the same plan (the shards of one run, or a rerun and
|
|
1217
|
+
the run before it) into one new run directory under `options.outputRoot`, folded in plan order. Media are
|
|
1218
|
+
copied in, so the merged run stands on its own. Later entries of `runDirs` win when two ran the same
|
|
1219
|
+
path; outcomes whose key is not in `paths` are ignored. The merged run's mode and driver name are
|
|
1220
|
+
taken from the run manifests unless `options` gives them.
|
|
1221
|
+
|
|
1222
|
+
**Returns** the new `runDir`, its `manifest`, and `ok` (privacy passed and no path failed).
|
|
1223
|
+
|
|
1224
|
+
**Throws** Error when a run directory has no `outcomes.json`.
|
|
1225
|
+
|
|
1226
|
+
#### `runPaths`
|
|
1227
|
+
|
|
1228
|
+
```ts
|
|
1229
|
+
export async function runPaths<C>(options: RunOptions<C>)
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
Runs planned paths against a system through a driver and writes the run to
|
|
1233
|
+
`<outputRoot>/<timestamp>-<mode>/`. Paths start from their seed (or the implementation's
|
|
1234
|
+
`setup`) and run on a pool of workers, longest first (by earlier timings, else `estimateMs`),
|
|
1235
|
+
each attempt in its own driver context; every state is recognised from the outside after every step;
|
|
1236
|
+
a failed path is retried once to tell flaky from broken. The manifest is rewritten as each path
|
|
1237
|
+
finishes, from all outcomes so far in plan order, so a crash keeps what ran and the order paths
|
|
1238
|
+
finished in never changes the report.
|
|
1239
|
+
|
|
1240
|
+
Workers are capped by `driver.concurrency` (1 if the driver gives none) and by the number of paths.
|
|
1241
|
+
Without `implementation.correlate`, a run on several workers reports business events as "no
|
|
1242
|
+
signal" rather than guess which attempt they belong to. While it runs it installs
|
|
1243
|
+
`uncaughtException` and `unhandledRejection` handlers that log and swallow stray errors from
|
|
1244
|
+
drivers, and removes them when it ends. Passed paths of `options.reuse` are kept instead of re-run.
|
|
1245
|
+
|
|
1246
|
+
**Returns** the run directory, the final manifest, and `ok` (privacy passed and no path failed).
|
|
1247
|
+
|
|
1248
|
+
See /statecharts.md
|
|
1249
|
+
|
|
1250
|
+
### Frame-based clip recorder
|
|
1251
|
+
|
|
1252
|
+
Source: `packages/core/src/run/frames.ts`
|
|
1253
|
+
|
|
1254
|
+
Builds a video clip for drivers that can only take screenshots: frames are captured repeatedly while
|
|
1255
|
+
a step runs and encoded afterwards with FFmpeg at their real timing. It owns no driver logic; the
|
|
1256
|
+
caller supplies the capture function. A missing or failing `ffmpeg` produces no clip, never an error.
|
|
1257
|
+
|
|
1258
|
+
#### `FrameRecorderOptions`
|
|
1259
|
+
|
|
1260
|
+
```ts
|
|
1261
|
+
export type FrameRecorderOptions = {
|
|
1262
|
+
/** The image format `capture` returns (default png). */
|
|
1263
|
+
extension?: 'png' | 'jpg';
|
|
1264
|
+
/** Upper bound on frames per second (default 25). */
|
|
1265
|
+
maxFps?: number;
|
|
1266
|
+
};
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
Options for `recordFrames`.
|
|
1270
|
+
|
|
1271
|
+
#### `recordFrames`
|
|
1272
|
+
|
|
1273
|
+
```ts
|
|
1274
|
+
export async function recordFrames(capture: () => Promise<Uint8Array>, workDirectory: string, options: FrameRecorderOptions = {})
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
Starts recording a clip from any driver that can take a screenshot: capture frames back to back
|
|
1278
|
+
while the step runs, then encode them with FFmpeg as H.264 MP4 at their real timing. Drivers
|
|
1279
|
+
without a native screen recorder (Bun WebView, WebDriver) use it; it needs `ffmpeg` on the PATH.
|
|
1280
|
+
|
|
1281
|
+
Capturing starts immediately and continues until `stop` is called. A `capture` that throws or
|
|
1282
|
+
returns no bytes just skips that frame. The returned `stop(output)` ends capture, writes the MP4 to
|
|
1283
|
+
`output` and resolves to the clip (`durationMs` is wall-clock time since the call), or to `null`
|
|
1284
|
+
when no frame was captured or FFmpeg is missing or fails. It writes no poster. Frames and the
|
|
1285
|
+
FFmpeg frame list are left in `workDirectory`.
|
|
1286
|
+
|
|
1287
|
+
- `capture`: returns one screenshot as encoded image bytes in `options.extension`'s format.
|
|
1288
|
+
|
|
1289
|
+
- `workDirectory`: where the frames are written; created if missing.
|
|
1290
|
+
|
|
1291
|
+
### HTTP driver
|
|
1292
|
+
|
|
1293
|
+
Source: `packages/core/src/run/http.ts`
|
|
1294
|
+
|
|
1295
|
+
A driver (and a bare client) for acting on or observing a system over HTTP, with no browser or
|
|
1296
|
+
device: a fetch with a base URL, a cookie jar and timeline logging. It is the fastest driver and
|
|
1297
|
+
runs any number of attempts side by side, each with its own cookie jar. Uses the global `fetch`.
|
|
1298
|
+
|
|
1299
|
+
#### `HttpResponse`
|
|
1300
|
+
|
|
1301
|
+
```ts
|
|
1302
|
+
export type HttpResponse = {
|
|
1303
|
+
status: number;
|
|
1304
|
+
/** True for a 2xx status. A non-2xx status does not throw. */
|
|
1305
|
+
ok: boolean;
|
|
1306
|
+
headers: Headers;
|
|
1307
|
+
/** The body as text. */
|
|
1308
|
+
text: string;
|
|
1309
|
+
/** The body parsed as JSON when the content type is JSON and it parses; `null` otherwise (also for a literal JSON `null`). */
|
|
1310
|
+
json: unknown;
|
|
1311
|
+
};
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
The response of an `HttpClient` call; the body is read in full.
|
|
1315
|
+
|
|
1316
|
+
#### `HttpRequestOptions`
|
|
1317
|
+
|
|
1318
|
+
```ts
|
|
1319
|
+
export type HttpRequestOptions = {
|
|
1320
|
+
/** A string is sent as is; anything else is sent as JSON with a `content-type: application/json` header. */
|
|
1321
|
+
body?: unknown;
|
|
1322
|
+
/** Added to the client's headers for this call and overriding them (and the cookie header) on conflict. */
|
|
1323
|
+
headers?: Record<string, string>;
|
|
1324
|
+
/** What the timeline says this call was for; `METHOD /path` when omitted. */
|
|
1325
|
+
label?: string;
|
|
1326
|
+
};
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
Per-call options of an `HttpClient`.
|
|
1330
|
+
|
|
1331
|
+
#### `HttpClient`
|
|
1332
|
+
|
|
1333
|
+
```ts
|
|
1334
|
+
export type HttpClient = {
|
|
1335
|
+
/** The URL that request paths are resolved against with `new URL(path, baseUrl)`: a path starting with `/` replaces any path in the base URL. */
|
|
1336
|
+
baseUrl: string;
|
|
1337
|
+
/** Sent with every request; set an `authorization` header here after signing in. */
|
|
1338
|
+
headers: Record<string, string>;
|
|
1339
|
+
/** Cookie name to value, stored from `Set-Cookie` responses and sent back on every request. Only the name and value are kept (no expiry, path or domain handling). */
|
|
1340
|
+
cookies: Map<string, string>;
|
|
1341
|
+
/** Sends a request. Rejects on a network error or timeout; resolves for any HTTP status. Each call adds a `system` entry (method, path, status, never bodies) to the timeline. */
|
|
1342
|
+
request(method: string, path: string, options?: HttpRequestOptions): Promise<HttpResponse>;
|
|
1343
|
+
get(path: string, options?: HttpRequestOptions): Promise<HttpResponse>;
|
|
1344
|
+
post(path: string, body?: unknown, options?: HttpRequestOptions): Promise<HttpResponse>;
|
|
1345
|
+
put(path: string, body?: unknown, options?: HttpRequestOptions): Promise<HttpResponse>;
|
|
1346
|
+
patch(path: string, body?: unknown, options?: HttpRequestOptions): Promise<HttpResponse>;
|
|
1347
|
+
delete(path: string, options?: HttpRequestOptions): Promise<HttpResponse>;
|
|
1348
|
+
};
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
What every event and recogniser receives from an HTTP client: a fetch with a base URL and a cookie jar.
|
|
1352
|
+
|
|
1353
|
+
#### `HttpDriverOptions`
|
|
1354
|
+
|
|
1355
|
+
```ts
|
|
1356
|
+
export type HttpDriverOptions = {
|
|
1357
|
+
/** The driver's name and the `source` recorded on timeline entries; `http` when omitted. */
|
|
1358
|
+
name?: string;
|
|
1359
|
+
/** Headers sent with every request. */
|
|
1360
|
+
headers?: Record<string, string>;
|
|
1361
|
+
/** Per request, in milliseconds (default 30 000). */
|
|
1362
|
+
timeoutMs?: number;
|
|
1363
|
+
};
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
Options for `httpDriver` and `httpClient`.
|
|
1367
|
+
|
|
1368
|
+
#### `httpClient`
|
|
1369
|
+
|
|
1370
|
+
```ts
|
|
1371
|
+
export function httpClient(baseUrl: string, timeline: Timeline, options: HttpDriverOptions = {}): HttpClient
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
Creates an `HttpClient` that logs each call to `timeline`. Use it directly when you need a client
|
|
1375
|
+
outside a driver; `httpDriver` creates one per path attempt.
|
|
1376
|
+
|
|
1377
|
+
- `baseUrl`: the URL request paths are resolved against.
|
|
1378
|
+
|
|
1379
|
+
#### `httpDriver`
|
|
1380
|
+
|
|
1381
|
+
```ts
|
|
1382
|
+
export function httpDriver(baseUrl: string | (() => string | Promise<string>), options: HttpDriverOptions = {}): Driver<HttpClient>
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
The fastest driver: a client of an HTTP API, with no browser or device. Use it for actors that
|
|
1386
|
+
only talk to the system (a partner webhook, an operator API, a back office job) or to drive a
|
|
1387
|
+
whole chart at the API level. Each path attempt gets a fresh cookie jar; every call is logged to
|
|
1388
|
+
the step's timeline (method, path, status; never bodies). Has no screenshots or clips, and no limit on
|
|
1389
|
+
concurrent attempts.
|
|
1390
|
+
|
|
1391
|
+
- `baseUrl`: the API's base URL, or a function returning it (called for each attempt).
|
|
1392
|
+
|
|
1393
|
+
**Example**
|
|
1394
|
+
|
|
1395
|
+
```ts
|
|
1396
|
+
const driver = httpDriver('http://127.0.0.1:4000', { headers: { 'x-test': '1' } });
|
|
1397
|
+
// events then receive an HttpClient: run: (api) => api.post('/orders', { sku: 'A1' })
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
### Run types and function driver
|
|
1401
|
+
|
|
1402
|
+
Source: `packages/core/src/run/types.ts`
|
|
1403
|
+
|
|
1404
|
+
The contracts between a run and the things it drives: `Driver` (owns the system under test for one
|
|
1405
|
+
path attempt), `Implementation` (how each event is made to happen and each state recognised), `Seed`,
|
|
1406
|
+
event sources and privacy rules, plus the status vocabulary and `Timeline`. These shapes are what
|
|
1407
|
+
integrators write against, so they change only by adding optional fields. Also holds
|
|
1408
|
+
`functionDriver`, the simplest complete driver.
|
|
1409
|
+
|
|
1410
|
+
See docs/statecharts.md
|
|
1411
|
+
|
|
1412
|
+
#### `RunMode`
|
|
1413
|
+
|
|
1414
|
+
```ts
|
|
1415
|
+
export type RunMode = 'fast' | 'showcase';
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
`fast` runs without pacing and takes screenshots only; `showcase` paces steps like a person and records a clip of every transition.
|
|
1419
|
+
|
|
1420
|
+
#### `Status`
|
|
1421
|
+
|
|
1422
|
+
```ts
|
|
1423
|
+
export type Status = 'passed' | 'failed' | 'flaky' | 'not-reached';
|
|
1424
|
+
```
|
|
1425
|
+
|
|
1426
|
+
The result of a path, state, transition or journey: `flaky` passed only on the retry; `not-reached` was never run, was blocked, or has no covering path.
|
|
1427
|
+
|
|
1428
|
+
#### `EventKind`
|
|
1429
|
+
|
|
1430
|
+
```ts
|
|
1431
|
+
export type EventKind = 'user' | 'system' | 'time' | 'hand-off';
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
Who or what makes an event happen: a `user` action, a `system` call, the passing of `time`, or a `hand-off` from a child chart (inserted by the planner, usually a no-op).
|
|
1435
|
+
|
|
1436
|
+
#### `Image`
|
|
1437
|
+
|
|
1438
|
+
```ts
|
|
1439
|
+
export type Image = {
|
|
1440
|
+
/** Path to the PNG. A driver returns an absolute path; in a manifest it is relative to the run directory. */
|
|
1441
|
+
png: string;
|
|
1442
|
+
/** Path to a WebP thumbnail, when one was made. */
|
|
1443
|
+
webp?: string;
|
|
1444
|
+
client?: string;
|
|
1445
|
+
also?: Image[];
|
|
1446
|
+
};
|
|
1447
|
+
```
|
|
1448
|
+
|
|
1449
|
+
`client` names which client's screen it is in a multi-client run; `also` holds the other clients' screens at the same moment.
|
|
1450
|
+
|
|
1451
|
+
#### `Clip`
|
|
1452
|
+
|
|
1453
|
+
```ts
|
|
1454
|
+
export type Clip = {
|
|
1455
|
+
/** Path to the MP4 (relative to the run directory in a manifest). */
|
|
1456
|
+
video: string;
|
|
1457
|
+
/** Path to a poster image, when one was made. */
|
|
1458
|
+
poster?: string;
|
|
1459
|
+
durationMs: number;
|
|
1460
|
+
/** The client whose screen was recorded, in a multi-client run. */
|
|
1461
|
+
client?: string;
|
|
1462
|
+
};
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
A recorded video of one transition.
|
|
1466
|
+
|
|
1467
|
+
#### `TimelineEntry`
|
|
1468
|
+
|
|
1469
|
+
```ts
|
|
1470
|
+
export type TimelineEntry = {
|
|
1471
|
+
/** Milliseconds since the step began (see `Timeline.restart`). */
|
|
1472
|
+
atMs: number;
|
|
1473
|
+
/** What kind of thing happened. */
|
|
1474
|
+
kind: 'tap' | 'type' | 'system' | 'screen' | 'toast' | 'note' | 'time' | 'notification';
|
|
1475
|
+
/** The caption. */
|
|
1476
|
+
label: string;
|
|
1477
|
+
/** For a tap, where it landed (page pixels for the Playwright driver). */
|
|
1478
|
+
x?: number;
|
|
1479
|
+
y?: number;
|
|
1480
|
+
/** For typing, what was typed; shown in the caption. */
|
|
1481
|
+
text?: string;
|
|
1482
|
+
/** For a system call, what it was (never bodies). */
|
|
1483
|
+
system?: { source?: string; endpoint?: string; eventType?: string; payload?: unknown; responseStatus?: number };
|
|
1484
|
+
/** The client that did it, in a multi-client run. */
|
|
1485
|
+
client?: string;
|
|
1486
|
+
};
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
One thing that happened during a step, which becomes a caption on the step's clip.
|
|
1490
|
+
|
|
1491
|
+
#### `Timeline`
|
|
1492
|
+
|
|
1493
|
+
```ts
|
|
1494
|
+
export class Timeline
|
|
1495
|
+
```
|
|
1496
|
+
|
|
1497
|
+
What a step records while it runs: taps, typing, system calls, the screen change. `for(client)`
|
|
1498
|
+
returns a view onto the same timeline that tags every entry with that client. The runner restarts
|
|
1499
|
+
it at the start of every step, so entries are relative to the step they belong to.
|
|
1500
|
+
|
|
1501
|
+
- `readonly client: string | undefined;` — The client this view tags entries with; `undefined` on the root timeline.
|
|
1502
|
+
- `get entries() { return this.state.entries; }` — Every entry added since the last `restart`, in order, shared by all views of the timeline.
|
|
1503
|
+
- `restart()` — Clears the entries and makes now the origin for `atMs`.
|
|
1504
|
+
- `add(entry: Omit<TimelineEntry, 'atMs'>)` — Appends an entry stamped with the time since the origin, and with this view's client when it has one.
|
|
1505
|
+
- `for(client: string)` — A view of the same timeline that tags what it adds with `client`.
|
|
1506
|
+
|
|
1507
|
+
#### `RecognizerResult`
|
|
1508
|
+
|
|
1509
|
+
```ts
|
|
1510
|
+
export type RecognizerResult = {
|
|
1511
|
+
/** Whether the state is showing. */
|
|
1512
|
+
matched: boolean;
|
|
1513
|
+
/** What was looked at, shown to explain a failure. `expected` is `visible` or `hidden`. */
|
|
1514
|
+
signals: { signal: string; expected: 'visible' | 'hidden' | string; visible: boolean }[];
|
|
1515
|
+
};
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
What a state recogniser found.
|
|
1519
|
+
|
|
1520
|
+
#### `CheckResult`
|
|
1521
|
+
|
|
1522
|
+
```ts
|
|
1523
|
+
export type CheckResult = {
|
|
1524
|
+
/** The check statement, quoted from the spec. */
|
|
1525
|
+
check: string;
|
|
1526
|
+
passed: boolean;
|
|
1527
|
+
expected?: string;
|
|
1528
|
+
actual?: string;
|
|
1529
|
+
};
|
|
1530
|
+
```
|
|
1531
|
+
|
|
1532
|
+
The result of one of a state's `meta.checks`; a failed check fails the state.
|
|
1533
|
+
|
|
1534
|
+
#### `StepTools`
|
|
1535
|
+
|
|
1536
|
+
```ts
|
|
1537
|
+
export type StepTools = {
|
|
1538
|
+
timeline: Timeline;
|
|
1539
|
+
mode: RunMode;
|
|
1540
|
+
/** Pause for the pacing of the current mode (no-op in fast mode for the default pauses). */
|
|
1541
|
+
hold(ms: number): Promise<void>;
|
|
1542
|
+
/** Scratch space shared by the events of one path attempt. */
|
|
1543
|
+
data: Record<string, unknown>;
|
|
1544
|
+
};
|
|
1545
|
+
```
|
|
1546
|
+
|
|
1547
|
+
Per-attempt tools handed to every implementation.
|
|
1548
|
+
|
|
1549
|
+
#### `Focus`
|
|
1550
|
+
|
|
1551
|
+
```ts
|
|
1552
|
+
export type Focus = { client?: string };
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
Which client a media hook should look at, in a multi-client run: the client a state is
|
|
1556
|
+
recognised on, or the client an event is done on. Single-client drivers ignore it.
|
|
1557
|
+
|
|
1558
|
+
#### `Driver`
|
|
1559
|
+
|
|
1560
|
+
```ts
|
|
1561
|
+
export type Driver<C> = {
|
|
1562
|
+
/** Shown in reports as the driver the run used. */
|
|
1563
|
+
name: string;
|
|
1564
|
+
/**
|
|
1565
|
+
* Creates a fresh context for one attempt of one path. `attempt` is 1, or 2 for the retry;
|
|
1566
|
+
* `directory` is a scratch directory for this attempt; the driver should log what it does to `timeline`.
|
|
1567
|
+
*/
|
|
1568
|
+
open(input: { path: PlannedPath; attempt: number; mode: RunMode; directory: string; timeline: Timeline }): Promise<C>;
|
|
1569
|
+
/**
|
|
1570
|
+
* Releases the context. `traceFile` is where a trace may be written when `failed`; return the path
|
|
1571
|
+
* actually written as `trace` (or one per client as `traces`) to attach it to the report.
|
|
1572
|
+
*/
|
|
1573
|
+
close(ctx: C, input: { failed: boolean; traceFile: string }): Promise<{ trace?: string; traces?: Record<string, string> } | void>;
|
|
1574
|
+
/** A settled, deterministic screenshot of the current screen. */
|
|
1575
|
+
screenshot?(ctx: C, file: string, focus?: Focus): Promise<Image | null>;
|
|
1576
|
+
/** Start recording one transition; `stop` encodes it and returns the clip. */
|
|
1577
|
+
record?(ctx: C, workDirectory: string, focus?: Focus): Promise<{ stop(output: string): Promise<Clip | null> }>;
|
|
1578
|
+
/** Visible text of the screen, for the privacy scan (every client's screen when unfocused). */
|
|
1579
|
+
text?(ctx: C, focus?: Focus): Promise<string>;
|
|
1580
|
+
/** Pause between actions; drivers can show it (a touch overlay idling) or ignore it. */
|
|
1581
|
+
hold?(ctx: C, ms: number, focus?: Focus): Promise<void>;
|
|
1582
|
+
/** The clients of a multi-client driver, by name, with the driver each runs on. */
|
|
1583
|
+
clients?: Record<string, string>;
|
|
1584
|
+
/** Pacing for showcase mode, in milliseconds. */
|
|
1585
|
+
pacing?: { before: number; after: number };
|
|
1586
|
+
/**
|
|
1587
|
+
* How many path attempts can have a context open at once (browser contexts are independent; a
|
|
1588
|
+
* single device is not). Runs use at most this many workers; 1 when omitted.
|
|
1589
|
+
*/
|
|
1590
|
+
concurrency?: number;
|
|
1591
|
+
/** Releases what is shared by all contexts (a browser, a server); called once after the last path. */
|
|
1592
|
+
dispose?(): Promise<void>;
|
|
1593
|
+
};
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
A driver owns the thing under test for one path attempt: a browser context (Playwright), a
|
|
1597
|
+
device (Detox, WebDriver/Appium), a model or an API client. Only `open` and `close` are
|
|
1598
|
+
required; media hooks are used when present. `combineDrivers` runs several at once.
|
|
1599
|
+
|
|
1600
|
+
#### `EventImplementation`
|
|
1601
|
+
|
|
1602
|
+
```ts
|
|
1603
|
+
export type EventImplementation<C> = {
|
|
1604
|
+
/** Who makes the event happen; recorded with the event in the manifest. */
|
|
1605
|
+
kind: EventKind;
|
|
1606
|
+
/** The client that does it, in a multi-client run: its screen is recorded for the clip. */
|
|
1607
|
+
client?: string;
|
|
1608
|
+
/** A plain-language note on how the runner makes it happen (shown to developers). */
|
|
1609
|
+
how: string;
|
|
1610
|
+
/** Test data the event needs before the path starts (a condition modelled as an event). */
|
|
1611
|
+
prepare?(ctx: C, tools: StepTools): Promise<void>;
|
|
1612
|
+
/** Makes the event happen. The runner then waits for the target state to be recognised, so `run` does not need to wait for it; it should throw only when the event cannot be done. */
|
|
1613
|
+
run(ctx: C, tools: StepTools): Promise<void>;
|
|
1614
|
+
/** Set when the event cannot be triggered reliably yet: the path stops before it. */
|
|
1615
|
+
blocked?: string;
|
|
1616
|
+
};
|
|
1617
|
+
```
|
|
1618
|
+
|
|
1619
|
+
How one event is made to happen. Keyed by the event's exact name in the spec.
|
|
1620
|
+
|
|
1621
|
+
#### `StateImplementation`
|
|
1622
|
+
|
|
1623
|
+
```ts
|
|
1624
|
+
export type StateImplementation<C> = {
|
|
1625
|
+
/** Looks at the system from the outside and says whether the state is showing. Called repeatedly (with growing pauses, until `stepTimeoutMs`) while the runner waits for the state, so it must be cheap and side-effect free. */
|
|
1626
|
+
recognize(ctx: C): Promise<RecognizerResult>;
|
|
1627
|
+
/** The client whose screen shows it, in a multi-client run: that screen is the screenshot. */
|
|
1628
|
+
client?: string;
|
|
1629
|
+
/** Shown too briefly to wait for: not seeing it is not a failure. */
|
|
1630
|
+
transient?: boolean;
|
|
1631
|
+
/** States the UI cannot tell apart; never reported as "looks like" each other. */
|
|
1632
|
+
sameAs?: string;
|
|
1633
|
+
/** Where to look when the state changes off-screen (open a tab, reload). */
|
|
1634
|
+
lookIn?(ctx: C): Promise<void>;
|
|
1635
|
+
/** Within-state actions after the screenshot (dismiss a success message). */
|
|
1636
|
+
settle?(ctx: C): Promise<void>;
|
|
1637
|
+
/** Results for the state's `meta.checks`, evaluated once the state is recognised. */
|
|
1638
|
+
checks?(ctx: C): Promise<CheckResult[]>;
|
|
1639
|
+
};
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
How one state is recognised from the outside, and what is checked there.
|
|
1643
|
+
|
|
1644
|
+
#### `Seed`
|
|
1645
|
+
|
|
1646
|
+
```ts
|
|
1647
|
+
export type Seed<C> = {
|
|
1648
|
+
/** A state name (with the default configuration inside and beside it) or a full state value. */
|
|
1649
|
+
at: string | StateValue;
|
|
1650
|
+
/** A plain-language note on how the seed gets there (shown to developers). */
|
|
1651
|
+
how: string;
|
|
1652
|
+
run(ctx: C, input: { path: PlannedPath; tools: StepTools }): Promise<void>;
|
|
1653
|
+
/**
|
|
1654
|
+
* The state the seed leaves on screen, recognised before the first step. In a parallel state
|
|
1655
|
+
* it says which region the seed opens. The state `at` names (or its active leaf), or the last
|
|
1656
|
+
* active leaf, when omitted.
|
|
1657
|
+
*/
|
|
1658
|
+
shows?: string;
|
|
1659
|
+
/**
|
|
1660
|
+
* Events this seed exists for: a variant of a configuration (the same states, different data,
|
|
1661
|
+
* such as a member at an address no lab serves). It starts only paths that take one of these
|
|
1662
|
+
* events, and paths that take one start from a seed that lists it.
|
|
1663
|
+
*/
|
|
1664
|
+
for?: string[];
|
|
1665
|
+
/** Set when the configuration cannot be seeded yet: paths start from another seed. */
|
|
1666
|
+
blocked?: string;
|
|
1667
|
+
};
|
|
1668
|
+
```
|
|
1669
|
+
|
|
1670
|
+
Puts the system directly in one configuration of the chart (create the data, sign in, open the
|
|
1671
|
+
screen), so paths can start there instead of walking every step before it. A seed must leave
|
|
1672
|
+
the system exactly as the steps that lead there would: then results of the stretch before it
|
|
1673
|
+
and the stretch after it compose, and Atlas checks that by recognising the state both ways.
|
|
1674
|
+
|
|
1675
|
+
#### `Implementation`
|
|
1676
|
+
|
|
1677
|
+
```ts
|
|
1678
|
+
export type Implementation<C> = {
|
|
1679
|
+
/** By event name; an event with no entry is reported as blocked. */
|
|
1680
|
+
events: Record<string, EventImplementation<C>>;
|
|
1681
|
+
/** By state name; only leaf states need one. */
|
|
1682
|
+
states: Record<string, StateImplementation<C>>;
|
|
1683
|
+
/**
|
|
1684
|
+
* Named seeds: configurations paths can start from. Journeys are cut at every seeded
|
|
1685
|
+
* configuration they pass through, so each stretch runs on its own and in parallel.
|
|
1686
|
+
*/
|
|
1687
|
+
seeds?: Record<string, Seed<C>>;
|
|
1688
|
+
/** Put the system in the scope's start configuration, for paths that start without a seed. */
|
|
1689
|
+
setup?(ctx: C, input: { path: PlannedPath; tools: StepTools }): Promise<void>;
|
|
1690
|
+
/** Whether `setup` can start a path in this configuration; a string is the reason it cannot. */
|
|
1691
|
+
canStart?(active: string[]): true | string;
|
|
1692
|
+
/**
|
|
1693
|
+
* Ids that tell this attempt's business events from those of attempts running beside it (a
|
|
1694
|
+
* user id, a trace id). Shared event sources keep only events and log lines that contain one.
|
|
1695
|
+
*/
|
|
1696
|
+
correlate?(ctx: C): string[] | Promise<string[]>;
|
|
1697
|
+
};
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
How a driver makes each event of the spec happen and recognises each state: everything the charts
|
|
1701
|
+
do not say. Events and states are keyed by their exact names in the spec; only leaf states need a
|
|
1702
|
+
recogniser.
|
|
1703
|
+
|
|
1704
|
+
#### `EventSource`
|
|
1705
|
+
|
|
1706
|
+
```ts
|
|
1707
|
+
export type EventSource = {
|
|
1708
|
+
/** Identifies the source; a name starting with `log:` marks a log file, anything else counts as a CloudEvents source. */
|
|
1709
|
+
name: string;
|
|
1710
|
+
/** Where "now" is in the source; `collect` returns what arrived after this mark. */
|
|
1711
|
+
mark(): unknown;
|
|
1712
|
+
/** Returns what arrived since `mark` (everything when omitted): CloudEvents and/or raw log text. */
|
|
1713
|
+
collect(mark?: unknown): Promise<{ events: CloudEvent[]; text?: string }>;
|
|
1714
|
+
/** Releases the source; called when the run ends. */
|
|
1715
|
+
close?(): Promise<void>;
|
|
1716
|
+
};
|
|
1717
|
+
```
|
|
1718
|
+
|
|
1719
|
+
Where business events come from: a CloudEvents endpoint, a log, a message queue.
|
|
1720
|
+
|
|
1721
|
+
#### `EventMatcher`
|
|
1722
|
+
|
|
1723
|
+
```ts
|
|
1724
|
+
export type EventMatcher = { type: string } | { pattern: RegExp } | ((events: CloudEvent[], text: string) => boolean);
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
How a business event named in `meta.events` is recognised in what a source collected.
|
|
1728
|
+
|
|
1729
|
+
#### `PrivacyRules`
|
|
1730
|
+
|
|
1731
|
+
```ts
|
|
1732
|
+
export type PrivacyRules = {
|
|
1733
|
+
/** Patterns that must never appear on screen or in a manifest. */
|
|
1734
|
+
deny?: { name: string; pattern: RegExp }[];
|
|
1735
|
+
/** Emails that may appear (synthetic domains, support addresses). */
|
|
1736
|
+
allowEmails?: RegExp[];
|
|
1737
|
+
/** Hosts the run may target; loopback only by default. */
|
|
1738
|
+
allowHosts?: string[];
|
|
1739
|
+
};
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
Rules for what may appear on screen and in a manifest. They extend the defaults (see `DEFAULT_PRIVACY`) except `allowHosts`, which replaces them.
|
|
1743
|
+
|
|
1744
|
+
#### `functionDriver`
|
|
1745
|
+
|
|
1746
|
+
```ts
|
|
1747
|
+
export function functionDriver<C>(
|
|
1748
|
+
create: (input: { path: PlannedPath; timeline: Timeline }) => C | Promise<C>,
|
|
1749
|
+
options: { name?: string; dispose?: (ctx: C) => void | Promise<void>; concurrency?: number } = {}
|
|
1750
|
+
): Driver<C>
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
A driver for anything you can reach from code: a domain model, an API client, a CLI. `create`
|
|
1754
|
+
returns a fresh context per path attempt; recognisers and events work on it directly. It takes
|
|
1755
|
+
no screenshots or clips, and any number of attempts can run side by side unless `concurrency` says otherwise.
|
|
1756
|
+
|
|
1757
|
+
- `create`: builds the context for one attempt; it may be async.
|
|
1758
|
+
|
|
1759
|
+
- `options.name`: the driver's name in reports; `function` when omitted.
|
|
1760
|
+
|
|
1761
|
+
- `options.dispose`: called with the context when the attempt ends.
|
|
1762
|
+
|
|
1763
|
+
- `options.concurrency`: how many contexts may be open at once; unlimited when omitted.
|
|
1764
|
+
|
|
1765
|
+
See /statecharts.md
|
|
1766
|
+
|
|
1767
|
+
**Example**
|
|
1768
|
+
|
|
1769
|
+
```ts
|
|
1770
|
+
const driver = functionDriver(() => new TodoModel());
|
|
1771
|
+
```
|
|
1772
|
+
|
|
1773
|
+
### UI signals
|
|
1774
|
+
|
|
1775
|
+
Source: `packages/core/src/signals.ts`. Import: `@crvouga/atlas/signals`
|
|
1776
|
+
|
|
1777
|
+
A driver-neutral vocabulary for saying what is on screen (`testId`, `role`, `label`, `text`, `css`),
|
|
1778
|
+
and `screen(...)`, which builds a state recogniser from signals. Every UI driver (Playwright, Bun
|
|
1779
|
+
WebView, WebDriver, Detox) resolves the same signals against its own UI tree, so an implementation
|
|
1780
|
+
written with them runs on any of them. Also holds the wire form and the in-page probe used by drivers
|
|
1781
|
+
that evaluate JavaScript in a page. This module is browser-safe and has no driver dependencies.
|
|
1782
|
+
|
|
1783
|
+
See docs/statecharts.md
|
|
1784
|
+
|
|
1785
|
+
#### `Signal`
|
|
1786
|
+
|
|
1787
|
+
```ts
|
|
1788
|
+
export type Signal =
|
|
1789
|
+
| { testId: string }
|
|
1790
|
+
| { role: string; name?: string | RegExp; exact?: boolean }
|
|
1791
|
+
| { text: string | RegExp; exact?: boolean }
|
|
1792
|
+
| { label: string | RegExp }
|
|
1793
|
+
| { css: string };
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
A UI signal: something visible (or not) that proves where the app is. Test IDs first, then
|
|
1797
|
+
accessible roles and names, then labels and text, as the Testing Library guidance orders them.
|
|
1798
|
+
The same values work with every driver that recognises screens by signals (Playwright, Bun
|
|
1799
|
+
WebView, WebDriver); each resolves them against its own UI tree.
|
|
1800
|
+
|
|
1801
|
+
#### `testId`
|
|
1802
|
+
|
|
1803
|
+
```ts
|
|
1804
|
+
export const testId = …
|
|
1805
|
+
```
|
|
1806
|
+
|
|
1807
|
+
A signal for the element with this test ID (`data-testid` on the web, the accessibility identifier on native).
|
|
1808
|
+
|
|
1809
|
+
#### `text`
|
|
1810
|
+
|
|
1811
|
+
```ts
|
|
1812
|
+
export const text = …
|
|
1813
|
+
```
|
|
1814
|
+
|
|
1815
|
+
A signal for visible text: a string matches as a case-insensitive substring unless `exact`, a RegExp is tested as is.
|
|
1816
|
+
|
|
1817
|
+
#### `role`
|
|
1818
|
+
|
|
1819
|
+
```ts
|
|
1820
|
+
export const role = …
|
|
1821
|
+
```
|
|
1822
|
+
|
|
1823
|
+
A signal for an element with an accessibility role (`button`, `heading`, ...), optionally filtered by its accessible name (same matching as `text`).
|
|
1824
|
+
|
|
1825
|
+
#### `label`
|
|
1826
|
+
|
|
1827
|
+
```ts
|
|
1828
|
+
export const label = …
|
|
1829
|
+
```
|
|
1830
|
+
|
|
1831
|
+
A signal for a form control by its label text.
|
|
1832
|
+
|
|
1833
|
+
#### `css`
|
|
1834
|
+
|
|
1835
|
+
```ts
|
|
1836
|
+
export const css = …
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
A signal for a CSS selector. Web only: the WebDriver and Detox drivers throw for it in native sessions, so prefer the other signals.
|
|
1840
|
+
|
|
1841
|
+
#### `describeSignal`
|
|
1842
|
+
|
|
1843
|
+
```ts
|
|
1844
|
+
export function describeSignal(signal: Signal)
|
|
1845
|
+
```
|
|
1846
|
+
|
|
1847
|
+
Describes a signal in words for recogniser output and failure messages, e.g. `button "Add"`.
|
|
1848
|
+
|
|
1849
|
+
#### `SignatureSpec`
|
|
1850
|
+
|
|
1851
|
+
```ts
|
|
1852
|
+
export type SignatureSpec<S = Signal> = {
|
|
1853
|
+
/** Every one must be visible. */
|
|
1854
|
+
all?: S[];
|
|
1855
|
+
/** At least one must be visible (ignored when empty). */
|
|
1856
|
+
any?: S[];
|
|
1857
|
+
/** None may be visible. */
|
|
1858
|
+
none?: S[];
|
|
1859
|
+
};
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
What a screen looks like, as signals.
|
|
1863
|
+
|
|
1864
|
+
#### `ScreenSpec`
|
|
1865
|
+
|
|
1866
|
+
```ts
|
|
1867
|
+
export type ScreenSpec<C, S = Signal> = SignatureSpec<S> & {
|
|
1868
|
+
/** Each `check` is a statement from the state's `meta.checks`; it passes when its `signal` is visible. */
|
|
1869
|
+
checks?: { check: string; signal: S }[];
|
|
1870
|
+
/** See `StateImplementation.transient`. */
|
|
1871
|
+
transient?: boolean;
|
|
1872
|
+
/** See `StateImplementation.sameAs`. */
|
|
1873
|
+
sameAs?: string;
|
|
1874
|
+
/** See `StateImplementation.lookIn`. */
|
|
1875
|
+
lookIn?: (ctx: C) => Promise<void>;
|
|
1876
|
+
/** See `StateImplementation.settle`. */
|
|
1877
|
+
settle?: (ctx: C) => Promise<void>;
|
|
1878
|
+
};
|
|
1879
|
+
```
|
|
1880
|
+
|
|
1881
|
+
A `SignatureSpec` plus the optional parts of a `StateImplementation`, which are passed through.
|
|
1882
|
+
|
|
1883
|
+
#### `signatureScreen`
|
|
1884
|
+
|
|
1885
|
+
```ts
|
|
1886
|
+
export function signatureScreen<C, S = Signal>(isVisible: (ctx: C, signal: S) => Promise<boolean>, describe: (signal: S) => string)
|
|
1887
|
+
```
|
|
1888
|
+
|
|
1889
|
+
Builds a `screen()` function for a driver from one question: is this signal visible right now?
|
|
1890
|
+
A state is recognised when every `all` is visible, one `any` is visible and no `none` is; the result
|
|
1891
|
+
lists every signal looked at with whether it was visible, for failure reports. An `isVisible` that
|
|
1892
|
+
rejects counts as not visible. Drivers with their own locators (WebDriver's) pass their signal type
|
|
1893
|
+
and how to describe it.
|
|
1894
|
+
|
|
1895
|
+
- `isVisible`: answers whether one signal is visible in the context.
|
|
1896
|
+
|
|
1897
|
+
- `describe`: names a signal in words.
|
|
1898
|
+
|
|
1899
|
+
#### `SignalContext`
|
|
1900
|
+
|
|
1901
|
+
```ts
|
|
1902
|
+
export type SignalContext = {
|
|
1903
|
+
/** Open a URL (a path resolves against the driver's base URL; a deep link in a native app). */
|
|
1904
|
+
goto(url: string): Promise<void>;
|
|
1905
|
+
/** Whether the signal is visible now. */
|
|
1906
|
+
isVisible(signal: Signal): Promise<boolean>;
|
|
1907
|
+
/** Actions a person does. Drivers log each to the step's timeline, which becomes the clip's captions. */
|
|
1908
|
+
user: {
|
|
1909
|
+
/** Taps or clicks the target; `label` is the caption shown for it. */
|
|
1910
|
+
tap(target: Signal, label: string): Promise<void>;
|
|
1911
|
+
/** Types `text` into the target; `label` is the caption shown for it. */
|
|
1912
|
+
type(target: Signal, text: string, label: string): Promise<void>;
|
|
1913
|
+
};
|
|
1914
|
+
};
|
|
1915
|
+
```
|
|
1916
|
+
|
|
1917
|
+
The driver-neutral surface of a screen: Playwright, Bun WebView and WebDriver (web and native)
|
|
1918
|
+
contexts all provide it, so an implementation written against it runs on any of them, and the
|
|
1919
|
+
clients of one run can use different drivers.
|
|
1920
|
+
|
|
1921
|
+
#### `screen`
|
|
1922
|
+
|
|
1923
|
+
```ts
|
|
1924
|
+
export const screen = …
|
|
1925
|
+
```
|
|
1926
|
+
|
|
1927
|
+
Builds a state implementation that is recognised from signals on any driver that provides a
|
|
1928
|
+
`SignalContext`.
|
|
1929
|
+
|
|
1930
|
+
**Example**
|
|
1931
|
+
|
|
1932
|
+
```ts
|
|
1933
|
+
states: { 'No todos': screen({ all: [text('Nothing to do yet')], none: [role('listitem')] }) }
|
|
1934
|
+
```
|
|
1935
|
+
|
|
1936
|
+
#### `WireSignal`
|
|
1937
|
+
|
|
1938
|
+
```ts
|
|
1939
|
+
export type WireSignal =
|
|
1940
|
+
| { kind: 'css'; selector: string }
|
|
1941
|
+
| { kind: 'testId'; id: string }
|
|
1942
|
+
| { kind: 'role'; role: string; name: WireMatch | null }
|
|
1943
|
+
| { kind: 'label'; match: WireMatch }
|
|
1944
|
+
| { kind: 'text'; match: WireMatch };
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
A `Signal` as plain JSON, the form sent to the in-page `DOM_PROBE`.
|
|
1948
|
+
|
|
1949
|
+
#### `toWire`
|
|
1950
|
+
|
|
1951
|
+
```ts
|
|
1952
|
+
export function toWire(signal: Signal): WireSignal
|
|
1953
|
+
```
|
|
1954
|
+
|
|
1955
|
+
Converts a signal to its JSON form for a probe that runs inside the page.
|
|
1956
|
+
|
|
1957
|
+
#### `DomProbeResult`
|
|
1958
|
+
|
|
1959
|
+
```ts
|
|
1960
|
+
export type DomProbeResult = { visible: boolean; count: number; x?: number; y?: number };
|
|
1961
|
+
```
|
|
1962
|
+
|
|
1963
|
+
Where a probe found a signal: whether it is visible, and the centre of the first visible match in CSS pixels.
|
|
1964
|
+
|
|
1965
|
+
#### `DOM_PROBE`
|
|
1966
|
+
|
|
1967
|
+
```ts
|
|
1968
|
+
export const DOM_PROBE = …
|
|
1969
|
+
```
|
|
1970
|
+
|
|
1971
|
+
A function expression, as source, that finds a wire signal in a DOM: `(signal, scroll,
|
|
1972
|
+
withElement) => DomProbeResult`, plus the element itself when `withElement` is set (WebDriver
|
|
1973
|
+
returns it as an element reference). It is plain JavaScript so any driver can run it in a page (Bun WebView's
|
|
1974
|
+
`evaluate`, WebDriver's Execute Script). Roles follow the HTML-AAM implicit role mapping for the
|
|
1975
|
+
common elements; names follow aria-label, the associated label, then text content.
|
|
1976
|
+
|
|
1977
|
+
### Static chart composition
|
|
1978
|
+
|
|
1979
|
+
Source: `packages/core/src/spec/compose.ts`
|
|
1980
|
+
|
|
1981
|
+
Flattens a bundle of charts into one machine by inlining each child chart into the state that
|
|
1982
|
+
invokes it, so a plain graph traversal sees the whole product. Pure and deterministic; the input
|
|
1983
|
+
bundle is not modified. State names are global across the composed product (nothing is renamed or
|
|
1984
|
+
namespaced), which is why they must be unique across charts.
|
|
1985
|
+
|
|
1986
|
+
See docs/statecharts.md
|
|
1987
|
+
|
|
1988
|
+
#### `HandOff`
|
|
1989
|
+
|
|
1990
|
+
```ts
|
|
1991
|
+
export type HandOff = { finalState: string; event: string; parentState: string; childChart: string };
|
|
1992
|
+
```
|
|
1993
|
+
|
|
1994
|
+
A child chart's final state and the event it passes to the parent, which the planner and replay insert automatically.
|
|
1995
|
+
|
|
1996
|
+
#### `Composition`
|
|
1997
|
+
|
|
1998
|
+
```ts
|
|
1999
|
+
export type Composition = {
|
|
2000
|
+
/** The single flattened machine (a deep copy; the bundle is untouched). */
|
|
2001
|
+
machine: MachineConfig;
|
|
2002
|
+
/** Which chart each state came from. */
|
|
2003
|
+
chartOf: Map<string, string>;
|
|
2004
|
+
/** A child's former final states and the event each hands to its parent. */
|
|
2005
|
+
handOffs: HandOff[];
|
|
2006
|
+
/** The state each child chart is inlined into. */
|
|
2007
|
+
hosts: Map<string, string>;
|
|
2008
|
+
};
|
|
2009
|
+
```
|
|
2010
|
+
|
|
2011
|
+
The result of composing a bundle.
|
|
2012
|
+
|
|
2013
|
+
#### `composeCharts`
|
|
2014
|
+
|
|
2015
|
+
```ts
|
|
2016
|
+
export function composeCharts(bundle: SpecBundle): Composition
|
|
2017
|
+
```
|
|
2018
|
+
|
|
2019
|
+
Inlines every child chart where its parent invokes it, so XState's graph traversal (which cannot
|
|
2020
|
+
enter an invoked actor) walks straight through. Supports:
|
|
2021
|
+
|
|
2022
|
+
- XState `invoke: { src, id, onDone }` and SCXML `<invoke>` + `done.invoke.<id>`;
|
|
2023
|
+
- a final state's `meta.doneEvent`, naming the event the parent receives from that final;
|
|
2024
|
+
- the legacy `meta.childMachine` + `meta.childFinalEvents` on the parent state.
|
|
2025
|
+
|
|
2026
|
+
A child's final states stay as states (they keep their screenshots and checks) but stop being
|
|
2027
|
+
final: each takes the parent's transition for its hand-off event.
|
|
2028
|
+
|
|
2029
|
+
Every state without an `id` gets its own name as `id`. A child's `initial` replaces the host
|
|
2030
|
+
state's `initial`, and the host's transitions that consumed a hand-off event are removed from it.
|
|
2031
|
+
|
|
2032
|
+
**Throws** Error when the root chart is not loaded, an invoked chart is not loaded, charts invoke
|
|
2033
|
+
each other in a cycle, or a child's final state has no matching transition (or `onDone`) in the parent.
|
|
2034
|
+
|
|
2035
|
+
**Example**
|
|
2036
|
+
|
|
2037
|
+
```ts
|
|
2038
|
+
const { machine, handOffs } = composeCharts(loadSpecDirectory('./specs'));
|
|
2039
|
+
```
|
|
2040
|
+
|
|
2041
|
+
### Spec entry point (browser-safe)
|
|
2042
|
+
|
|
2043
|
+
Source: `packages/core/src/spec/index.ts`. Import: `@crvouga/atlas/spec`
|
|
2044
|
+
|
|
2045
|
+
Re-exports the parts of the spec layer that need no Node built-ins: the spec types, chart and
|
|
2046
|
+
journey parsing, SCXML conversion and static composition. `loadSpecDirectory` (which reads the
|
|
2047
|
+
file system) is deliberately not here; import it from the package root.
|
|
2048
|
+
|
|
2049
|
+
See docs/statecharts.md
|
|
2050
|
+
|
|
2051
|
+
### Spec directory loader
|
|
2052
|
+
|
|
2053
|
+
Source: `packages/core/src/spec/load.ts`
|
|
2054
|
+
|
|
2055
|
+
Reads a specs directory from disk into a `SpecBundle`: every chart file and journeys file directly
|
|
2056
|
+
inside it (no recursion), and works out the root chart. Node only. Parsing itself lives in `./parse`;
|
|
2057
|
+
loading throws on the first malformed file rather than skipping it.
|
|
2058
|
+
|
|
2059
|
+
See docs/statecharts.md
|
|
2060
|
+
|
|
2061
|
+
#### `childChartIds`
|
|
2062
|
+
|
|
2063
|
+
```ts
|
|
2064
|
+
export function childChartIds(machine: MachineConfig)
|
|
2065
|
+
```
|
|
2066
|
+
|
|
2067
|
+
The ids of the charts a machine composes, by `invoke` or the legacy `meta.childMachine`.
|
|
2068
|
+
|
|
2069
|
+
#### `bundleOf`
|
|
2070
|
+
|
|
2071
|
+
```ts
|
|
2072
|
+
export function bundleOf(directory: string, charts: Chart[], journeys: Journey[]): SpecBundle
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
Assembles a bundle from already-parsed charts and journeys, choosing the root chart: the first
|
|
2076
|
+
chart (in the order given) that no other chart invokes, or the first chart when every chart is
|
|
2077
|
+
invoked by another (a cycle, which `composeCharts` later rejects).
|
|
2078
|
+
|
|
2079
|
+
**Throws** Error when `charts` is empty.
|
|
2080
|
+
|
|
2081
|
+
#### `loadSpecDirectory`
|
|
2082
|
+
|
|
2083
|
+
```ts
|
|
2084
|
+
export function loadSpecDirectory(directory: string): SpecBundle
|
|
2085
|
+
```
|
|
2086
|
+
|
|
2087
|
+
Loads every chart and journeys file directly inside `directory` into a bundle.
|
|
2088
|
+
|
|
2089
|
+
Chart files are `*.scxml`, `*.machine.json` and `*.machine.yaml`/`.yml`; journeys files are
|
|
2090
|
+
`journeys.yaml`/`.yml`/`.json`. When two files declare the same chart id the first wins, in the
|
|
2091
|
+
order SCXML, then YAML, then JSON, then by file name, so the result does not depend on directory
|
|
2092
|
+
order.
|
|
2093
|
+
|
|
2094
|
+
**Throws** Error when the directory is missing, a file does not parse, or no chart is found.
|
|
2095
|
+
|
|
2096
|
+
See /statecharts.md
|
|
2097
|
+
|
|
2098
|
+
**Example**
|
|
2099
|
+
|
|
2100
|
+
```ts
|
|
2101
|
+
const bundle = loadSpecDirectory('./specs');
|
|
2102
|
+
bundle.root; // the id of the chart no other chart invokes
|
|
2103
|
+
```
|
|
2104
|
+
|
|
2105
|
+
### Chart and journeys parsers
|
|
2106
|
+
|
|
2107
|
+
Source: `packages/core/src/spec/parse.ts`
|
|
2108
|
+
|
|
2109
|
+
Turns the text of one spec file into a `Chart` or a list of `Journey`. Pure functions of their
|
|
2110
|
+
input (no file access), so they run in a browser too. They check only the shape that later stages
|
|
2111
|
+
rely on; whether the chart is executable is the linter's job.
|
|
2112
|
+
|
|
2113
|
+
See docs/statecharts.md
|
|
2114
|
+
|
|
2115
|
+
#### `parseChart`
|
|
2116
|
+
|
|
2117
|
+
```ts
|
|
2118
|
+
export function parseChart(file: string, text: string): Chart
|
|
2119
|
+
```
|
|
2120
|
+
|
|
2121
|
+
Parses one chart from its text. The file name picks the format: `.scxml` is SCXML (the machine id
|
|
2122
|
+
falls back to the file's base name), `.json` is an XState config, anything else is read as YAML.
|
|
2123
|
+
|
|
2124
|
+
- `file`: the file name, relative to the specs directory; stored as `Chart.file`.
|
|
2125
|
+
|
|
2126
|
+
- `text`: the file contents.
|
|
2127
|
+
|
|
2128
|
+
**Throws** Error when an XState config is not an object with a string `id` and a `states` object, or
|
|
2129
|
+
when the text is not valid JSON/YAML/SCXML.
|
|
2130
|
+
|
|
2131
|
+
#### `parseJourneys`
|
|
2132
|
+
|
|
2133
|
+
```ts
|
|
2134
|
+
export function parseJourneys(file: string, text: string): Journey[]
|
|
2135
|
+
```
|
|
2136
|
+
|
|
2137
|
+
Parses a journeys file of the form `{ journeys: { <name>: { description, events, endsIn } } }`
|
|
2138
|
+
(JSON when the name ends in `.json`, otherwise YAML). Journeys come back in declaration order.
|
|
2139
|
+
`description` defaults to `''` and `endsIn` to `[]`; events are coerced to strings.
|
|
2140
|
+
|
|
2141
|
+
**Throws** Error when there is no `journeys` map, or a journey has no `events` list.
|
|
2142
|
+
|
|
2143
|
+
### SCXML reader and writer
|
|
2144
|
+
|
|
2145
|
+
Source: `packages/core/src/spec/scxml.ts`
|
|
2146
|
+
|
|
2147
|
+
Converts W3C SCXML to and from the XState-shaped `MachineConfig` the rest of Atlas uses, for the
|
|
2148
|
+
pure subset Atlas executes. Business names travel in an `atlas:name` attribute next to a token id,
|
|
2149
|
+
and metadata in the `atlas` namespace, so reading what `machineToScxml` wrote gives the same chart
|
|
2150
|
+
back. Browser-safe (no Node built-ins). Reading is synchronous and keeps its id-to-name table in
|
|
2151
|
+
module state for the duration of one `scxmlToMachine` call, so do not call it re-entrantly.
|
|
2152
|
+
|
|
2153
|
+
See docs/statecharts.md
|
|
2154
|
+
|
|
2155
|
+
#### `ATLAS_NS`
|
|
2156
|
+
|
|
2157
|
+
```ts
|
|
2158
|
+
export const ATLAS_NS = …
|
|
2159
|
+
```
|
|
2160
|
+
|
|
2161
|
+
The XML namespace of Atlas's SCXML extensions (`atlas:name`, `<atlas:meta>`, `atlas:done-event`).
|
|
2162
|
+
|
|
2163
|
+
The SCXML conventions Atlas reads and writes:
|
|
2164
|
+
|
|
2165
|
+
- `<state>`, `<parallel>`, `<final>`, `initial`, `<transition event target>`.
|
|
2166
|
+
- `<invoke type="scxml" src="other.scxml" id>` composes another chart; a transition on
|
|
2167
|
+
`done.invoke.<id>` (or `done.invoke`) becomes the invoke's `onDone`.
|
|
2168
|
+
- Business metadata lives in a foreign namespace, as SCXML allows:
|
|
2169
|
+
`<atlas:meta><atlas:description>…</atlas:description><atlas:event>…</atlas:event></atlas:meta>`.
|
|
2170
|
+
A final state's `atlas:done-event` attribute names the event its parent receives. Meta values
|
|
2171
|
+
that are not strings are written as JSON in an element marked `type="json"`.
|
|
2172
|
+
|
|
2173
|
+
#### `scxmlToken`
|
|
2174
|
+
|
|
2175
|
+
```ts
|
|
2176
|
+
export function scxmlToken(name: string)
|
|
2177
|
+
```
|
|
2178
|
+
|
|
2179
|
+
Turns a business name into a valid SCXML id/event token: ASCII lowercase letters, digits and `-`,
|
|
2180
|
+
starting with a letter or `_` (`s-` is prefixed otherwise). Lossy, so the original name is kept in `atlas:name`.
|
|
2181
|
+
|
|
2182
|
+
**Example**
|
|
2183
|
+
|
|
2184
|
+
```ts
|
|
2185
|
+
scxmlToken('Adds a todo') // 'adds-a-todo'
|
|
2186
|
+
```
|
|
2187
|
+
|
|
2188
|
+
#### `scxmlToMachine`
|
|
2189
|
+
|
|
2190
|
+
```ts
|
|
2191
|
+
export function scxmlToMachine(xml: string, fallbackId = 'machine'): MachineConfig
|
|
2192
|
+
```
|
|
2193
|
+
|
|
2194
|
+
Reads an SCXML document into an XState machine config, restoring business names from `atlas:name`.
|
|
2195
|
+
|
|
2196
|
+
The machine id is the `name` attribute of `<scxml>`, else `fallbackId`. `initial` is the attribute
|
|
2197
|
+
when given, else the first child state. A state's `initial` defaults to its first child state
|
|
2198
|
+
(none for `<parallel>`). Transitions without both an event and a target are skipped.
|
|
2199
|
+
|
|
2200
|
+
- `xml`: the document text.
|
|
2201
|
+
|
|
2202
|
+
- `fallbackId`: the machine id when the document has no `name`.
|
|
2203
|
+
|
|
2204
|
+
**Throws** Error when there is no `<scxml>` root element.
|
|
2205
|
+
|
|
2206
|
+
#### `machineToScxml`
|
|
2207
|
+
|
|
2208
|
+
```ts
|
|
2209
|
+
export function machineToScxml(machine: MachineConfig): string
|
|
2210
|
+
```
|
|
2211
|
+
|
|
2212
|
+
Writes a machine config as an SCXML document with Atlas's `atlas:` extensions, so that
|
|
2213
|
+
`scxmlToMachine(machineToScxml(m))` gives the same chart. Ids are `scxmlToken` forms of the names;
|
|
2214
|
+
the names are kept in `atlas:name`. Only the pure subset is written: `on`, `invoke`, `meta`, nesting.
|
|
2215
|
+
|
|
2216
|
+
### Statechart spec types
|
|
2217
|
+
|
|
2218
|
+
Source: `packages/core/src/spec/types.ts`
|
|
2219
|
+
|
|
2220
|
+
The data model every other module works on: a chart, a bundle of charts plus journeys, and the
|
|
2221
|
+
helpers that walk and name states and transitions. Atlas reads statecharts in two open formats:
|
|
2222
|
+
|
|
2223
|
+
- W3C SCXML (`*.scxml`), the standard for statecharts.
|
|
2224
|
+
- XState v5 machine config (`*.machine.json`, `*.machine.yaml`), the JSON form Stately Studio
|
|
2225
|
+
imports and exports.
|
|
2226
|
+
|
|
2227
|
+
Both load into the XState config shape below. Atlas executes only the pure subset that can be
|
|
2228
|
+
walked without running code: states, nesting, parallel regions, final states, event
|
|
2229
|
+
transitions, statically resolvable child charts (`invoke` with a `src` naming another chart),
|
|
2230
|
+
and `meta`. Guards, actions, context, delays and `always` are rejected by the linter.
|
|
2231
|
+
This module has no I/O and no Node built-ins.
|
|
2232
|
+
|
|
2233
|
+
See docs/statecharts.md
|
|
2234
|
+
|
|
2235
|
+
#### `TransitionConfig`
|
|
2236
|
+
|
|
2237
|
+
```ts
|
|
2238
|
+
export type TransitionConfig = { target: string };
|
|
2239
|
+
```
|
|
2240
|
+
|
|
2241
|
+
The object form of a transition. `target` is a state name, optionally prefixed with `#` (a reference by name across the composed product).
|
|
2242
|
+
|
|
2243
|
+
#### `InvokeConfig`
|
|
2244
|
+
|
|
2245
|
+
```ts
|
|
2246
|
+
export type InvokeConfig = {
|
|
2247
|
+
/** The id of another chart in the bundle; composed statically. */
|
|
2248
|
+
src: string;
|
|
2249
|
+
/** Names the invocation; the parent hears `done.invoke.<id>` (`<src>` when omitted) when the child ends. */
|
|
2250
|
+
id?: string;
|
|
2251
|
+
/** Where the parent goes when the child reaches a final state (`done.invoke.<id>`). */
|
|
2252
|
+
onDone?: string | TransitionConfig;
|
|
2253
|
+
};
|
|
2254
|
+
```
|
|
2255
|
+
|
|
2256
|
+
An `invoke` that runs another chart inside a state. It is never executed: `composeCharts` inlines the child chart statically.
|
|
2257
|
+
|
|
2258
|
+
#### `StateMeta`
|
|
2259
|
+
|
|
2260
|
+
```ts
|
|
2261
|
+
export type StateMeta = {
|
|
2262
|
+
/** What is true in this state. Required on every state by default (`requiredMeta` in the config changes that). */
|
|
2263
|
+
description?: string;
|
|
2264
|
+
/** What a person sees in this state, in words. */
|
|
2265
|
+
snapshot?: string;
|
|
2266
|
+
/** Business events that happen on entering the state, compared with what event sources observed. */
|
|
2267
|
+
events?: string[];
|
|
2268
|
+
/** Whether the behaviour is `confirmed` against the product or only `assumed`. */
|
|
2269
|
+
confidence?: 'confirmed' | 'assumed';
|
|
2270
|
+
/** Where the behaviour is documented. */
|
|
2271
|
+
source?: string[];
|
|
2272
|
+
/** Things to verify on screen, quoted from the spec; the implementation's `checks()` returns their results. */
|
|
2273
|
+
checks?: string[];
|
|
2274
|
+
/** Gherkin-style contracts attached to the state. Core does not read them; they are carried through for viewers. */
|
|
2275
|
+
contracts?: { id: string; name: string; source: string; description?: string; steps: { keyword: string; text: string; table?: string[][]; docString?: string }[] }[];
|
|
2276
|
+
/** Why a state with no way out is meant to be one. Without it, a state with no outgoing transition is a lint error. */
|
|
2277
|
+
deadEnd?: string;
|
|
2278
|
+
/** Legacy composition: the chart that runs inside this state. Prefer `invoke`. */
|
|
2279
|
+
childMachine?: string;
|
|
2280
|
+
/** Legacy composition: child final state → the event the parent receives. Prefer `invoke`. */
|
|
2281
|
+
childFinalEvents?: Record<string, string>;
|
|
2282
|
+
/** On a final state: the event the parent receives when this final is reached. */
|
|
2283
|
+
doneEvent?: string;
|
|
2284
|
+
[key: string]: unknown;
|
|
2285
|
+
};
|
|
2286
|
+
```
|
|
2287
|
+
|
|
2288
|
+
The `meta` of a state: the business-language facts about it that Atlas lints, shows and checks.
|
|
2289
|
+
Open object: unknown keys are kept and ignored.
|
|
2290
|
+
|
|
2291
|
+
See /statecharts.md
|
|
2292
|
+
|
|
2293
|
+
#### `StateConfig`
|
|
2294
|
+
|
|
2295
|
+
```ts
|
|
2296
|
+
export type StateConfig = {
|
|
2297
|
+
/** Set by `composeCharts` to the state's name when absent; used by `#<name>` targets. */
|
|
2298
|
+
id?: string;
|
|
2299
|
+
/** The child state entered first. */
|
|
2300
|
+
initial?: string;
|
|
2301
|
+
/** `parallel`: all child regions are active together. `final`: the state ends its chart (or hands off to the parent). */
|
|
2302
|
+
type?: 'parallel' | 'final';
|
|
2303
|
+
meta?: StateMeta;
|
|
2304
|
+
states?: Record<string, StateConfig>;
|
|
2305
|
+
/** Event name (exact business wording) to its transition: a target string or `{ target }`. */
|
|
2306
|
+
on?: Record<string, TransitionConfig | string>;
|
|
2307
|
+
/** Child chart(s) that run inside this state. */
|
|
2308
|
+
invoke?: InvokeConfig | InvokeConfig[];
|
|
2309
|
+
[key: string]: unknown;
|
|
2310
|
+
};
|
|
2311
|
+
```
|
|
2312
|
+
|
|
2313
|
+
One state of an XState-shaped chart, recursively. Keys not listed here (guards, actions, `after`,
|
|
2314
|
+
...) are accepted by the type so the linter can report them; the planner never executes them.
|
|
2315
|
+
|
|
2316
|
+
#### `MachineConfig`
|
|
2317
|
+
|
|
2318
|
+
```ts
|
|
2319
|
+
export type MachineConfig = StateConfig & { id: string };
|
|
2320
|
+
```
|
|
2321
|
+
|
|
2322
|
+
A whole chart: a state config with a required `id`, which is the name other charts `invoke` it by.
|
|
2323
|
+
|
|
2324
|
+
#### `Journey`
|
|
2325
|
+
|
|
2326
|
+
```ts
|
|
2327
|
+
export type Journey = {
|
|
2328
|
+
/** The key in `journeys.yaml`. */
|
|
2329
|
+
name: string;
|
|
2330
|
+
/** Empty string when the file gives none. */
|
|
2331
|
+
description: string;
|
|
2332
|
+
/** Event names in order, using the exact spec wording. Hand-off events are never listed; the planner inserts them. */
|
|
2333
|
+
events: string[];
|
|
2334
|
+
/** State names that must all be active after the last event. Empty when the file gives none. */
|
|
2335
|
+
endsIn: string[];
|
|
2336
|
+
};
|
|
2337
|
+
```
|
|
2338
|
+
|
|
2339
|
+
A curated, named sequence of events, replayed by the linter and run as its own path(s).
|
|
2340
|
+
|
|
2341
|
+
#### `Chart`
|
|
2342
|
+
|
|
2343
|
+
```ts
|
|
2344
|
+
export type Chart = {
|
|
2345
|
+
/** Path relative to the spec directory. */
|
|
2346
|
+
file: string;
|
|
2347
|
+
/** The notation the file was parsed from. */
|
|
2348
|
+
format: 'scxml' | 'xstate';
|
|
2349
|
+
machine: MachineConfig;
|
|
2350
|
+
};
|
|
2351
|
+
```
|
|
2352
|
+
|
|
2353
|
+
One loaded chart file.
|
|
2354
|
+
|
|
2355
|
+
#### `SpecBundle`
|
|
2356
|
+
|
|
2357
|
+
```ts
|
|
2358
|
+
export type SpecBundle = {
|
|
2359
|
+
/** The directory the files were read from. */
|
|
2360
|
+
directory: string;
|
|
2361
|
+
/** Charts in load order, one per chart id. */
|
|
2362
|
+
charts: Chart[];
|
|
2363
|
+
/** Journeys from every journeys file, in file then declaration order. */
|
|
2364
|
+
journeys: Journey[];
|
|
2365
|
+
/** The chart the others are composed into: the one no other chart invokes. */
|
|
2366
|
+
root: string;
|
|
2367
|
+
};
|
|
2368
|
+
```
|
|
2369
|
+
|
|
2370
|
+
Everything loaded from a specs directory.
|
|
2371
|
+
|
|
2372
|
+
#### `TRANSITION_SEPARATOR`
|
|
2373
|
+
|
|
2374
|
+
```ts
|
|
2375
|
+
export const TRANSITION_SEPARATOR = …
|
|
2376
|
+
```
|
|
2377
|
+
|
|
2378
|
+
The separator inside a transition id (`<source> :: <event>`). The ids are not escaped, so a state name containing it makes `splitTransitionId` ambiguous.
|
|
2379
|
+
|
|
2380
|
+
#### `transitionId`
|
|
2381
|
+
|
|
2382
|
+
```ts
|
|
2383
|
+
export function transitionId(source: string, event: string)
|
|
2384
|
+
```
|
|
2385
|
+
|
|
2386
|
+
The id of a transition: the source state's name and the event, joined by `TRANSITION_SEPARATOR`.
|
|
2387
|
+
|
|
2388
|
+
#### `splitTransitionId`
|
|
2389
|
+
|
|
2390
|
+
```ts
|
|
2391
|
+
export function splitTransitionId(id: string)
|
|
2392
|
+
```
|
|
2393
|
+
|
|
2394
|
+
Splits a transition id at its first separator into `{ source, event }`; the inverse of `transitionId`.
|
|
2395
|
+
|
|
2396
|
+
#### `targetOf`
|
|
2397
|
+
|
|
2398
|
+
```ts
|
|
2399
|
+
export function targetOf(t: TransitionConfig | string | undefined)
|
|
2400
|
+
```
|
|
2401
|
+
|
|
2402
|
+
The state name a transition leads to, with any leading `#` removed; `undefined` when there is no transition.
|
|
2403
|
+
|
|
2404
|
+
#### `StateEntry`
|
|
2405
|
+
|
|
2406
|
+
```ts
|
|
2407
|
+
export type StateEntry = { name: string; node: StateConfig; trail: string[] };
|
|
2408
|
+
```
|
|
2409
|
+
|
|
2410
|
+
A state found by `walkStates`: its name, its config, and the names of its ancestors from the outermost down.
|
|
2411
|
+
|
|
2412
|
+
#### `walkStates`
|
|
2413
|
+
|
|
2414
|
+
```ts
|
|
2415
|
+
export function walkStates(config: StateConfig, trail: string[] = []): StateEntry[]
|
|
2416
|
+
```
|
|
2417
|
+
|
|
2418
|
+
Lists every state under `config` depth-first, parents before their children, in declaration order. The root `config` itself is not listed.
|
|
2419
|
+
|
|
2420
|
+
#### `invokesOf`
|
|
2421
|
+
|
|
2422
|
+
```ts
|
|
2423
|
+
export function invokesOf(node: StateConfig): InvokeConfig[]
|
|
2424
|
+
```
|
|
2425
|
+
|
|
2426
|
+
The `invoke` entries of a state as an array (empty when it has none).
|