@elench/testkit 0.1.28 → 0.1.30

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.
@@ -642,7 +642,7 @@ async function runPlaywrightBatch(targetConfig, batch) {
642
642
  const startedAt = Date.now();
643
643
  const result = await execa(
644
644
  "npx",
645
- ["playwright", "test", "--config", playwrightConfigPath, "--reporter=json", ...requestedFiles],
645
+ ["playwright", "test", "--config", playwrightConfigPath, "--reporter=json"],
646
646
  {
647
647
  cwd,
648
648
  env: buildPlaywrightEnv(targetConfig, local.baseUrl),
@@ -655,7 +655,8 @@ async function runPlaywrightBatch(targetConfig, batch) {
655
655
  }
656
656
 
657
657
  const parsed = parsePlaywrightJsonResults(result.stdout, cwd);
658
- const batchDurationMs = Date.now() - startedAt;
658
+ const finishedAt = Date.now();
659
+ const batchDurationMs = finishedAt - startedAt;
659
660
  const genericError =
660
661
  result.exitCode === 0
661
662
  ? parsed.errors[0] || null
@@ -677,6 +678,8 @@ async function runPlaywrightBatch(targetConfig, batch) {
677
678
  fileResult.durationMs > 0
678
679
  ? fileResult.durationMs
679
680
  : Math.round(batchDurationMs / Math.max(1, batch.tasks.length)),
681
+ startedAt,
682
+ finishedAt,
680
683
  };
681
684
  }
682
685
 
@@ -685,6 +688,8 @@ async function runPlaywrightBatch(targetConfig, batch) {
685
688
  failed: result.exitCode !== 0,
686
689
  error: result.exitCode !== 0 ? genericError : null,
687
690
  durationMs: Math.round(batchDurationMs / Math.max(1, batch.tasks.length)),
691
+ startedAt,
692
+ finishedAt,
688
693
  };
689
694
  });
690
695
  }
@@ -1087,12 +1092,15 @@ async function runDefaultRuntimeTask(targetConfig, task, args) {
1087
1092
 
1088
1093
  const summary = readDefaultRuntimeSummary(summaryFile);
1089
1094
  const runtimeError = determineDefaultRuntimeFailure(result, summary);
1095
+ const finishedAt = Date.now();
1090
1096
 
1091
1097
  return {
1092
1098
  task,
1093
1099
  failed: runtimeError !== null,
1094
1100
  error: runtimeError,
1095
- durationMs: Date.now() - startedAt,
1101
+ durationMs: finishedAt - startedAt,
1102
+ startedAt,
1103
+ finishedAt,
1096
1104
  };
1097
1105
  }
1098
1106
 
@@ -17,6 +17,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
17
17
  startedAt,
18
18
  firstTaskAt: null,
19
19
  lastTaskAt: null,
20
+ totalTaskDurationMs: 0,
20
21
  });
21
22
  continue;
22
23
  }
@@ -72,6 +73,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
72
73
  startedAt,
73
74
  firstTaskAt: null,
74
75
  lastTaskAt: null,
76
+ totalTaskDurationMs: 0,
75
77
  });
76
78
  }
77
79
 
@@ -82,26 +84,38 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
82
84
  const tracker = trackers.get(task.serviceName);
83
85
  if (!tracker || tracker.skipped) return;
84
86
 
85
- if (!tracker.firstTaskAt) tracker.firstTaskAt = finishedAt;
86
- tracker.lastTaskAt = finishedAt;
87
-
88
87
  const suite = tracker.suitesByKey.get(task.suiteKey);
89
88
  if (!suite) return;
90
89
 
90
+ const outcomeDurationMs = Number(outcome.durationMs ?? 0);
91
+ const outcomeFinishedAt = Number(outcome.finishedAt ?? finishedAt);
92
+ const outcomeStartedAt = Number(
93
+ outcome.startedAt ?? Math.max(0, outcomeFinishedAt - outcomeDurationMs)
94
+ );
95
+ tracker.firstTaskAt =
96
+ tracker.firstTaskAt === null
97
+ ? outcomeStartedAt
98
+ : Math.min(tracker.firstTaskAt, outcomeStartedAt);
99
+ tracker.lastTaskAt =
100
+ tracker.lastTaskAt === null
101
+ ? outcomeFinishedAt
102
+ : Math.max(tracker.lastTaskAt, outcomeFinishedAt);
103
+ tracker.totalTaskDurationMs += outcomeDurationMs;
104
+
91
105
  suite.completedFileCount += 1;
