@crvouga/atlas 0.4.0 → 0.6.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 +13 -1
- package/README.md +191 -611
- 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 +35 -1
- package/dist/core/src/config.d.ts +77 -4
- package/dist/core/src/config.js +110 -17
- 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 +75 -16
- package/dist/core/src/run/execute.js +71 -18
- 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 +85 -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 +35 -5
- package/dist/reports/src/evidence-live.js +18 -2
- package/dist/reports/src/evidence-types.d.ts +68 -1
- package/dist/reports/src/evidence-types.js +5 -1
- package/dist/reports/src/evidence-view.d.ts +43 -4
- package/dist/reports/src/evidence-view.js +13 -2
- 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 +56 -0
- package/dist/reports/src/http-store.js +148 -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 +369 -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 +35 -5
- package/dist/reports/src/layer-axes.js +25 -5
- package/dist/reports/src/layer-catalog.d.ts +28 -1
- package/dist/reports/src/layer-catalog.js +18 -1
- package/dist/reports/src/layer-merge.d.ts +98 -9
- package/dist/reports/src/layer-merge.js +147 -31
- package/dist/reports/src/layer-source.d.ts +18 -0
- package/dist/reports/src/layer-source.js +11 -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 +32 -1
- package/dist/reports/src/layers-route.d.ts +47 -4
- package/dist/reports/src/layers-route.js +86 -7
- package/dist/reports/src/live-reporter.d.ts +34 -0
- package/dist/reports/src/live-reporter.js +122 -0
- 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 +26 -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 +42 -9
- package/dist/reports/src/publish.js +36 -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 +486 -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 +37 -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/runners/src/codecept.d.ts +14 -0
- package/dist/runners/src/codecept.js +16 -0
- package/dist/runners/src/jest.d.ts +50 -0
- package/dist/runners/src/jest.js +52 -0
- package/dist/runners/src/mocha.d.ts +40 -0
- package/dist/runners/src/mocha.js +39 -0
- package/dist/runners/src/node-test.d.ts +28 -0
- package/dist/runners/src/node-test.js +44 -0
- package/dist/runners/src/publish.d.ts +30 -0
- package/dist/runners/src/publish.js +68 -0
- package/dist/schema/src/api.d.ts +800 -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 +16 -0
- package/dist/schema/src/index.js +16 -0
- package/dist/schema/src/layer.d.ts +2273 -0
- package/dist/schema/src/layer.js +462 -0
- package/dist/schema/src/live-run.d.ts +123 -0
- package/dist/schema/src/live-run.js +30 -0
- package/dist/schema/src/manifest.d.ts +97 -2
- package/dist/schema/src/manifest.js +84 -0
- package/dist/schema/src/project.d.ts +20 -0
- package/dist/schema/src/project.js +10 -0
- package/dist/schema/src/report-sources.d.ts +29 -0
- package/dist/schema/src/report-sources.js +27 -1
- package/dist/schema/src/runs-index.d.ts +17 -1
- package/dist/schema/src/runs-index.js +15 -0
- package/dist/schema/src/server-config.d.ts +177 -0
- package/dist/schema/src/server-config.js +85 -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 +72 -0
- package/dist/server/src/app.js +557 -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 +204 -0
- package/dist/server/src/dev.d.ts +12 -0
- package/dist/server/src/dev.js +89 -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 +18 -0
- package/dist/server/src/index.js +18 -0
- package/dist/server/src/live-runs.d.ts +52 -0
- package/dist/server/src/live-runs.js +229 -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 +114 -0
- package/dist/server/src/openapi.d.ts +56 -0
- package/dist/server/src/openapi.js +438 -0
- package/dist/server/src/problem.d.ts +8 -0
- package/dist/server/src/problem.js +39 -0
- package/dist/server/src/projects.d.ts +3 -0
- package/dist/server/src/projects.js +154 -0
- package/dist/server/src/settings-page.d.ts +9 -0
- package/dist/server/src/settings-page.js +93 -0
- package/dist/server/src/settings.d.ts +23 -0
- package/dist/server/src/settings.js +72 -0
- package/dist/server/src/site.d.ts +15 -0
- package/dist/server/src/site.js +78 -0
- package/dist/server/src/store-api.d.ts +28 -0
- package/dist/server/src/store-api.js +180 -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 +148 -0
- package/docs/reference/core.md +2432 -0
- package/docs/reference/detox.md +150 -0
- package/docs/reference/http-api.md +799 -0
- package/docs/reference/playwright.md +303 -0
- package/docs/reference/reports.md +7152 -0
- package/docs/reference/runners.md +169 -0
- package/docs/reference/schema.md +1762 -0
- package/docs/reference/server.md +825 -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/runners.md +69 -0
- package/docs/server.md +321 -0
- package/docs/statecharts.md +643 -0
- package/llms-full.txt +4114 -0
- package/llms.txt +49 -0
- package/package.json +61 -11
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +65 -0
- package/packages/reports/STORAGE.md +4 -0
- package/packages/runners/README.md +6 -0
- package/packages/runners/compat/codecept.cjs +2 -0
- package/packages/runners/compat/jest.cjs +2 -0
- package/packages/runners/compat/mocha.cjs +2 -0
- package/packages/schema/README.md +14 -0
- package/packages/schema/schemas/atlas-change.schema.json +4 -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 +26 -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/live-run-start.schema.json +42 -0
- package/packages/schema/schemas/live-run-update.schema.json +93 -0
- package/packages/schema/schemas/live-run.schema.json +225 -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/project-slug.schema.json +8 -0
- package/packages/schema/schemas/project.schema.json +22 -0
- package/packages/schema/schemas/projects.schema.json +33 -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 +208 -0
- package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
- package/packages/schema/schemas/server-info.schema.json +114 -0
- package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -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 +11021 -0
- package/packages/visualizer/README.md +32 -11
- package/packages/visualizer/bin/atlas-client.mjs +176 -0
- package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
- package/packages/visualizer/src/app/AppShell.tsx +32 -16
- package/packages/visualizer/src/app/report-sources.tsx +7 -4
- package/packages/visualizer/src/app/router.tsx +3 -3
- package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
- package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
- package/packages/visualizer/src/components/Header.module.css +101 -192
- package/packages/visualizer/src/components/Header.tsx +88 -13
- package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
- package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
- package/packages/visualizer/src/data/adapters/index.ts +10 -1
- package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
- package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
- package/packages/visualizer/src/data/queries.ts +14 -7
- package/packages/visualizer/src/evidence/api.ts +16 -3
- 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 +21 -4
- package/packages/visualizer/src/evidence/document-page.tsx +6 -2
- package/packages/visualizer/src/evidence/evidence-context.tsx +79 -12
- package/packages/visualizer/src/evidence/evidence.module.css +11 -1
- package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
- package/packages/visualizer/src/evidence/model-page.tsx +6 -2
- package/packages/visualizer/src/evidence/project-home.tsx +45 -0
- package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
- package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
- package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
- package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
- package/packages/visualizer/src/find/Finder.tsx +1 -7
- package/packages/visualizer/src/find/open.ts +4 -0
- package/packages/visualizer/src/live/live-query.ts +21 -0
- package/packages/visualizer/src/live/live-runs.tsx +69 -0
- package/packages/visualizer/src/live/live-stream.ts +225 -0
- package/packages/visualizer/src/live/report-progress.ts +62 -0
- package/packages/visualizer/src/live/test-progress.tsx +34 -0
- package/packages/visualizer/src/runtime.ts +35 -0
- package/packages/visualizer/src/state/ui-store.ts +3 -2
- package/packages/visualizer/src/views/ChartPage.tsx +2 -0
- package/packages/visualizer/src/views/ChartTests.tsx +43 -0
- package/packages/visualizer/src/views/SidePanel.tsx +2 -0
- package/packages/visualizer/vite/atlas-files.test.ts +4 -4
- package/packages/visualizer/vite/atlas-files.ts +1 -5
- package/packages/visualizer/vite/atlas-server-proxy.ts +37 -0
- package/packages/visualizer/vite.config.ts +11 -3
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import type { ActiveRun, BackendInfo, EvidencePage, LayerEvidence, LayerHeader, LayerRows } from '@crvouga/atlas/reports/evidence-types';
|
|
2
|
+
import { applyEffectiveDelta, isEffectiveDelta, type EffectiveDelta } from '@crvouga/atlas/reports/effective-delta';
|
|
3
|
+
import type { EffectiveEvidence } from '@crvouga/atlas/reports/report-index';
|
|
2
4
|
import { contributes, evidenceView, type EvidenceView, type SourceSelection } from '@crvouga/atlas/reports/evidence-view';
|
|
3
5
|
import type { SpecEntry } from '@crvouga/atlas/reports/explorer-types';
|
|
4
6
|
import { combineEvidence, distinctHeaders } from '@crvouga/atlas/reports/layer-merge';
|
|
5
7
|
import type { ReportOutline } from '@crvouga/atlas/reports/layer-types';
|
|
6
8
|
import { SpecIndexSchema } from '@crvouga/atlas/schema';
|
|
7
|
-
import { useInfiniteQuery, useQueries, useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { useInfiniteQuery, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
8
10
|
import { createContext, useContext, useEffect, useMemo, useRef, type ReactNode } from 'react';
|
|
9
11
|
|
|
10
12
|
import { keys, specKey, useAdapter } from '../data/queries';
|
|
13
|
+
import { eventsAddress, listenLive, useLiveStatus } from '../live/live-stream';
|
|
14
|
+
import { liveInvalidator } from '../live/live-query';
|
|
15
|
+
import { useLiveTests } from '../live/live-runs';
|
|
16
|
+
import { withLiveProgress } from '../live/report-progress';
|
|
11
17
|
import { fileName, folderOf, kindOfFile, outlinePath } from '../spec-kind';
|
|
12
18
|
import { useUiStore } from '../state/ui-store';
|
|
13
|
-
import { reportJson, staticEvidence } from './api';
|
|
19
|
+
import { effectiveEvidence, reportJson, staticEvidence } from './api';
|
|
14
20
|
|
|
15
21
|
/** How often the layer list is asked for while a spec run is writing results, and while nothing is. */
|
|
16
22
|
const LIVE_POLL_MS = 2_000;
|
|
17
23
|
const IDLE_POLL_MS = 15_000;
|
|
24
|
+
/** The effective report is revalidated with its ETag, so asking often costs one small answer. */
|
|
25
|
+
const EFFECTIVE_POLL_MS = 5_000;
|
|
26
|
+
/** While the server's stream is open it says when to read again; this is only the safety net behind it. */
|
|
27
|
+
const LIVE_SAFETY_POLL_MS = 60_000;
|
|
28
|
+
/** At most this often is the report read again because the server said it changed. */
|
|
29
|
+
const LIVE_REREAD_MS = 250;
|
|
18
30
|
|
|
19
31
|
export type EvidenceState = {
|
|
20
32
|
/** The layer list has been read at least once. */
|
|
@@ -63,26 +75,77 @@ function outlineOf(files: readonly string[]): ReportOutline {
|
|
|
63
75
|
return { specs, sha: null, changedFiles: [] };
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
const NONE: readonly LayerEvidence[] = [];
|
|
79
|
+
|
|
80
|
+
function hasExclusions(excluded: SourceSelection): boolean {
|
|
81
|
+
return Object.values(excluded).some((list) => list.length > 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
export function EvidenceProvider({ children }: { children: ReactNode }) {
|
|
67
85
|
const adapter = useAdapter();
|
|
68
86
|
const source = specKey(adapter);
|
|
69
87
|
const excluded = useUiStore((s) => s.excluded);
|
|
70
88
|
const setExcluded = useUiStore((s) => s.setExcluded);
|
|
71
89
|
|
|
90
|
+
const client = useQueryClient();
|
|
91
|
+
const stream = useLiveStatus();
|
|
92
|
+
const streaming = stream === 'live';
|
|
93
|
+
// The server says when results arrive: read the report again then, not on a timer.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const url = eventsAddress();
|
|
96
|
+
if (!url) return;
|
|
97
|
+
// A burst of results (a CI run finishing, many shards at once) is read once, not once for each.
|
|
98
|
+
const reads = [liveInvalidator(client, ['evidence-effective'], LIVE_REREAD_MS), liveInvalidator(client, ['evidence-index'], LIVE_REREAD_MS)];
|
|
99
|
+
const outlines = [liveInvalidator(client, ['evidence-outline'], LIVE_REREAD_MS), liveInvalidator(client, ['spec', source], LIVE_REREAD_MS)];
|
|
100
|
+
const reread = () => reads.forEach(refresh => refresh());
|
|
101
|
+
const reoutline = () => { outlines.forEach(refresh => refresh()); reread(); };
|
|
102
|
+
const stop = listenLive(url, (event) => {
|
|
103
|
+
if (event.type === 'change' && event.change.scope === 'specs') return reoutline();
|
|
104
|
+
if (event.type === 'change' && event.change.scope === 'runs' && !event.change.liveRunId) return reread();
|
|
105
|
+
if (event.type !== 'report' && event.type !== 'open') return;
|
|
106
|
+
reread();
|
|
107
|
+
if (event.type === 'open') reoutline();
|
|
108
|
+
});
|
|
109
|
+
return () => { stop(); [...reads, ...outlines].forEach(refresh => refresh.cancel()); };
|
|
110
|
+
}, [client, source]);
|
|
111
|
+
|
|
72
112
|
const specIndex = useQuery({ queryKey: keys.specIndex(source), queryFn: adapter.specIndex, retry: false });
|
|
73
113
|
const files = useMemo(() => {
|
|
74
114
|
const parsed = SpecIndexSchema.safeParse(specIndex.data);
|
|
75
115
|
return parsed.success ? parsed.data.files : [];
|
|
76
116
|
}, [specIndex.data]);
|
|
77
117
|
|
|
118
|
+
// The node answers with only what the report shows, in one request. Leaving a source out needs the
|
|
119
|
+
// history it hides, so that one case reads every layer, as a store without a node always does.
|
|
120
|
+
const effective = effectiveEvidence() && !staticEvidence && !hasExclusions(excluded);
|
|
121
|
+
const overview = useQuery({
|
|
122
|
+
queryKey: ['evidence-effective'],
|
|
123
|
+
// Holding a report, ask only for what changed since it; the server answers with all of it when it cannot.
|
|
124
|
+
queryFn: async ({ signal }) => {
|
|
125
|
+
const held = client.getQueryData<EffectiveEvidence>(['evidence-effective']);
|
|
126
|
+
const answer = await reportJson<EffectiveEvidence | EffectiveDelta>(held ? `/evidence/effective?since=${encodeURIComponent(held.version)}` : '/evidence/effective', signal);
|
|
127
|
+
if (!isEffectiveDelta(answer)) return answer;
|
|
128
|
+
return (held && applyEffectiveDelta(held, answer)) ?? reportJson<EffectiveEvidence>('/evidence/effective', signal);
|
|
129
|
+
},
|
|
130
|
+
enabled: effective,
|
|
131
|
+
staleTime: 0,
|
|
132
|
+
refetchOnWindowFocus: 'always',
|
|
133
|
+
refetchOnReconnect: 'always',
|
|
134
|
+
retry: false,
|
|
135
|
+
refetchInterval: streaming ? LIVE_SAFETY_POLL_MS : EFFECTIVE_POLL_MS
|
|
136
|
+
});
|
|
137
|
+
|
|
78
138
|
const index = useInfiniteQuery({
|
|
79
139
|
queryKey: ['evidence-index'],
|
|
140
|
+
enabled: !effective,
|
|
80
141
|
initialPageParam: null as string | null,
|
|
81
142
|
queryFn: ({ pageParam, signal }) => reportJson<EvidencePage>(`/evidence${pageParam ? `?cursor=${encodeURIComponent(pageParam)}` : ''}`, signal),
|
|
82
143
|
getNextPageParam: (page) => page.cursor,
|
|
83
144
|
staleTime: 0,
|
|
145
|
+
refetchOnWindowFocus: 'always',
|
|
146
|
+
refetchOnReconnect: 'always',
|
|
84
147
|
retry: false,
|
|
85
|
-
refetchInterval: (query) => (staticEvidence ? false : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
|
|
148
|
+
refetchInterval: (query) => (staticEvidence ? false : streaming ? LIVE_SAFETY_POLL_MS : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
|
|
86
149
|
});
|
|
87
150
|
// Show the newest layers at once, then read the older pages behind them.
|
|
88
151
|
const { hasNextPage, isFetching, isError, fetchNextPage } = index;
|
|
@@ -98,7 +161,7 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
|
|
|
98
161
|
const serverOutline = useQuery({ queryKey: ['evidence-outline'], queryFn: ({ signal }) => reportJson<ReportOutline>('/outline', signal), staleTime: 30_000, retry: false });
|
|
99
162
|
const outline = useMemo(() => serverOutline.data ?? outlineOf(files), [serverOutline.data, files]);
|
|
100
163
|
|
|
101
|
-
const wanted = useMemo(() => layers.filter((layer) => contributes(layer, excluded)), [layers, excluded]);
|
|
164
|
+
const wanted = useMemo(() => (effective ? [] : layers.filter((layer) => contributes(layer, excluded))), [layers, excluded, effective]);
|
|
102
165
|
const rows = useQueries({
|
|
103
166
|
queries: wanted.map((layer) => ({
|
|
104
167
|
queryKey: rowsKey(layer),
|
|
@@ -126,23 +189,27 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
|
|
|
126
189
|
// `stamp` changes exactly when a layer's rows do.
|
|
127
190
|
}, [wanted, rows.stamp]);
|
|
128
191
|
|
|
129
|
-
const
|
|
192
|
+
const shown = effective ? overview.data?.layers ?? NONE : evidence;
|
|
193
|
+
const { tests: liveTests } = useLiveTests();
|
|
194
|
+
const view = useMemo(() => withLiveProgress(evidenceView(combineEvidence(outline, shown)), liveTests), [outline, shown, liveTests]);
|
|
130
195
|
const titles = useMemo(() => new Map(outline.specs.map((spec) => [spec.path, spec.title])), [outline]);
|
|
196
|
+
const effectiveHeaders = useMemo(() => (overview.data?.layers ?? NONE).map((entry) => entry.layer), [overview.data]);
|
|
131
197
|
|
|
132
198
|
const state: EvidenceState = {
|
|
133
|
-
ready: pages !== undefined,
|
|
134
|
-
unavailable: pages === undefined && index.error ? index.error.message : null,
|
|
199
|
+
ready: effective ? overview.data !== undefined : pages !== undefined,
|
|
200
|
+
unavailable: effective ? (overview.data === undefined && overview.error ? overview.error.message : null) : pages === undefined && index.error ? index.error.message : null,
|
|
135
201
|
retry: () => {
|
|
202
|
+
if (effective) { void Promise.all([overview.refetch(), serverOutline.refetch()]); return; }
|
|
136
203
|
void reportJson<EvidencePage>('/evidence?retry=1')
|
|
137
204
|
.catch(() => null)
|
|
138
205
|
.then(() => Promise.all([index.refetch(), serverOutline.refetch()]));
|
|
139
206
|
},
|
|
140
207
|
pending: wanted.length - evidence.length,
|
|
141
|
-
layers,
|
|
142
|
-
evidence,
|
|
143
|
-
backends: first?.backends ?? [],
|
|
144
|
-
active: first?.active ?? [],
|
|
145
|
-
notices,
|
|
208
|
+
layers: effective ? effectiveHeaders : layers,
|
|
209
|
+
evidence: shown,
|
|
210
|
+
backends: effective ? overview.data?.backends ?? [] : first?.backends ?? [],
|
|
211
|
+
active: effective ? [] : first?.active ?? [],
|
|
212
|
+
notices: effective ? overview.data?.notices ?? [] : notices,
|
|
146
213
|
view,
|
|
147
214
|
files,
|
|
148
215
|
filesPending: specIndex.isPending,
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
.mark[data-status='running'] {
|
|
17
17
|
color: var(--place);
|
|
18
|
-
animation: turn
|
|
18
|
+
animation: turn 1.8s linear infinite;
|
|
19
|
+
opacity: .8;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
@keyframes turn {
|
|
@@ -509,6 +510,11 @@
|
|
|
509
510
|
transform: rotate(-135deg);
|
|
510
511
|
}
|
|
511
512
|
|
|
513
|
+
.excerpt[data-progress] {
|
|
514
|
+
color: var(--ink-3);
|
|
515
|
+
font-family: inherit;
|
|
516
|
+
}
|
|
517
|
+
|
|
512
518
|
.excerpt {
|
|
513
519
|
margin: -4px 14px 10px 40px;
|
|
514
520
|
overflow: hidden;
|
|
@@ -895,3 +901,7 @@ a.listRow:hover {
|
|
|
895
901
|
display: none;
|
|
896
902
|
}
|
|
897
903
|
}
|
|
904
|
+
|
|
905
|
+
@media (prefers-reduced-motion: reduce) {
|
|
906
|
+
.mark[data-status='running'] { animation: none; }
|
|
907
|
+
}
|
|
@@ -8,6 +8,7 @@ import { featureModel, scenarioLabel, slotsByScenario, type FeatureModel, type F
|
|
|
8
8
|
import { Bounded } from './marks';
|
|
9
9
|
import { SourceText, SpecHeader } from './spec-header';
|
|
10
10
|
import { SpecSnapshot } from './spec-snapshot';
|
|
11
|
+
import { TestFilter, useRunningFilter } from './test-filter';
|
|
11
12
|
|
|
12
13
|
type Props = { file: string; text: string; kind: SpecKind; entry: SpecEvidence | undefined; selection: CheckSelection; select: SelectCheck };
|
|
13
14
|
|
|
@@ -23,6 +24,7 @@ function parse(text: string): { feature: FeatureModel | null; error: string | nu
|
|
|
23
24
|
export function FeaturePage({ file, text, kind, entry, selection, select }: Props) {
|
|
24
25
|
const { feature, error } = useMemo(() => parse(text), [text]);
|
|
25
26
|
const slots = entry?.slots ?? [];
|
|
27
|
+
const runningOnly = useRunningFilter();
|
|
26
28
|
const filed = useMemo(() => (feature ? slotsByScenario(feature, slots) : { byScenario: new Map<string, CheckSlot[]>(), leftOver: slots }), [feature, slots]);
|
|
27
29
|
const scenarios = feature?.rules.flatMap((rule) => rule.scenarios.filter((scenario) => !scenario.background)) ?? [];
|
|
28
30
|
const planned = scenarios.reduce((count, scenario) => count + Math.max(1, filed.byScenario.get(scenario.name)?.length ?? 0), 0) + filed.leftOver.length;
|
|
@@ -30,8 +32,9 @@ export function FeaturePage({ file, text, kind, entry, selection, select }: Prop
|
|
|
30
32
|
|
|
31
33
|
const rows = (scenario: FeatureScenario) => {
|
|
32
34
|
const written = <PlainSteps steps={scenario.steps} examples={scenario.examples} />;
|
|
33
|
-
const recorded = filed.byScenario.get(scenario.name) ?? [];
|
|
35
|
+
const recorded = (filed.byScenario.get(scenario.name) ?? []).filter(slot => !runningOnly || slot.status === 'running');
|
|
34
36
|
if (!recorded.length) {
|
|
37
|
+
if (runningOnly) return null;
|
|
35
38
|
const id = `scenario:${scenario.name}`;
|
|
36
39
|
return (
|
|
37
40
|
<ResultRow key={id} label={scenario.name} slot={null} open={selection.check === id} onToggle={() => toggle(id)}>
|
|
@@ -53,6 +56,7 @@ export function FeaturePage({ file, text, kind, entry, selection, select }: Prop
|
|
|
53
56
|
{feature?.description && <p className={styles.lead}>{feature.description}</p>}
|
|
54
57
|
</SpecHeader>
|
|
55
58
|
<SpecSnapshot title={feature?.name || file.split('/').at(-1)!} text={text} kind={kind.one} />
|
|
59
|
+
<TestFilter slots={slots} />
|
|
56
60
|
{error && <p role="alert" className={styles.notice}>This feature could not be parsed: {error}</p>}
|
|
57
61
|
{feature?.rules.map((rule, index) => (
|
|
58
62
|
<section key={index} className={styles.block}>
|
|
@@ -67,12 +71,12 @@ export function FeaturePage({ file, text, kind, entry, selection, select }: Prop
|
|
|
67
71
|
<ul className={styles.results}>{rule.scenarios.filter((scenario) => !scenario.background).map(rows)}</ul>
|
|
68
72
|
</section>
|
|
69
73
|
))}
|
|
70
|
-
{filed.leftOver.
|
|
74
|
+
{filed.leftOver.some(slot => !runningOnly || slot.status === 'running') && (
|
|
71
75
|
<section className={styles.block}>
|
|
72
76
|
<h2 className={styles.blockTitle}>{feature ? 'Other recorded results' : 'Recorded results'}</h2>
|
|
73
77
|
{feature && <p className={styles.blockLead}>Recorded against this file under a name no scenario has now.</p>}
|
|
74
78
|
<ul className={styles.results}>
|
|
75
|
-
<Bounded items={filed.leftOver} size={40} more="More results">
|
|
79
|
+
<Bounded items={filed.leftOver.filter(slot => !runningOnly || slot.status === 'running')} size={40} more="More results">
|
|
76
80
|
{(slot) => (
|
|
77
81
|
<ResultRow key={slot.key} label={feature ? scenarioLabel(feature.name, slot.title) : slot.title} slot={slot} open={selection.check === slot.id} onToggle={() => toggle(slot.id)}>
|
|
78
82
|
<CheckDetail slot={slot} layerId={selection.check === slot.id ? selection.layer : undefined} select={(patch) => select({ check: slot.id, ...patch })} reads="steps" />
|
|
@@ -8,6 +8,7 @@ import { Bounded } from './marks';
|
|
|
8
8
|
import { QUINT_GROUPS, quintOutline } from './quint';
|
|
9
9
|
import { SourceText, SpecHeader } from './spec-header';
|
|
10
10
|
import { SpecSnapshot } from './spec-snapshot';
|
|
11
|
+
import { TestFilter, useRunningFilter } from './test-filter';
|
|
11
12
|
|
|
12
13
|
type Props = { file: string; text: string; kind: SpecKind; entry: SpecEvidence | undefined; selection: CheckSelection; select: SelectCheck };
|
|
13
14
|
|
|
@@ -16,17 +17,20 @@ export function ModelPage({ file, text, kind, entry, selection, select }: Props)
|
|
|
16
17
|
const items = useMemo(() => quintOutline(text), [text]);
|
|
17
18
|
const moduleName = items.find((item) => item.kind === 'module')?.name;
|
|
18
19
|
const slots = useMemo(() => [...(entry?.slots ?? [])].sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true })), [entry]);
|
|
20
|
+
const runningOnly = useRunningFilter();
|
|
21
|
+
const visible = slots.filter(slot => !runningOnly || slot.status === 'running');
|
|
19
22
|
const toggle = (id: string) => select({ check: selection.check === id ? null : id, layer: null });
|
|
20
23
|
const label = (title: string) => (moduleName && title.startsWith(`${moduleName} · `) ? title.slice(moduleName.length + 3) : title);
|
|
21
24
|
return (
|
|
22
25
|
<article className={styles.page}>
|
|
23
26
|
<SpecHeader title={moduleName ?? file.split('/').at(-1)!} file={file} kind={kind} entry={entry} />
|
|
24
27
|
<SpecSnapshot title={moduleName ?? file.split('/').at(-1)!} text={text} kind={kind.one} />
|
|
25
|
-
{slots
|
|
28
|
+
<TestFilter slots={slots} />
|
|
29
|
+
{visible.length > 0 && (
|
|
26
30
|
<section className={styles.block}>
|
|
27
31
|
<h2 className={styles.blockTitle}>Traces</h2>
|
|
28
32
|
<ul className={styles.results}>
|
|
29
|
-
<Bounded items={
|
|
33
|
+
<Bounded items={visible} size={40} more="More traces">
|
|
30
34
|
{(slot) => (
|
|
31
35
|
<ResultRow key={slot.key} label={label(slot.title)} slot={slot} open={selection.check === slot.id} onToggle={() => toggle(slot.id)}>
|
|
32
36
|
<CheckDetail slot={slot} layerId={selection.check === slot.id ? selection.layer : undefined} select={(patch) => select({ check: slot.id, ...patch })} reads="trace" />
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router';
|
|
2
|
+
import { lazy, Suspense, useState } from 'react';
|
|
3
|
+
import { isStatechartFile } from '@crvouga/atlas/schema';
|
|
4
|
+
import { useEvidence } from './evidence-context';
|
|
5
|
+
import { CheckDetail, ResultRow, type CheckSelection } from './check-detail';
|
|
6
|
+
import { TestFilter, useRunningFilter } from './test-filter';
|
|
7
|
+
import { Bounded } from './marks';
|
|
8
|
+
import { SPEC_KINDS, kindOfFile } from '../spec-kind';
|
|
9
|
+
import { currentProject } from '../runtime';
|
|
10
|
+
import styles from './evidence.module.css';
|
|
11
|
+
const RecordingPage = lazy(() => import('./recording-page').then(module => ({ default: module.RecordingPage })));
|
|
12
|
+
|
|
13
|
+
/** A project can contain any test runner or spec notation; statecharts are optional. */
|
|
14
|
+
export function ProjectHome() {
|
|
15
|
+
const evidence = useEvidence();
|
|
16
|
+
const runningOnly = useRunningFilter();
|
|
17
|
+
const [selection, setSelection] = useState<CheckSelection>({});
|
|
18
|
+
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
|
19
|
+
const [recording, setRecording] = useState(false);
|
|
20
|
+
const [moment, setMoment] = useState<number | null>(null);
|
|
21
|
+
const kinds = SPEC_KINDS.map(kind => ({ kind, files: evidence.files.filter(kind.accepts) })).filter(entry => entry.files.length);
|
|
22
|
+
const checks = [...evidence.view.unassigned, ...[...evidence.view.specs].filter(([file]) => !kindOfFile(file) && !isStatechartFile(file)).flatMap(([, entry]) => entry.slots)];
|
|
23
|
+
const visible = checks.filter(slot => !runningOnly || slot.status === 'running');
|
|
24
|
+
return <article className={styles.page}>
|
|
25
|
+
<header className={styles.specHead}><h1 className={styles.title}>{currentProject()?.split('-').map(word => word[0]?.toUpperCase() + word.slice(1)).join(' ') || 'Project reports'}</h1></header>
|
|
26
|
+
{evidence.unavailable && <p role="alert" className={styles.notice}>{evidence.unavailable}</p>}
|
|
27
|
+
{kinds.length > 0 && <section className={styles.block}><h2 className={styles.blockTitle}>Specs</h2><ul className={styles.list}>{kinds.map(({ kind, files }) => <li key={kind.id}>
|
|
28
|
+
<Link className={styles.listRow} to="/specs/$kind" params={{ kind: kind.id }} search={{}}><strong>{kind.label}</strong><span>{files.length} {files.length === 1 ? kind.one : kind.many}</span></Link>
|
|
29
|
+
</li>)}</ul></section>}
|
|
30
|
+
{checks.length > 0 && <section className={styles.block}><h2 className={styles.blockTitle}>Tests</h2><TestFilter slots={checks} /><ul className={styles.results}>
|
|
31
|
+
<Bounded items={visible} size={40} more="More tests">{slot => <ResultRow key={slot.key} label={slot.title} slot={slot} open={selectedKey === slot.key} onToggle={() => { setSelectedKey(selectedKey === slot.key ? null : slot.key); setSelection({ check: slot.id }); setRecording(false); }}>
|
|
32
|
+
{recording ? <Suspense fallback={<p role="status">Loading the recording…</p>}><RecordingPage slot={slot} layerId={selection.layer} label={slot.title} back="Test report" moment={moment} onMoment={setMoment} onClose={() => setRecording(false)} /></Suspense>
|
|
33
|
+
: <CheckDetail slot={slot} layerId={selection.layer} reads="steps" select={patch => { setSelection({ check: patch.check ?? slot.id, layer: patch.layer ?? undefined }); if (patch.view === 'recording') { setRecording(true); setMoment(null); } }} />}
|
|
34
|
+
</ResultRow>}</Bounded>
|
|
35
|
+
</ul></section>}
|
|
36
|
+
{!kinds.length && !checks.length && <p className={styles.aside}>No reports yet. Publish tests and specs to this project to populate its report.</p>}
|
|
37
|
+
</article>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ProjectSpecIndex() {
|
|
41
|
+
const { files } = useEvidence();
|
|
42
|
+
return <nav className={styles.index} aria-label="Project specs"><h2>Specs</h2><ul>{SPEC_KINDS.filter(kind => files.some(kind.accepts)).map(kind => <li key={kind.id}>
|
|
43
|
+
<Link className={styles.indexRow} to="/specs/$kind" params={{ kind: kind.id }} search={{}}>{kind.label}</Link>
|
|
44
|
+
</li>)}</ul></nav>;
|
|
45
|
+
}
|
|
@@ -10,6 +10,7 @@ import styles from './evidence.module.css';
|
|
|
10
10
|
import { duration, statusWord, useNow } from './format';
|
|
11
11
|
import { Origin, StatusMark } from './marks';
|
|
12
12
|
import { Timeline } from './recording';
|
|
13
|
+
import { TestProgress, useTestProgress } from '../live/test-progress';
|
|
13
14
|
|
|
14
15
|
type Props = {
|
|
15
16
|
/** The check whose recording is open, once the combined report holds it. */
|
|
@@ -39,6 +40,7 @@ function needsStylesheet(report: RunReport | undefined): boolean {
|
|
|
39
40
|
|
|
40
41
|
/** One recorded test, opened in place of its spec. */
|
|
41
42
|
export function RecordingPage({ slot, layerId, label, back, moment, onMoment, onClose }: Props) {
|
|
43
|
+
const progress = useTestProgress(slot ?? null);
|
|
42
44
|
const { evidence, backends, pending, ready, unavailable } = useEvidence();
|
|
43
45
|
const now = useNow();
|
|
44
46
|
const history = useMemo(() => (slot ? checkHistory(evidence, slot.specPath, slot.id) : []), [evidence, slot]);
|
|
@@ -64,8 +66,9 @@ export function RecordingPage({ slot, layerId, label, back, moment, onMoment, on
|
|
|
64
66
|
</p>
|
|
65
67
|
)}
|
|
66
68
|
</header>
|
|
69
|
+
<TestProgress {...progress} />
|
|
67
70
|
{waiting && <p className={styles.aside} role="status">Loading the recording…</p>}
|
|
68
|
-
{!chosen && !waiting && <p className={styles.notice}>{unavailable ?? 'This result is not in the combined report any more. A newer run may have replaced it, or its source is switched off.'}</p>}
|
|
71
|
+
{!chosen && !waiting && !progress.tests.length && <p className={styles.notice}>{unavailable ?? 'This result is not in the combined report any more. A newer run may have replaced it, or its source is switched off.'}</p>}
|
|
69
72
|
{chosen && !paths && <p className={styles.notice}>This result has an outcome and no recorded timeline.</p>}
|
|
70
73
|
{report.isLoading && <p className={styles.aside} role="status">Loading the recording…</p>}
|
|
71
74
|
{report.isError && (
|
|
@@ -11,6 +11,7 @@ import { StatusMark, Tally } from './marks';
|
|
|
11
11
|
const FILTERS: readonly { readonly id: ResultFilter; readonly label: string; readonly keeps: (summary: SpecSummary) => boolean }[] = [
|
|
12
12
|
{ id: 'all', label: 'All', keeps: () => true },
|
|
13
13
|
{ id: 'results', label: 'Results', keeps: (summary) => summary.status !== 'none' },
|
|
14
|
+
{ id: 'running', label: 'Running', keeps: (summary) => summary.counts.running > 0 },
|
|
14
15
|
{ id: 'failed', label: 'Failing', keeps: (summary) => summary.counts.failed > 0 },
|
|
15
16
|
{ id: 'none', label: 'None', keeps: (summary) => summary.status === 'none' }
|
|
16
17
|
];
|
|
@@ -47,7 +48,7 @@ export function SpecIndex({ kind }: { kind: SpecKind }) {
|
|
|
47
48
|
{evidence.filesError && <p className={styles.indexNote}>The file list could not be loaded.</p>}
|
|
48
49
|
{!evidence.filesPending && !evidence.filesError && visible.length === 0 && (
|
|
49
50
|
<p className={styles.indexNote}>
|
|
50
|
-
{rows.length === 0 ? `No ${kind.many} in this spec set.` : filter === 'failed' ? `No failing ${kind.many}.` : filter === 'results' ? `No ${kind.many} with results yet.` : `Every ${kind.one} has results.`}
|
|
51
|
+
{rows.length === 0 ? `No ${kind.many} in this spec set.` : filter === 'running' ? `No running ${kind.many}.` : filter === 'failed' ? `No failing ${kind.many}.` : filter === 'results' ? `No ${kind.many} with results yet.` : `Every ${kind.one} has results.`}
|
|
51
52
|
</p>
|
|
52
53
|
)}
|
|
53
54
|
{groups.map(([folder, entries]) => (
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query';
|
|
2
2
|
import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
|
|
3
|
+
import { lazy, Suspense } from 'react';
|
|
3
4
|
|
|
4
5
|
import { keys, specKey, useAdapter } from '../data/queries';
|
|
5
6
|
import { outlinePath, specFile, specKind } from '../spec-kind';
|
|
6
7
|
import type { SelectCheck } from './check-detail';
|
|
7
|
-
import { DocumentPage } from './document-page';
|
|
8
8
|
import { useEvidence } from './evidence-context';
|
|
9
9
|
import styles from './evidence.module.css';
|
|
10
|
-
import { FeaturePage } from './feature-page';
|
|
11
|
-
import { ModelPage } from './model-page';
|
|
12
10
|
import { Overview } from './overview';
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
// Each kind of spec has a page with a parser of its own (Gherkin, Quint, DMN); a page loads only the one it shows.
|
|
13
|
+
const DocumentPage = lazy(() => import('./document-page').then((m) => ({ default: m.DocumentPage })));
|
|
14
|
+
const FeaturePage = lazy(() => import('./feature-page').then((m) => ({ default: m.FeaturePage })));
|
|
15
|
+
const ModelPage = lazy(() => import('./model-page').then((m) => ({ default: m.ModelPage })));
|
|
16
|
+
const RecordingPage = lazy(() => import('./recording-page').then((m) => ({ default: m.RecordingPage })));
|
|
17
|
+
|
|
18
|
+
function Loading({ children }: { children: React.ReactNode }) {
|
|
19
|
+
return <Suspense fallback={<article className={styles.page}><p className={styles.aside} role="status">Loading…</p></article>}>{children}</Suspense>;
|
|
20
|
+
}
|
|
14
21
|
|
|
15
22
|
/** One spec tab: its overview, one of its documents, or a recording opened from that document. */
|
|
16
23
|
export function SpecPage() {
|
|
@@ -63,15 +70,17 @@ export function SpecPage() {
|
|
|
63
70
|
const spec = `${evidence.titleOf(file)}: `;
|
|
64
71
|
const label = slot ? (slot.title.startsWith(spec) ? slot.title.slice(spec.length) : slot.title) : 'Recorded test';
|
|
65
72
|
return (
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
<Loading>
|
|
74
|
+
<RecordingPage
|
|
75
|
+
slot={slot}
|
|
76
|
+
layerId={search.layer}
|
|
77
|
+
label={label}
|
|
78
|
+
back={evidence.titleOf(file)}
|
|
79
|
+
moment={search.moment ?? null}
|
|
80
|
+
onMoment={(moment, replace) => void navigate({ search: (prev) => ({ ...prev, moment }), replace })}
|
|
81
|
+
onClose={() => select({ view: null })}
|
|
82
|
+
/>
|
|
83
|
+
</Loading>
|
|
75
84
|
);
|
|
76
85
|
}
|
|
77
86
|
if (text.isPending) return <article className={styles.page}><p className={styles.aside} role="status">Loading the {kind.one}…</p></article>;
|
|
@@ -86,7 +95,5 @@ export function SpecPage() {
|
|
|
86
95
|
);
|
|
87
96
|
}
|
|
88
97
|
const page = { file, text: text.data, kind, entry, selection: { check: search.check, layer: search.layer }, select };
|
|
89
|
-
|
|
90
|
-
if (kind.id === 'quint') return <ModelPage {...page} />;
|
|
91
|
-
return <DocumentPage {...page} />;
|
|
98
|
+
return <Loading>{kind.id === 'gherkin' ? <FeaturePage {...page} /> : kind.id === 'quint' ? <ModelPage {...page} /> : <DocumentPage {...page} />}</Loading>;
|
|
92
99
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CheckSlot } from '@crvouga/atlas/reports/evidence-view';
|
|
2
|
+
import { useUiStore } from '../state/ui-store';
|
|
3
|
+
import styles from './evidence.module.css';
|
|
4
|
+
|
|
5
|
+
/** Filter actual test rows as well as the spec index. Counts always describe the whole document. */
|
|
6
|
+
export function TestFilter({ slots }: { slots: readonly CheckSlot[] }) {
|
|
7
|
+
const filter = useUiStore(s => s.resultFilter);
|
|
8
|
+
const setFilter = useUiStore(s => s.setResultFilter);
|
|
9
|
+
const running = slots.filter(slot => slot.status === 'running').length;
|
|
10
|
+
return <>
|
|
11
|
+
<div className={styles.filters} role="group" aria-label="Test status">
|
|
12
|
+
<button type="button" aria-pressed={filter !== 'running'} onClick={() => setFilter('all')}>All tests <span>{slots.length}</span></button>
|
|
13
|
+
<button type="button" aria-pressed={filter === 'running'} onClick={() => setFilter('running')}>Running <span>{running}</span></button>
|
|
14
|
+
</div>
|
|
15
|
+
{filter === 'running' && running === 0 && <p className={styles.aside} role="status">No running tests.</p>}
|
|
16
|
+
</>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useRunningFilter() {
|
|
20
|
+
return useUiStore(s => s.resultFilter === 'running');
|
|
21
|
+
}
|
|
@@ -9,7 +9,7 @@ import { StatusMark } from '../evidence/marks';
|
|
|
9
9
|
import { useEvidence } from '../evidence/evidence-context';
|
|
10
10
|
import { kindOfFile } from '../spec-kind';
|
|
11
11
|
import { corpusFrom } from './corpus';
|
|
12
|
-
import { setFinderOpen, setHelpOpen, toggleFinder, useFinderOpen, useHelpOpen, isHelpOpen } from './open';
|
|
12
|
+
import { commandKey, setFinderOpen, setHelpOpen, toggleFinder, useFinderOpen, useHelpOpen, isHelpOpen } from './open';
|
|
13
13
|
import { FIND_GROUPS, indexCorpus, notationOf, rankIndex, splitQuery, type FindDoc, type FindKind, type FindTarget, type Ranked } from './rank';
|
|
14
14
|
import { applySemantic, createWorkerEmbedder, embeddingsAvailable, embedText, textHash, type Embedder } from './semantic';
|
|
15
15
|
import styles from './Finder.module.css';
|
|
@@ -36,10 +36,6 @@ const SHORTCUTS: readonly { readonly keys: string; readonly does: string }[] = [
|
|
|
36
36
|
{ keys: 'Home End', does: 'Start and end of a recording' }
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
-
function commandKey() {
|
|
40
|
-
return typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent) ? '⌘K' : 'Ctrl+K';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
39
|
function isTypingTarget(target: EventTarget | null) {
|
|
44
40
|
if (!(target instanceof HTMLElement)) return false;
|
|
45
41
|
const tag = target.tagName;
|
|
@@ -299,5 +295,3 @@ function Row({ hit }: { hit: Ranked }) {
|
|
|
299
295
|
</>
|
|
300
296
|
);
|
|
301
297
|
}
|
|
302
|
-
|
|
303
|
-
export { commandKey };
|
|
@@ -49,3 +49,7 @@ export function useFinderOpen() {
|
|
|
49
49
|
export function useHelpOpen() {
|
|
50
50
|
return useSyncExternalStore(subscribeFinder, isHelpOpen, () => false);
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export function commandKey() {
|
|
54
|
+
return typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent) ? '⌘K' : 'Ctrl+K';
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { QueryClient, QueryKey } from '@tanstack/react-query';
|
|
2
|
+
import { throttled } from './live-stream';
|
|
3
|
+
|
|
4
|
+
/** Coalesce bursts without cancelling useful work; an event during a fetch guarantees one trailing read. */
|
|
5
|
+
export function liveInvalidator(client: QueryClient, queryKey: QueryKey, ms = 150) {
|
|
6
|
+
let dirty = false;
|
|
7
|
+
let reading = false;
|
|
8
|
+
let stopped = false;
|
|
9
|
+
const drain = throttled(() => {
|
|
10
|
+
if (stopped || reading || !dirty) return;
|
|
11
|
+
dirty = false;
|
|
12
|
+
reading = true;
|
|
13
|
+
void client.invalidateQueries({ queryKey }, { cancelRefetch: false }).finally(() => {
|
|
14
|
+
reading = false;
|
|
15
|
+
if (dirty && !stopped) drain();
|
|
16
|
+
});
|
|
17
|
+
}, ms);
|
|
18
|
+
const refresh = () => { if (!stopped) { dirty = true; drain(); } };
|
|
19
|
+
refresh.cancel = () => { stopped = true; dirty = false; drain.cancel(); };
|
|
20
|
+
return refresh;
|
|
21
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react';
|
|
2
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import type { LiveRun } from '@crvouga/atlas/schema';
|
|
4
|
+
import { runtimeConfig, runtimeUrl } from '../runtime';
|
|
5
|
+
import { eventsAddress, listenLive, useLiveStatus } from './live-stream';
|
|
6
|
+
import { liveInvalidator } from './live-query';
|
|
7
|
+
import type { LiveCheck } from '@crvouga/atlas/schema';
|
|
8
|
+
import { useUiStore } from '../state/ui-store';
|
|
9
|
+
import { indexLiveTests } from './report-progress';
|
|
10
|
+
|
|
11
|
+
export type LiveTest = { run: LiveRun; check: LiveCheck };
|
|
12
|
+
type LiveState = { tests: readonly LiveTest[]; index: ReturnType<typeof indexLiveTests>; error: string | null };
|
|
13
|
+
const EMPTY: LiveState = { tests: [], index: new Map(), error: null };
|
|
14
|
+
const LiveContext = createContext<LiveState>(EMPTY);
|
|
15
|
+
|
|
16
|
+
/** One subscription shared by report rows; leases never rewrite historical report results. */
|
|
17
|
+
export function LiveRunsProvider({ children, enabled = true }: { children: ReactNode; enabled?: boolean }) {
|
|
18
|
+
const client = useQueryClient();
|
|
19
|
+
const streaming = useLiveStatus() === 'live';
|
|
20
|
+
const excluded = useUiStore(s => s.excluded);
|
|
21
|
+
const api = runtimeUrl(runtimeConfig()?.api);
|
|
22
|
+
const url = api && enabled ? `${api.replace(/\/$/, '')}/live` : undefined;
|
|
23
|
+
const key = ['live-runs', url] as const;
|
|
24
|
+
const query = useQuery({ queryKey: key, enabled: !!url, retry: false, staleTime: 0, refetchOnWindowFocus: 'always', refetchOnReconnect: 'always',
|
|
25
|
+
queryFn: async ({ signal }) => {
|
|
26
|
+
const response = await fetch(url!, { signal, cache: 'no-store' });
|
|
27
|
+
if (!response.ok) throw new Error(`Live feed unavailable (${response.status}). Last received results may be stale.`);
|
|
28
|
+
const fetched = (await response.json()) as { runs: LiveRun[] };
|
|
29
|
+
// An event can arrive while this HTTP snapshot is in flight. Keep its newer progress,
|
|
30
|
+
// while still loading the other runs from the snapshot rather than cancelling it.
|
|
31
|
+
const held = client.getQueryData<{ runs: LiveRun[] }>(key);
|
|
32
|
+
const runs = new Map(fetched.runs.map(run => [run.id, run]));
|
|
33
|
+
for (const run of held?.runs ?? []) {
|
|
34
|
+
const read = runs.get(run.id);
|
|
35
|
+
if (!read || run.sequence > read.sequence || (run.sequence === read.sequence &&
|
|
36
|
+
((run.status !== 'running' && read.status === 'running') || run.lastHeartbeatAtMs > read.lastHeartbeatAtMs))) runs.set(run.id, run);
|
|
37
|
+
}
|
|
38
|
+
return { runs: [...runs.values()].sort((a, b) => b.startedAtMs - a.startedAtMs || a.id.localeCompare(b.id)).slice(0, 50) };
|
|
39
|
+
}, refetchInterval: streaming ? 60_000 : 2000, refetchIntervalInBackground: true });
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const address = eventsAddress();
|
|
42
|
+
if (!address || !url) return;
|
|
43
|
+
const refresh = liveInvalidator(client, ['live-runs', url]);
|
|
44
|
+
const stop = listenLive(address, event => {
|
|
45
|
+
if (event.type === 'run') {
|
|
46
|
+
client.setQueryData<{ runs: LiveRun[] }>(['live-runs', url], held => ({ runs: [event.run, ...(held?.runs ?? []).filter(run => run.id !== event.run.id)]
|
|
47
|
+
.sort((a, b) => b.startedAtMs - a.startedAtMs || a.id.localeCompare(b.id)).slice(0, 50) }));
|
|
48
|
+
}
|
|
49
|
+
if (event.type === 'open' || (event.type === 'change' && event.change.scope === 'runs' && !event.change.liveRunId)) refresh();
|
|
50
|
+
});
|
|
51
|
+
return () => { stop(); refresh.cancel(); };
|
|
52
|
+
}, [client, url]);
|
|
53
|
+
const value = useMemo(() => {
|
|
54
|
+
const tests: LiveTest[] = [];
|
|
55
|
+
for (const run of [...(query.data?.runs ?? [])].sort((a, b) => b.startedAtMs - a.startedAtMs)) {
|
|
56
|
+
if (excluded.branches.includes(run.branch ?? 'Unknown branch') || excluded.versions.includes(run.sha ?? 'unknown') ||
|
|
57
|
+
excluded.environments.includes(run.environment || 'unspecified') || excluded.backends.length > 0) continue;
|
|
58
|
+
const checks = new Map([...run.recent, ...run.current].map(check => [check.id, check]));
|
|
59
|
+
for (const check of checks.values()) tests.push({ run, check: run.status !== 'running' && check.status === 'running'
|
|
60
|
+
? { ...check, status: 'not-run', detail: run.reason ?? 'Run ended before this test completed.' } : check });
|
|
61
|
+
}
|
|
62
|
+
return { tests, index: indexLiveTests(tests), error: query.error?.message ?? null };
|
|
63
|
+
}, [query.data, query.error, excluded]);
|
|
64
|
+
return <LiveContext.Provider value={url ? value : EMPTY}>{children}</LiveContext.Provider>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useLiveTests() {
|
|
68
|
+
return useContext(LiveContext);
|
|
69
|
+
}
|