donobu 5.28.0 → 5.30.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.
Files changed (43) hide show
  1. package/dist/cli/donobu-cli.d.ts +7 -1
  2. package/dist/cli/donobu-cli.js +195 -84
  3. package/dist/esm/cli/donobu-cli.d.ts +7 -1
  4. package/dist/esm/cli/donobu-cli.js +195 -84
  5. package/dist/esm/lib/test/testExtension.d.ts +31 -1
  6. package/dist/esm/lib/test/testExtension.js +273 -0
  7. package/dist/esm/managers/DonobuStack.js +1 -1
  8. package/dist/esm/managers/TestsManager.d.ts +2 -2
  9. package/dist/esm/managers/TestsManager.js +10 -0
  10. package/dist/esm/models/PaginatedResult.d.ts +15 -0
  11. package/dist/esm/models/PaginatedResult.js +13 -2
  12. package/dist/esm/models/TestMetadata.d.ts +630 -0
  13. package/dist/esm/models/TestMetadata.js +10 -1
  14. package/dist/esm/persistence/tests/TestsPersistence.d.ts +2 -2
  15. package/dist/esm/persistence/tests/TestsPersistenceDonobuApi.d.ts +3 -3
  16. package/dist/esm/persistence/tests/TestsPersistenceDonobuApi.js +18 -2
  17. package/dist/esm/persistence/tests/TestsPersistenceRegistry.d.ts +9 -1
  18. package/dist/esm/persistence/tests/TestsPersistenceRegistry.js +9 -2
  19. package/dist/esm/persistence/tests/TestsPersistenceSqlite.d.ts +2 -1
  20. package/dist/esm/persistence/tests/TestsPersistenceSqlite.js +65 -6
  21. package/dist/esm/persistence/tests/TestsPersistenceVolatile.d.ts +22 -3
  22. package/dist/esm/persistence/tests/TestsPersistenceVolatile.js +69 -4
  23. package/dist/esm/reporter/renderMarkdown.js +1 -1
  24. package/dist/lib/test/testExtension.d.ts +31 -1
  25. package/dist/lib/test/testExtension.js +273 -0
  26. package/dist/managers/DonobuStack.js +1 -1
  27. package/dist/managers/TestsManager.d.ts +2 -2
  28. package/dist/managers/TestsManager.js +10 -0
  29. package/dist/models/PaginatedResult.d.ts +15 -0
  30. package/dist/models/PaginatedResult.js +13 -2
  31. package/dist/models/TestMetadata.d.ts +630 -0
  32. package/dist/models/TestMetadata.js +10 -1
  33. package/dist/persistence/tests/TestsPersistence.d.ts +2 -2
  34. package/dist/persistence/tests/TestsPersistenceDonobuApi.d.ts +3 -3
  35. package/dist/persistence/tests/TestsPersistenceDonobuApi.js +18 -2
  36. package/dist/persistence/tests/TestsPersistenceRegistry.d.ts +9 -1
  37. package/dist/persistence/tests/TestsPersistenceRegistry.js +9 -2
  38. package/dist/persistence/tests/TestsPersistenceSqlite.d.ts +2 -1
  39. package/dist/persistence/tests/TestsPersistenceSqlite.js +65 -6
  40. package/dist/persistence/tests/TestsPersistenceVolatile.d.ts +22 -3
  41. package/dist/persistence/tests/TestsPersistenceVolatile.js +69 -4
  42. package/dist/reporter/renderMarkdown.js +1 -1
  43. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import type { PaginatedResult } from '../../models/PaginatedResult';
2
- import type { TestMetadata, TestsQuery } from '../../models/TestMetadata';
2
+ import type { TestListItem, TestMetadata, TestsQuery } from '../../models/TestMetadata';
3
3
  /**
4
4
  * Persistence interface for test metadata.
5
5
  *
@@ -15,7 +15,7 @@ export interface TestsPersistence {
15
15
  /** Get a test by ID. Throws TestNotFoundException if not found. */
