@crvouga/atlas 1.18.0 → 1.19.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 +8 -1
- package/dist/reports/src/evidence-detail.js +13 -3
- package/dist/reports/src/evidence-types.d.ts +4 -0
- package/dist/schema/src/api.d.ts +50 -0
- package/dist/schema/src/api.js +2 -0
- package/docs/reference/reports.md +4 -0
- package/docs/rich-evidence.md +1 -0
- package/docs/ui.md +40 -1
- package/llms-full.txt +41 -1
- package/package.json +1 -1
- package/packages/schema/schemas/effective-delta.schema.json +18 -0
- package/packages/schema/schemas/effective-evidence.schema.json +18 -0
- package/packages/schema/schemas/evidence-page.schema.json +9 -0
- package/packages/schema/schemas/layer-rows.schema.json +9 -0
- package/packages/schema/schemas/report-run.schema.json +9 -0
- package/packages/schema/schemas/review-collection.schema.json +9 -0
- package/packages/schema/schemas/review-manifest.schema.json +9 -0
- package/packages/schema/schemas/test-catalog.schema.json +9 -0
- package/packages/schema/schemas/test-history.schema.json +18 -0
- package/packages/server/openapi.json +126 -0
- package/packages/visualizer/src/app/AppShell.tsx +2 -2
- package/packages/visualizer/src/app/router.tsx +25 -1
- package/packages/visualizer/src/components/Header.tsx +15 -4
- package/packages/visualizer/src/evidence/behavior-index.test.ts +119 -0
- package/packages/visualizer/src/evidence/behavior-index.ts +162 -0
- package/packages/visualizer/src/evidence/behavior-panel.tsx +36 -22
- package/packages/visualizer/src/evidence/behavior-return.test.ts +41 -0
- package/packages/visualizer/src/evidence/behavior-return.ts +59 -0
- package/packages/visualizer/src/evidence/behavior.ts +32 -1
- package/packages/visualizer/src/evidence/behaviors-page.tsx +465 -0
- package/packages/visualizer/src/evidence/feature-page.tsx +5 -26
- package/packages/visualizer/src/evidence/scoped-summary-view.tsx +110 -0
- package/packages/visualizer/src/evidence/scoped-summary.test.ts +62 -0
- package/packages/visualizer/src/evidence/scoped-summary.ts +118 -0
- package/packages/visualizer/src/evidence/spec-texts.ts +88 -0
- package/packages/visualizer/src/explorer/ArtifactsView.tsx +4 -4
- package/packages/visualizer/src/explorer/Explorer.tsx +1 -1
- package/packages/visualizer/src/explorer/RunsView.tsx +4 -4
- package/packages/visualizer/src/explorer/collections.tsx +5 -5
- package/packages/visualizer/src/explorer/navigate.ts +17 -0
- package/packages/visualizer/src/explorer/playlist-entries.test.ts +10 -1
- package/packages/visualizer/src/explorer/playlist-entries.ts +10 -0
- package/packages/visualizer/src/explorer/playlist.tsx +43 -8
- package/packages/visualizer/src/explorer/search.ts +7 -1
- package/packages/visualizer/src/find/Finder.tsx +1 -62
|
@@ -225,6 +225,15 @@
|
|
|
225
225
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
226
226
|
"type": "string"
|
|
227
227
|
},
|
|
228
|
+
"durationMs": {
|
|
229
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
230
|
+
"type": "number",
|
|
231
|
+
"minimum": 0
|
|
232
|
+
},
|
|
233
|
+
"poster": {
|
|
234
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
235
|
+
"type": "string"
|
|
236
|
+
},
|
|
228
237
|
"scope": {
|
|
229
238
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
230
239
|
"type": "object",
|
|
@@ -1007,6 +1007,15 @@
|
|
|
1007
1007
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
1008
1008
|
"type": "string"
|
|
1009
1009
|
},
|
|
1010
|
+
"durationMs": {
|
|
1011
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
1012
|
+
"type": "number",
|
|
1013
|
+
"minimum": 0
|
|
1014
|
+
},
|
|
1015
|
+
"poster": {
|
|
1016
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
1017
|
+
"type": "string"
|
|
1018
|
+
},
|
|
1010
1019
|
"scope": {
|
|
1011
1020
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
1012
1021
|
"type": "object",
|
|
@@ -301,6 +301,15 @@
|
|
|
301
301
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
302
302
|
"type": "string"
|
|
303
303
|
},
|
|
304
|
+
"durationMs": {
|
|
305
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
306
|
+
"type": "number",
|
|
307
|
+
"minimum": 0
|
|
308
|
+
},
|
|
309
|
+
"poster": {
|
|
310
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
311
|
+
"type": "string"
|
|
312
|
+
},
|
|
304
313
|
"scope": {
|
|
305
314
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
306
315
|
"type": "object",
|
|
@@ -976,6 +985,15 @@
|
|
|
976
985
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
977
986
|
"type": "string"
|
|
978
987
|
},
|
|
988
|
+
"durationMs": {
|
|
989
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
990
|
+
"type": "number",
|
|
991
|
+
"minimum": 0
|
|
992
|
+
},
|
|
993
|
+
"poster": {
|
|
994
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
995
|
+
"type": "string"
|
|
996
|
+
},
|
|
979
997
|
"scope": {
|
|
980
998
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
981
999
|
"type": "object",
|
|
@@ -10752,6 +10752,15 @@
|
|
|
10752
10752
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
10753
10753
|
"type": "string"
|
|
10754
10754
|
},
|
|
10755
|
+
"durationMs": {
|
|
10756
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
10757
|
+
"type": "number",
|
|
10758
|
+
"minimum": 0
|
|
10759
|
+
},
|
|
10760
|
+
"poster": {
|
|
10761
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
10762
|
+
"type": "string"
|
|
10763
|
+
},
|
|
10755
10764
|
"scope": {
|
|
10756
10765
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
10757
10766
|
"type": "object",
|
|
@@ -11235,6 +11244,15 @@
|
|
|
11235
11244
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
11236
11245
|
"type": "string"
|
|
11237
11246
|
},
|
|
11247
|
+
"durationMs": {
|
|
11248
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
11249
|
+
"type": "number",
|
|
11250
|
+
"minimum": 0
|
|
11251
|
+
},
|
|
11252
|
+
"poster": {
|
|
11253
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
11254
|
+
"type": "string"
|
|
11255
|
+
},
|
|
11238
11256
|
"scope": {
|
|
11239
11257
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
11240
11258
|
"type": "object",
|
|
@@ -11753,6 +11771,15 @@
|
|
|
11753
11771
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
11754
11772
|
"type": "string"
|
|
11755
11773
|
},
|
|
11774
|
+
"durationMs": {
|
|
11775
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
11776
|
+
"type": "number",
|
|
11777
|
+
"minimum": 0
|
|
11778
|
+
},
|
|
11779
|
+
"poster": {
|
|
11780
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
11781
|
+
"type": "string"
|
|
11782
|
+
},
|
|
11756
11783
|
"scope": {
|
|
11757
11784
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
11758
11785
|
"type": "object",
|
|
@@ -12236,6 +12263,15 @@
|
|
|
12236
12263
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
12237
12264
|
"type": "string"
|
|
12238
12265
|
},
|
|
12266
|
+
"durationMs": {
|
|
12267
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
12268
|
+
"type": "number",
|
|
12269
|
+
"minimum": 0
|
|
12270
|
+
},
|
|
12271
|
+
"poster": {
|
|
12272
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
12273
|
+
"type": "string"
|
|
12274
|
+
},
|
|
12239
12275
|
"scope": {
|
|
12240
12276
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
12241
12277
|
"type": "object",
|
|
@@ -15648,6 +15684,15 @@
|
|
|
15648
15684
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
15649
15685
|
"type": "string"
|
|
15650
15686
|
},
|
|
15687
|
+
"durationMs": {
|
|
15688
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
15689
|
+
"type": "number",
|
|
15690
|
+
"minimum": 0
|
|
15691
|
+
},
|
|
15692
|
+
"poster": {
|
|
15693
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
15694
|
+
"type": "string"
|
|
15695
|
+
},
|
|
15651
15696
|
"scope": {
|
|
15652
15697
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
15653
15698
|
"type": "object",
|
|
@@ -16146,6 +16191,15 @@
|
|
|
16146
16191
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
16147
16192
|
"type": "string"
|
|
16148
16193
|
},
|
|
16194
|
+
"durationMs": {
|
|
16195
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
16196
|
+
"type": "number",
|
|
16197
|
+
"minimum": 0
|
|
16198
|
+
},
|
|
16199
|
+
"poster": {
|
|
16200
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
16201
|
+
"type": "string"
|
|
16202
|
+
},
|
|
16149
16203
|
"scope": {
|
|
16150
16204
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
16151
16205
|
"type": "object",
|
|
@@ -18760,6 +18814,15 @@
|
|
|
18760
18814
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
18761
18815
|
"type": "string"
|
|
18762
18816
|
},
|
|
18817
|
+
"durationMs": {
|
|
18818
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
18819
|
+
"type": "number",
|
|
18820
|
+
"minimum": 0
|
|
18821
|
+
},
|
|
18822
|
+
"poster": {
|
|
18823
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
18824
|
+
"type": "string"
|
|
18825
|
+
},
|
|
18763
18826
|
"scope": {
|
|
18764
18827
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
18765
18828
|
"type": "object",
|
|
@@ -19246,6 +19309,15 @@
|
|
|
19246
19309
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
19247
19310
|
"type": "string"
|
|
19248
19311
|
},
|
|
19312
|
+
"durationMs": {
|
|
19313
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
19314
|
+
"type": "number",
|
|
19315
|
+
"minimum": 0
|
|
19316
|
+
},
|
|
19317
|
+
"poster": {
|
|
19318
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
19319
|
+
"type": "string"
|
|
19320
|
+
},
|
|
19249
19321
|
"scope": {
|
|
19250
19322
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
19251
19323
|
"type": "object",
|
|
@@ -19892,6 +19964,15 @@
|
|
|
19892
19964
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
19893
19965
|
"type": "string"
|
|
19894
19966
|
},
|
|
19967
|
+
"durationMs": {
|
|
19968
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
19969
|
+
"type": "number",
|
|
19970
|
+
"minimum": 0
|
|
19971
|
+
},
|
|
19972
|
+
"poster": {
|
|
19973
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
19974
|
+
"type": "string"
|
|
19975
|
+
},
|
|
19895
19976
|
"scope": {
|
|
19896
19977
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
19897
19978
|
"type": "object",
|
|
@@ -21151,6 +21232,15 @@
|
|
|
21151
21232
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
21152
21233
|
"type": "string"
|
|
21153
21234
|
},
|
|
21235
|
+
"durationMs": {
|
|
21236
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
21237
|
+
"type": "number",
|
|
21238
|
+
"minimum": 0
|
|
21239
|
+
},
|
|
21240
|
+
"poster": {
|
|
21241
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
21242
|
+
"type": "string"
|
|
21243
|
+
},
|
|
21154
21244
|
"scope": {
|
|
21155
21245
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
21156
21246
|
"type": "object",
|
|
@@ -22346,6 +22436,15 @@
|
|
|
22346
22436
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
22347
22437
|
"type": "string"
|
|
22348
22438
|
},
|
|
22439
|
+
"durationMs": {
|
|
22440
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
22441
|
+
"type": "number",
|
|
22442
|
+
"minimum": 0
|
|
22443
|
+
},
|
|
22444
|
+
"poster": {
|
|
22445
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
22446
|
+
"type": "string"
|
|
22447
|
+
},
|
|
22349
22448
|
"scope": {
|
|
22350
22449
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
22351
22450
|
"type": "object",
|
|
@@ -23021,6 +23120,15 @@
|
|
|
23021
23120
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
23022
23121
|
"type": "string"
|
|
23023
23122
|
},
|
|
23123
|
+
"durationMs": {
|
|
23124
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
23125
|
+
"type": "number",
|
|
23126
|
+
"minimum": 0
|
|
23127
|
+
},
|
|
23128
|
+
"poster": {
|
|
23129
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
23130
|
+
"type": "string"
|
|
23131
|
+
},
|
|
23024
23132
|
"scope": {
|
|
23025
23133
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
23026
23134
|
"type": "object",
|
|
@@ -24600,6 +24708,15 @@
|
|
|
24600
24708
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
24601
24709
|
"type": "string"
|
|
24602
24710
|
},
|
|
24711
|
+
"durationMs": {
|
|
24712
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
24713
|
+
"type": "number",
|
|
24714
|
+
"minimum": 0
|
|
24715
|
+
},
|
|
24716
|
+
"poster": {
|
|
24717
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
24718
|
+
"type": "string"
|
|
24719
|
+
},
|
|
24603
24720
|
"scope": {
|
|
24604
24721
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
24605
24722
|
"type": "object",
|
|
@@ -26136,6 +26253,15 @@
|
|
|
26136
26253
|
"description": "Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable",
|
|
26137
26254
|
"type": "string"
|
|
26138
26255
|
},
|
|
26256
|
+
"durationMs": {
|
|
26257
|
+
"description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
|
|
26258
|
+
"type": "number",
|
|
26259
|
+
"minimum": 0
|
|
26260
|
+
},
|
|
26261
|
+
"poster": {
|
|
26262
|
+
"description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
|
|
26263
|
+
"type": "string"
|
|
26264
|
+
},
|
|
26139
26265
|
"scope": {
|
|
26140
26266
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
26141
26267
|
"type": "object",
|
|
@@ -26,7 +26,7 @@ function Index() {
|
|
|
26
26
|
<UiCatalogFilters />
|
|
27
27
|
) : kind ? (
|
|
28
28
|
<SpecIndex kind={kind} />
|
|
29
|
-
) : pathname.startsWith('/specs/') ? (
|
|
29
|
+
) : pathname.startsWith('/specs/') || pathname === '/behaviors' ? (
|
|
30
30
|
<Box component="nav" aria-label="Specs" />
|
|
31
31
|
) : evidence.ready && !evidence.files.some(isStatechartFile) ? (
|
|
32
32
|
<ProjectIndex />
|
|
@@ -39,7 +39,7 @@ function Body() {
|
|
|
39
39
|
const { view, spec } = useAtlas();
|
|
40
40
|
const { pathname } = useLocation();
|
|
41
41
|
if (pathname === '/' && evidence.ready && !evidence.filesPending && !evidence.files.some(isStatechartFile)) return <Explorer />;
|
|
42
|
-
if (!pathname.startsWith('/specs/') && pathname !== '/catalog' && !view && spec.error)
|
|
42
|
+
if (!pathname.startsWith('/specs/') && pathname !== '/catalog' && pathname !== '/behaviors' && !view && spec.error)
|
|
43
43
|
return (
|
|
44
44
|
<Container py="xl">
|
|
45
45
|
<Alert color="red" title="The spec couldn’t be loaded">
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import { z } from 'zod/v4';
|
|
13
13
|
import { CatalogSearchFields } from '../catalog/search';
|
|
14
14
|
import { UiCatalogPage } from '../catalog/UiCatalogPage';
|
|
15
|
+
import { COVERAGE_ORDER } from '../evidence/behavior';
|
|
16
|
+
import { BehaviorsPage } from '../evidence/behaviors-page';
|
|
15
17
|
import { SpecPage } from '../evidence/spec-page';
|
|
16
18
|
import { ExplorerSearchFields } from '../explorer/search';
|
|
17
19
|
import { ProductPage } from '../views/ProductPage';
|
|
@@ -44,6 +46,21 @@ const SpecSearch = RootSearch.extend({
|
|
|
44
46
|
evidence: z.enum(['stale']).optional().catch(undefined)
|
|
45
47
|
});
|
|
46
48
|
export type SpecSearch = z.infer<typeof SpecSearch>;
|
|
49
|
+
/**
|
|
50
|
+
* The project-wide behavior index and its read-and-watch workspace: the search words, the coverage filter, the open behavior
|
|
51
|
+
* (`behavior`, an id from `evidence/behavior-index.ts`), whether it is expanded into the workspace, and the run and recorded
|
|
52
|
+
* moment shown. Scope and selection live here so that a link, a reload and the way back from the workspace all reproduce them.
|
|
53
|
+
*/
|
|
54
|
+
const BehaviorsSearch = RootSearch.extend({
|
|
55
|
+
q: z.string().min(1).optional().catch(undefined),
|
|
56
|
+
coverage: z.enum(COVERAGE_ORDER as [(typeof COVERAGE_ORDER)[number], ...(typeof COVERAGE_ORDER)[number][]]).optional().catch(undefined),
|
|
57
|
+
behavior: z.string().min(1).optional().catch(undefined),
|
|
58
|
+
workspace: z.preprocess((value) => (value === 'true' ? true : value), z.literal(true)).optional().catch(undefined),
|
|
59
|
+
layer: z.string().min(1).optional().catch(undefined),
|
|
60
|
+
clip: z.coerce.string().min(1).optional().catch(undefined),
|
|
61
|
+
clipAt: z.coerce.number().nonnegative().optional().catch(undefined)
|
|
62
|
+
});
|
|
63
|
+
export type BehaviorsSearch = z.infer<typeof BehaviorsSearch>;
|
|
47
64
|
/** A fresh route tree per router, so a test can run its own viewer beside the page's. */
|
|
48
65
|
function routeTree() {
|
|
49
66
|
const rootRoute = createRootRoute({
|
|
@@ -96,7 +113,14 @@ function routeTree() {
|
|
|
96
113
|
validateSearch: (search) => RootSearch.extend(CatalogSearchFields).parse(search),
|
|
97
114
|
component: UiCatalogPage
|
|
98
115
|
});
|
|
99
|
-
|
|
116
|
+
/** Every behavior of the project in one searchable list, and the dedicated workspace to read and watch one. */
|
|
117
|
+
const behaviorsRoute = createRoute({
|
|
118
|
+
getParentRoute: () => rootRoute,
|
|
119
|
+
path: '/behaviors',
|
|
120
|
+
validateSearch: (search) => BehaviorsSearch.parse(search),
|
|
121
|
+
component: BehaviorsPage
|
|
122
|
+
});
|
|
123
|
+
return rootRoute.addChildren([productRoute, chartRoute, specRoute, catalogRoute, behaviorsRoute]);
|
|
100
124
|
}
|
|
101
125
|
export function createAtlasRouter(history: RouterHistory) {
|
|
102
126
|
return createRouter({ routeTree: routeTree(), history, defaultPreload: false, scrollRestoration: true });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SpecLogo } from './SpecLogo';
|
|
2
|
-
import { IconDots, IconLayoutGrid } from '@tabler/icons-react';
|
|
2
|
+
import { IconDots, IconLayoutGrid, IconListSearch } from '@tabler/icons-react';
|
|
3
3
|
import { isUiKind } from '@crvouga/atlas/reports/ui-state';
|
|
4
4
|
import { isStatechartFile } from '@crvouga/atlas/schema';
|
|
5
5
|
import { ActionIcon, Anchor, Badge, Box, Button, Group, Menu, NativeSelect, Popover, Stack, Tooltip } from '@mantine/core';
|
|
@@ -139,6 +139,8 @@ export function Header() {
|
|
|
139
139
|
);
|
|
140
140
|
const project = currentProject();
|
|
141
141
|
const catalogOpen = pathname === '/catalog';
|
|
142
|
+
const behaviorsOpen = pathname === '/behaviors';
|
|
143
|
+
const hasFeatures = evidence.files.some(SPEC_KINDS[0]!.accepts);
|
|
142
144
|
// The catalog is offered once the report holds a state or a plan for one.
|
|
143
145
|
const hasCatalog = useMemo(() => evidence.evidence.some((layer) => layer.rows.some((row) => isUiKind(row.kind))), [evidence.evidence]);
|
|
144
146
|
return (
|
|
@@ -155,13 +157,13 @@ export function Header() {
|
|
|
155
157
|
leftSection={kind ? <SpecLogo kind={kind} /> : undefined}
|
|
156
158
|
rightSection={<ChevronDownIcon aria-hidden="true" size={12} />}
|
|
157
159
|
>
|
|
158
|
-
{catalogOpen ? 'UI catalog' : (kind?.label ?? (hasCharts ? 'Statecharts' : evidence.ready && !tabs.length ? 'Tests' : 'Specs'))}
|
|
160
|
+
{catalogOpen ? 'UI catalog' : behaviorsOpen ? 'Behaviors' : (kind?.label ?? (hasCharts ? 'Statecharts' : evidence.ready && !tabs.length ? 'Tests' : 'Specs'))}
|
|
159
161
|
</Button>
|
|
160
162
|
</Menu.Target>
|
|
161
163
|
<Menu.Dropdown>
|
|
162
164
|
<Box component="nav" aria-label="Spec type">
|
|
163
165
|
<Menu.Item
|
|
164
|
-
aria-current={pathname.startsWith('/specs/') || catalogOpen ? undefined : 'page'}
|
|
166
|
+
aria-current={pathname.startsWith('/specs/') || catalogOpen || behaviorsOpen ? undefined : 'page'}
|
|
165
167
|
renderRoot={(props) => <Link {...props} to="/" search={(prev) => runOnly(prev)} />}
|
|
166
168
|
>
|
|
167
169
|
{hasCharts || !evidence.ready ? 'Statecharts' : 'Tests'}
|
|
@@ -183,6 +185,15 @@ export function Header() {
|
|
|
183
185
|
{tab.kind.label}
|
|
184
186
|
</Menu.Item>
|
|
185
187
|
))}
|
|
188
|
+
{(hasFeatures || behaviorsOpen) && (
|
|
189
|
+
<Menu.Item
|
|
190
|
+
leftSection={<IconListSearch size={16} aria-hidden="true" />}
|
|
191
|
+
aria-current={behaviorsOpen ? 'page' : undefined}
|
|
192
|
+
renderRoot={(props) => <Link {...props} to="/behaviors" search={{}} />}
|
|
193
|
+
>
|
|
194
|
+
Behaviors
|
|
195
|
+
</Menu.Item>
|
|
196
|
+
)}
|
|
186
197
|
{(hasCatalog || catalogOpen) && (
|
|
187
198
|
<Menu.Item
|
|
188
199
|
leftSection={<IconLayoutGrid size={16} aria-hidden="true" />}
|
|
@@ -263,7 +274,7 @@ function SpecBreadcrumb() {
|
|
|
263
274
|
name: item.name,
|
|
264
275
|
running: (evidence.view.specs.get(outlinePath(item.file))?.summary.counts.running ?? 0) > 0
|
|
265
276
|
}));
|
|
266
|
-
if (!options.length || pathname === '/catalog') return null;
|
|
277
|
+
if (!options.length || pathname === '/catalog' || pathname === '/behaviors') return null;
|
|
267
278
|
return (
|
|
268
279
|
<NativeSelect
|
|
269
280
|
data-running={options.some((option) => option.running) || undefined}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { LayerHeader, LayerRow } from '@crvouga/atlas/reports/evidence-types';
|
|
2
|
+
import type { CheckSlot, EvidenceView } from '@crvouga/atlas/reports/evidence-view';
|
|
3
|
+
import type { EvidenceResult } from '@crvouga/atlas/reports/layer-merge';
|
|
4
|
+
import { expect, test } from 'bun:test';
|
|
5
|
+
|
|
6
|
+
import { passesFilters, parseSearch } from '../find/query';
|
|
7
|
+
import { behaviorDoc, indexBehaviors } from './behavior-index';
|
|
8
|
+
|
|
9
|
+
const PAY = `Feature: Pay an invoice
|
|
10
|
+
Rule: Cards
|
|
11
|
+
@smoke
|
|
12
|
+
Scenario: Pay by card
|
|
13
|
+
Given an invoice
|
|
14
|
+
When paying by card
|
|
15
|
+
Then the invoice is paid
|
|
16
|
+
|
|
17
|
+
Scenario Outline: Pay in a currency
|
|
18
|
+
When paying in "<code>"
|
|
19
|
+
Examples:
|
|
20
|
+
| code |
|
|
21
|
+
| EUR |
|
|
22
|
+
| USD |
|
|
23
|
+
`;
|
|
24
|
+
const LISTS = 'Feature: Share a list\n Scenario: Add an item\n When adding tea\n';
|
|
25
|
+
|
|
26
|
+
const header = (layer: string): LayerHeader => ({
|
|
27
|
+
id: layer, backend: 'b', producer: 'e2e', packageDir: null, recordedAtMs: 1, sha: null, branch: 'main', environment: 'desktop-web', runUrl: null, parameters: {},
|
|
28
|
+
recording: null, live: false, counts: { passed: 0, failed: 0, running: 0, 'not-run': 0 }, execution: layer
|
|
29
|
+
});
|
|
30
|
+
const VIDEO = { attachments: 1, steps: 0, annotations: 0, attempts: 0, links: 0, tags: [], labels: {}, roles: ['video'], videos: [{ name: 'a.webm', owner: 'check' as const, recording: 'sha256:1', available: true }] };
|
|
31
|
+
function slot(title: string, layer: string, row: Partial<LayerRow> = {}): CheckSlot {
|
|
32
|
+
const result: EvidenceResult = {
|
|
33
|
+
key: layer, slot: title, layer: header(layer), parameters: {}, warning: null,
|
|
34
|
+
row: { kind: 'behavior', id: title, title, status: 'passed', specPath: 'specs/billing/pay.feature', detail: '', measurements: [], testId: null, variant: {}, durationMs: null, ...row }
|
|
35
|
+
};
|
|
36
|
+
return { key: title, id: title, title, specPath: result.row.specPath, status: result.row.status, outcomes: [result], diagnostics: [], latestAtMs: 1, live: false };
|
|
37
|
+
}
|
|
38
|
+
const view = (slots: Record<string, CheckSlot[]>) => ({ specs: new Map(Object.entries(slots).map(([path, list]) => [path, { slots: list }])) }) as unknown as EvidenceView;
|
|
39
|
+
const specs = [
|
|
40
|
+
{ path: 'specs/billing/pay.feature', domain: 'billing', kind: 'gherkin', title: 'Pay', fingerprint: 'now' },
|
|
41
|
+
{ path: 'specs/lists/add.feature', domain: '', kind: 'gherkin', title: 'Lists' }
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
test('every feature\'s behaviors are listed once, an outline as one per example row and never as its template', () => {
|
|
45
|
+
const index = indexBehaviors({
|
|
46
|
+
files: ['billing/pay.feature', 'lists/add.feature'],
|
|
47
|
+
texts: new Map([['billing/pay.feature', PAY], ['lists/add.feature', LISTS]]),
|
|
48
|
+
specs,
|
|
49
|
+
view: view({ 'specs/billing/pay.feature': [slot('Pay an invoice: Pay by card', 'l1', { evidence: VIDEO })] }),
|
|
50
|
+
evidence: [{ layer: header('l1'), fingerprints: { 'specs/billing/pay.feature': 'now' } }]
|
|
51
|
+
});
|
|
52
|
+
expect(index.entries.map((entry) => entry.title)).toEqual(['Pay by card', 'Pay in a currency — EUR', 'Pay in a currency — USD', 'Add an item']);
|
|
53
|
+
expect(index.entries.map((entry) => entry.behavior.state)).toEqual(['demonstrated', 'unexecuted', 'unexecuted', 'unexecuted']);
|
|
54
|
+
expect(index.entries[0]).toMatchObject({ file: 'billing/pay.feature', feature: 'Pay an invoice', rule: 'Cards', domain: 'billing' });
|
|
55
|
+
// A domain nobody declared is not invented.
|
|
56
|
+
expect(index.entries[3]!.domain).toBe('');
|
|
57
|
+
expect(new Set(index.entries.map((entry) => entry.id)).size).toBe(4);
|
|
58
|
+
expect(index).toMatchObject({ features: 2, unread: 0, unreadable: [] });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a recording of an older revision is not demonstrated here either', () => {
|
|
62
|
+
const index = indexBehaviors({
|
|
63
|
+
files: ['billing/pay.feature'],
|
|
64
|
+
texts: new Map([['billing/pay.feature', PAY]]),
|
|
65
|
+
specs,
|
|
66
|
+
view: view({ 'specs/billing/pay.feature': [slot('Pay an invoice: Pay by card', 'l1', { evidence: VIDEO })] }),
|
|
67
|
+
evidence: [{ layer: header('l1'), fingerprints: { 'specs/billing/pay.feature': 'earlier' } }]
|
|
68
|
+
});
|
|
69
|
+
expect(index.entries[0]!.behavior.state).toBe('outdated-recording');
|
|
70
|
+
expect(index.entries[0]!.assessment.verdict).toBe('stale');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('features not read yet or not readable are counted and named, never silently absent', () => {
|
|
74
|
+
const index = indexBehaviors({
|
|
75
|
+
files: ['billing/pay.feature', 'lists/add.feature', 'broken.feature', 'gone.feature'],
|
|
76
|
+
texts: new Map([['billing/pay.feature', PAY], ['broken.feature', 'this is not gherkin'], ['gone.feature', '']]),
|
|
77
|
+
specs,
|
|
78
|
+
view: view({}),
|
|
79
|
+
evidence: []
|
|
80
|
+
});
|
|
81
|
+
expect(index.unread).toBe(1);
|
|
82
|
+
expect(index.unreadable).toEqual(['broken.feature', 'gone.feature']);
|
|
83
|
+
expect(index.features).toBe(4);
|
|
84
|
+
expect(index.entries).toHaveLength(3);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a result no scenario claims is still a behavior that was checked', () => {
|
|
88
|
+
const index = indexBehaviors({
|
|
89
|
+
files: ['lists/add.feature'],
|
|
90
|
+
texts: new Map([['lists/add.feature', LISTS]]),
|
|
91
|
+
specs,
|
|
92
|
+
view: view({ 'specs/lists/add.feature': [slot('Share a list: Renamed since', 'l9', { specPath: 'specs/lists/add.feature' })] }),
|
|
93
|
+
evidence: []
|
|
94
|
+
});
|
|
95
|
+
expect(index.entries.map((entry) => entry.title)).toEqual(['Add an item', 'Renamed since']);
|
|
96
|
+
expect(index.entries[1]!.behavior.state).toBe('no-recording');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('the search document carries wording, steps, domain and tags, and its facets feed the typed filters', () => {
|
|
100
|
+
const index = indexBehaviors({
|
|
101
|
+
files: ['billing/pay.feature'],
|
|
102
|
+
texts: new Map([['billing/pay.feature', PAY]]),
|
|
103
|
+
specs,
|
|
104
|
+
view: view({ 'specs/billing/pay.feature': [slot('Pay an invoice: Pay by card', 'l1', { evidence: VIDEO })] }),
|
|
105
|
+
evidence: []
|
|
106
|
+
});
|
|
107
|
+
const doc = behaviorDoc(index.entries[0]!);
|
|
108
|
+
expect(doc.fields.find((field) => field.name === 'steps')!.text).toContain('paying by card');
|
|
109
|
+
expect(doc.fields.find((field) => field.name === 'metadata')!.text).toContain('billing');
|
|
110
|
+
expect(doc.target).toMatchObject({ to: '/specs/$kind', kind: 'gherkin', search: { spec: 'billing/pay.feature' } });
|
|
111
|
+
const passes = (query: string) => passesFilters(doc.facets, parseSearch(query).filters);
|
|
112
|
+
expect(passes('has:video')).toBe(true);
|
|
113
|
+
expect(passes('status:passed tag:smoke domain:billing')).toBe(true);
|
|
114
|
+
expect(passes('status:failed')).toBe(false);
|
|
115
|
+
// A behavior nothing reported on has no status to match and no video.
|
|
116
|
+
const unreported = behaviorDoc(index.entries[1]!);
|
|
117
|
+
expect(passesFilters(unreported.facets, parseSearch('has:video').filters)).toBe(false);
|
|
118
|
+
expect(unreported.status).toBe('none');
|
|
119
|
+
});
|