@elench/testkit 0.1.112 → 0.1.114

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.
@@ -11,6 +11,7 @@ const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
11
11
  const CONFIG_ENTRY = path.join(PACKAGE_ROOT, "lib", "config-api", "index.mjs");
12
12
  const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
13
13
  const MANIFEST_FILE = "manifest.json";
14
+ const BUNDLE_WRAPPER_VERSION = "summary-check-artifacts-v2";
14
15
  const bundleCache = new Map();
15
16
 
16
17
  export async function bundleK6File({
@@ -105,7 +106,9 @@ async function buildCacheMetadata(sourceFile, configFile = null) {
105
106
  .update("\0")
106
107
  .update(source)
107
108
  .update("\0")
108
- .update(packageJsonText);
109
+ .update(packageJsonText)
110
+ .update("\0")
111
+ .update(BUNDLE_WRAPPER_VERSION);
109
112
  let configHash = null;
110
113
 
111
114
  if (configFile && fs.existsSync(configFile)) {
@@ -119,6 +122,7 @@ async function buildCacheMetadata(sourceFile, configFile = null) {
119
122
  cacheKey: hash.digest("hex"),
120
123
  sourceHash,
121
124
  configHash,
125
+ wrapperVersion: BUNDLE_WRAPPER_VERSION,
122
126
  testkitVersion: packageJson.version || null,
123
127
  };
124
128
  }
@@ -169,6 +173,96 @@ export function setup(...args) {
169
173
  export default function exec(...args) {
170
174
  return suite.exec(...args);
171
175
  }
176
+ export function handleSummary(data) {
177
+ const checksData = extractChecksFromSummary(data);
178
+ emitTestkitArtifact("checks", checksData, {
179
+ kind: "testkit.checks",
180
+ summary: checksData.summary
181
+ ? \`\${checksData.summary.passed}/\${checksData.summary.total} checks passed\`
182
+ : "no checks",
183
+ });
184
+ return {};
185
+ }
186
+
187
+ function extractChecksFromSummary(data) {
188
+ const checks = [];
189
+ collectChecksRecursive(data && data.root_group, [], checks);
190
+ const total = checks.length;
191
+ const passed = checks.filter((c) => c.passes > 0 && c.fails === 0).length;
192
+
193
+ const thresholds = {};
194
+ for (const [name, metric] of Object.entries((data && data.metrics) || {})) {
195
+ if (metric && metric.thresholds) {
196
+ thresholds[name] = Object.entries(metric.thresholds).map(([expr, result]) => ({
197
+ expression: expr,
198
+ ok: Boolean(result && result.ok),
199
+ }));
200
+ }
201
+ }
202
+
203
+ const http = {};
204
+ for (const key of ["http_reqs", "http_req_duration", "http_req_failed"]) {
205
+ if (data && data.metrics && data.metrics[key]) {
206
+ http[key] = data.metrics[key].values || {};
207
+ }
208
+ }
209
+
210
+ return { checks, summary: { total, passed, failed: total - passed }, thresholds, http };
211
+ }
212
+
213
+ function collectChecksRecursive(group, path, out) {
214
+ if (!group) return;
215
+ const currentPath = normalizeNonEmptyString(group.name) ? [...path, normalizeNonEmptyString(group.name)] : path;
216
+ for (const check of Object.values(group.checks || {})) {
217
+ if (!check) continue;
218
+ out.push({
219
+ name: normalizeNonEmptyString(check.name) || "unnamed",
220
+ path: currentPath,
221
+ passes: normalizeCount(check.passes),
222
+ fails: normalizeCount(check.fails),
223
+ });
224
+ }
225
+ for (const subGroup of Object.values(group.groups || {})) {
226
+ collectChecksRecursive(subGroup, currentPath, out);
227
+ }
228
+ }
229
+
230
+ const TESTKIT_ARTIFACT_MARKER = "TESTKIT_ARTIFACT:";
231
+
232
+ function emitTestkitArtifact(name, data, options = {}) {
233
+ const payload = encodeURIComponent(JSON.stringify({
234
+ name: normalizeArtifactName(name),
235
+ kind: normalizeOptionalString(options.kind),
236
+ summary: normalizeOptionalString(options.summary),
237
+ contentType: normalizeOptionalString(options.contentType) || "application/json",
238
+ data,
239
+ emittedAt: new Date().toISOString(),
240
+ }));
241
+ console.log(\`\${TESTKIT_ARTIFACT_MARKER}\${payload}\`);
242
+ }
243
+
244
+ function normalizeArtifactName(name) {
245
+ const normalized = normalizeNonEmptyString(name);
246
+ if (!normalized) {
247
+ throw new Error("emitArtifact(name, data) requires a non-empty artifact name");
248
+ }
249
+ return normalized;
250
+ }
251
+
252
+ function normalizeOptionalString(value) {
253
+ if (value === undefined || value === null) return null;
254
+ const normalized = String(value).trim();
255
+ return normalized.length > 0 ? normalized : null;
256
+ }
257
+
258
+ function normalizeNonEmptyString(value) {
259
+ return normalizeOptionalString(value);
260
+ }
261
+
262
+ function normalizeCount(value) {
263
+ const count = Number(value);
264
+ return Number.isFinite(count) && count > 0 ? count : 0;
265
+ }
172
266
 
173
267
  function normalizeTestkitSuite(module) {
174
268
  const candidate = module?.default;
@@ -12,8 +12,8 @@ import {
12
12
  yellow,
13
13
  } from "../../terminal/colors.mjs";
14
14
  import { renderSummaryBox } from "../primitives/summary-box.mjs";
15
- import { applyHighlight } from "../../state/tree/fuzzy-match.mjs";
16
- import { FilterBar } from "../primitives/filter-bar.mjs";
15
+ import { getTerminalWidth } from "../../terminal/layout.mjs";
16
+ import { renderFailureDetail, renderPassedDetail } from "../../renderers/run/inline-detail.mjs";
17
17
 
18
18
  const SPINNER_FRAMES = ["|", "/", "-", "\\"];
19
19
 
@@ -50,38 +50,10 @@ export function RunTreeView({
50
50
  return;
51
51
  }
52
52
 
53
- if (snapshot.filter.active) {
54
- if (key.escape) {
55
- runState.deactivateFilter();
56
- return;
57
- }
58
- if (key.return) return;
59
- if (key.downArrow || input === "j") {
60
- runState.moveCursorDown();
61
- return;
62
- }
63
- if (key.upArrow || input === "k") {
64
- runState.moveCursorUp();
65
- return;
66
- }
67
- if (key.backspace || key.delete) {
68
- runState.updateFilterQuery(snapshot.filter.query.slice(0, -1));
69
- return;
70
- }
71
- if (isPrintableInput(input, key)) {
72
- runState.updateFilterQuery(`${snapshot.filter.query}${input}`);
73
- }
74
- return;
75
- }
76
-
77
53
  if (input === "q") {
78
54
  (onRequestClose || exit)();
79
55
  return;
80
56
  }
81
- if (input === "/") {
82
- runState.activateFilter();
83
- return;
84
- }
85
57
  if (key.downArrow || input === "j") {
86
58
  runState.moveCursorDown();
87
59
  return;
@@ -97,6 +69,7 @@ export function RunTreeView({
97
69
  }, { isActive: interactive });
98
70
 
99
71
  const visibleTreeEntries = useMemo(() => snapshot.visibleEntries || [], [snapshot.visibleEntries]);
72
+ const terminalWidth = getTerminalWidth(stdout, 100);
100
73
  const summaryLines = snapshot.finished && snapshot.summaryData
101
74
  ? renderSummaryBox(snapshot.summaryData.rows, { stdout })
102
75
  : [];
@@ -109,10 +82,8 @@ export function RunTreeView({
109
82
  createElement(
110
83
  Box,
111
84
  { key: "main", marginTop: 1, flexDirection: "column" },
112
- ...visibleTreeEntries.map(renderTreeLine.bind(null, snapshot, spinnerFrame))
85
+ ...visibleTreeEntries.flatMap(renderTreeLine.bind(null, snapshot, spinnerFrame, terminalWidth))
113
86
  ),
114
- snapshot.filter.active ? createElement(Text, { key: "filter-gap" }, "") : null,
115
- snapshot.filter.active ? createElement(FilterBar, { key: "filter-bar", filter: snapshot.filter }) : null,
116
87
  summaryLines.length > 0 ? createElement(Text, { key: "summary-gap" }, "") : null,
117
88
  ...summaryLines.map((line, index) => createElement(Text, { key: `summary-${index}` }, line)),
118
89
  createElement(Text, { key: "footer-gap" }, ""),
@@ -124,31 +95,36 @@ export function buildHeaderText(snapshot) {
124
95
  const progressText = snapshot.totalCount > 0 ? `[${snapshot.completedCount}/${snapshot.totalCount}]` : "[0/0]";
125
96
  const phaseText = snapshot.phase || (snapshot.finished ? "run complete" : "preparing");
126
97
  const sourceText = snapshot.dataSource === "artifact" ? "artifact run" : snapshot.finished ? "live summary" : "live run";
127
- const filterText = snapshot.filter.active ? `filter ${snapshot.filter.count}` : null;
128
- return [progressText, phaseText, sourceText, filterText].filter(Boolean).join(" · ");
98
+ return [progressText, phaseText, sourceText].filter(Boolean).join(" · ");
129
99
  }
130
100
 
131
101
  export function buildFooterText(snapshot, { interactive = true } = {}) {
132
102
  if (!snapshot.finished) return "Run in progress";
133
- if (snapshot.filter.active) {
134
- return "type to filter · ↑/↓ move · Esc clear filter · q quit";
135
- }
136
- return interactive ? "↑/↓ move · Enter collapse/expand · / filter · q quit" : "Run complete";
103
+ return interactive ? "↑/↓ move · Enter toggle detail · q quit" : "Run complete";
137
104
  }
138
105
 
139
- function renderTreeLine(snapshot, spinnerFrame, entry) {
106
+ function renderTreeLine(snapshot, spinnerFrame, terminalWidth, entry) {
140
107
  const selected = entry.id === snapshot.selectedEntryId;
141
108
  const pointer = selected ? `${bold(">")} ` : " ";
142
109
  const indent = " ".repeat(entry.depth);
143
- const rawLabel = entry.label;
144
- const match = entry.match;
145
- const highlightedLabel = match?.field === "label"
146
- ? applyHighlight(rawLabel, match.positions, bold)
147
- : rawLabel;
148
- const renderedLabel = decorateEntryLabel(entry, highlightedLabel, match);
110
+ const renderedLabel = decorateEntryLabel(entry, entry.label);
149
111
  const icon = entryIcon(entry, spinnerFrame);
150
112
  const line = `${pointer}${indent}${icon ? `${icon} ` : ""}${renderedLabel}${entrySuffix(entry)}`;
151
- return createElement(Text, { key: entry.id }, line);
113
+
114
+ const elements = [createElement(Text, { key: entry.id }, line)];
115
+
116
+ if (entry.kind === "file" && !entry.collapsed && snapshot.finished) {
117
+ const detailLines = entry.status === "failed"
118
+ ? renderFailureDetail(entry, { width: terminalWidth, regressionCatalog: snapshot.regressionCatalog })
119
+ : entry.status === "passed"
120
+ ? renderPassedDetail(entry, { width: terminalWidth })
121
+ : [];
122
+ for (let i = 0; i < detailLines.length; i++) {
123
+ elements.push(createElement(Text, { key: `${entry.id}-detail-${i}` }, detailLines[i]));
124
+ }
125
+ }
126
+
127
+ return elements;
152
128
  }
153
129
 
154
130
  function entryIcon(entry, spinnerFrame) {
@@ -162,16 +138,11 @@ function entryIcon(entry, spinnerFrame) {
162
138
  return dim("·");
163
139
  }
164
140
 
165
- function decorateEntryLabel(entry, label, match) {
166
- let rendered = label;
167
- if (entry.kind === "service") rendered = colorService(label);
168
- else if (entry.kind === "type") rendered = colorTypeBadge(label.toUpperCase());
169
- else if (entry.kind === "suite") rendered = bold(label);
170
-
171
- if (match?.field === "path" && entry.filePath) {
172
- rendered += ` ${dim(`(${applyHighlight(entry.filePath, match.positions, bold)})`)}`;
173
- }
174
- return rendered;
141
+ function decorateEntryLabel(entry, label) {
142
+ if (entry.kind === "service") return colorService(label);
143
+ if (entry.kind === "type") return colorTypeBadge(label.toUpperCase());
144
+ if (entry.kind === "suite") return bold(label);
145
+ return label;
175
146
  }
176
147
 
177
148
  function entrySuffix(entry) {
@@ -187,9 +158,3 @@ function entrySuffix(entry) {
187
158
  }
188
159
  return "";
189
160
  }
190
-
191
- function isPrintableInput(input, key) {
192
- if (!input) return false;
193
- if (key.ctrl || key.meta || key.escape || key.return || key.tab) return false;
194
- return input >= " ";
195
- }
@@ -0,0 +1,64 @@
1
+ import { buildFailurePresentation } from "../../../runner/formatting.mjs";
2
+ import { renderIndentedBlock } from "../../terminal/layout.mjs";
3
+ import { dim, green, red } from "../../terminal/colors.mjs";
4
+ import figures from "figures";
5
+
6
+ export function renderFailureDetail(entry, { width, regressionCatalog } = {}) {
7
+ const fileSummary = {
8
+ service: entry.serviceName,
9
+ type: normalizeType(entry),
10
+ path: entry.filePath,
11
+ error: entry.error || null,
12
+ failureDetails: Array.isArray(entry.failureDetails) ? entry.failureDetails : [],
13
+ suiteError: null,
14
+ };
15
+
16
+ const failureView = buildFailurePresentation(fileSummary, regressionCatalog);
17
+ const lines = [];
18
+ const indent = " ".repeat(2 + (entry.depth || 0) * 2 + 2);
19
+
20
+ if (failureView.primary) {
21
+ lines.push(...renderIndentedBlock(failureView.primary, { width, indent }));
22
+ }
23
+ for (const detail of failureView.details) {
24
+ lines.push(...renderIndentedBlock(detail, { width, indent }));
25
+ }
26
+ return lines;
27
+ }
28
+
29
+ export function renderPassedDetail(entry, { width } = {}) {
30
+ const lines = [];
31
+ const indent = " ".repeat(2 + (entry.depth || 0) * 2 + 2);
32
+
33
+ const checks = Array.isArray(entry.checkDetails) ? entry.checkDetails : [];
34
+ if (checks.length > 0) {
35
+ const passed = checks.filter((c) => c.passed).length;
36
+ lines.push(...renderIndentedBlock(dim(`${passed}/${checks.length} checks passed`), { width, indent }));
37
+ const maxDisplay = 8;
38
+ const displayed = checks.slice(0, maxDisplay);
39
+ for (const check of displayed) {
40
+ const icon = check.passed ? green(figures.tick) : red(figures.cross);
41
+ lines.push(...renderIndentedBlock(`${icon} ${dim(check.name)}`, { width, indent: `${indent} ` }));
42
+ }
43
+ if (checks.length > maxDisplay) {
44
+ lines.push(...renderIndentedBlock(dim(`+${checks.length - maxDisplay} more`), { width, indent: `${indent} ` }));
45
+ }
46
+ }
47
+
48
+ const artifacts = Array.isArray(entry.artifacts) ? entry.artifacts : [];
49
+ for (const artifact of artifacts) {
50
+ if (artifact.kind === "testkit.checks") continue;
51
+ if (artifact.kind === "runtime.output") continue;
52
+ if (artifact.summary) {
53
+ lines.push(...renderIndentedBlock(dim(artifact.summary), { width, indent }));
54
+ }
55
+ }
56
+
57
+ return lines;
58
+ }
59
+
60
+ function normalizeType(entry) {
61
+ if (entry.framework === "playwright" || entry.type === "ui") return "ui";
62
+ if (entry.type === "integration") return "int";
63
+ return entry.type;
64
+ }
@@ -1,7 +1,6 @@
1
1
  import { fileDisplayName } from "../../../discovery/index.mjs";
2
2
  import { suiteSelectionType } from "../../../runner/suite-selection.mjs";
3
3
  import { formatDuration } from "../../../runner/formatting.mjs";
4
- import { matchRunTreeEntry } from "../tree/fuzzy-match.mjs";
5
4
 
6
5
  export function buildSummaryRows({
7
6
  result,
@@ -51,9 +50,6 @@ export function createEmptyRunModel(dataSource = "live", options = {}) {
51
50
  totalCount: 0,
52
51
  completedCount: 0,
53
52
  phase: null,
54
- filterActive: false,
55
- filterQuery: "",
56
- filterMatches: new Map(),
57
53
  collapsedOverrides: new Map(),
58
54
  selectedEntryId: null,
59
55
  };
@@ -69,9 +65,6 @@ export function resetRunModel(model, dataSource = model.dataSource) {
69
65
  model.totalCount = 0;
70
66
  model.completedCount = 0;
71
67
  model.phase = null;
72
- model.filterActive = false;
73
- model.filterQuery = "";
74
- model.filterMatches = new Map();
75
68
  model.collapsedOverrides = new Map();
76
69
  model.selectedEntryId = null;
77
70
  }
@@ -111,6 +104,7 @@ export function initModelFromPlans(model, servicePlans) {
111
104
  diagnosis: null,
112
105
  skipReason: null,
113
106
  artifacts: [],
107
+ checkDetails: [],
114
108
  });
115
109
  }
116
110
  }
@@ -175,6 +169,7 @@ export function applyArtifactToModel(model, artifact) {
175
169
  diagnosis: fileResult.diagnosis || null,
176
170
  skipReason: fileResult.reason || null,
177
171
  artifacts: Array.isArray(fileResult.artifacts) ? fileResult.artifacts : [],
172
+ checkDetails: Array.isArray(fileResult.checkDetails) ? fileResult.checkDetails : [],
178
173
  });
179
174
  }
180
175
  }
@@ -210,7 +205,10 @@ export function markFileFinished(model, task, outcome) {
210
205
  file.status = "passed";
211
206
  file.error = null;
212
207
  file.failureDetails = [];
208
+ file.artifacts = Array.isArray(outcome.artifacts) ? outcome.artifacts : file.artifacts;
209
+ file.diagnosis = outcome.diagnosis || null;
213
210
  }
211
+ file.checkDetails = Array.isArray(outcome.checkDetails) ? outcome.checkDetails : file.checkDetails;
214
212
  file.durationMs = outcome.durationMs || null;
215
213
  model.completedCount += 1;
216
214
  if (file.status === "failed" && (!selectedEntry || selectedEntry.kind !== "file" || selectedEntry.status !== "failed")) {
@@ -277,23 +275,9 @@ export function finishModel(model, results, durationMs, regressionReport) {
277
275
  model.selectedEntryId = findFirstFailureEntryId(model) || model.selectedEntryId || findFirstNavigableEntryId(model);
278
276
  }
279
277
 
280
- export function updateFilter(model, query) {
281
- model.filterActive = true;
282
- model.filterQuery = String(query || "");
283
- model.filterMatches = new Map();
284
- const normalizedQuery = model.filterQuery.trim();
285
- if (!normalizedQuery) return;
286
- for (const entry of collectAllEntries(model)) {
287
- const match = matchRunTreeEntry(normalizedQuery, entry);
288
- if (match.matched) {
289
- model.filterMatches.set(entry.id, match);
290
- }
291
- }
292
- }
293
-
294
278
  export function toggleCollapsed(model, entryId) {
295
279
  const entry = getEntryById(model, entryId);
296
- if (!entry || (entry.kind !== "type" && entry.kind !== "suite")) return;
280
+ if (!entry || (entry.kind !== "type" && entry.kind !== "suite" && entry.kind !== "file")) return;
297
281
  const current = isCollapsed(model, entry);
298
282
  model.collapsedOverrides.set(entryId, !current);
299
283
  }
@@ -317,9 +301,6 @@ export function buildSnapshot(model) {
317
301
  || null;
318
302
  const selectedEntryId = selectedEntry?.id || null;
319
303
  const selectedFailure = toSelectedFailure(selectedEntry);
320
- const filterResults = [...model.filterMatches.entries()]
321
- .map(([id, match]) => ({ id, ...match }))
322
- .sort((left, right) => right.score - left.score || left.id.localeCompare(right.id));
323
304
 
324
305
  return {
325
306
  dataSource: model.dataSource,
@@ -336,12 +317,6 @@ export function buildSnapshot(model) {
336
317
  finished: model.finished,
337
318
  summaryData: model.summaryData,
338
319
  regressionCatalog: model.regressionCatalog,
339
- filter: {
340
- active: model.filterActive,
341
- query: model.filterQuery,
342
- results: filterResults,
343
- count: filterResults.length,
344
- },
345
320
  runArtifact: model.runArtifact,
346
321
  };
347
322
  }
@@ -445,22 +420,18 @@ function buildNestedServices(model) {
445
420
 
446
421
  function buildVisibleEntries(model, services) {
447
422
  const entries = [];
448
- const filterActive = model.filterActive && model.filterQuery.trim().length > 0;
449
- const includedIds = filterActive ? buildFilteredIdSet(model, services) : null;
450
423
 
451
424
  for (const service of services) {
452
- pushVisibleEntry(entries, model, service, 0, includedIds);
425
+ pushVisibleEntry(entries, model, service, 0);
453
426
  if (service.skipped) continue;
454
427
  for (const typeNode of service.types) {
455
- if (!pushVisibleEntry(entries, model, typeNode, 1, includedIds)) continue;
456
- const typeExpanded = filterActive || !typeNode.collapsed;
457
- if (!typeExpanded) continue;
428
+ if (!pushVisibleEntry(entries, model, typeNode, 1)) continue;
429
+ if (typeNode.collapsed) continue;
458
430
  for (const suite of typeNode.suites) {
459
- if (!pushVisibleEntry(entries, model, suite, 2, includedIds)) continue;
460
- const suiteExpanded = filterActive || !suite.collapsed;
461
- if (!suiteExpanded) continue;
431
+ if (!pushVisibleEntry(entries, model, suite, 2)) continue;
432
+ if (suite.collapsed) continue;
462
433
  for (const file of suite.files) {
463
- pushVisibleEntry(entries, model, file, 3, includedIds);
434
+ pushVisibleEntry(entries, model, file, 3);
464
435
  }
465
436
  }
466
437
  }
@@ -469,62 +440,12 @@ function buildVisibleEntries(model, services) {
469
440
  return entries;
470
441
  }
471
442
 
472
- function pushVisibleEntry(entries, model, entry, depth, includedIds) {
473
- if (includedIds && !includedIds.has(entry.id)) return false;
474
- const base = toPublicEntry(entry, depth);
475
- const match = model.filterMatches.get(entry.id) || null;
476
- entries.push({
477
- ...base,
478
- match,
479
- });
443
+ function pushVisibleEntry(entries, model, entry, depth) {
444
+ entries.push(toPublicEntry(entry, depth, model));
480
445
  return true;
481
446
  }
482
447
 
483
- function buildFilteredIdSet(model, services) {
484
- const included = new Set();
485
- const childrenById = new Map();
486
- const parentsById = new Map();
487
-
488
- for (const service of services) {
489
- childrenById.set(service.id, service.types.map((entry) => entry.id));
490
- for (const typeNode of service.types) {
491
- parentsById.set(typeNode.id, service.id);
492
- childrenById.set(typeNode.id, typeNode.suites.map((entry) => entry.id));
493
- for (const suite of typeNode.suites) {
494
- parentsById.set(suite.id, typeNode.id);
495
- childrenById.set(suite.id, suite.files.map((entry) => entry.id));
496
- for (const file of suite.files) {
497
- parentsById.set(file.id, suite.id);
498
- childrenById.set(file.id, []);
499
- }
500
- }
501
- }
502
- }
503
-
504
- for (const entryId of model.filterMatches.keys()) {
505
- included.add(entryId);
506
- let current = parentsById.get(entryId) || null;
507
- while (current) {
508
- included.add(current);
509
- current = parentsById.get(current) || null;
510
- }
511
- const entry = getEntryById(model, entryId);
512
- if (entry && entry.kind !== "file") {
513
- includeDescendants(entryId, childrenById, included);
514
- }
515
- }
516
-
517
- return included;
518
- }
519
-
520
- function includeDescendants(entryId, childrenById, included) {
521
- for (const childId of childrenById.get(entryId) || []) {
522
- included.add(childId);
523
- includeDescendants(childId, childrenById, included);
524
- }
525
- }
526
-
527
- function toPublicEntry(entry, depth) {
448
+ function toPublicEntry(entry, depth, model = null) {
528
449
  if (entry.kind === "service") {
529
450
  return {
530
451
  id: entry.id,
@@ -567,6 +488,7 @@ function toPublicEntry(entry, depth) {
567
488
  framework: entry.framework,
568
489
  };
569
490
  }
491
+ const autoCollapsed = defaultAutoCollapsed(entry);
570
492
  return {
571
493
  id: entry.id,
572
494
  kind: "file",
@@ -586,6 +508,8 @@ function toPublicEntry(entry, depth) {
586
508
  diagnosis: entry.diagnosis,
587
509
  skipReason: entry.skipReason,
588
510
  artifacts: entry.artifacts,
511
+ checkDetails: entry.checkDetails,
512
+ collapsed: model ? isCollapsed(model, { id: entry.id, autoCollapsed }) : autoCollapsed,
589
513
  };
590
514
  }
591
515
 
@@ -625,7 +549,12 @@ function isCollapsed(model, entry) {
625
549
  if (model.collapsedOverrides.has(entry.id)) {
626
550
  return Boolean(model.collapsedOverrides.get(entry.id));
627
551
  }
628
- return Boolean(entry.autoCollapsed);
552
+ return Boolean(entry.autoCollapsed ?? defaultAutoCollapsed(entry));
553
+ }
554
+
555
+ function defaultAutoCollapsed(entry) {
556
+ if (entry?.kind !== "file") return false;
557
+ return entry.status === "passed" || entry.status === "skipped" || entry.status === "pending";
629
558
  }
630
559
 
631
560
  function summarizeFiles(files) {
@@ -17,7 +17,6 @@ import {
17
17
  setRegressionCatalog,
18
18
  setTotalFileCount,
19
19
  toggleCollapsed,
20
- updateFilter,
21
20
  } from "./model.mjs";
22
21
 
23
22
  export function createRunState({ dataSource = "live", autoCollapsePassedTreeBranches = true } = {}) {
@@ -143,27 +142,6 @@ export function createRunState({ dataSource = "live", autoCollapsePassedTreeBran
143
142
  notify();
144
143
  },
145
144
 
146
- activateFilter() {
147
- model.filterActive = true;
148
- notify();
149
- },
150
-
151
- updateFilterQuery(query) {
152
- updateFilter(model, query);
153
- const snapshot = buildSnapshot(model);
154
- if (snapshot.filter.results.length > 0) {
155
- model.selectedEntryId = snapshot.filter.results[0].id;
156
- }
157
- notify();
158
- },
159
-
160
- deactivateFilter() {
161
- model.filterActive = false;
162
- model.filterQuery = "";
163
- model.filterMatches = new Map();
164
- notify();
165
- },
166
-
167
145
  revealFile(serviceName, filePath) {
168
146
  const entryId = findEntryIdForFile(model, serviceName, filePath);
169
147
  if (!entryId) return false;
@@ -11,6 +11,7 @@ export declare function defineConfig<T extends Record<string, unknown>>(
11
11
  config: T,
12
12
  options?: PlaywrightConfigOptions
13
13
  ): T & {
14
+ globalTimeout?: number;
14
15
  timeout: number;
15
16
  expect: Record<string, unknown> & { timeout: number };
16
17
  use: Record<string, unknown> & {
@@ -27,6 +27,7 @@ export function defineConfig(config = {}, options = {}) {
27
27
 
28
28
  return {
29
29
  ...config,
30
+ ...(managed ? { globalTimeout: timeoutMs } : {}),
30
31
  timeout: timeoutMs,
31
32
  expect: {
32
33
  ...(config.expect || {}),