92
- suite.durationMs += outcome.durationMs;
106
+ suite.durationMs += outcomeDurationMs;
93
107
  const normalizedPath = normalizePathSeparators(task.file);
94
108
  const existingFileResult = suite.fileResultsByPath.get(normalizedPath);
95
109
  if (existingFileResult) {
96
110
  existingFileResult.failed = outcome.failed;
97
- existingFileResult.durationMs = outcome.durationMs;
111
+ existingFileResult.durationMs = outcomeDurationMs;
98
112
  existingFileResult.error = outcome.error;
99
113
  existingFileResult.status = outcome.failed ? "failed" : "passed";
100
114
  } else {
101
115
  const fileResult = {
102
116
  path: normalizedPath,
103
117
  failed: outcome.failed,
104
- durationMs: outcome.durationMs,
118
+ durationMs: outcomeDurationMs,
105
119
  error: outcome.error,
106
120
  status: outcome.failed ? "failed" : "passed",
107
121
  };
@@ -144,6 +158,7 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
144
158
  completedSuiteCount: 0,
145
159
  failedSuiteCount: 0,
146
160
  durationMs: 0,
161
+ totalTaskDurationMs: 0,
147
162
  suites: [],
148
163
  errors: [],
149
164
  };
@@ -169,11 +184,12 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
169
184
  0
170
185
  );
171
186
  const notRunFileCount = totalFileCount - completedFileCount;
172
- const accumulatedDurationMs = suites.reduce((sum, suite) => sum + suite.durationMs, 0);
187
+ const totalTaskDurationMs =
188
+ tracker.totalTaskDurationMs || suites.reduce((sum, suite) => sum + suite.durationMs, 0);
173
189
  const durationMs =
174
- tracker.firstTaskAt && tracker.lastTaskAt
175
- ? Math.max(tracker.lastTaskAt - tracker.firstTaskAt, accumulatedDurationMs)
176
- : Math.max(finishedAt - startedAt, accumulatedDurationMs);
190
+ tracker.firstTaskAt !== null && tracker.lastTaskAt !== null
191
+ ? Math.max(0, tracker.lastTaskAt - tracker.firstTaskAt)
192
+ : Math.max(0, finishedAt - startedAt);
177
193
 
