@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,3 +1,4 @@
|
|
|
1
|
+
import { liveInvalidator } from '../live/live-query';
|
|
1
2
|
import type { DataIssue } from '@crvouga/atlas/schema';
|
|
2
3
|
import { useContext, useEffect, useMemo } from 'react';
|
|
3
4
|
import { isStatechartFile, RunsIndexSchema, SpecIndexSchema } from '@crvouga/atlas/schema';
|
|
@@ -71,28 +72,34 @@ export function useLiveUpdates(enabled = true) {
|
|
|
71
72
|
useEffect(() => {
|
|
72
73
|
if (!enabled) return;
|
|
73
74
|
const unsubscribe: (() => void)[] = [];
|
|
75
|
+
const refreshers = new Map<string, ReturnType<typeof liveInvalidator>>();
|
|
76
|
+
const refresh = (key: readonly string[]) => {
|
|
77
|
+
const id = JSON.stringify(key);
|
|
78
|
+
let reread = refreshers.get(id);
|
|
79
|
+
if (!reread) { reread = liveInvalidator(client, key); refreshers.set(id, reread); }
|
|
80
|
+
reread();
|
|
81
|
+
};
|
|
74
82
|
if (adapter.subscribe)
|
|
75
83
|
unsubscribe.push(
|
|
76
84
|
adapter.subscribe((change) => {
|
|
77
85
|
if (change.scope !== 'specs') return;
|
|
78
|
-
|
|
79
|
-
if (!change.files.length)
|
|
80
|
-
for (const file of change.files)
|
|
86
|
+
refresh(keys.specIndex(specKey(adapter)));
|
|
87
|
+
if (!change.files.length) refresh(['spec', specKey(adapter)]);
|
|
88
|
+
for (const file of change.files) refresh(keys.specFile(specKey(adapter), file));
|
|
81
89
|
})
|
|
82
90
|
);
|
|
83
91
|
for (const { backend, enabled: active } of entries) {
|
|
84
92
|
if (!active || !backend.subscribe) continue;
|
|
85
93
|
unsubscribe.push(
|
|
86
94
|
backend.subscribe((change) => {
|
|
87
|
-
if (change.scope !== 'runs') return;
|
|
88
|
-
|
|
89
|
-
if (!change.runIds.length) void client.invalidateQueries({ queryKey: keys.manifests(backend.cacheKey) });
|
|
90
|
-
for (const id of change.runIds) void client.invalidateQueries({ queryKey: keys.manifest(backend.cacheKey, id) });
|
|
95
|
+
if (change.scope !== 'runs' || change.liveRunId) return;
|
|
96
|
+
refresh(['reports', backend.cacheKey]);
|
|
91
97
|
})
|
|
92
98
|
);
|
|
93
99
|
}
|
|
94
100
|
return () => {
|
|
95
101
|
for (const stop of unsubscribe) stop();
|
|
102
|
+
for (const refresh of refreshers.values()) refresh.cancel();
|
|
96
103
|
};
|
|
97
104
|
}, [adapter, entries, client, enabled]);
|
|
98
105
|
}
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { evidenceFile } from '@crvouga/atlas/reports/export-evidence';
|
|
2
2
|
|
|
3
|
+
import { runtimeConfig } from '../runtime';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An Atlas server writes its address book into the page it serves (`atlas serve`, and `atlas dev` through
|
|
7
|
+
* Vite), so one build of the viewer works for every project. A build can instead be pointed at a reports
|
|
8
|
+
* API when it is made.
|
|
9
|
+
*/
|
|
10
|
+
const runtime = runtimeConfig;
|
|
11
|
+
|
|
3
12
|
/** A built site without a reports API reads the evidence exported beside it. */
|
|
4
|
-
export const staticEvidence = !import.meta.env.DEV && !import.meta.env.VITE_ATLAS_REPORTS_URL;
|
|
5
|
-
export const reportsBase = (import.meta.env.VITE_ATLAS_REPORTS_URL as string | undefined)?.replace(/\/$/, '') ?? (staticEvidence ? './data/evidence' : '/api/reports');
|
|
13
|
+
export const staticEvidence = !import.meta.env.DEV && !import.meta.env.VITE_ATLAS_REPORTS_URL && !runtime()?.reportsUrl;
|
|
14
|
+
export const reportsBase = (import.meta.env.VITE_ATLAS_REPORTS_URL as string | undefined)?.replace(/\/$/, '') ?? runtime()?.reportsUrl?.replace(/\/$/, '') ?? (staticEvidence ? './data/evidence' : '/api/reports');
|
|
15
|
+
|
|
16
|
+
/** Does the server answer `/evidence/effective`: the whole report in one request, without per-layer history? */
|
|
17
|
+
export const effectiveEvidence = () => runtime()?.effective === true;
|
|
6
18
|
|
|
7
19
|
export class ReportsError extends Error {
|
|
8
20
|
constructor(
|
|
@@ -21,7 +33,8 @@ function explain(status: number) {
|
|
|
21
33
|
|
|
22
34
|
/** A captured file's address in the browser. Pictures and video use it directly; fetches use the same address. */
|
|
23
35
|
export function reportAssetUrl(path: string): string {
|
|
24
|
-
|
|
36
|
+
const base = runtime()?.reportsUrl?.replace(/\/$/, '') ?? reportsBase;
|
|
37
|
+
return staticEvidence ? `${base}/${evidenceFile(path).split('/').map(encodeURIComponent).join('/')}` : `${base}${path}`;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
export async function reportResponse(path: string, signal?: AbortSignal) {
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
.panel {
|
|
2
|
+
display: grid;
|
|
3
|
+
gap: 14px;
|
|
4
|
+
margin: 12px 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.panel h4 {
|
|
8
|
+
margin: 0 0 6px;
|
|
9
|
+
font-size: 0.78rem;
|
|
10
|
+
font-weight: 600;
|
|
11
|
+
letter-spacing: 0.04em;
|
|
12
|
+
text-transform: uppercase;
|
|
13
|
+
color: var(--faint);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.chips {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-wrap: wrap;
|
|
19
|
+
gap: 6px;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
list-style: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.chip {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
gap: 4px;
|
|
28
|
+
padding: 2px 8px;
|
|
29
|
+
border-radius: var(--pill);
|
|
30
|
+
background: var(--unrun);
|
|
31
|
+
color: var(--ink-3);
|
|
32
|
+
font-size: 0.78rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.chip b {
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
color: var(--ink-2, inherit);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.links a {
|
|
41
|
+
font-size: 0.85rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.gallery {
|
|
45
|
+
display: grid;
|
|
46
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
47
|
+
gap: 12px;
|
|
48
|
+
margin: 0;
|
|
49
|
+
padding: 0;
|
|
50
|
+
list-style: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.figure {
|
|
54
|
+
display: grid;
|
|
55
|
+
gap: 4px;
|
|
56
|
+
margin: 0;
|
|
57
|
+
min-width: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.figure[data-wide] {
|
|
61
|
+
grid-column: 1 / -1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.figure img,
|
|
65
|
+
.figure video {
|
|
66
|
+
width: 100%;
|
|
67
|
+
max-height: 420px;
|
|
68
|
+
object-fit: contain;
|
|
69
|
+
border: 1px solid var(--line, #dfe3e8);
|
|
70
|
+
border-radius: 6px;
|
|
71
|
+
background: #f6f7f9;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.figure iframe {
|
|
75
|
+
width: 100%;
|
|
76
|
+
height: 360px;
|
|
77
|
+
border: 1px solid var(--line, #dfe3e8);
|
|
78
|
+
border-radius: 6px;
|
|
79
|
+
background: #fff;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.figure audio {
|
|
83
|
+
width: 100%;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.figure figcaption {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-wrap: wrap;
|
|
89
|
+
gap: 6px;
|
|
90
|
+
align-items: baseline;
|
|
91
|
+
font-size: 0.8rem;
|
|
92
|
+
color: var(--ink-3);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.figure figcaption strong {
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
overflow-wrap: anywhere;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.role {
|
|
101
|
+
padding: 0 6px;
|
|
102
|
+
border-radius: var(--pill);
|
|
103
|
+
background: var(--unrun);
|
|
104
|
+
font-size: 0.72rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.text {
|
|
108
|
+
max-height: 260px;
|
|
109
|
+
margin: 0;
|
|
110
|
+
padding: 8px 10px;
|
|
111
|
+
overflow: auto;
|
|
112
|
+
border: 1px solid var(--line, #dfe3e8);
|
|
113
|
+
border-radius: 6px;
|
|
114
|
+
background: #f6f7f9;
|
|
115
|
+
font: 0.78rem/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
116
|
+
white-space: pre-wrap;
|
|
117
|
+
overflow-wrap: anywhere;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.timeline,
|
|
121
|
+
.timeline ol {
|
|
122
|
+
display: grid;
|
|
123
|
+
gap: 4px;
|
|
124
|
+
margin: 0;
|
|
125
|
+
padding: 0;
|
|
126
|
+
list-style: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.timeline ol {
|
|
130
|
+
margin-left: 18px;
|
|
131
|
+
padding-left: 8px;
|
|
132
|
+
border-left: 1px solid var(--line, #dfe3e8);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.step {
|
|
136
|
+
display: grid;
|
|
137
|
+
gap: 4px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.stepRow {
|
|
141
|
+
display: flex;
|
|
142
|
+
gap: 8px;
|
|
143
|
+
align-items: center;
|
|
144
|
+
font-size: 0.9rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.stepRow .spacer {
|
|
148
|
+
flex: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.stepKind,
|
|
152
|
+
.stepTime {
|
|
153
|
+
font-size: 0.75rem;
|
|
154
|
+
color: var(--faint);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.step[data-status='failed'] > .stepRow .stepName {
|
|
158
|
+
color: var(--failed);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.stepDetail {
|
|
162
|
+
margin: 0 0 0 24px;
|
|
163
|
+
font-size: 0.82rem;
|
|
164
|
+
color: var(--ink-3);
|
|
165
|
+
white-space: pre-wrap;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.notes {
|
|
169
|
+
display: grid;
|
|
170
|
+
gap: 6px;
|
|
171
|
+
margin: 0;
|
|
172
|
+
padding: 0;
|
|
173
|
+
list-style: none;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.note {
|
|
177
|
+
display: flex;
|
|
178
|
+
gap: 8px;
|
|
179
|
+
align-items: baseline;
|
|
180
|
+
font-size: 0.88rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.level {
|
|
184
|
+
flex: none;
|
|
185
|
+
padding: 0 7px;
|
|
186
|
+
border-radius: var(--pill);
|
|
187
|
+
background: var(--unrun);
|
|
188
|
+
color: var(--ink-3);
|
|
189
|
+
font-size: 0.72rem;
|
|
190
|
+
text-transform: uppercase;
|
|
191
|
+
letter-spacing: 0.04em;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.level[data-level='warning'] {
|
|
195
|
+
background: #fff4d6;
|
|
196
|
+
color: #7a5200;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.level[data-level='error'] {
|
|
200
|
+
background: #fde4e4;
|
|
201
|
+
color: var(--failed);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.where {
|
|
205
|
+
color: var(--faint);
|
|
206
|
+
font-size: 0.78rem;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.raw summary {
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
font-size: 0.82rem;
|
|
212
|
+
color: var(--ink-3);
|
|
213
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { LayerHeader, LayerRow } from '@crvouga/atlas/reports/evidence-types';
|
|
2
|
+
import type { CheckDetail, EvidenceHolder } from '@crvouga/atlas/reports/evidence-detail';
|
|
3
|
+
import { roleOfType } from '@crvouga/atlas/reports/evidence-detail';
|
|
4
|
+
import { contentTypeOf } from '@crvouga/atlas/reports/media-types';
|
|
5
|
+
import type { Annotation, Attachment, Attempt, Step } from '@crvouga/atlas/schema';
|
|
6
|
+
import { useQuery } from '@tanstack/react-query';
|
|
7
|
+
import { useMemo, type ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
import { reportAssetUrl, reportJson, reportText } from './api';
|
|
10
|
+
import styles from './artifacts.module.css';
|
|
11
|
+
import { duration, plural } from './format';
|
|
12
|
+
import { StatusMark } from './marks';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How one kind of attachment is shown. The first renderer that matches wins, so a new kind of artifact (a
|
|
16
|
+
* flame graph, a HAR, a coverage map) is one more entry, and an attachment nobody has a renderer for is still
|
|
17
|
+
* listed, with its name, what it is and a way to download it. Nothing here fails on what it does not know.
|
|
18
|
+
*/
|
|
19
|
+
export type ArtifactRenderer = {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly matches: (attachment: Attachment, type: string) => boolean;
|
|
22
|
+
/** `url` is where the bytes are, if they are anywhere a browser can reach; `path` is the report API path of a stored file. */
|
|
23
|
+
readonly render: (attachment: Attachment, context: { url: string | null; path: string | null; type: string }) => ReactNode;
|
|
24
|
+
/** Wide renderers take a whole row of the gallery. */
|
|
25
|
+
readonly wide?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mediaType = (attachment: Attachment) => (attachment.contentType ?? contentTypeOf(attachment.name)).split(';')[0]!.trim().toLowerCase();
|
|
29
|
+
const label = (attachment: Attachment) => attachment.description ?? attachment.name;
|
|
30
|
+
|
|
31
|
+
/** Only addresses a browser may follow: never `javascript:` or `data:` from a producer. */
|
|
32
|
+
function safeUri(uri: string | undefined): string | null {
|
|
33
|
+
if (!uri) return null;
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(uri, 'https://atlas.invalid/');
|
|
36
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? uri : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function TextValue({ attachment, path }: { attachment: Attachment; path: string | null }) {
|
|
43
|
+
const stored = useQuery({ queryKey: ['artifact-text', path ?? ''], queryFn: ({ signal }) => reportText(path!, signal), enabled: attachment.text === undefined && path !== null, staleTime: Number.POSITIVE_INFINITY, retry: false });
|
|
44
|
+
const raw = attachment.text ?? stored.data;
|
|
45
|
+
if (raw === undefined) return <p className={styles.where}>{stored.isError ? 'Could not be read.' : 'Loading…'}</p>;
|
|
46
|
+
const shown = raw.length > 200_000 ? raw.slice(0, 200_000) : raw;
|
|
47
|
+
const pretty = /json/.test(mediaType(attachment)) ? tryPretty(shown) : shown;
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<pre className={styles.text}>{pretty}</pre>
|
|
51
|
+
{shown.length < raw.length && <p className={styles.where}>Showing the first 200 000 characters.</p>}
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tryPretty(text: string) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
59
|
+
} catch {
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** In order: the first that matches. Add to the front with {@link registerArtifactRenderer}. */
|
|
65
|
+
export const artifactRenderers: ArtifactRenderer[] = [
|
|
66
|
+
{ id: 'image', matches: (_, type) => type.startsWith('image/') && type !== 'image/svg+xml', render: (a, { url }) => (url ? <a href={url} target="_blank" rel="noreferrer"><img src={url} alt={label(a)} loading="lazy" width={a.width} height={a.height} /></a> : null) },
|
|
67
|
+
// SVG can carry script: shown as a picture only, never inlined.
|
|
68
|
+
{ id: 'svg', matches: (_, type) => type === 'image/svg+xml', render: (a, { url }) => (url ? <img src={url} alt={label(a)} loading="lazy" /> : null) },
|
|
69
|
+
{ id: 'video', wide: true, matches: (_, type) => type.startsWith('video/'), render: (a, { url }) => (url ? <video src={url} controls preload="metadata" aria-label={label(a)} /> : null) },
|
|
70
|
+
{ id: 'audio', matches: (_, type) => type.startsWith('audio/'), render: (a, { url }) => (url ? <audio src={url} controls preload="none" aria-label={label(a)} /> : null) },
|
|
71
|
+
// A page snapshot runs nothing: the frame has no permissions and the file is served sandboxed.
|
|
72
|
+
{ id: 'snapshot', wide: true, matches: (_, type) => type === 'text/html' || type === 'application/xhtml+xml', render: (a, { url }) => (url ? <iframe sandbox="" src={url} title={label(a)} loading="lazy" /> : null) },
|
|
73
|
+
{ id: 'pdf', wide: true, matches: (_, type) => type === 'application/pdf', render: (a, { url }) => (url ? <iframe sandbox="" src={url} title={label(a)} loading="lazy" /> : null) },
|
|
74
|
+
{ id: 'text', wide: true, matches: (a, type) => a.text !== undefined || type.startsWith('text/') || /(json|xml|yaml|x-ndjson|x-diff)/.test(type), render: (a, { path }) => <TextValue attachment={a} path={path} /> }
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export function registerArtifactRenderer(renderer: ArtifactRenderer) {
|
|
78
|
+
artifactRenderers.unshift(renderer);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sizeOf(bytes: number | undefined) {
|
|
82
|
+
if (bytes === undefined) return '';
|
|
83
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
84
|
+
return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(0)} KiB` : `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Artifact({ attachment, layer }: { attachment: Attachment; layer: string }) {
|
|
88
|
+
const type = mediaType(attachment);
|
|
89
|
+
const path = attachment.bundleKey ? `/layers/${encodeURIComponent(layer)}/blobs/${attachment.bundleKey.split('/').map(encodeURIComponent).join('/')}` : null;
|
|
90
|
+
const url = path ? reportAssetUrl(path) : safeUri(attachment.uri);
|
|
91
|
+
const renderer = artifactRenderers.find((candidate) => candidate.matches(attachment, type));
|
|
92
|
+
const body = renderer?.render(attachment, { url, path, type });
|
|
93
|
+
return (
|
|
94
|
+
<li className={styles.figure} data-wide={renderer?.wide || undefined} data-role={attachment.role ?? roleOfType(type)}>
|
|
95
|
+
{body}
|
|
96
|
+
<figcaption>
|
|
97
|
+
<strong>{attachment.name}</strong>
|
|
98
|
+
{attachment.role && <span className={styles.role}>{attachment.role}</span>}
|
|
99
|
+
<span>{[type, sizeOf(attachment.bytes), attachment.durationMs ? duration(attachment.durationMs) : ''].filter(Boolean).join(' · ')}</span>
|
|
100
|
+
{url && <a href={url} download={attachment.name} target="_blank" rel="noreferrer">{body ? 'Download' : 'Open'}</a>}
|
|
101
|
+
{!url && attachment.text === undefined && <span>No file to show</span>}
|
|
102
|
+
</figcaption>
|
|
103
|
+
</li>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function Attachments({ attachments, layer }: { attachments: readonly Attachment[]; layer: string }) {
|
|
108
|
+
if (!attachments.length) return null;
|
|
109
|
+
return (
|
|
110
|
+
<section aria-label="Attachments">
|
|
111
|
+
<h4>{plural(attachments.length, 'attachment', 'attachments')}</h4>
|
|
112
|
+
<ul className={styles.gallery}>
|
|
113
|
+
{attachments.map((attachment, index) => <Artifact key={`${attachment.id ?? attachment.name}:${index}`} attachment={attachment} layer={layer} />)}
|
|
114
|
+
</ul>
|
|
115
|
+
</section>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const STEP_STATUS: Record<string, 'passed' | 'failed' | 'running' | 'not-run'> = { passed: 'passed', pass: 'passed', ok: 'passed', failed: 'failed', fail: 'failed', error: 'failed', running: 'running', skipped: 'not-run', 'not-run': 'not-run', 'not-reached': 'not-run', pending: 'not-run' };
|
|
120
|
+
|
|
121
|
+
/** The timeline: ordered by when each step started, nested by `parentId`. A step whose parent is missing is shown at the top. */
|
|
122
|
+
export function Timeline({ steps, layer }: { steps: readonly Step[]; layer: string }) {
|
|
123
|
+
const tree = useMemo(() => {
|
|
124
|
+
const ids = new Set(steps.flatMap((step) => (step.id ? [step.id] : [])));
|
|
125
|
+
const children = new Map<string | null, Step[]>();
|
|
126
|
+
for (const step of steps) {
|
|
127
|
+
const parent = step.parentId && ids.has(step.parentId) ? step.parentId : null;
|
|
128
|
+
children.set(parent, [...(children.get(parent) ?? []), step]);
|
|
129
|
+
}
|
|
130
|
+
for (const list of children.values()) list.sort((a, b) => (a.offsetMs ?? 0) - (b.offsetMs ?? 0));
|
|
131
|
+
return children;
|
|
132
|
+
}, [steps]);
|
|
133
|
+
const render = (parent: string | null): ReactNode => (
|
|
134
|
+
<ol>
|
|
135
|
+
{(tree.get(parent) ?? []).map((step, index) => {
|
|
136
|
+
const status = STEP_STATUS[(step.status ?? '').toLowerCase()];
|
|
137
|
+
return (
|
|
138
|
+
<li key={`${step.id ?? step.name}:${index}`} className={styles.step} data-status={status ?? step.status ?? 'none'}>
|
|
139
|
+
<div className={styles.stepRow}>
|
|
140
|
+
<StatusMark status={status ?? 'none'} size={14} />
|
|
141
|
+
<span className={styles.stepName}>{step.name}</span>
|
|
142
|
+
{step.kind && <span className={styles.stepKind}>{step.kind}</span>}
|
|
143
|
+
{step.status && !status && <span className={styles.stepKind}>{step.status}</span>}
|
|
144
|
+
<span className={styles.spacer} />
|
|
145
|
+
<span className={styles.stepTime}>{[step.offsetMs !== undefined ? `+${duration(step.offsetMs)}` : '', duration(step.durationMs)].filter(Boolean).join(' · ')}</span>
|
|
146
|
+
</div>
|
|
147
|
+
{step.detail && <p className={styles.stepDetail}>{step.detail}</p>}
|
|
148
|
+
{step.annotations && <Notes notes={step.annotations} />}
|
|
149
|
+
{step.attachments && <Attachments attachments={step.attachments} layer={layer} />}
|
|
150
|
+
{step.id && tree.has(step.id) && render(step.id)}
|
|
151
|
+
</li>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</ol>
|
|
155
|
+
);
|
|
156
|
+
return (
|
|
157
|
+
<section aria-label="Timeline" className={styles.timeline}>
|
|
158
|
+
<h4>{plural(steps.length, 'step', 'steps')}</h4>
|
|
159
|
+
{render(null)}
|
|
160
|
+
</section>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function Notes({ notes }: { notes: readonly Annotation[] }) {
|
|
165
|
+
if (!notes.length) return null;
|
|
166
|
+
return (
|
|
167
|
+
<ul className={styles.notes} aria-label="Annotations">
|
|
168
|
+
{notes.map((note, index) => (
|
|
169
|
+
<li key={index} className={styles.note}>
|
|
170
|
+
<span className={styles.level} data-level={note.level ?? 'info'}>{note.level && ['info', 'warning', 'error'].includes(note.level) ? note.level : note.type}</span>
|
|
171
|
+
<span>
|
|
172
|
+
{note.message}
|
|
173
|
+
{note.source?.file && <span className={styles.where}> {note.source.file}{note.source.line ? `:${note.source.line}` : ''}</span>}
|
|
174
|
+
{note.level && !['info', 'warning', 'error'].includes(note.level) && <span className={styles.where}> ({note.level})</span>}
|
|
175
|
+
</span>
|
|
176
|
+
</li>
|
|
177
|
+
))}
|
|
178
|
+
</ul>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function Tries({ attempts, layer }: { attempts: readonly Attempt[]; layer: string }) {
|
|
183
|
+
if (!attempts.length) return null;
|
|
184
|
+
return (
|
|
185
|
+
<section aria-label="Tries">
|
|
186
|
+
<h4>{plural(attempts.length, 'try', 'tries')}</h4>
|
|
187
|
+
<ol className={styles.timeline}>
|
|
188
|
+
{attempts.map((attempt) => (
|
|
189
|
+
<li key={attempt.index} className={styles.step}>
|
|
190
|
+
<div className={styles.stepRow}>
|
|
191
|
+
<StatusMark status={attempt.status} size={14} />
|
|
192
|
+
<span className={styles.stepName}>Try {attempt.index + 1}</span>
|
|
193
|
+
<span className={styles.spacer} />
|
|
194
|
+
<span className={styles.stepTime}>{duration(attempt.durationMs)}</span>
|
|
195
|
+
</div>
|
|
196
|
+
{attempt.detail && <p className={styles.stepDetail}>{attempt.detail}</p>}
|
|
197
|
+
{attempt.attachments && <Attachments attachments={attempt.attachments} layer={layer} />}
|
|
198
|
+
</li>
|
|
199
|
+
))}
|
|
200
|
+
</ol>
|
|
201
|
+
</section>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** What is not known: kept, and shown as it is, so nothing a tool recorded is out of reach. */
|
|
206
|
+
function Raw({ extensions }: { extensions: Readonly<Record<string, unknown>> }) {
|
|
207
|
+
const names = Object.keys(extensions);
|
|
208
|
+
if (!names.length) return null;
|
|
209
|
+
return (
|
|
210
|
+
<details className={styles.raw}>
|
|
211
|
+
<summary>{plural(names.length, 'extension', 'extensions')}: {names.join(', ')}</summary>
|
|
212
|
+
<pre className={styles.text}>{JSON.stringify(extensions, null, 2)}</pre>
|
|
213
|
+
</details>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Facts({ holder }: { holder: EvidenceHolder }) {
|
|
218
|
+
const labels = Object.entries(holder.labels ?? {});
|
|
219
|
+
const tags = holder.tags ?? [];
|
|
220
|
+
const links = (holder.links ?? []).flatMap((link) => { const url = safeUri(link.url); return url ? [{ ...link, url }] : []; });
|
|
221
|
+
if (!labels.length && !tags.length && !links.length) return null;
|
|
222
|
+
return (
|
|
223
|
+
<div>
|
|
224
|
+
<ul className={styles.chips} aria-label="Facts">
|
|
225
|
+
{tags.map((tag) => <li key={`t:${tag}`} className={styles.chip}>{tag}</li>)}
|
|
226
|
+
{labels.map(([name, value]) => <li key={`l:${name}`} className={styles.chip}>{name}<b>{value}</b></li>)}
|
|
227
|
+
{links.map((link) => <li key={`k:${link.url}`} className={styles.chip}><a href={link.url} target="_blank" rel="noreferrer">{link.name ?? link.rel ?? link.url}</a></li>)}
|
|
228
|
+
</ul>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function Evidence({ holder, layer, steps }: { holder: EvidenceHolder; layer: string; steps?: boolean }) {
|
|
234
|
+
return (
|
|
235
|
+
<>
|
|
236
|
+
<Facts holder={holder} />
|
|
237
|
+
{holder.annotations && <Notes notes={holder.annotations} />}
|
|
238
|
+
{steps && holder.steps && holder.steps.length > 0 && <Timeline steps={holder.steps} layer={layer} />}
|
|
239
|
+
{holder.attempts && holder.attempts.length > 1 && <Tries attempts={holder.attempts} layer={layer} />}
|
|
240
|
+
{holder.attachments && <Attachments attachments={holder.attachments} layer={layer} />}
|
|
241
|
+
{holder.extensions && <Raw extensions={holder.extensions} />}
|
|
242
|
+
</>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Everything one result carries beyond its outcome, read when it is opened, and what the run around it carried. */
|
|
247
|
+
export function ResultEvidence({ layer, row }: { layer: LayerHeader; row: LayerRow }) {
|
|
248
|
+
const wanted = row.evidence !== undefined || layer.evidence !== undefined;
|
|
249
|
+
const detail = useQuery({
|
|
250
|
+
queryKey: ['evidence-detail', layer.backend, layer.id, row.kind, row.id],
|
|
251
|
+
queryFn: ({ signal }) => reportJson<CheckDetail>(`/evidence/detail/${encodeURIComponent(layer.backend)}/${encodeURIComponent(layer.id)}?check=${encodeURIComponent(row.id)}&kind=${encodeURIComponent(row.kind)}`, signal),
|
|
252
|
+
enabled: wanted && /^[0-9a-f]{64}$/.test(layer.id),
|
|
253
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
254
|
+
retry: false
|
|
255
|
+
});
|
|
256
|
+
if (!wanted) return null;
|
|
257
|
+
if (detail.isLoading) return <p className={styles.where} role="status">Loading what this result carries…</p>;
|
|
258
|
+
if (detail.isError || !detail.data) return <p className={styles.where} role="alert">What this result carries could not be read.</p>;
|
|
259
|
+
const { check, run } = detail.data;
|
|
260
|
+
const hasRun = Object.values(run).some((value) => (Array.isArray(value) ? value.length > 0 : value && typeof value === 'object' ? Object.keys(value).length > 0 : false));
|
|
261
|
+
return (
|
|
262
|
+
<div className={styles.panel} aria-label="Evidence">
|
|
263
|
+
<Evidence holder={check} layer={layer.id} steps />
|
|
264
|
+
{hasRun && (
|
|
265
|
+
<section aria-label="From the run">
|
|
266
|
+
<h4>From the run</h4>
|
|
267
|
+
<Evidence holder={run} layer={layer.id} />
|
|
268
|
+
</section>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
@@ -8,6 +8,7 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
8
8
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
9
9
|
|
|
10
10
|
import { reportJson } from './api';
|
|
11
|
+
import { ResultEvidence } from './artifacts';
|
|
11
12
|
import { useEvidence } from './evidence-context';
|
|
12
13
|
import styles from './evidence.module.css';
|
|
13
14
|
import { ago, duration, plural, statusWord, useNow } from './format';
|
|
@@ -15,6 +16,7 @@ import type { FeatureExamples, FeatureStep } from './gherkin';
|
|
|
15
16
|
import { Bounded, Origin, StatusMark } from './marks';
|
|
16
17
|
import { ModelTrace } from './model-trace';
|
|
17
18
|
import { recordedModelStates } from './quint';
|
|
19
|
+
import { TestProgress, useTestProgress } from '../live/test-progress';
|
|
18
20
|
|
|
19
21
|
/** What the address says is open: a check, and optionally one particular layer's record of it. */
|
|
20
22
|
export type CheckSelection = { readonly check?: string; readonly layer?: string };
|
|
@@ -58,8 +60,11 @@ function outcomeName(outcome: EvidenceResult) {
|
|
|
58
60
|
|
|
59
61
|
export function ResultRow({ label, slot, open, onToggle, children }: { label: string; slot: CheckSlot | null; open: boolean; onToggle: () => void; children: ReactNode }) {
|
|
60
62
|
const now = useNow();
|
|
63
|
+
const progress = useTestProgress(slot);
|
|
64
|
+
const current = progress.tests.find(({ run, check }) => run.status === 'running' && check.status === 'running');
|
|
65
|
+
const interrupted = !current && progress.tests.find(({ run, check }) => run.status === 'interrupted' && check.status === 'not-run');
|
|
61
66
|
const newest = slot?.outcomes[0];
|
|
62
|
-
const marks = slot ? environmentMarks(slot) : [];
|
|
67
|
+
const marks = slot && !current ? environmentMarks(slot) : [];
|
|
63
68
|
const failure = slot?.status === 'failed' ? slot.outcomes.find((outcome) => outcome.row.status === 'failed')?.row.detail.split('\n')[0] : '';
|
|
64
69
|
return (
|
|
65
70
|
<li className={styles.result} data-status={slot?.status ?? 'none'} data-open={open || undefined}>
|
|
@@ -76,12 +81,15 @@ export function ResultRow({ label, slot, open, onToggle, children }: { label: st
|
|
|
76
81
|
)}
|
|
77
82
|
</span>
|
|
78
83
|
{slot?.live && <span className={styles.liveTag}>Running now</span>}
|
|
84
|
+
{interrupted && <span className={styles.liveTag}>Interrupted</span>}
|
|
79
85
|
<span className={styles.resultAside}>
|
|
80
|
-
{newest ? [duration(newest.row.durationMs), slot.live ? '' : ago(slot.latestAtMs, now)].filter(Boolean).join(' · ') : 'No result'}
|
|
86
|
+
{current ? duration(current.check.durationMs ?? null) || 'In progress' : interrupted ? 'No completed result' : newest ? [newest.row.evidence?.attachments ? plural(newest.row.evidence.attachments, 'attachment', 'attachments') : '', duration(newest.row.durationMs), slot.live ? '' : ago(slot.latestAtMs, now)].filter(Boolean).join(' · ') : 'No result'}
|
|
81
87
|
</span>
|
|
82
88
|
<span className={styles.chevron} aria-hidden="true" />
|
|
83
89
|
</button>
|
|
84
|
-
{!open &&
|
|
90
|
+
{!open && current && <p className={styles.excerpt} data-progress="true" role="status">{current.check.detail || current.check.steps?.at(-1)?.name || 'Test in progress…'}{progress.error ? ` · ${progress.error}` : ''}</p>}
|
|
91
|
+
{!open && interrupted && <p className={styles.excerpt} data-progress="true" role="status">{interrupted.run.reason ?? 'The test run was interrupted.'}</p>}
|
|
92
|
+
{!open && !current && failure && <p className={styles.excerpt}>{failure}</p>}
|
|
85
93
|
{open && <div className={styles.resultBody}>{children}</div>}
|
|
86
94
|
</li>
|
|
87
95
|
);
|
|
@@ -196,7 +204,15 @@ type Props = {
|
|
|
196
204
|
};
|
|
197
205
|
|
|
198
206
|
/** Everything recorded for one check: the chosen result, how it went, and every other run of it. */
|
|
199
|
-
export function CheckDetail(
|
|
207
|
+
export function CheckDetail(props: Props) {
|
|
208
|
+
const progress = useTestProgress(props.slot);
|
|
209
|
+
const inProgress = progress.tests.some(({ run, check }) => check.status === 'running' && run.status === 'running' || check.status === 'not-run' && run.status === 'interrupted');
|
|
210
|
+
return <><TestProgress {...progress} />{props.slot.outcomes.length > 0
|
|
211
|
+
? inProgress && !props.layerId ? <details className={styles.actions}><summary>Previous recorded result</summary><RecordedCheckDetail {...props} /></details> : <RecordedCheckDetail {...props} />
|
|
212
|
+
: props.written}</>;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function RecordedCheckDetail({ slot, layerId, select, reads, written }: Props) {
|
|
200
216
|
const { evidence, backends } = useEvidence();
|
|
201
217
|
const now = useNow();
|
|
202
218
|
const [variant, setVariant] = useState(0);
|
|
@@ -253,6 +269,7 @@ export function CheckDetail({ slot, layerId, select, reads, written }: Props) {
|
|
|
253
269
|
</p>
|
|
254
270
|
);
|
|
255
271
|
}))}
|
|
272
|
+
<ResultEvidence layer={chosen.layer} row={chosen.row} />
|
|
256
273
|
{(moments > 0 || chosen.layer.runUrl) && (
|
|
257
274
|
<p className={styles.actionsLine}>
|
|
258
275
|
{moments > 0 && (
|
|
@@ -7,23 +7,27 @@ import styles from './evidence.module.css';
|
|
|
7
7
|
import { Bounded } from './marks';
|
|
8
8
|
import { SourceText, SpecHeader } from './spec-header';
|
|
9
9
|
import { SpecSnapshot } from './spec-snapshot';
|
|
10
|
+
import { TestFilter, useRunningFilter } from './test-filter';
|
|
10
11
|
|
|
11
12
|
type Props = { file: string; text: string; kind: SpecKind; entry: SpecEvidence | undefined; selection: CheckSelection; select: SelectCheck };
|
|
12
13
|
|
|
13
14
|
/** A decision table, structure or objective: what it says, and each check recorded against it. */
|
|
14
15
|
export function DocumentPage({ file, text, kind, entry, selection, select }: Props) {
|
|
15
16
|
const slots = entry?.slots ?? [];
|
|
17
|
+
const runningOnly = useRunningFilter();
|
|
18
|
+
const visible = slots.filter(slot => !runningOnly || slot.status === 'running');
|
|
16
19
|
const toggle = (id: string) => select({ check: selection.check === id ? null : id, layer: null });
|
|
17
20
|
const name = file.split('/').at(-1)!.replace(/\.[^.]+$/, '').replace(/\.openslo$/, '');
|
|
18
21
|
return (
|
|
19
22
|
<article className={styles.page}>
|
|
20
23
|
<SpecHeader title={name} file={file} kind={kind} entry={entry} />
|
|
21
24
|
<SpecSnapshot title={name} text={text} kind={kind.one} />
|
|
22
|
-
{slots
|
|
25
|
+
<TestFilter slots={slots} />
|
|
26
|
+
{visible.length > 0 && (
|
|
23
27
|
<section className={styles.block}>
|
|
24
28
|
<h2 className={styles.blockTitle}>Checks</h2>
|
|
25
29
|
<ul className={styles.results}>
|
|
26
|
-
<Bounded items={
|
|
30
|
+
<Bounded items={visible} size={40} more="More checks">
|
|
27
31
|
{(slot) => (
|
|
28
32
|
<ResultRow key={slot.key} label={slot.title} slot={slot} open={selection.check === slot.id} onToggle={() => toggle(slot.id)}>
|
|
29
33
|
<CheckDetail slot={slot} layerId={selection.check === slot.id ? selection.layer : undefined} select={(patch) => select({ check: slot.id, ...patch })} reads="steps" />
|