16
16
  getTestById(testId: string): Promise<TestMetadata>;
17
17
  /** List tests with optional filtering and pagination. */
18
- getTests(query: TestsQuery): Promise<PaginatedResult<TestMetadata>>;
18
+ getTests(query: TestsQuery): Promise<PaginatedResult<TestListItem>>;
19
19
  /** Delete a test. */
20
20
  deleteTest(testId: string): Promise<void>;
21
21
  }
@@ -1,5 +1,5 @@
1
- import type { PaginatedResult } from '../../models/PaginatedResult';
2
- import { type TestMetadata, type TestsQuery } from '../../models/TestMetadata';
1
+ import { type PaginatedResult } from '../../models/PaginatedResult';
2
+ import { type TestListItem, type TestMetadata, type TestsQuery } from '../../models/TestMetadata';
3
3
  import { DonobuApiClient } from '../DonobuApiClient';
4
4
  import type { TestsPersistence } from './TestsPersistence';
5
5
  export declare class TestsPersistenceDonobuApi extends DonobuApiClient implements TestsPersistence {
@@ -7,7 +7,7 @@ export declare class TestsPersistenceDonobuApi extends DonobuApiClient implement
7
7
  createTest(testMetadata: TestMetadata): Promise<void>;
8
8
  updateTest(testMetadata: TestMetadata): Promise<void>;
9
9
  getTestById(testId: string): Promise<TestMetadata>;
10
- getTests(query: TestsQuery): Promise<PaginatedResult<TestMetadata>>;
10
+ getTests(query: TestsQuery): Promise<PaginatedResult<TestListItem>>;
11
11
  deleteTest(testId: string): Promise<void>;
12
12
  }
13
13
  //# sourceMappingURL=TestsPersistenceDonobuApi.d.ts.map
@@ -2,8 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TestsPersistenceDonobuApi = void 0;
4
4
  const TestNotFoundException_1 = require("../../exceptions/TestNotFoundException");
5
+ const PaginatedResult_1 = require("../../models/PaginatedResult");
5
6
  const TestMetadata_1 = require("../../models/TestMetadata");
6
7
  const DonobuApiClient_1 = require("../DonobuApiClient");