178
194
  return {
179
195
  name: tracker.name,
@@ -192,6 +208,7 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
192
208
  failedFileCount,
193
209
  notRunFileCount,
194
210
  durationMs,
211
+ totalTaskDurationMs,
195
212
  suites: suites.map((suite) => ({
196
213
  name: suite.name,
197
214
  type: suite.type,
@@ -385,6 +402,7 @@ export function buildRunArtifact({
385
402
  failedFileCount: result.failedFileCount,
386
403
  notRunFileCount: result.notRunFileCount,
387
404
  durationMs: result.durationMs,
405
+ totalTaskDurationMs: result.totalTaskDurationMs,
388
406
  dbBackend: result.dbBackend,
389
407
  suites: result.suites,
390
408
  errors: result.errors,
@@ -81,6 +81,123 @@ describe("runner-results", () => {
81
81
  ]);
82
82
  });
83
83
 
84
+ it("reports service duration as wall-clock time, not accumulated file time", () => {
85
+ const trackers = buildServiceTrackers(
86
+ [
87
+ {
88
+ skipped: false,
89
+ config: {
90
+ name: "frontend",
91
+ testkit: {
92
+ database: {
93
+ selectedBackend: null,
94
+ },
95
+ },
96
+ },
97
+ suites: [
98
+ {
99
+ name: "browser",
100
+ type: "e2e",
101
+ framework: "playwright",
102
+ files: ["tests/a.pw.testkit.ts", "tests/b.pw.testkit.ts"],
103
+ orderIndex: 0,
104
+ },
105
+ ],
106
+ },
107
+ ],
108
+ 1000
109
+ );
110
+
111
+ recordTaskOutcome(
112
+ trackers,
113
+ {
114
+ serviceName: "frontend",
115
+ suiteKey: "e2e:browser",
116
+ file: "tests/a.pw.testkit.ts",
117
+ },
118
+ {
119
+ failed: false,
120
+ durationMs: 30_000,
121
+ startedAt: 1000,
122
+ finishedAt: 8000,
123
+ error: null,
124
+ },
125
+ 8000
126
+ );
127
+ recordTaskOutcome(
128
+ trackers,
129
+ {
130
+ serviceName: "frontend",
131
+ suiteKey: "e2e:browser",
132
+ file: "tests/b.pw.testkit.ts",
133
+ },
134
+ {
135
+ failed: false,
136
+ durationMs: 45_000,
137
+ startedAt: 3000,
138
+ finishedAt: 9000,
139
+ error: null,
140
+ },
141
+ 9000
142
+ );
143
+
144
+ const result = finalizeServiceResult(trackers.get("frontend"), 1000, 10_000);
145
+
146
+ expect(result.durationMs).toBe(8000);
147
+ expect(result.totalTaskDurationMs).toBe(75_000);
148
+ expect(result.suites[0].durationMs).toBe(75_000);
149
+ });
150
+
151
+ it("handles epoch-zero task timestamps when calculating wall-clock duration", () => {
152
+ const trackers = buildServiceTrackers(
153
+ [
154
+ {
155
+ skipped: false,
156
+ config: {
157
+ name: "frontend",
158
+ testkit: {
159
+ database: {
160
+ selectedBackend: null,
161
+ },
162
+ },
163
+ },
164
+ suites: [
165
+ {
166
+ name: "browser",
167
+ type: "e2e",
168
+ framework: "playwright",
169
+ files: ["tests/a.pw.testkit.ts"],
170
+ orderIndex: 0,
171
+ },
172
+ ],
173
+ },
174
+ ],
175
+ 0
176
+ );
177
+
178
+ recordTaskOutcome(
179
+ trackers,
180
+ {
181
+ serviceName: "frontend",
182
+ suiteKey: "e2e:browser",
183
+ file: "tests/a.pw.testkit.ts",
184
+ },
185
+ {
186
+ failed: false,
187
+ durationMs: 3_000,
188
+ startedAt: 0,
189
+ finishedAt: 8_000,
190
+ error: null,
191
+ },
192
+ 8_000
193
+ );
194
+
195
+ const result = finalizeServiceResult(trackers.get("frontend"), 0, 10_000);
196
+
197
+ expect(result.durationMs).toBe(8_000);
198
+ expect(result.totalTaskDurationMs).toBe(3_000);
199
+ });
200
+
84
201
  it("builds run artifacts and formatting helpers", () => {
85
202
  const results = [
86
203
  {
@@ -96,6 +213,7 @@ describe("runner-results", () => {
96
213
  failedFileCount: 0,
97
214
  notRunFileCount: 0,
98
215
  durationMs: 1200,
216
+ totalTaskDurationMs: 2400,
99
217
  dbBackend: "local",
100
218
  suites: [],
101
219
  errors: [],
@@ -113,6 +231,7 @@ describe("runner-results", () => {
113
231
  failedFileCount: 0,
114
232
  notRunFileCount: 0,
115
233
  durationMs: 0,
234
+ totalTaskDurationMs: 0,
116
235
  dbBackend: null,
117
236
  suites: [],
118
237
  errors: [],
@@ -154,6 +273,8 @@ describe("runner-results", () => {
154
273
  failed: 0,
155
274
  notRun: 0,
156
275
  });
276
+ expect(artifact.services[0].durationMs).toBe(1200);
277
+ expect(artifact.services[0].totalTaskDurationMs).toBe(2400);
157
278
  expect(summarizeDbBackend(results)).toBe("local");
158
279
  expect(formatDuration(65_000)).toBe("1m 05s");
159
280
  expect(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "types": "./lib/index.d.ts",