8
+ const normalizeFlowMetadata_1 = require("../normalizeFlowMetadata");
7
9
  class TestsPersistenceDonobuApi extends DonobuApiClient_1.DonobuApiClient {
8
10
  constructor(baseUrl, apiKey) {
9
11
  super(baseUrl, apiKey);
@@ -76,9 +78,23 @@ class TestsPersistenceDonobuApi extends DonobuApiClient_1.DonobuApiClient {
76
78
  if (!response.ok) {
77
79
  throw new Error(`Failed to list tests: ${response.status} ${response.statusText}`);
78
80
  }
79
- const json = (await response.json());
81
+ // Validate the basic pagination shape of the API response, but keep the
82
+ // items as `unknown` for now.
83
+ const json = PaginatedResult_1.PaginatedResultSchema.parse(await response.json());
80
84
  return {
81
- items: json.items.map((item) => TestMetadata_1.TestMetadataSchema.parse(item)),
85
+ items: json.items.map((item) => {
86
+ // Cast this enough to be able to easily pull out the `latestFlow`
87
+ // field. It'll be properly validated by the schema in the `return`
88
+ // statement below, after normalizing the flow metadata.
89
+ const testListItem = item;
90
+ // Run full validation after normalizing the flow metadata.
91
+ return TestMetadata_1.TestListItemSchema.parse({
92
+ ...testListItem,
93
+ latestFlow: testListItem.latestFlow
94
+ ? (0, normalizeFlowMetadata_1.normalizeFlowMetadata)(testListItem.latestFlow)
95
+ : null,
96
+ });
97
+ }),
82
98
  nextPageToken: json.nextPageToken,
83
99
  };
84
100
  }
@@ -1,5 +1,6 @@
1
1
  import type { EnvPick } from 'env-struct';
2
2
  import type { env } from '../../envVars';
3
+ import type { FlowsPersistenceRegistry } from '../flows/FlowsPersistenceRegistry';
3
4
  import { PersistencePluginRegistry } from '../PersistencePlugin';
4
5
  import type { TestsPersistence } from './TestsPersistence';
5
6
  export interface TestsPersistenceRegistry {
@@ -11,7 +12,14 @@ export interface TestsPersistenceRegistry {
11
12
  export declare class TestsPersistenceRegistryImpl implements TestsPersistenceRegistry {
12
13
  private readonly layers;
13
14
  constructor(layers: TestsPersistence[]);
14
- static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'PERSISTENCE_PRIORITY'>, persistencePlugins?: PersistencePluginRegistry): Promise<TestsPersistenceRegistryImpl>;
15
+ static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'PERSISTENCE_PRIORITY'>,
16
+ /**
17
+ * Flows registry — used by the volatile tests layer to compute
18
+ * flow-derived sort keys (flow_count, latest_flow_created_at).
19
+ * Optional: when omitted, the volatile layer falls back to
20
+ * best-effort no-op sorting on those keys.
21
+ */
22
+ flowsRegistry?: FlowsPersistenceRegistry, persistencePlugins?: PersistencePluginRegistry): Promise<TestsPersistenceRegistryImpl>;
15
23
  get(): Promise<TestsPersistence>;
16
24
  getAll(): Promise<TestsPersistence[]>;
17
25
  }
@@ -13,7 +13,14 @@ class TestsPersistenceRegistryImpl {
13
13
  throw new Error('No valid test persistence implementation available!');
14
14
  }
15
15
  }
16
- static async fromEnvironment(environ, persistencePlugins = new PersistencePlugin_1.PersistencePluginRegistry()) {
16
+ static async fromEnvironment(environ,
17
+ /**
18
+ * Flows registry — used by the volatile tests layer to compute
19
+ * flow-derived sort keys (flow_count, latest_flow_created_at).
20
+ * Optional: when omitted, the volatile layer falls back to
21
+ * best-effort no-op sorting on those keys.
22
+ */
23
+ flowsRegistry, persistencePlugins = new PersistencePlugin_1.PersistencePluginRegistry()) {
17
24
  const donobuApiBaseUrl = environ.data.DONOBU_API_BASE_URL;
18
25
  const donobuApiKey = environ.data.DONOBU_API_KEY;
19
26
  const layers = [];
@@ -28,7 +35,7 @@ class TestsPersistenceRegistryImpl {
28
35
  layers.push(await TestsPersistenceSqlite_1.TestsPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()));
29
36
  break;
30
37
  case 'RAM':
31
- layers.push(new TestsPersistenceVolatile_1.TestsPersistenceVolatile());
38
+ layers.push(new TestsPersistenceVolatile_1.TestsPersistenceVolatile(undefined, flowsRegistry ? () => flowsRegistry.get() : undefined));
32
39
  break;
33
40
  default: {
34
41
  const plugin = persistencePlugins.get(key);
@@ -1,5 +1,6 @@
1
1
  import type Database from 'better-sqlite3';
2
2
  import type { PaginatedResult } from '../../models/PaginatedResult';
3
+ import type { TestListItem } from '../../models/TestMetadata';
3
4
  import { type TestMetadata, type TestsQuery } from '../../models/TestMetadata';
4
5
  import type { TestsPersistence } from './TestsPersistence';
5
6
  export declare class TestsPersistenceSqlite implements TestsPersistence {
@@ -9,7 +10,7 @@ export declare class TestsPersistenceSqlite implements TestsPersistence {
9
10
  createTest(testMetadata: TestMetadata): Promise<void>;
10
11
  updateTest(testMetadata: TestMetadata): Promise<void>;
11
12
  getTestById(testId: string): Promise<TestMetadata>;
12
- getTests(query: TestsQuery): Promise<PaginatedResult<TestMetadata>>;
13
+ getTests(query: TestsQuery): Promise<PaginatedResult<TestListItem>>;
13
14
  deleteTest(testId: string): Promise<void>;
14
15
  }
15
16
  //# sourceMappingURL=TestsPersistenceSqlite.d.ts.map
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TestsPersistenceSqlite = void 0;
4
4
  const TestNotFoundException_1 = require("../../exceptions/TestNotFoundException");
5
+ const FlowMetadata_1 = require("../../models/FlowMetadata");
5
6
  const TestMetadata_1 = require("../../models/TestMetadata");
7
+ const normalizeFlowMetadata_1 = require("../normalizeFlowMetadata");
6
8
  class TestsPersistenceSqlite {
7
9
  constructor(db) {
8
10
  this.db = db;
@@ -70,13 +72,60 @@ class TestsPersistenceSqlite {
70
72
  whereConditions.push('suite_id = ?');
71
73
  params.push(query.suiteId);
72
74
  }
73
- let sql = 'SELECT metadata FROM test_metadata';
74
- if (whereConditions.length > 0) {
75
- sql += ' WHERE ' + whereConditions.join(' AND ');
75
+ // LEFT JOIN against a per-test aggregate of flow_metadata so the
76
+ // `flow_count` and `latest_flow_created_at` columns are queryable for
77
+ // ORDER BY without a separate round-trip per test. A second LEFT JOIN
78
+ // uses ROW_NUMBER() OVER (PARTITION BY test_id …) to pick the single
79
+ // most-recent flow per test, so we can return its full metadata blob
80
+ // alongside the count without an N+1 fetch.
81
+ let sql = `
82
+ SELECT
83
+ t.metadata AS metadata,
84
+ COALESCE(fc.flow_count, 0) AS flow_count,
85
+ fc.latest_flow_created_at AS latest_flow_created_at,
86
+ lf.metadata AS latest_flow_metadata
87
+ FROM test_metadata t
88
+ LEFT JOIN (
89
+ SELECT test_id,
90
+ COUNT(*) AS flow_count,
91
+ MAX(created_at) AS latest_flow_created_at
92
+ FROM flow_metadata
93
+ WHERE test_id IS NOT NULL
94
+ GROUP BY test_id
95
+ ) fc ON fc.test_id = t.id
96
+ LEFT JOIN (
97
+ SELECT test_id, metadata,
98
+ ROW_NUMBER() OVER (
99
+ PARTITION BY test_id
100
+ ORDER BY created_at DESC, id DESC
101
+ ) AS rn
102
+ FROM flow_metadata
103
+ WHERE test_id IS NOT NULL
104
+ ) lf ON lf.test_id = t.id AND lf.rn = 1
105
+ `;
106
+ // The WHERE clauses still target test_metadata columns; rewrite them to
107
+ // the aliased table.
108
+ const aliasedWhere = whereConditions.map((c) => `t.${c}`);
109
+ if (aliasedWhere.length > 0) {
110
+ sql += ' WHERE ' + aliasedWhere.join(' AND ');
76
111
  }
77
- const sortCol = query.sortBy ?? 'created_at';
112
+ // Map the API-level sortBy to the actual SQL column. test_metadata
113
+ // columns are referenced through the `t.` alias; the join-derived
114
+ // columns are referenced by the alias defined in the SELECT list.
115
+ const sortColMap = {
116
+ created_at: 't.created_at',
117
+ name: 't.name',
118
+ suite_id: 't.suite_id',
119
+ next_run_mode: 't.next_run_mode',
120
+ flow_count: 'flow_count',
121
+ latest_flow_created_at: 'latest_flow_created_at',
122
+ };
123
+ const sortCol = sortColMap[query.sortBy ?? 'created_at'] ?? 't.created_at';
78
124
  const sortDir = query.sortOrder === 'asc' ? 'ASC' : 'DESC';
79
- sql += ` ORDER BY ${sortCol} ${sortDir} LIMIT ? OFFSET ?`;
125
+ // Stable secondary sort by created_at so flows with identical sort keys
126
+ // (e.g. flow_count = 0 across many tests) come back in a deterministic
127
+ // order across requests.
128
+ sql += ` ORDER BY ${sortCol} ${sortDir}, t.created_at DESC LIMIT ? OFFSET ?`;
80
129
  params.push(validLimit + 1);
81
130
  params.push(offset);
82
131
  const stmt = this.db.prepare(sql);
@@ -85,7 +134,17 @@ class TestsPersistenceSqlite {
85
134
  const results = hasMore ? rows.slice(0, validLimit) : rows;
86
135
  const nextPageToken = hasMore ? `${offset + validLimit}` : undefined;
87
136
  return {
88
- items: results.map((row) => TestMetadata_1.TestMetadataSchema.parse(JSON.parse(row.metadata))),
137
+ items: results.map((row) => {
138
+ const test = TestMetadata_1.TestMetadataSchema.parse(JSON.parse(row.metadata));
139
+ const latestFlow = row.latest_flow_metadata
140
+ ? FlowMetadata_1.FlowMetadataSchema.parse((0, normalizeFlowMetadata_1.normalizeFlowMetadata)(JSON.parse(row.latest_flow_metadata)))
141
+ : null;
142
+ return {
143
+ ...test,
144
+ flowCount: row.flow_count,
145
+ latestFlow,
146
+ };
147
+ }),
89
148
  nextPageToken,
90
149
  };
91
150
  }
@@ -1,16 +1,35 @@
1
1
  import type { PaginatedResult } from '../../models/PaginatedResult';
2
- import type { TestMetadata, TestsQuery } from '../../models/TestMetadata';
2
+ import type { TestListItem, TestMetadata, TestsQuery } from '../../models/TestMetadata';
3
+ import type { FlowsPersistence } from '../flows/FlowsPersistence';
3
4
  import type { TestsPersistence } from './TestsPersistence';
4
5
  /**
5
6
  * A volatile (in-memory) implementation of TestsPersistence.
6
7
  */
7
8
  export declare class TestsPersistenceVolatile implements TestsPersistence {
8
9
  private readonly tests;
9
- constructor(tests?: Map<string, TestMetadata>);
10
+ /**
11
+ * Optional flows-source callback used to compute flow-derived sort
12
+ * keys (`flow_count`, `latest_flow_created_at`). When supplied, the
13
+ * volatile layer can natively sort by those keys; otherwise they
14
+ * fall back to the existing best-effort string-index lookup. The
15
+ * callback is async so the registry can resolve the flows
16
+ * persistence layer lazily.
17
+ */
18
+ private readonly flowsSource?;
19
+ constructor(tests?: Map<string, TestMetadata>,
20
+ /**
21
+ * Optional flows-source callback used to compute flow-derived sort
22
+ * keys (`flow_count`, `latest_flow_created_at`). When supplied, the
23
+ * volatile layer can natively sort by those keys; otherwise they
24
+ * fall back to the existing best-effort string-index lookup. The
25
+ * callback is async so the registry can resolve the flows
26
+ * persistence layer lazily.
27
+ */
28
+ flowsSource?: (() => Promise<FlowsPersistence>) | undefined);
10
29
  createTest(testMetadata: TestMetadata): Promise<void>;
11
30
  updateTest(testMetadata: TestMetadata): Promise<void>;
12
31
  getTestById(testId: string): Promise<TestMetadata>;
13
- getTests(query: TestsQuery): Promise<PaginatedResult<TestMetadata>>;
32
+ getTests(query: TestsQuery): Promise<PaginatedResult<TestListItem>>;
14
33
  deleteTest(testId: string): Promise<void>;
15
34
  }
16
35
  //# sourceMappingURL=TestsPersistenceVolatile.d.ts.map
@@ -9,8 +9,18 @@ function deepCopy(test) {
9
9
  * A volatile (in-memory) implementation of TestsPersistence.
10
10
  */
11
11
  class TestsPersistenceVolatile {
12
- constructor(tests = new Map()) {
12
+ constructor(tests = new Map(),
13
+ /**
14
+ * Optional flows-source callback used to compute flow-derived sort
15
+ * keys (`flow_count`, `latest_flow_created_at`). When supplied, the
16
+ * volatile layer can natively sort by those keys; otherwise they
17
+ * fall back to the existing best-effort string-index lookup. The
18
+ * callback is async so the registry can resolve the flows
19
+ * persistence layer lazily.
20
+ */
21
+ flowsSource) {
13
22
  this.tests = tests;
23
+ this.flowsSource = flowsSource;
14
24
  }
15
25
  async createTest(testMetadata) {
16
26
  this.tests.set(testMetadata.id, deepCopy(testMetadata));
@@ -44,9 +54,57 @@ class TestsPersistenceVolatile {
44
54
  }
45
55
  const sortCol = query.sortBy ?? 'created_at';
46
56
  const sortAsc = query.sortOrder === 'asc';
57
+ // Pre-compute a per-test summary (count + latest flow) from the flows
58
+ // source (volatile flows layer in the typical RAM setup; can be any
59
+ // FlowsPersistence). Done in parallel; results populate `summaries`
60
+ // before the synchronous sort comparator and the final TestListItem
61
+ // construction below. Without a flows source we still serve the list
62
+ // — flowCount defaults to 0 and latestFlow to null.
63
+ const summaries = new Map();
64
+ if (this.flowsSource) {
65
+ const flowsLayer = await this.flowsSource();
66
+ await Promise.all(filtered.map(async (test) => {
67
+ let count = 0;
68
+ let latestCreatedAt = 0;
69
+ let latestFlow = null;
70
+ let pageToken = undefined;
71
+ do {
72
+ const page = await flowsLayer.getFlowsMetadata({
73
+ testId: test.id,
74
+ limit: 100,
75
+ pageToken,
76
+ });
77
+ count += page.items.length;
78
+ for (const flow of page.items) {
79
+ // Volatile flows don't track a separate created_at column;
80
+ // startedAt is the closest proxy and the same value the
81
+ // SQLite migration uses for backfilled rows.
82
+ const ts = flow.startedAt ?? 0;
83
+ if (ts > latestCreatedAt) {
84
+ latestCreatedAt = ts;
85
+ latestFlow = flow;
86
+ }
87
+ }
88
+ pageToken = page.nextPageToken;
89
+ } while (pageToken);
90
+ summaries.set(test.id, { count, latestCreatedAt, latestFlow });
91
+ }));
92
+ }
47
93
  filtered.sort((a, b) => {
48
- const aVal = String(a[sortCol] ?? '');
49
- const bVal = String(b[sortCol] ?? '');
94
+ let aVal;
95
+ let bVal;
96
+ if (sortCol === 'flow_count') {
97
+ aVal = summaries.get(a.id)?.count ?? 0;
98
+ bVal = summaries.get(b.id)?.count ?? 0;
99
+ }
100
+ else if (sortCol === 'latest_flow_created_at') {
101
+ aVal = summaries.get(a.id)?.latestCreatedAt ?? 0;
102
+ bVal = summaries.get(b.id)?.latestCreatedAt ?? 0;
103
+ }
104
+ else {
105
+ aVal = String(a[sortCol] ?? '');
106
+ bVal = String(b[sortCol] ?? '');
107
+ }
50
108
  const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
51
109
  return sortAsc ? cmp : -cmp;
52
110
  });
@@ -54,7 +112,14 @@ class TestsPersistenceVolatile {
54
112
  const paged = filtered.slice(offset, offset + validLimit);
55
113
  const hasMore = offset + validLimit < filtered.length;
56
114
  return {
57
- items: paged.map(deepCopy),
115
+ items: paged.map((test) => {
116
+ const summary = summaries.get(test.id);
117
+ return {
118
+ ...deepCopy(test),
119
+ flowCount: summary?.count ?? 0,
120
+ latestFlow: summary?.latestFlow ?? null,
121
+ };
122
+ }),
58
123
  nextPageToken: hasMore ? (offset + validLimit).toString() : undefined,
59
124
  };
60
125
  }
@@ -152,7 +152,7 @@ function renderMarkdown(report) {
152
152
  }
153
153
  if (result.status === 'failed' && result.error) {
154
154
  markdown += `\n<details>\n<summary>⚠️ Error Details</summary>\n\n`;
155
- markdown += `\`\`\`\n${result.error.message || 'No error message available'}\n\`\`\`\n\n`;
155
+ markdown += `\`\`\`\n${(0, ansi_1.stripAnsi)(result.error.message || '') || 'No error message available'}\n\`\`\`\n\n`;
156
156
  if (result.error.snippet) {
157
157
  markdown += `**Code Snippet**:\n\`\`\`\n${(0, ansi_1.stripAnsi)(result.error.snippet)}\n\`\`\`\n\n`;
158
158
  }
@@ -1,4 +1,4 @@
1
- import type { PlaywrightWorkerOptions } from '@playwright/test';
1
+ import type { PlaywrightWorkerOptions, TestInfo } from '@playwright/test';
2
2
  import type { GptClient } from '../../clients/GptClient';
3
3
  import type { BrowserStorageState } from '../../models/BrowserStorageState';
4
4
  import { FlowLogBuffer } from '../../utils/FlowLogBuffer';
@@ -9,6 +9,7 @@ export declare const test: import("@playwright/test").TestType<import("@playwrig
9
9
  flowId: string;
10
10
  logBuffer: FlowLogBuffer;
11
11
  };
12
+ pwApiStepLogger: void;
12
13
  storageState?: BrowserStorageState | Promise<BrowserStorageState>;
13
14
  gptClient?: GptClient;
14
15
  page: DonobuExtendedPage;
@@ -26,4 +27,33 @@ export declare const test: import("@playwright/test").TestType<import("@playwrig
26
27
  */
27
28
  donobuFileUploadDrainGuard: void;
28
29
  }>;
30
+ export declare function emitPlaywrightStepLog(step: any): void;
31
+ /** Reset the source-line cache. Test-only. */
32
+ export declare function _resetSourceLineCacheForTests(): void;
33
+ /**
34
+ * Monkey-patch `testInfo._callbacks.onStepEnd` so each Playwright step in
35
+ * the `pw:api` / `expect` categories emits an `appLogger.info` entry once
36
+ * Playwright signals the step has finished. Returns a teardown function
37
+ * that restores the original callback.
38
+ *
39
+ * Why patch `onStepEnd` and not `_addStep`:
40
+ * Playwright captures the step's source location with a stack walk inside
41
+ * `_addStep` (see playwright/lib/worker/testInfo.js). Patching `_addStep`
42
+ * inserts a Donobu library frame into that stack, and Playwright's
43
+ * built-in filter only excludes `@playwright/test` / `playwright-core`
44
+ * paths — so for steps that don't pre-set `data.location` (most notably
45
+ * `expect`), the captured location lands inside our wrapper in
46
+ * `node_modules/donobu/...`. The downstream node_modules filter would
47
+ * then drop the entry. `_callbacks.onStepEnd` runs AFTER the stack
48
+ * capture, so this hook is invisible to Playwright's location resolution.
49
+ *
50
+ * Verified against @playwright/test 1.57+. `_callbacks` / `_stepMap` and
51
+ * the step shape (category, title, params, endWallTime, error, location)
52
+ * are private Playwright internals — install and per-step logging are
53
+ * wrapped in try/catch so any shape drift degrades to a no-op rather than
54
+ * failing the test.
55
+ *
56
+ * Exported for unit testing.
57
+ */
58
+ export declare function installPlaywrightStepLogger(testInfo: TestInfo): () => void;
29
59
  //# sourceMappingURL=testExtension.d.ts.map