@midscene/test 1.16.1 → 1.16.5
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/dist/es/{1~77.mjs → 1~731.mjs} +9 -9
- package/dist/es/1~731.mjs.map +1 -0
- package/dist/es/cli/index.mjs +342 -83
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +45 -1
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/test-cli.mjs +1532 -134
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/cli/index.js +340 -81
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +45 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/test-cli.js +1523 -131
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/templates/test/android/.env.example +1 -1
- package/dist/templates/test/android/README.md +1 -1
- package/dist/templates/test/android/README.zh.md +1 -1
- package/dist/templates/test/computer/.env.example +1 -1
- package/dist/templates/test/computer/README.md +1 -1
- package/dist/templates/test/computer/README.zh.md +1 -1
- package/dist/templates/test/harmony/.env.example +1 -1
- package/dist/templates/test/harmony/README.md +1 -1
- package/dist/templates/test/harmony/README.zh.md +1 -1
- package/dist/templates/test/ios/.env.example +1 -1
- package/dist/templates/test/ios/README.md +1 -1
- package/dist/templates/test/ios/README.zh.md +1 -1
- package/dist/templates/test/web/.env.example +1 -1
- package/dist/templates/test/web/README.md +1 -1
- package/dist/templates/test/web/README.zh.md +1 -1
- package/dist/types/cli/index.d.ts +75 -0
- package/dist/types/index.d.ts +3 -0
- package/package.json +14 -13
- package/dist/es/1~77.mjs.map +0 -1
package/dist/lib/test-cli.js
CHANGED
|
@@ -29,7 +29,7 @@ var __webpack_modules__ = {
|
|
|
29
29
|
},
|
|
30
30
|
"./package.json" (module) {
|
|
31
31
|
module.exports = {
|
|
32
|
-
rE: "1.16.
|
|
32
|
+
rE: "1.16.5"
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
};
|
|
@@ -93,6 +93,7 @@ var __webpack_exports__ = {};
|
|
|
93
93
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
94
94
|
var external_node_path_ = __webpack_require__("node:path");
|
|
95
95
|
const test_runner_namespaceObject = require("@midscene/core/internal/test-runner");
|
|
96
|
+
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
96
97
|
var package_0 = __webpack_require__("./package.json");
|
|
97
98
|
const external_node_util_namespaceObject = require("node:util");
|
|
98
99
|
const formatCliError = (error)=>'string' == typeof error ? error : (0, external_node_util_namespaceObject.inspect)(error, {
|
|
@@ -100,6 +101,1021 @@ var __webpack_exports__ = {};
|
|
|
100
101
|
depth: null,
|
|
101
102
|
customInspect: false
|
|
102
103
|
});
|
|
104
|
+
function formatConciseError(error) {
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
const messages = [];
|
|
107
|
+
let current = error;
|
|
108
|
+
while(current instanceof Error && !seen.has(current)){
|
|
109
|
+
seen.add(current);
|
|
110
|
+
if ('NODE_EXECUTION_ERROR' === current.code && void 0 !== current.cause) {
|
|
111
|
+
current = current.cause;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (current.message && !messages.includes(current.message)) messages.push(current.message);
|
|
115
|
+
if (current instanceof AggregateError) {
|
|
116
|
+
for (const nested of current.errors)if (!(nested === current || seen.has(nested))) {
|
|
117
|
+
seen.add(nested);
|
|
118
|
+
messages.push(nested instanceof Error ? nested.message : String(nested));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
current = current.cause;
|
|
122
|
+
}
|
|
123
|
+
if (!messages.length) return current instanceof Error ? current.message : formatCliError(current);
|
|
124
|
+
return messages.join('\nCaused by: ');
|
|
125
|
+
}
|
|
126
|
+
var external_chalk_ = __webpack_require__("chalk");
|
|
127
|
+
var external_chalk_default = /*#__PURE__*/ __webpack_require__.n(external_chalk_);
|
|
128
|
+
const external_string_width_namespaceObject = require("string-width");
|
|
129
|
+
var external_string_width_default = /*#__PURE__*/ __webpack_require__.n(external_string_width_namespaceObject);
|
|
130
|
+
const external_node_url_namespaceObject = require("node:url");
|
|
131
|
+
const oneLine = (value)=>(0, external_node_util_namespaceObject.stripVTControlCharacters)(value).replace(/\s+/g, ' ').trim();
|
|
132
|
+
const shorten = (value, length)=>{
|
|
133
|
+
const text = oneLine(value);
|
|
134
|
+
return text.length <= length ? text : `${text.slice(0, length - 1)}…`;
|
|
135
|
+
};
|
|
136
|
+
function firstBusinessFrame(error, projectRoot) {
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
const frames = [];
|
|
139
|
+
const visit = (value, depth = 0)=>{
|
|
140
|
+
if (!value || 'object' != typeof value || seen.has(value) || depth > 12) return;
|
|
141
|
+
seen.add(value);
|
|
142
|
+
const detail = value;
|
|
143
|
+
visit(detail.cause, depth + 1);
|
|
144
|
+
if (Array.isArray(detail.errors)) for (const child of detail.errors.slice(0, 10))visit(child, depth + 1);
|
|
145
|
+
if ('string' != typeof detail.stack) return;
|
|
146
|
+
for (const line of detail.stack.split('\n')){
|
|
147
|
+
if (!line.trim().startsWith('at ')) continue;
|
|
148
|
+
let location = line.trim().slice(3).replace(/^async /, '');
|
|
149
|
+
if (location.endsWith(')')) location = location.slice(location.lastIndexOf('(') + 1, -1);
|
|
150
|
+
const match = location.match(/^(.*):(\d+):(\d+)$/);
|
|
151
|
+
if (!match) continue;
|
|
152
|
+
let file = match[1];
|
|
153
|
+
if (file.startsWith('file://')) try {
|
|
154
|
+
file = (0, external_node_url_namespaceObject.fileURLToPath)(file);
|
|
155
|
+
} catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const normalized = file.replace(/\\/g, '/');
|
|
159
|
+
if (!(normalized.startsWith('node:') || normalized.includes('/node_modules/') || /(?:^|\/)packages\/(?:core|test|shared|web-integration)\/(?:dist|src)\//.test(normalized)) && /\.[cm]?[jt]sx?$/.test(normalized)) frames.push(`${file}:${match[2]}:${match[3]}`);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
visit(error);
|
|
163
|
+
const chosen = frames[0];
|
|
164
|
+
if (!chosen || !projectRoot) return chosen;
|
|
165
|
+
const match = chosen.match(/^(.*):(\d+):(\d+)$/);
|
|
166
|
+
const file = match[1];
|
|
167
|
+
const paths = /^[A-Za-z]:[\\/]/.test(file) ? external_node_path_.win32 : {
|
|
168
|
+
isAbsolute: external_node_path_.isAbsolute,
|
|
169
|
+
relative: external_node_path_.relative
|
|
170
|
+
};
|
|
171
|
+
const display = paths.isAbsolute(file) ? paths.relative(projectRoot, file) : file;
|
|
172
|
+
return `${display.replace(/\\/g, '/')}:${match[2]}:${match[3]}`;
|
|
173
|
+
}
|
|
174
|
+
function formatFailureInput(input) {
|
|
175
|
+
if (void 0 === input) return [];
|
|
176
|
+
const value = (0, test_runner_namespaceObject.sanitizeReportValue)(input).value;
|
|
177
|
+
const render = (value)=>shorten((0, test_runner_namespaceObject.sanitizeReportError)({
|
|
178
|
+
message: 'string' == typeof value ? value : (0, external_node_util_namespaceObject.inspect)(value, {
|
|
179
|
+
colors: false,
|
|
180
|
+
depth: 2,
|
|
181
|
+
maxArrayLength: 4,
|
|
182
|
+
maxStringLength: 180,
|
|
183
|
+
breakLength: 1 / 0,
|
|
184
|
+
customInspect: false,
|
|
185
|
+
getters: false
|
|
186
|
+
})
|
|
187
|
+
}).message, 160);
|
|
188
|
+
if (value && 'object' == typeof value && !Array.isArray(value)) {
|
|
189
|
+
const entries = Object.entries(value);
|
|
190
|
+
const rows = entries.slice(0, 4).map(([key, value])=>`${shorten(key, 48)}: ${render(value)}`);
|
|
191
|
+
if (entries.length > 4) rows.push(`… ${entries.length - 4} more fields (see Summary)`);
|
|
192
|
+
return rows;
|
|
193
|
+
}
|
|
194
|
+
return [
|
|
195
|
+
render(value)
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
function formatFailureDetails(source, error, context = {}) {
|
|
199
|
+
const title = context.projectName ? `[${oneLine(context.projectName)}] ${oneLine(context.caseName ?? context.sourcePath ?? context.phase ?? source)}` : oneLine(source);
|
|
200
|
+
const body = [];
|
|
201
|
+
const field = (label, value)=>body.push(` ${label.padEnd(9)}${value}`);
|
|
202
|
+
const step = context.step;
|
|
203
|
+
if (step) {
|
|
204
|
+
const phase = 'steps' === step.phase ? '' : `${step.phase} `;
|
|
205
|
+
const index = step.stepIndex + 1;
|
|
206
|
+
const position = void 0 === context.stepCount ? String(index) : `${index}/${Math.max(index, context.stepCount)}`;
|
|
207
|
+
field('Step', `${phase}${position} · ${oneLine(step.node)}`);
|
|
208
|
+
} else if (context.phase) field('Phase', oneLine(context.phase));
|
|
209
|
+
if (context.sourcePath) {
|
|
210
|
+
const location = step?.meta.sourceLocation;
|
|
211
|
+
field('Location', `${oneLine(context.sourcePath)}${location ? `:${location.line}` : ''}`);
|
|
212
|
+
}
|
|
213
|
+
for (const [label, attempt] of [
|
|
214
|
+
[
|
|
215
|
+
'Attempt',
|
|
216
|
+
context.attempt
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
'File try',
|
|
220
|
+
context.fileAttempt
|
|
221
|
+
]
|
|
222
|
+
])if (attempt && (attempt.total > 1 || attempt.index > 0)) field(label, `${attempt.index + 1}/${attempt.total}`);
|
|
223
|
+
if (body.length) body.push('');
|
|
224
|
+
const safeMessage = (0, test_runner_namespaceObject.sanitizeReportError)({
|
|
225
|
+
message: formatConciseError(error)
|
|
226
|
+
}).message;
|
|
227
|
+
const lines = (0, external_node_util_namespaceObject.stripVTControlCharacters)(safeMessage).split(/\r?\n/);
|
|
228
|
+
body.push(` Error: ${shorten(lines[0], 320)}`, ...lines.slice(1, 6).map((line)=>` ${shorten(line, 320)}`));
|
|
229
|
+
if (lines.length > 6) body.push(' … (use --reporter verbose for details)');
|
|
230
|
+
const input = step ? formatFailureInput(step.input) : [];
|
|
231
|
+
if (input.length) {
|
|
232
|
+
body.push('');
|
|
233
|
+
input.forEach((line, index)=>field(0 === index ? 'Input' : '', line));
|
|
234
|
+
}
|
|
235
|
+
const frame = firstBusinessFrame(error, context.projectRoot);
|
|
236
|
+
if (frame) body.push('', ` at ${oneLine(frame)}`);
|
|
237
|
+
return {
|
|
238
|
+
title,
|
|
239
|
+
body
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
var env_ = __webpack_require__("@midscene/shared/env");
|
|
243
|
+
function _define_property(obj, key, value) {
|
|
244
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
245
|
+
value: value,
|
|
246
|
+
enumerable: true,
|
|
247
|
+
configurable: true,
|
|
248
|
+
writable: true
|
|
249
|
+
});
|
|
250
|
+
else obj[key] = value;
|
|
251
|
+
return obj;
|
|
252
|
+
}
|
|
253
|
+
function configuredModelNames() {
|
|
254
|
+
const env = env_.globalConfigManager.getAllEnvConfig();
|
|
255
|
+
const defaultName = env.MIDSCENE_MODEL_NAME;
|
|
256
|
+
const models = new Map();
|
|
257
|
+
for (const [intent, name] of [
|
|
258
|
+
[
|
|
259
|
+
'default',
|
|
260
|
+
defaultName
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
'planning',
|
|
264
|
+
env.MIDSCENE_PLANNING_MODEL_NAME || defaultName
|
|
265
|
+
],
|
|
266
|
+
[
|
|
267
|
+
'insight',
|
|
268
|
+
env.MIDSCENE_INSIGHT_MODEL_NAME || defaultName
|
|
269
|
+
]
|
|
270
|
+
]){
|
|
271
|
+
if (!name) continue;
|
|
272
|
+
const intents = models.get(name) ?? [];
|
|
273
|
+
intents.push(intent);
|
|
274
|
+
models.set(name, intents);
|
|
275
|
+
}
|
|
276
|
+
return [
|
|
277
|
+
...models
|
|
278
|
+
].map(([name, intents])=>({
|
|
279
|
+
name,
|
|
280
|
+
intents
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
class CliModelUsage {
|
|
284
|
+
add(step) {
|
|
285
|
+
for (const trace of step.report?.traces ?? [])if (trace.metrics) {
|
|
286
|
+
this.executions.set(trace.executionId, trace.metrics);
|
|
287
|
+
this.missing.delete(trace.executionId);
|
|
288
|
+
} else if (!this.executions.has(trace.executionId)) this.missing.add(trace.executionId);
|
|
289
|
+
}
|
|
290
|
+
snapshot() {
|
|
291
|
+
if (!this.executions.size && !this.missing.size) return;
|
|
292
|
+
return (0, test_runner_namespaceObject.sumTestRunReportMetrics)([
|
|
293
|
+
...this.executions.values()
|
|
294
|
+
], this.missing.size ? [
|
|
295
|
+
'Some execution usage has not been recorded.'
|
|
296
|
+
] : []);
|
|
297
|
+
}
|
|
298
|
+
constructor(){
|
|
299
|
+
_define_property(this, "executions", new Map());
|
|
300
|
+
_define_property(this, "missing", new Set());
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function formatTokenCount(value) {
|
|
304
|
+
if (value < 1000) return String(Math.round(value));
|
|
305
|
+
if (value < 1000000) return `${(value / 1000).toFixed(1)}k`;
|
|
306
|
+
return `${(value / 1000000).toFixed(1)}m`;
|
|
307
|
+
}
|
|
308
|
+
function formatModelUsage(metrics) {
|
|
309
|
+
if (!metrics) return [
|
|
310
|
+
'—'
|
|
311
|
+
];
|
|
312
|
+
if (!metrics.models?.length) {
|
|
313
|
+
const name = metrics.modelCallCount || metrics.collection?.status === 'unknown' ? 'unknown' : '—';
|
|
314
|
+
return [
|
|
315
|
+
`${name} · ${formatTokenUsage(metrics)}`
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
return metrics.models.map((model)=>{
|
|
319
|
+
const name = (0, external_node_util_namespaceObject.stripVTControlCharacters)(model.modelName).replace(/\s+/g, ' ').trim() || 'unknown';
|
|
320
|
+
const usage = formatTokenUsage({
|
|
321
|
+
...metrics,
|
|
322
|
+
modelCallCount: model.callCount,
|
|
323
|
+
promptTokens: model.promptTokens,
|
|
324
|
+
cachedInputTokens: model.cachedInputTokens,
|
|
325
|
+
completionTokens: model.completionTokens,
|
|
326
|
+
totalTokens: model.totalTokens
|
|
327
|
+
});
|
|
328
|
+
return `${name} · ${usage}`;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function formatTokenUsage(metrics, includeCached = true) {
|
|
332
|
+
if (!metrics) return '—';
|
|
333
|
+
const state = (field)=>metrics.collection?.fields?.[field] ?? metrics.collection?.status ?? 'complete';
|
|
334
|
+
if ('unknown' === state('totalTokens')) return '— (usage unavailable)';
|
|
335
|
+
if (!metrics.modelCallCount && !metrics.totalTokens && 'complete' === state('totalTokens')) return '0 (no model calls)';
|
|
336
|
+
const count = (field)=>'unknown' === state(field) ? '—' : `${'partial' === state(field) ? '≥' : ''}${formatTokenCount(metrics[field])}`;
|
|
337
|
+
return [
|
|
338
|
+
`${count('totalTokens')} total${'partial' === state('totalTokens') ? ' (partial)' : ''}`,
|
|
339
|
+
`${count('promptTokens')} input${includeCached && metrics.cachedInputTokens ? ` (${formatTokenCount(metrics.cachedInputTokens)} cached)` : ''}`,
|
|
340
|
+
`${count('completionTokens')} output`
|
|
341
|
+
].join(' · ');
|
|
342
|
+
}
|
|
343
|
+
function terminal_progress_define_property(obj, key, value) {
|
|
344
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
345
|
+
value: value,
|
|
346
|
+
enumerable: true,
|
|
347
|
+
configurable: true,
|
|
348
|
+
writable: true
|
|
349
|
+
});
|
|
350
|
+
else obj[key] = value;
|
|
351
|
+
return obj;
|
|
352
|
+
}
|
|
353
|
+
const graphemes = new Intl.Segmenter(void 0, {
|
|
354
|
+
granularity: 'grapheme'
|
|
355
|
+
});
|
|
356
|
+
const SYNC_START = '\u001b[?2026h';
|
|
357
|
+
const SYNC_END = '\u001b[?2026l';
|
|
358
|
+
const truncateTerminalLine = (text, columns)=>{
|
|
359
|
+
if (columns <= 0) return '';
|
|
360
|
+
if (external_string_width_default()(text) <= columns) return text;
|
|
361
|
+
let output = '';
|
|
362
|
+
let width = 0;
|
|
363
|
+
for (const { segment } of graphemes.segment((0, external_node_util_namespaceObject.stripVTControlCharacters)(text))){
|
|
364
|
+
width += external_string_width_default()(segment);
|
|
365
|
+
if (width > columns - 1) break;
|
|
366
|
+
output += segment;
|
|
367
|
+
}
|
|
368
|
+
return `${output}…`;
|
|
369
|
+
};
|
|
370
|
+
class TerminalProgress {
|
|
371
|
+
erase() {
|
|
372
|
+
if (!this.lines.length) return '';
|
|
373
|
+
const columns = Math.max(1, this.io.columns ?? 100);
|
|
374
|
+
const occupiedRows = this.lines.reduce((sum, line)=>sum + Math.max(1, Math.ceil(external_string_width_default()(line) / columns)), 0);
|
|
375
|
+
const rows = Math.min(occupiedRows, Math.max(0, (this.io.rows ?? 24) - 1));
|
|
376
|
+
this.lines = [];
|
|
377
|
+
return `\r${rows ? `\u001b[${rows}A` : ''}\u001b[0J`;
|
|
378
|
+
}
|
|
379
|
+
frame(previousHeight = 0) {
|
|
380
|
+
const availableRows = Math.max(0, (this.io.rows ?? 24) - 2);
|
|
381
|
+
const columns = Math.max(0, (this.io.columns ?? 100) - 1);
|
|
382
|
+
if (!availableRows || !columns) return '';
|
|
383
|
+
const footer = this.footer.slice(0, availableRows);
|
|
384
|
+
const budget = availableRows - footer.length;
|
|
385
|
+
const overflow = this.content.length > budget;
|
|
386
|
+
const visible = Math.max(0, budget - (overflow ? 1 : 0));
|
|
387
|
+
const body = this.content.slice(0, visible);
|
|
388
|
+
if (overflow && budget > 0) body.push(` … ${this.content.length - visible} more rows`);
|
|
389
|
+
const lines = [
|
|
390
|
+
...body,
|
|
391
|
+
...footer
|
|
392
|
+
].map((line)=>truncateTerminalLine(line, columns));
|
|
393
|
+
const blanks = Math.max(0, Math.min(previousHeight, availableRows) - lines.length);
|
|
394
|
+
this.lines = [
|
|
395
|
+
...Array.from({
|
|
396
|
+
length: blanks
|
|
397
|
+
}, ()=>''),
|
|
398
|
+
...lines
|
|
399
|
+
];
|
|
400
|
+
return this.lines.length ? `${this.lines.join('\n')}\n` : '';
|
|
401
|
+
}
|
|
402
|
+
render(content, footer) {
|
|
403
|
+
this.update(content, footer);
|
|
404
|
+
const previousHeight = this.lines.length;
|
|
405
|
+
const erase = this.erase();
|
|
406
|
+
this.io.write?.(`${SYNC_START}${erase}${this.frame(previousHeight)}${SYNC_END}`);
|
|
407
|
+
}
|
|
408
|
+
update(content, footer) {
|
|
409
|
+
this.content = content;
|
|
410
|
+
this.footer = footer;
|
|
411
|
+
}
|
|
412
|
+
output(message, write) {
|
|
413
|
+
this.io.write?.(SYNC_START);
|
|
414
|
+
try {
|
|
415
|
+
const erase = this.erase();
|
|
416
|
+
if (erase) this.io.write?.(erase);
|
|
417
|
+
write(message.endsWith('\n') ? message : `${message}\n`);
|
|
418
|
+
const frame = this.frame();
|
|
419
|
+
if (frame) this.io.write?.(frame);
|
|
420
|
+
} finally{
|
|
421
|
+
this.io.write?.(SYNC_END);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
clear() {
|
|
425
|
+
const erase = this.erase();
|
|
426
|
+
this.content = [];
|
|
427
|
+
this.footer = [];
|
|
428
|
+
if (erase) this.io.write?.(`${SYNC_START}${erase}${SYNC_END}`);
|
|
429
|
+
}
|
|
430
|
+
constructor(io){
|
|
431
|
+
terminal_progress_define_property(this, "io", void 0);
|
|
432
|
+
terminal_progress_define_property(this, "lines", void 0);
|
|
433
|
+
terminal_progress_define_property(this, "content", void 0);
|
|
434
|
+
terminal_progress_define_property(this, "footer", void 0);
|
|
435
|
+
this.io = io;
|
|
436
|
+
this.lines = [];
|
|
437
|
+
this.content = [];
|
|
438
|
+
this.footer = [];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const warning_format_oneLine = (text)=>(0, external_node_util_namespaceObject.stripVTControlCharacters)(text).replace(/\s+/g, ' ').trim();
|
|
442
|
+
function formatWarningScope(scope) {
|
|
443
|
+
if (!scope) return '';
|
|
444
|
+
const parts = [
|
|
445
|
+
`[${warning_format_oneLine(scope.projectName)}]`
|
|
446
|
+
];
|
|
447
|
+
if (scope.sourcePath) parts.push(warning_format_oneLine(scope.sourcePath));
|
|
448
|
+
if (scope.caseName) parts.push(warning_format_oneLine(scope.caseName));
|
|
449
|
+
if (scope.phase) {
|
|
450
|
+
const phase = 'steps' === scope.phase ? 'step' : scope.phase;
|
|
451
|
+
parts.push(`${phase}${void 0 === scope.stepIndex ? '' : `[${scope.stepIndex + 1}]`}${scope.node ? ` ${warning_format_oneLine(scope.node)}` : ''}`);
|
|
452
|
+
}
|
|
453
|
+
return `${parts.join(' › ')} · `;
|
|
454
|
+
}
|
|
455
|
+
function errorMessage(error) {
|
|
456
|
+
const seen = new Set();
|
|
457
|
+
let current = error;
|
|
458
|
+
let message = '';
|
|
459
|
+
let status;
|
|
460
|
+
while(current && 'object' == typeof current && !seen.has(current)){
|
|
461
|
+
seen.add(current);
|
|
462
|
+
const detail = current;
|
|
463
|
+
if ('string' == typeof detail.message) message = detail.message;
|
|
464
|
+
if ('number' == typeof detail.status) status = detail.status;
|
|
465
|
+
current = detail.cause;
|
|
466
|
+
}
|
|
467
|
+
const firstLine = warning_format_oneLine((message || String(error)).split(/\r?\n/)[0]);
|
|
468
|
+
return status && !firstLine.startsWith(String(status)) ? `${status}: ${firstLine}` : firstLine;
|
|
469
|
+
}
|
|
470
|
+
function formatWarning(entry) {
|
|
471
|
+
const details = entry.warning;
|
|
472
|
+
let message;
|
|
473
|
+
if (details?.kind === 'model-request-retry') message = `model request retry ${details.attempt + 1}/${details.maxAttempts} in ${details.delayMs / 1000}s (${warning_format_oneLine(details.modelName)}): ${errorMessage(details.error)}`;
|
|
474
|
+
else {
|
|
475
|
+
const error = entry.args?.find((arg)=>arg instanceof Error);
|
|
476
|
+
const prefix = 'string' == typeof entry.args?.[0] ? warning_format_oneLine(entry.args[0].split(/\r?\n/)[0]).replace(/:\s*$/, '') : '';
|
|
477
|
+
message = error ? `${prefix ? `${prefix}: ` : ''}${errorMessage(error)}` : warning_format_oneLine(entry.message.split(/\r?\n/)[0]);
|
|
478
|
+
message = message.replace(/^\[midscene:warning\]\s*/, '');
|
|
479
|
+
}
|
|
480
|
+
if (message.startsWith('waitForNetworkIdle is skipped for Playwright.')) message = 'Playwright: post-action network-idle wait skipped.';
|
|
481
|
+
return message.length > 240 ? `${message.slice(0, 239)}…` : message;
|
|
482
|
+
}
|
|
483
|
+
function warningKey(entry, message) {
|
|
484
|
+
const details = entry.warning;
|
|
485
|
+
if (details?.kind === 'model-request-retry') return `${entry.topic}\n${JSON.stringify(entry.scope)}\n${details.callId}\n${details.attempt}`;
|
|
486
|
+
const scope = entry.args?.some((arg)=>arg instanceof Error) ? JSON.stringify(entry.scope) : '';
|
|
487
|
+
return `${entry.topic}\n${scope}\n${message}`;
|
|
488
|
+
}
|
|
489
|
+
function terminal_reporter_define_property(obj, key, value) {
|
|
490
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
491
|
+
value: value,
|
|
492
|
+
enumerable: true,
|
|
493
|
+
configurable: true,
|
|
494
|
+
writable: true
|
|
495
|
+
});
|
|
496
|
+
else obj[key] = value;
|
|
497
|
+
return obj;
|
|
498
|
+
}
|
|
499
|
+
const terminal_reporter_oneLine = (value)=>(0, external_node_util_namespaceObject.stripVTControlCharacters)(value).replace(/\s+/g, ' ').trim();
|
|
500
|
+
const formatDuration = (ms)=>{
|
|
501
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
502
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
503
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor(ms % 60000 / 1000)}s`;
|
|
504
|
+
};
|
|
505
|
+
const identity = (project, path, name)=>`[${terminal_reporter_oneLine(project)}] ${terminal_reporter_oneLine(path)}${name ? ` › ${terminal_reporter_oneLine(name)}` : ''}`;
|
|
506
|
+
const projectPhaseLabel = (phase)=>'setup' === phase ? 'setting up' : 'cleaning up';
|
|
507
|
+
const fileStatus = (status, cases)=>{
|
|
508
|
+
if ('failed' === status || cases.some((item)=>'failed' === item.status)) return 'failed';
|
|
509
|
+
if (cases.length && cases.every((item)=>'not-run' === item.status)) return 'not-run';
|
|
510
|
+
if (cases.some((item)=>'not-run' === item.status)) return 'failed';
|
|
511
|
+
return status ?? (cases.length ? 'success' : 'not-run');
|
|
512
|
+
};
|
|
513
|
+
class TerminalReporter {
|
|
514
|
+
get interactive() {
|
|
515
|
+
return !!this.io.isTTY && !!this.io.write && 'default' === this.mode;
|
|
516
|
+
}
|
|
517
|
+
log(message) {
|
|
518
|
+
if (this.interactive) this.output(`${message}\n`, (text)=>this.io.write(text));
|
|
519
|
+
else this.io.log(message);
|
|
520
|
+
this.lastOutput = Date.now();
|
|
521
|
+
}
|
|
522
|
+
output(message, write) {
|
|
523
|
+
this.renderProgress(false);
|
|
524
|
+
this.progressView.output(message, write);
|
|
525
|
+
this.lastOutput = Date.now();
|
|
526
|
+
}
|
|
527
|
+
progress(message) {
|
|
528
|
+
if ('verbose' === this.mode) this.log(message);
|
|
529
|
+
}
|
|
530
|
+
console(entry) {
|
|
531
|
+
if ('report' === entry.type) {
|
|
532
|
+
if ('verbose' === this.mode) this.log(entry.message);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const scope = formatWarningScope(entry.scope);
|
|
536
|
+
if ('verbose' === this.mode) {
|
|
537
|
+
const kind = entry.warning?.kind === 'model-request-retry' ? 'model request retry · ' : entry.warning?.kind === 'model-request-detail' ? 'model request error · ' : '';
|
|
538
|
+
this.log(`WARN ${scope}${kind}${entry.message}`);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (entry.warning?.kind === 'model-request-detail') return;
|
|
542
|
+
const message = formatWarning(entry);
|
|
543
|
+
const key = warningKey(entry, message);
|
|
544
|
+
if (this.warnings.has(key)) return;
|
|
545
|
+
this.warnings.set(key, {
|
|
546
|
+
scope,
|
|
547
|
+
message
|
|
548
|
+
});
|
|
549
|
+
if (this.interactive) {
|
|
550
|
+
let attached = false;
|
|
551
|
+
if (entry.warning?.kind === 'model-request-retry' && entry.scope) {
|
|
552
|
+
const owner = entry.scope;
|
|
553
|
+
for (const [id, task] of this.active)if (task.file.projectName === owner.projectName && (!owner.sourcePath || task.file.sourcePath === owner.sourcePath) && (!owner.caseId || id === owner.caseId) && (!owner.runId || task.runId === owner.runId) && (!owner.phase || task.phase === owner.phase) && (void 0 === owner.stepIndex || task.stepIndex === owner.stepIndex)) {
|
|
554
|
+
task.warning = message;
|
|
555
|
+
attached = true;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (!attached) this.latestNotice = message.startsWith('Playwright:') ? 'Playwright: network-idle wait skipped.' : `${scope}${message}`;
|
|
559
|
+
this.schedule();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
this.log(`${this.color.yellow('WARN')} ${this.color.dim(scope)}${message}`);
|
|
563
|
+
}
|
|
564
|
+
file(projectName, sourcePath, total = 0) {
|
|
565
|
+
const key = `${projectName}\0${sourcePath}`;
|
|
566
|
+
let file = this.files.get(key);
|
|
567
|
+
if (!file) {
|
|
568
|
+
file = {
|
|
569
|
+
projectName,
|
|
570
|
+
sourcePath,
|
|
571
|
+
total,
|
|
572
|
+
outcomes: new Map(),
|
|
573
|
+
attemptOutcomes: new Map(),
|
|
574
|
+
started: false,
|
|
575
|
+
printed: false,
|
|
576
|
+
durationMs: 0,
|
|
577
|
+
retries: 0,
|
|
578
|
+
completionOrder: 0
|
|
579
|
+
};
|
|
580
|
+
this.files.set(key, file);
|
|
581
|
+
}
|
|
582
|
+
file.total = Math.max(file.total, total);
|
|
583
|
+
return file;
|
|
584
|
+
}
|
|
585
|
+
event(event) {
|
|
586
|
+
if ('run-start' === event.type) {
|
|
587
|
+
this.total = event.cases;
|
|
588
|
+
this.totalFiles = event.documents;
|
|
589
|
+
this.startedAt = event.startedAt ? Date.parse(event.startedAt) : Date.now();
|
|
590
|
+
for (const file of event.files ?? [])this.file(file.projectName, file.sourcePath, file.cases);
|
|
591
|
+
const c = this.color;
|
|
592
|
+
this.log(`\n ${c.bgGreen.black.bold(' RUN ')} ${c.green(`midscene-test v${package_0.rE}`)} ${c.dim(terminal_reporter_oneLine(event.projectRoot ?? process.cwd()))}`);
|
|
593
|
+
this.log(` ${c.dim(`${event.projects} projects · ${event.documents} files · ${event.cases} cases`)}`);
|
|
594
|
+
if (void 0 !== event.maxConcurrentProjects && void 0 !== event.maxConcurrentFiles) this.log(` ${c.dim(`Concurrency ${event.maxConcurrentProjects ?? 'unlimited'} projects · ${event.maxConcurrentFiles ?? 'unlimited'} files (global limits)`)}`);
|
|
595
|
+
if (event.configPath) this.log(` ${c.dim(`Config ${terminal_reporter_oneLine(event.configPath)}`)}`);
|
|
596
|
+
for (const model of event.configuredModels ?? [])this.log(` ${c.dim('Models (env)')} ${terminal_reporter_oneLine(model.name)} ${c.dim(`· ${model.intents.map(terminal_reporter_oneLine).join(', ')}`)}`);
|
|
597
|
+
this.log('');
|
|
598
|
+
if ('default' === this.mode) {
|
|
599
|
+
this.timer = setInterval(()=>this.tick(), this.interactive ? 1000 : 30000);
|
|
600
|
+
this.timer.unref();
|
|
601
|
+
if (this.interactive) this.removeResizeListener = this.io.onResize?.(()=>this.renderProgress());
|
|
602
|
+
this.renderProgress();
|
|
603
|
+
}
|
|
604
|
+
} else if ('project-end' === event.type) {
|
|
605
|
+
this.completeProject(event.project);
|
|
606
|
+
this.schedule();
|
|
607
|
+
} else if ('project-phase' === event.type) {
|
|
608
|
+
if ('running' === event.phase) this.projectPhases.delete(event.projectName);
|
|
609
|
+
else this.projectPhases.set(event.projectName, {
|
|
610
|
+
phase: event.phase,
|
|
611
|
+
since: Date.now()
|
|
612
|
+
});
|
|
613
|
+
this.schedule();
|
|
614
|
+
} else if ('document-start' === event.type) {
|
|
615
|
+
this.file(event.projectName, event.sourcePath, event.cases).started = true;
|
|
616
|
+
this.schedule();
|
|
617
|
+
} else if ('document-end' === event.type) {
|
|
618
|
+
const file = this.file(event.document.projectName, event.document.sourcePath, event.cases.length);
|
|
619
|
+
file.status = fileStatus(event.document.status, event.cases);
|
|
620
|
+
file.started = true;
|
|
621
|
+
file.completionOrder = ++this.completionOrder;
|
|
622
|
+
file.durationMs = event.durationMs;
|
|
623
|
+
file.retries = event.document.attemptIndex ?? 0;
|
|
624
|
+
file.outcomes = new Map(event.cases.map((outcome)=>[
|
|
625
|
+
outcome.caseId,
|
|
626
|
+
outcome
|
|
627
|
+
]));
|
|
628
|
+
file.attemptOutcomes.clear();
|
|
629
|
+
for (const [key, task] of this.active)if (task.file === file) this.active.delete(key);
|
|
630
|
+
if (!this.interactive) this.printFile(file);
|
|
631
|
+
this.schedule();
|
|
632
|
+
} else if ('step-start' === event.type) this.startStep(event.projectName, event.info);
|
|
633
|
+
else if ('step-end' === event.type) {
|
|
634
|
+
if (event.result) this.usage.add(event.result);
|
|
635
|
+
const key = 'case' === event.info.scope ? event.info.case.caseId : event.info.document.documentId;
|
|
636
|
+
const task = this.active.get(key);
|
|
637
|
+
if (task) if ('document' === event.info.scope) this.active.delete(key);
|
|
638
|
+
else {
|
|
639
|
+
task.step = void 0;
|
|
640
|
+
task.warning = void 0;
|
|
641
|
+
}
|
|
642
|
+
this.schedule();
|
|
643
|
+
} else if ('case-attempt-end' === event.type) this.attempts.set(event.attempt.caseId, {
|
|
644
|
+
run: event.attempt,
|
|
645
|
+
max: event.maxAttempts
|
|
646
|
+
});
|
|
647
|
+
else if ('case-end' === event.type || 'case-progress' === event.type) {
|
|
648
|
+
const file = this.file(event.outcome.projectName, event.outcome.sourcePath);
|
|
649
|
+
if ('case-progress' === event.type) {
|
|
650
|
+
file.attemptOutcomes.set(event.outcome.caseId, event.outcome);
|
|
651
|
+
file.retries = Math.max(file.retries, event.documentAttemptIndex);
|
|
652
|
+
} else {
|
|
653
|
+
file.attemptOutcomes.delete(event.outcome.caseId);
|
|
654
|
+
file.outcomes.set(event.outcome.caseId, event.outcome);
|
|
655
|
+
file.retries = Math.max(file.retries, event.documentRetries ?? 0);
|
|
656
|
+
}
|
|
657
|
+
this.active.delete(event.outcome.caseId);
|
|
658
|
+
this.schedule();
|
|
659
|
+
} else if ('document-retry' === event.type) {
|
|
660
|
+
const file = this.file(event.document.projectName, event.document.sourcePath);
|
|
661
|
+
file.outcomes.clear();
|
|
662
|
+
file.attemptOutcomes.clear();
|
|
663
|
+
file.status = void 0;
|
|
664
|
+
file.completionOrder = 0;
|
|
665
|
+
for (const [key, task] of this.active)if (task.file === file) this.active.delete(key);
|
|
666
|
+
this.log(this.color.yellow(` ↻ ${identity(file.projectName, file.sourcePath)} · file retry ${(event.document.attemptIndex ?? 0) + 2}/${event.maxAttempts}`));
|
|
667
|
+
this.schedule();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
startStep(projectName, info) {
|
|
671
|
+
const position = 'case' === info.scope ? info.case : info.document;
|
|
672
|
+
const file = this.file(projectName, position.sourcePath);
|
|
673
|
+
file.started = true;
|
|
674
|
+
const key = 'case' === info.scope ? info.case.caseId : info.document.documentId;
|
|
675
|
+
const attempt = 'case' === info.scope ? info.case.attemptIndex : 0;
|
|
676
|
+
const previous = this.attempts.get(key);
|
|
677
|
+
if (previous && previous.run.attemptIndex < attempt) {
|
|
678
|
+
this.attempts.delete(key);
|
|
679
|
+
if ('default' === this.mode && !this.interactive) this.log(this.color.yellow(` ↻ ${identity(projectName, position.sourcePath, 'case' === info.scope ? info.case.name : void 0)} · case retry · attempt ${attempt + 1}/${previous.max}`));
|
|
680
|
+
}
|
|
681
|
+
const existing = this.active.get(key);
|
|
682
|
+
const now = Date.now();
|
|
683
|
+
this.active.set(key, {
|
|
684
|
+
file,
|
|
685
|
+
name: 'case' === info.scope ? info.case.name : void 0,
|
|
686
|
+
step: `${'steps' === position.phase ? 'step' : position.phase} ${position.stepIndex + 1}/${info.stepCount} · ${terminal_reporter_oneLine(info.node)}`,
|
|
687
|
+
since: existing?.attempt === attempt ? existing.since : now,
|
|
688
|
+
attempt,
|
|
689
|
+
maxAttempts: previous?.max ?? existing?.maxAttempts,
|
|
690
|
+
runId: 'case' === info.scope ? info.case.runId : info.document.documentRunId,
|
|
691
|
+
phase: position.phase,
|
|
692
|
+
stepIndex: position.stepIndex,
|
|
693
|
+
caseIndex: 'case' === info.scope ? info.case.caseIndex : void 0
|
|
694
|
+
});
|
|
695
|
+
this.schedule();
|
|
696
|
+
}
|
|
697
|
+
schedule() {
|
|
698
|
+
if (!this.interactive || !this.timer || this.scheduled) return;
|
|
699
|
+
this.scheduled = setTimeout(()=>{
|
|
700
|
+
this.scheduled = void 0;
|
|
701
|
+
this.renderProgress();
|
|
702
|
+
}, 100);
|
|
703
|
+
this.scheduled.unref();
|
|
704
|
+
}
|
|
705
|
+
counts(items, total) {
|
|
706
|
+
const failed = items.filter((item)=>'failed' === item.status).length;
|
|
707
|
+
const passed = items.filter((item)=>'success' === item.status).length;
|
|
708
|
+
const notRun = items.filter((item)=>'not-run' === item.status).length;
|
|
709
|
+
return this.stateCounts(passed, failed, notRun, total);
|
|
710
|
+
}
|
|
711
|
+
stateCounts(passed, failed, notRun, total) {
|
|
712
|
+
const c = this.color;
|
|
713
|
+
return [
|
|
714
|
+
failed ? c.red.bold(`${failed} failed`) : '',
|
|
715
|
+
c.green.bold(`${passed} passed`),
|
|
716
|
+
notRun ? c.yellow(`${notRun} not run`) : ''
|
|
717
|
+
].filter(Boolean).join(c.dim(' | ')) + c.dim(` (${total})`);
|
|
718
|
+
}
|
|
719
|
+
footer(durationMs = Date.now() - this.startedAt, result) {
|
|
720
|
+
const files = [
|
|
721
|
+
...this.files.values()
|
|
722
|
+
];
|
|
723
|
+
const cases = files.flatMap((file)=>[
|
|
724
|
+
...file.outcomes.values()
|
|
725
|
+
]);
|
|
726
|
+
const c = this.color;
|
|
727
|
+
const metrics = result ? this.finalMetrics : this.usage.snapshot();
|
|
728
|
+
return [
|
|
729
|
+
` ${c.dim('Test Files')} ${this.counts(files, this.totalFiles)}`,
|
|
730
|
+
` ${c.dim(' Cases')} ${result ? this.stateCounts(result.summary.passed, result.summary.failed, result.summary.notRun, result.summary.total) : this.counts(cases, this.total)}`,
|
|
731
|
+
` ${c.dim(' Start at')} ${new Date(this.startedAt).toLocaleTimeString('en-GB', {
|
|
732
|
+
hour12: false
|
|
733
|
+
})}`,
|
|
734
|
+
` ${c.dim(' Duration')} ${formatDuration(durationMs)}`,
|
|
735
|
+
...formatModelUsage(metrics).map((model, index)=>` ${c.dim(index ? ' ' : ' Models:')} ${model}`),
|
|
736
|
+
...!result || this.warnings.size ? [
|
|
737
|
+
` ${c.dim(' Warnings')} ${this.warnings.size ? c.yellow(String(this.warnings.size)) : c.dim('0')}${!result && this.latestNotice ? c.dim(` · ${this.latestNotice}`) : ''}`
|
|
738
|
+
] : []
|
|
739
|
+
];
|
|
740
|
+
}
|
|
741
|
+
renderProgress(render = true) {
|
|
742
|
+
if (!this.interactive || !this.timer) return;
|
|
743
|
+
const projects = new Map();
|
|
744
|
+
const files = [
|
|
745
|
+
...this.files.values()
|
|
746
|
+
].filter((file)=>file.started && !file.printed).sort((a, b)=>a.projectName.localeCompare(b.projectName) || a.sourcePath.localeCompare(b.sourcePath));
|
|
747
|
+
for (const file of files){
|
|
748
|
+
const group = projects.get(file.projectName) ?? [];
|
|
749
|
+
group.push(file);
|
|
750
|
+
projects.set(file.projectName, group);
|
|
751
|
+
}
|
|
752
|
+
for (const projectName of this.projectPhases.keys())if (!projects.has(projectName)) projects.set(projectName, []);
|
|
753
|
+
const footer = this.footer();
|
|
754
|
+
const budget = Math.max(0, (this.io.rows ?? 24) - 2 - footer.length - 1);
|
|
755
|
+
let lines = this.liveTree(projects, 'full');
|
|
756
|
+
if (lines.length > budget) lines = this.liveTree(projects, 'recent');
|
|
757
|
+
if (lines.length > budget) lines = this.liveTree(projects, 'compact');
|
|
758
|
+
if (lines.length > budget) lines = this.liveTree(projects, 'active');
|
|
759
|
+
if (lines.length) lines.push('');
|
|
760
|
+
if (render) this.progressView.render(lines, footer);
|
|
761
|
+
else this.progressView.update(lines, footer);
|
|
762
|
+
}
|
|
763
|
+
liveTree(projects, mode) {
|
|
764
|
+
const c = this.color;
|
|
765
|
+
const lines = [];
|
|
766
|
+
const groups = [
|
|
767
|
+
...projects
|
|
768
|
+
];
|
|
769
|
+
const isActive = ([projectName, files])=>this.projectPhases.has(projectName) || files.some((file)=>!file.status);
|
|
770
|
+
if ('full' !== mode) groups.sort((a, b)=>Number(isActive(b)) - Number(isActive(a)));
|
|
771
|
+
for (const [projectName, files] of groups){
|
|
772
|
+
const phase = this.projectPhases.get(projectName);
|
|
773
|
+
const running = files.filter((file)=>!file.status);
|
|
774
|
+
const completed = files.filter((file)=>file.status);
|
|
775
|
+
const recent = [
|
|
776
|
+
...completed
|
|
777
|
+
].sort((a, b)=>b.completionOrder - a.completionOrder).slice(0, 'recent' === mode ? 1 : 0);
|
|
778
|
+
const visible = 'full' === mode ? files : [
|
|
779
|
+
...running,
|
|
780
|
+
...recent
|
|
781
|
+
];
|
|
782
|
+
const hidden = completed.filter((file)=>!visible.includes(file));
|
|
783
|
+
const hiddenFailures = hidden.filter((file)=>'failed' === file.status).length;
|
|
784
|
+
const details = [
|
|
785
|
+
phase ? c.yellow(`${projectPhaseLabel(phase.phase)} ${formatDuration(Date.now() - phase.since)}`) : '',
|
|
786
|
+
c.yellow(`${running.length} ${1 === running.length ? 'file' : 'files'} running`),
|
|
787
|
+
completed.length ? c.dim(`${completed.length} completed`) : '',
|
|
788
|
+
'active' === mode && hiddenFailures ? c.red(`${hiddenFailures} failed`) : ''
|
|
789
|
+
].filter(Boolean);
|
|
790
|
+
lines.push(this.liveRow(` ${c.yellow('❯')} ${c.bold(terminal_reporter_oneLine(projectName))}`, c.dim(' · ') + details.join(c.dim(' · '))));
|
|
791
|
+
for (const [fileIndex, file] of visible.entries()){
|
|
792
|
+
const lastFile = fileIndex === visible.length - 1 && (!hidden.length || 'active' === mode);
|
|
793
|
+
const stem = lastFile ? ' ' : ` ${c.dim('│')} `;
|
|
794
|
+
const outcomes = [
|
|
795
|
+
...new Map([
|
|
796
|
+
...file.attemptOutcomes,
|
|
797
|
+
...file.outcomes
|
|
798
|
+
]).values()
|
|
799
|
+
].sort((a, b)=>a.caseIndex - b.caseIndex);
|
|
800
|
+
const failedCases = outcomes.filter((outcome)=>'failed' === outcome.status).length;
|
|
801
|
+
lines.push(this.liveRow(` ${c.dim(lastFile ? '└─' : '├─')} ${file.status ? `${this.statusSymbol(file.status)} ` : ''}${terminal_reporter_oneLine(file.sourcePath)}`, ` ${c.dim(`${outcomes.length}/${file.total} cases`)}${file.status ? c.dim(` ${formatDuration(file.durationMs)}`) : ''}${'active' === mode && failedCases ? c.red(` · ${failedCases} failed`) : ''}`));
|
|
802
|
+
const tasks = [
|
|
803
|
+
...this.active.values()
|
|
804
|
+
].filter((task)=>task.file === file);
|
|
805
|
+
const shown = 'full' === mode ? outcomes : 'recent' === mode ? outcomes.slice(-1) : [];
|
|
806
|
+
const children = [
|
|
807
|
+
...tasks.map((task)=>({
|
|
808
|
+
task
|
|
809
|
+
})),
|
|
810
|
+
...shown.map((outcome)=>({
|
|
811
|
+
outcome
|
|
812
|
+
}))
|
|
813
|
+
];
|
|
814
|
+
if ('full' === mode) children.sort((a, b)=>this.liveChildIndex(a) - this.liveChildIndex(b));
|
|
815
|
+
const hiddenCases = outcomes.filter((outcome)=>!shown.includes(outcome));
|
|
816
|
+
if (hiddenCases.length && 'active' !== mode) {
|
|
817
|
+
const failed = hiddenCases.filter((outcome)=>'failed' === outcome.status).length;
|
|
818
|
+
children.push({
|
|
819
|
+
summary: `… ${hiddenCases.length} completed ${1 === hiddenCases.length ? 'case' : 'cases'}${failed ? ` · ${failed} failed` : ''}`,
|
|
820
|
+
failed: failed > 0
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
for (const [index, child] of children.entries()){
|
|
824
|
+
const last = index === children.length - 1;
|
|
825
|
+
const prefix = `${stem}${c.dim(last ? '└─' : '├─')} `;
|
|
826
|
+
if ('outcome' in child) {
|
|
827
|
+
lines.push(this.liveRow(`${prefix}${this.formatCaseResult(child.outcome, file.retries)}`, ''));
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if ('summary' in child) {
|
|
831
|
+
lines.push(this.liveRow(`${prefix}${child.failed ? c.red(child.summary) : c.dim(child.summary)}`, ''));
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const { task } = child;
|
|
835
|
+
const hint = task.name && task.step ? truncateTerminalLine(` · ${task.step}`, Math.floor(Math.max(0, (this.io.columns ?? 100) - 1) / 2)) : '';
|
|
836
|
+
const retry = task.attempt ? ` · attempt ${task.attempt + 1}${task.maxAttempts ? `/${task.maxAttempts}` : ''}` : '';
|
|
837
|
+
lines.push(this.liveRow(`${prefix}${terminal_reporter_oneLine(task.name ?? task.step ?? 'file lifecycle')}`, `${c.dim(hint)} ${c.yellow(formatDuration(Date.now() - task.since))}${c.yellow(retry)}`));
|
|
838
|
+
if (task.warning) lines.push(this.liveRow(`${stem}${last ? ' ' : `${c.dim('│')} `}${c.dim('└─')} ${c.yellow(`⚠ ${task.warning}`)}`, ''));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (hidden.length && 'active' !== mode) {
|
|
842
|
+
const summary = `… ${hidden.length} completed ${1 === hidden.length ? 'file' : 'files'}${hiddenFailures ? ` · ${hiddenFailures} failed` : ''}`;
|
|
843
|
+
lines.push(this.liveRow(` ${c.dim('└─')} ${hiddenFailures ? c.red(summary) : c.dim(summary)}`, ''));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return lines;
|
|
847
|
+
}
|
|
848
|
+
liveChildIndex(child) {
|
|
849
|
+
if ('outcome' in child) return child.outcome.caseIndex;
|
|
850
|
+
if ('summary' in child) return Number.MAX_SAFE_INTEGER;
|
|
851
|
+
return child.task.caseIndex ?? ('beforeAll' === child.task.phase ? -1 : Number.MAX_SAFE_INTEGER);
|
|
852
|
+
}
|
|
853
|
+
liveRow(label, suffix) {
|
|
854
|
+
const columns = Math.max(0, (this.io.columns ?? 100) - 1);
|
|
855
|
+
const tail = truncateTerminalLine(suffix, columns);
|
|
856
|
+
return truncateTerminalLine(label, columns - external_string_width_default()(tail)) + tail;
|
|
857
|
+
}
|
|
858
|
+
tick() {
|
|
859
|
+
if (this.interactive) return void this.renderProgress();
|
|
860
|
+
if ((this.active.size || this.projectPhases.size) && Date.now() - this.lastOutput >= 30000) this.log(`… Still running: ${[
|
|
861
|
+
...[
|
|
862
|
+
...this.projectPhases
|
|
863
|
+
].map(([projectName, phase])=>`[${terminal_reporter_oneLine(projectName)}] · ${projectPhaseLabel(phase.phase)} (${formatDuration(Date.now() - phase.since)})`),
|
|
864
|
+
...[
|
|
865
|
+
...this.active.values()
|
|
866
|
+
].map((task)=>`${identity(task.file.projectName, task.file.sourcePath, task.name)} · ${task.step ?? 'between steps'} (${formatDuration(Date.now() - task.since)})`)
|
|
867
|
+
].join(' | ')}`);
|
|
868
|
+
}
|
|
869
|
+
formatCaseResult(outcome, documentRetries) {
|
|
870
|
+
const c = this.color;
|
|
871
|
+
const attempts = outcome.attempts ?? (outcome.run ? [
|
|
872
|
+
outcome.run
|
|
873
|
+
] : []);
|
|
874
|
+
const retries = Math.max(attempts.length - 1, outcome.run?.attemptIndex ?? 0);
|
|
875
|
+
const duration = formatDuration(attempts.reduce((sum, attempt)=>sum + attempt.durationMs, 0));
|
|
876
|
+
const symbol = 'success' === outcome.status ? c.green('✓') : 'failed' === outcome.status ? c.red('×') : c.yellow('–');
|
|
877
|
+
const detail = 'not-run' === outcome.status ? c.dim(` · ${outcome.notRunReason ?? 'not run'}`) : ` ${c.dim(duration)}${retries ? c.yellow(` · ${'success' === outcome.status ? 'passed after retry' : 'failed after retries'} (${retries + 1} attempts)`) : documentRetries ? c.yellow(` · file retried ${documentRetries}x`) : ''}`;
|
|
878
|
+
return `${symbol} ${terminal_reporter_oneLine(outcome.name)}${detail}`;
|
|
879
|
+
}
|
|
880
|
+
statusSymbol(status) {
|
|
881
|
+
return 'failed' === status ? this.color.red('×') : 'not-run' === status ? this.color.yellow('–') : this.color.green('✓');
|
|
882
|
+
}
|
|
883
|
+
resultFileLines(file, lastFile) {
|
|
884
|
+
const c = this.color;
|
|
885
|
+
const stem = lastFile ? ' ' : ` ${c.dim('│')} `;
|
|
886
|
+
const cases = [
|
|
887
|
+
...file.outcomes.values()
|
|
888
|
+
].sort((a, b)=>a.caseIndex - b.caseIndex);
|
|
889
|
+
const failed = cases.filter((item)=>'failed' === item.status).length;
|
|
890
|
+
const notRun = cases.filter((item)=>'not-run' === item.status).length;
|
|
891
|
+
const stats = `(${cases.length} cases${failed ? ` | ${failed} failed` : ''}${notRun ? ` | ${notRun} not run` : ''})`;
|
|
892
|
+
return [
|
|
893
|
+
` ${c.dim(lastFile ? '└─' : '├─')} ${this.statusSymbol(file.status)} ${c.bold(terminal_reporter_oneLine(file.sourcePath))} ${c.dim(stats)} ${c.dim(formatDuration(file.durationMs))}`,
|
|
894
|
+
...cases.map((item, caseIndex)=>`${stem}${c.dim(caseIndex === cases.length - 1 ? '└─' : '├─')} ${this.formatCaseResult(item, file.retries)}`)
|
|
895
|
+
];
|
|
896
|
+
}
|
|
897
|
+
printFile(file) {
|
|
898
|
+
if (file.printed) return;
|
|
899
|
+
file.printed = true;
|
|
900
|
+
this.log([
|
|
901
|
+
'',
|
|
902
|
+
` ${this.color.bold(terminal_reporter_oneLine(file.projectName))}`,
|
|
903
|
+
...this.resultFileLines(file, true)
|
|
904
|
+
].join('\n'));
|
|
905
|
+
}
|
|
906
|
+
printProject(projectName, files, failed) {
|
|
907
|
+
if (!files.length || files.every((file)=>file.printed)) return;
|
|
908
|
+
const c = this.color;
|
|
909
|
+
const status = failed || files.some((file)=>'failed' === file.status) ? 'failed' : files.every((file)=>'not-run' === file.status) ? 'not-run' : files.some((file)=>'not-run' === file.status) ? 'failed' : 'success';
|
|
910
|
+
const lines = [
|
|
911
|
+
'',
|
|
912
|
+
` ${this.statusSymbol(status)} ${c.bold(terminal_reporter_oneLine(projectName))} ${c.dim(`(${files.length} ${1 === files.length ? 'file' : 'files'})`)}`
|
|
913
|
+
];
|
|
914
|
+
for (const [index, file] of files.entries())if (!file.printed) {
|
|
915
|
+
file.printed = true;
|
|
916
|
+
lines.push(...this.resultFileLines(file, index === files.length - 1));
|
|
917
|
+
}
|
|
918
|
+
this.log(lines.join('\n'));
|
|
919
|
+
}
|
|
920
|
+
failure(source, error, context) {
|
|
921
|
+
this.failures.push({
|
|
922
|
+
source,
|
|
923
|
+
error,
|
|
924
|
+
context
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
completeProject(project) {
|
|
928
|
+
this.projectPhases.delete(project.name);
|
|
929
|
+
const projectFiles = new Set();
|
|
930
|
+
const documents = new Map(project.documents.map((document)=>[
|
|
931
|
+
document.documentId,
|
|
932
|
+
document
|
|
933
|
+
]));
|
|
934
|
+
const resultSources = new Set([
|
|
935
|
+
...project.cases.map((outcome)=>outcome.sourcePath),
|
|
936
|
+
...project.documents.map((document)=>document.sourcePath),
|
|
937
|
+
...project.collectionErrors.map((error)=>error.sourcePath)
|
|
938
|
+
]);
|
|
939
|
+
for (const plan of project.plans ?? []){
|
|
940
|
+
const selected = plan.cases.filter((item)=>item.selected).length;
|
|
941
|
+
if (selected || resultSources.has(plan.sourcePath)) projectFiles.add(this.file(project.name, plan.sourcePath, selected));
|
|
942
|
+
}
|
|
943
|
+
for (const outcome of project.cases)this.file(project.name, outcome.sourcePath).outcomes.set(outcome.caseId, outcome);
|
|
944
|
+
for (const document of documents.values()){
|
|
945
|
+
const file = this.file(project.name, document.sourcePath);
|
|
946
|
+
file.status = document.status;
|
|
947
|
+
file.retries = document.attemptIndex ?? 0;
|
|
948
|
+
file.durationMs = project.documents.filter((item)=>item.documentId === document.documentId).reduce((sum, item)=>sum + item.durationMs, 0);
|
|
949
|
+
}
|
|
950
|
+
for (const error of project.collectionErrors)this.file(project.name, error.sourcePath).status = 'failed';
|
|
951
|
+
for (const file of this.files.values())if (file.projectName === project.name) {
|
|
952
|
+
file.attemptOutcomes.clear();
|
|
953
|
+
const cases = [
|
|
954
|
+
...file.outcomes.values()
|
|
955
|
+
];
|
|
956
|
+
file.status = fileStatus(file.status, cases);
|
|
957
|
+
projectFiles.add(file);
|
|
958
|
+
}
|
|
959
|
+
for (const [key, task] of this.active)if (task.file.projectName === project.name) this.active.delete(key);
|
|
960
|
+
if (this.interactive) this.printProject(project.name, [
|
|
961
|
+
...projectFiles
|
|
962
|
+
], 'failed' === project.status);
|
|
963
|
+
else for (const file of projectFiles)this.printFile(file);
|
|
964
|
+
}
|
|
965
|
+
finish(result) {
|
|
966
|
+
this.dispose();
|
|
967
|
+
this.finalMetrics = result.metrics ?? this.usage.snapshot();
|
|
968
|
+
if (result.startedAt) this.startedAt = Date.parse(result.startedAt);
|
|
969
|
+
for (const project of result.projects)this.completeProject(project);
|
|
970
|
+
this.total = result.summary.total;
|
|
971
|
+
this.totalFiles = this.files.size;
|
|
972
|
+
}
|
|
973
|
+
sectionHeading(label, kind) {
|
|
974
|
+
const c = this.color;
|
|
975
|
+
const width = Math.max(0, (this.io.columns ?? 80) - 2);
|
|
976
|
+
const title = truncateTerminalLine(` ${label} `, width);
|
|
977
|
+
const padding = Math.max(0, width - external_string_width_default()(title));
|
|
978
|
+
const left = Math.floor(padding / 2);
|
|
979
|
+
const line = 'warning' === kind ? c.yellow : c.red;
|
|
980
|
+
const badge = 'warning' === kind ? c.bgYellow.black.bold : c.bgRed.white.bold;
|
|
981
|
+
this.log(`\n ${line('─'.repeat(left))}${badge(title)}${line('─'.repeat(padding - left))}\n`);
|
|
982
|
+
}
|
|
983
|
+
summary(result) {
|
|
984
|
+
const c = this.color;
|
|
985
|
+
if (this.interactive && this.warnings.size) {
|
|
986
|
+
this.sectionHeading(`Warnings ${this.warnings.size}`, 'warning');
|
|
987
|
+
for (const warning of [
|
|
988
|
+
...this.warnings.values()
|
|
989
|
+
].slice(0, 5))this.log(` ${c.yellow('WARN')} ${c.dim(warning.scope)}${warning.message}`);
|
|
990
|
+
if (this.warnings.size > 5) this.log(c.dim(` … ${this.warnings.size - 5} more warnings; see debug logs or --reporter verbose.`));
|
|
991
|
+
}
|
|
992
|
+
if (this.failures.length) {
|
|
993
|
+
this.sectionHeading(`Failures ${this.failures.length}`, 'failure');
|
|
994
|
+
for (const failure of this.failures){
|
|
995
|
+
if ('default' === this.mode) {
|
|
996
|
+
const detail = formatFailureDetails(failure.source, failure.error, failure.context);
|
|
997
|
+
const body = detail.body.map((line)=>{
|
|
998
|
+
if (line.startsWith(' Error:')) return c.red(line);
|
|
999
|
+
if (line.startsWith(' at ')) return c.dim(line);
|
|
1000
|
+
return line.replace(/^ {2}(?:Step|Location|Attempt|File try|Phase|Input)\s+/, (label)=>c.dim(label));
|
|
1001
|
+
}).join('\n');
|
|
1002
|
+
const block = `${c.bgRed.white.bold(' FAIL ')} ${c.bold(detail.title)}\n${body}\n`;
|
|
1003
|
+
if (this.interactive) this.log(block);
|
|
1004
|
+
else this.io.error(`FAIL ${detail.title}\n${detail.body.join('\n')}\n`);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const message = formatCliError(failure.error);
|
|
1008
|
+
this.io.error(`FAIL midscene-test: ${failure.source}:\n${message.split('\n').map((line)=>` ${line}`).join('\n')}\n`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
this.log(`\n${this.footer(result.durationMs, result).join('\n')}`);
|
|
1012
|
+
this.log('');
|
|
1013
|
+
this.failures = [];
|
|
1014
|
+
}
|
|
1015
|
+
dispose() {
|
|
1016
|
+
if (this.timer) clearInterval(this.timer);
|
|
1017
|
+
if (this.scheduled) clearTimeout(this.scheduled);
|
|
1018
|
+
this.timer = void 0;
|
|
1019
|
+
this.scheduled = void 0;
|
|
1020
|
+
this.removeResizeListener?.();
|
|
1021
|
+
this.removeResizeListener = void 0;
|
|
1022
|
+
this.progressView.clear();
|
|
1023
|
+
}
|
|
1024
|
+
constructor(io, mode = 'default'){
|
|
1025
|
+
terminal_reporter_define_property(this, "io", void 0);
|
|
1026
|
+
terminal_reporter_define_property(this, "mode", void 0);
|
|
1027
|
+
terminal_reporter_define_property(this, "total", void 0);
|
|
1028
|
+
terminal_reporter_define_property(this, "completionOrder", void 0);
|
|
1029
|
+
terminal_reporter_define_property(this, "totalFiles", void 0);
|
|
1030
|
+
terminal_reporter_define_property(this, "files", void 0);
|
|
1031
|
+
terminal_reporter_define_property(this, "warnings", void 0);
|
|
1032
|
+
terminal_reporter_define_property(this, "latestNotice", void 0);
|
|
1033
|
+
terminal_reporter_define_property(this, "usage", void 0);
|
|
1034
|
+
terminal_reporter_define_property(this, "finalMetrics", void 0);
|
|
1035
|
+
terminal_reporter_define_property(this, "attempts", void 0);
|
|
1036
|
+
terminal_reporter_define_property(this, "active", void 0);
|
|
1037
|
+
terminal_reporter_define_property(this, "projectPhases", void 0);
|
|
1038
|
+
terminal_reporter_define_property(this, "failures", void 0);
|
|
1039
|
+
terminal_reporter_define_property(this, "lastOutput", void 0);
|
|
1040
|
+
terminal_reporter_define_property(this, "startedAt", void 0);
|
|
1041
|
+
terminal_reporter_define_property(this, "timer", void 0);
|
|
1042
|
+
terminal_reporter_define_property(this, "scheduled", void 0);
|
|
1043
|
+
terminal_reporter_define_property(this, "progressView", void 0);
|
|
1044
|
+
terminal_reporter_define_property(this, "removeResizeListener", void 0);
|
|
1045
|
+
terminal_reporter_define_property(this, "color", void 0);
|
|
1046
|
+
this.io = io;
|
|
1047
|
+
this.mode = mode;
|
|
1048
|
+
this.total = 0;
|
|
1049
|
+
this.completionOrder = 0;
|
|
1050
|
+
this.totalFiles = 0;
|
|
1051
|
+
this.files = new Map();
|
|
1052
|
+
this.warnings = new Map();
|
|
1053
|
+
this.usage = new CliModelUsage();
|
|
1054
|
+
this.attempts = new Map();
|
|
1055
|
+
this.active = new Map();
|
|
1056
|
+
this.projectPhases = new Map();
|
|
1057
|
+
this.failures = [];
|
|
1058
|
+
this.lastOutput = Date.now();
|
|
1059
|
+
this.startedAt = Date.now();
|
|
1060
|
+
this.progressView = new TerminalProgress(io);
|
|
1061
|
+
this.color = new (external_chalk_default()).Instance({
|
|
1062
|
+
level: io.isTTY && !process.env.NO_COLOR ? 1 : 0
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const escapeData = (text)=>text.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
1067
|
+
const escapeProperty = (text)=>escapeData(text).replace(/:/g, '%3A').replace(/,/g, '%2C');
|
|
1068
|
+
const markdown = (text)=>text.replace(/[&<>`\r\n]/g, (char)=>({
|
|
1069
|
+
'&': '&',
|
|
1070
|
+
'<': '<',
|
|
1071
|
+
'>': '>',
|
|
1072
|
+
'`': '`',
|
|
1073
|
+
'\r': ' ',
|
|
1074
|
+
'\n': ' '
|
|
1075
|
+
})[char]);
|
|
1076
|
+
function githubFailureAnnotation(source, message, file) {
|
|
1077
|
+
if ('true' !== process.env.GITHUB_ACTIONS) return;
|
|
1078
|
+
const workspace = process.env.GITHUB_WORKSPACE ?? process.cwd();
|
|
1079
|
+
const path = file ? (0, external_node_path_.relative)(workspace, (0, external_node_path_.resolve)(file)).split(external_node_path_.sep).join('/') : void 0;
|
|
1080
|
+
return `::error ${path ? `file=${escapeProperty(path)},` : ''}title=${escapeProperty(source)}::${escapeData(message)}`;
|
|
1081
|
+
}
|
|
1082
|
+
function writeGitHubSummary(result) {
|
|
1083
|
+
const output = process.env.GITHUB_STEP_SUMMARY;
|
|
1084
|
+
if ('true' !== process.env.GITHUB_ACTIONS || !output) return;
|
|
1085
|
+
const failedCases = [
|
|
1086
|
+
...new Map(result.cases.map((item)=>[
|
|
1087
|
+
item.caseId,
|
|
1088
|
+
item
|
|
1089
|
+
])).values()
|
|
1090
|
+
].filter((item)=>'failed' === item.status);
|
|
1091
|
+
const summary = [
|
|
1092
|
+
'## Midscene Test',
|
|
1093
|
+
'',
|
|
1094
|
+
'| Passed | Failed | Not run | Passed after retry | Duration |',
|
|
1095
|
+
'| ---: | ---: | ---: | ---: | ---: |',
|
|
1096
|
+
`| ${result.summary.passed} | ${result.summary.failed} | ${result.summary.notRun} | ${result.summary.passedAfterRetry} | ${formatDuration(result.durationMs)} |`,
|
|
1097
|
+
'',
|
|
1098
|
+
'Passed after retry is included in Passed.',
|
|
1099
|
+
'',
|
|
1100
|
+
...failedCases.map((item)=>`- Failed: **${markdown(item.name)}** — ${markdown(item.projectName)}/\`${markdown(item.sourcePath)}\``),
|
|
1101
|
+
...result.summary.collectionErrors ? [
|
|
1102
|
+
`- Collection errors: ${result.summary.collectionErrors}`
|
|
1103
|
+
] : [],
|
|
1104
|
+
...result.summary.documentFailures ? [
|
|
1105
|
+
`- Failed files: ${result.summary.documentFailures}`
|
|
1106
|
+
] : [],
|
|
1107
|
+
...result.summary.projectFailures ? [
|
|
1108
|
+
`- Failed projects: ${result.summary.projectFailures}`
|
|
1109
|
+
] : [],
|
|
1110
|
+
'',
|
|
1111
|
+
...result.reportPath ? [
|
|
1112
|
+
`Report artifact entry: \`${markdown(result.reportPath)}\``
|
|
1113
|
+
] : [],
|
|
1114
|
+
`Structured results: \`${markdown(result.summaryPath)}\``,
|
|
1115
|
+
''
|
|
1116
|
+
].join('\n');
|
|
1117
|
+
(0, external_node_fs_.appendFileSync)(output, `${summary}\n`);
|
|
1118
|
+
}
|
|
103
1119
|
const v4_namespaceObject = require("zod/v4");
|
|
104
1120
|
const GENERATED_COMMENT = '<!-- Generated by `midscene-test nodes`. Do not edit directly. -->';
|
|
105
1121
|
const sortNodesForSpec = (nodes)=>{
|
|
@@ -233,7 +1249,32 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
233
1249
|
warnings
|
|
234
1250
|
};
|
|
235
1251
|
};
|
|
236
|
-
|
|
1252
|
+
function captureTerminalOutput(output, streams = [
|
|
1253
|
+
process.stdout,
|
|
1254
|
+
process.stderr
|
|
1255
|
+
]) {
|
|
1256
|
+
const cleanups = [];
|
|
1257
|
+
for (const stream of streams){
|
|
1258
|
+
const original = stream.write;
|
|
1259
|
+
const forward = original.bind(stream);
|
|
1260
|
+
const intercepted = (chunk, encoding, callback)=>{
|
|
1261
|
+
const done = 'function' == typeof encoding ? encoding : callback;
|
|
1262
|
+
const message = 'string' == typeof chunk ? chunk : Buffer.from(chunk).toString();
|
|
1263
|
+
let accepted = true;
|
|
1264
|
+
output(message, (text)=>{
|
|
1265
|
+
accepted = forward(text, done);
|
|
1266
|
+
});
|
|
1267
|
+
return accepted;
|
|
1268
|
+
};
|
|
1269
|
+
stream.write = intercepted;
|
|
1270
|
+
cleanups.push(()=>{
|
|
1271
|
+
if (stream.write === intercepted) stream.write = original;
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
return ()=>{
|
|
1275
|
+
for (const cleanup of cleanups)cleanup();
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
237
1278
|
const api_namespaceObject = require("tsx/cjs/api");
|
|
238
1279
|
const NodeRegistry = test_runner_namespaceObject.NodeRegistry;
|
|
239
1280
|
const projectIdFromIndex = (index)=>`project-${index}`;
|
|
@@ -585,11 +1626,13 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
585
1626
|
start: state.position + sourceOffset,
|
|
586
1627
|
end: 0,
|
|
587
1628
|
value: null,
|
|
588
|
-
children: []
|
|
1629
|
+
children: [],
|
|
1630
|
+
kind: null
|
|
589
1631
|
});
|
|
590
1632
|
const node = stack.pop();
|
|
591
1633
|
node.end = state.position + sourceOffset;
|
|
592
1634
|
node.value = state.result;
|
|
1635
|
+
node.kind = state.kind;
|
|
593
1636
|
stack.at(-1)?.children.push(node);
|
|
594
1637
|
nodes.push(node);
|
|
595
1638
|
if ('mapping' !== state.kind) return;
|
|
@@ -619,6 +1662,36 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
619
1662
|
const value = (0, test_runner_namespaceObject.sanitizeReportText)(redacted, 65536);
|
|
620
1663
|
return {
|
|
621
1664
|
parsed,
|
|
1665
|
+
locate (path) {
|
|
1666
|
+
let node = nodes.at(-1);
|
|
1667
|
+
for (const part of path){
|
|
1668
|
+
if (!node) return;
|
|
1669
|
+
while(1 === node.children.length && node.children[0].value === node.value)node = node.children[0];
|
|
1670
|
+
if ('number' == typeof part && 'sequence' === node.kind) node = node.children[part];
|
|
1671
|
+
else {
|
|
1672
|
+
if ('string' != typeof part || 'mapping' !== node.kind) return;
|
|
1673
|
+
const children = node.children;
|
|
1674
|
+
node = void 0;
|
|
1675
|
+
for(let index = 0; index < children.length; index += 2)if (children[index].value === part) {
|
|
1676
|
+
node = children[index + 1];
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (!node) return;
|
|
1682
|
+
let start = node.start;
|
|
1683
|
+
while(start < node.end)if (/\s/.test(source[start])) start++;
|
|
1684
|
+
else if ('#' === source[start]) {
|
|
1685
|
+
const end = source.indexOf('\n', start);
|
|
1686
|
+
if (end < 0) return;
|
|
1687
|
+
start = end + 1;
|
|
1688
|
+
} else break;
|
|
1689
|
+
const prefix = source.slice(0, start).split(/\r\n|\r|\n/);
|
|
1690
|
+
return {
|
|
1691
|
+
line: prefix.length,
|
|
1692
|
+
column: prefix.at(-1).length + 1
|
|
1693
|
+
};
|
|
1694
|
+
},
|
|
622
1695
|
sourceYaml: {
|
|
623
1696
|
value,
|
|
624
1697
|
...redactedPaths.size ? {
|
|
@@ -677,7 +1750,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
677
1750
|
]));
|
|
678
1751
|
return value;
|
|
679
1752
|
};
|
|
680
|
-
function
|
|
1753
|
+
function collect_define_property(obj, key, value) {
|
|
681
1754
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
682
1755
|
value: value,
|
|
683
1756
|
enumerable: true,
|
|
@@ -694,7 +1767,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
694
1767
|
sourcePath,
|
|
695
1768
|
actualFormat,
|
|
696
1769
|
expectedFormat: 'native'
|
|
697
|
-
}),
|
|
1770
|
+
}), collect_define_property(this, "actualFormat", void 0);
|
|
698
1771
|
this.actualFormat = actualFormat;
|
|
699
1772
|
}
|
|
700
1773
|
}
|
|
@@ -731,6 +1804,18 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
731
1804
|
const sourcePath = source.sourcePath.split(external_node_path_.sep).join('/');
|
|
732
1805
|
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
733
1806
|
...step,
|
|
1807
|
+
meta: {
|
|
1808
|
+
...step.meta,
|
|
1809
|
+
sourceLocation: snapshot.locate('steps' === phase ? [
|
|
1810
|
+
'cases',
|
|
1811
|
+
caseIndex,
|
|
1812
|
+
'steps',
|
|
1813
|
+
stepIndex
|
|
1814
|
+
] : [
|
|
1815
|
+
phase,
|
|
1816
|
+
stepIndex
|
|
1817
|
+
])
|
|
1818
|
+
},
|
|
734
1819
|
templateInput: step.input,
|
|
735
1820
|
input: resolveWorkflowVariables(step.input, {
|
|
736
1821
|
variables: options.variables,
|
|
@@ -906,6 +1991,9 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
906
1991
|
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
907
1992
|
} : {},
|
|
908
1993
|
summary: result.summary,
|
|
1994
|
+
...result.metrics ? {
|
|
1995
|
+
metrics: result.metrics
|
|
1996
|
+
} : {},
|
|
909
1997
|
projects: result.projects.map((project)=>({
|
|
910
1998
|
projectId: project.projectId,
|
|
911
1999
|
name: project.name,
|
|
@@ -1302,14 +2390,14 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1302
2390
|
preflightScope: hasFormatMismatch ? 'run' : 'project',
|
|
1303
2391
|
reportEnabled: true,
|
|
1304
2392
|
publications: [],
|
|
1305
|
-
onProgress: options.onProgress ?? (()=>{})
|
|
2393
|
+
onProgress: options.onProgress ?? (()=>{}),
|
|
2394
|
+
onEvent: options.onEvent
|
|
1306
2395
|
};
|
|
1307
2396
|
}
|
|
1308
2397
|
const external_node_events_namespaceObject = require("node:events");
|
|
1309
2398
|
const report_namespaceObject = require("@midscene/core/report");
|
|
1310
|
-
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
1311
2399
|
const result_markdown_text = (value)=>value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('|', '|').replaceAll('[', '[').replaceAll(']', ']').replaceAll('*', '*').replaceAll('_', '_').replaceAll('`', '`').replaceAll('\\', '\').replace(/[\r\n]+/g, ' ');
|
|
1312
|
-
const
|
|
2400
|
+
const result_markdown_duration = (ms)=>{
|
|
1313
2401
|
const seconds = Math.round(ms / 1000);
|
|
1314
2402
|
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
1315
2403
|
};
|
|
@@ -1320,7 +2408,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1320
2408
|
const lines = [
|
|
1321
2409
|
'## Midscene Test',
|
|
1322
2410
|
'',
|
|
1323
|
-
`${outcome} · ${
|
|
2411
|
+
`${outcome} · ${result_markdown_duration(dump.durationMs)}`,
|
|
1324
2412
|
''
|
|
1325
2413
|
];
|
|
1326
2414
|
if (summary.filtered) lines.push(`**Filtered:** ${summary.filtered}`, '');
|
|
@@ -1383,6 +2471,38 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1383
2471
|
}));
|
|
1384
2472
|
return output;
|
|
1385
2473
|
}
|
|
2474
|
+
function scopeNodeConsole(node) {
|
|
2475
|
+
return {
|
|
2476
|
+
...node,
|
|
2477
|
+
execute (ctx) {
|
|
2478
|
+
const position = 'case' === ctx.scope ? ctx.case : ctx.document;
|
|
2479
|
+
const scope = {
|
|
2480
|
+
projectName: position.projectName,
|
|
2481
|
+
sourcePath: position.sourcePath,
|
|
2482
|
+
...'case' === ctx.scope ? {
|
|
2483
|
+
caseId: ctx.case.caseId,
|
|
2484
|
+
caseName: ctx.case.name,
|
|
2485
|
+
runId: ctx.case.runId
|
|
2486
|
+
} : {
|
|
2487
|
+
runId: ctx.document.documentRunId
|
|
2488
|
+
},
|
|
2489
|
+
phase: position.phase,
|
|
2490
|
+
stepIndex: position.stepIndex,
|
|
2491
|
+
node: node.name
|
|
2492
|
+
};
|
|
2493
|
+
return (0, logger_namespaceObject.runWithConsoleLogScope)(scope, ()=>node.execute({
|
|
2494
|
+
...ctx,
|
|
2495
|
+
onTeardown (teardown) {
|
|
2496
|
+
ctx.onTeardown(()=>(0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2497
|
+
...scope,
|
|
2498
|
+
phase: 'teardown',
|
|
2499
|
+
stepIndex: void 0
|
|
2500
|
+
}, teardown));
|
|
2501
|
+
}
|
|
2502
|
+
}));
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
1386
2506
|
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1387
2507
|
const formatStep = (info)=>{
|
|
1388
2508
|
const position = stepPosition(info);
|
|
@@ -1429,69 +2549,121 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1429
2549
|
const documentRetry = 'document' === invocation.retry.scope;
|
|
1430
2550
|
const attempts = [];
|
|
1431
2551
|
const fileSetup = invocation.bindings.documentSetup;
|
|
1432
|
-
const documentSetup =
|
|
2552
|
+
const documentSetup = fileSetup ? {
|
|
1433
2553
|
...fileSetup,
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
}
|
|
2554
|
+
setup (ctx) {
|
|
2555
|
+
const scope = {
|
|
2556
|
+
projectName: project.name,
|
|
2557
|
+
sourcePath: document.sourcePath,
|
|
2558
|
+
runId: ctx.document.documentRunId,
|
|
2559
|
+
phase: 'fileSetup'
|
|
2560
|
+
};
|
|
2561
|
+
return (0, logger_namespaceObject.runWithConsoleLogScope)(scope, async ()=>{
|
|
2562
|
+
try {
|
|
2563
|
+
return await fileSetup.setup({
|
|
2564
|
+
...ctx,
|
|
2565
|
+
onTeardown (teardown) {
|
|
2566
|
+
ctx.onTeardown(()=>(0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2567
|
+
...scope,
|
|
2568
|
+
phase: 'teardown'
|
|
2569
|
+
}, teardown));
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
if (isProjectResourceFailure(error)) options.markProjectFatal();
|
|
2574
|
+
throw error;
|
|
2575
|
+
}
|
|
2576
|
+
});
|
|
1441
2577
|
}
|
|
1442
|
-
} :
|
|
2578
|
+
} : void 0;
|
|
2579
|
+
options.onEvent?.({
|
|
2580
|
+
type: 'document-start',
|
|
2581
|
+
documentId: document.documentId,
|
|
2582
|
+
projectName: project.name,
|
|
2583
|
+
sourcePath: document.sourcePath,
|
|
2584
|
+
cases: document.cases.length
|
|
2585
|
+
});
|
|
1443
2586
|
await (0, test_runner_namespaceObject.runDocumentAttempts)({
|
|
1444
2587
|
retry: documentRetry ? invocation.retry.count : 0,
|
|
1445
2588
|
signal: options.signal,
|
|
1446
2589
|
shouldStop: ()=>options.shouldBail() || options.isProjectFatal()
|
|
1447
2590
|
}, async (documentAttemptIndex)=>{
|
|
1448
2591
|
if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
|
|
1449
|
-
const execution = await (0,
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
2592
|
+
const execution = await (0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2593
|
+
projectName: project.name,
|
|
2594
|
+
sourcePath: document.sourcePath
|
|
2595
|
+
}, ()=>(0, test_runner_namespaceObject.runWorkflowDocument)(document, {
|
|
2596
|
+
plan: invocation.plan,
|
|
2597
|
+
documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
|
|
2598
|
+
...invocation.bindings,
|
|
2599
|
+
resolveNode: (name)=>scopeNodeConsole(invocation.bindings.resolveNode(name)),
|
|
2600
|
+
documentSetup,
|
|
2601
|
+
setupTimeoutMs: options.setupTimeoutMs,
|
|
2602
|
+
teardownTimeoutMs: options.teardownTimeoutMs,
|
|
2603
|
+
project,
|
|
2604
|
+
projectContext: options.projectContext,
|
|
2605
|
+
retry: documentRetry ? 0 : invocation.retry.count,
|
|
2606
|
+
signal: options.signal,
|
|
2607
|
+
defaultTimeoutMs: invocation.defaultTimeoutMs,
|
|
2608
|
+
shouldStop: ()=>options.rootSignal.aborted || options.shouldBail() || options.isProjectFatal(),
|
|
2609
|
+
stopReason: ()=>options.rootSignal.aborted ? 'interrupted' : options.isProjectFatal() ? 'fatal-error' : 'bail',
|
|
2610
|
+
isFatalError: (run)=>[
|
|
2611
|
+
...run.beforeEach,
|
|
2612
|
+
...run.steps,
|
|
2613
|
+
...run.afterEach
|
|
2614
|
+
].some((step)=>step.error && ((0, test_runner_namespaceObject.isFatalDeviceError)(step.error) || isProjectResourceFailure(step.error))) || (run.teardownErrors ?? []).some((error)=>(0, test_runner_namespaceObject.isFatalDeviceError)(error) || isProjectResourceFailure(error)),
|
|
2615
|
+
onCaseStart: (collectedCase)=>{
|
|
2616
|
+
options.onProgress(` [case ${collectedCase.caseIndex + 1}/${document.cases.length}] ${collectedCase.definition.name}`);
|
|
2617
|
+
},
|
|
2618
|
+
onStepStart: (info)=>{
|
|
2619
|
+
options.onEvent?.({
|
|
2620
|
+
type: 'step-start',
|
|
2621
|
+
projectName: project.name,
|
|
2622
|
+
info
|
|
2623
|
+
});
|
|
2624
|
+
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
2625
|
+
options.onProgress(`${indent}→ ${formatStep(info)}`);
|
|
2626
|
+
},
|
|
2627
|
+
onStepResult: async (info, result)=>{
|
|
2628
|
+
if (isProjectResourceFailure(result.error)) options.markProjectFatal();
|
|
2629
|
+
await invocation.bindings.onStepResult?.(info, result);
|
|
2630
|
+
options.onProgress(formatStepResult(info, result));
|
|
2631
|
+
options.onEvent?.({
|
|
2632
|
+
type: 'step-end',
|
|
2633
|
+
projectName: project.name,
|
|
2634
|
+
info,
|
|
2635
|
+
result
|
|
2636
|
+
});
|
|
2637
|
+
},
|
|
2638
|
+
onCaseResult: async (attempt)=>{
|
|
2639
|
+
await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
2640
|
+
options.onProgress(` ${'success' === attempt.status ? '✓' : '✗'} case attempt ${attempt.attemptIndex + 1}/${documentRetry ? 1 : invocation.retry.count + 1}: ${attempt.name} (${attempt.durationMs} ms)`);
|
|
2641
|
+
if (!documentRetry) options.onEvent?.({
|
|
2642
|
+
type: 'case-attempt-end',
|
|
2643
|
+
attempt,
|
|
2644
|
+
maxAttempts: invocation.retry.count + 1
|
|
2645
|
+
});
|
|
2646
|
+
},
|
|
2647
|
+
onCaseOutcome: (outcome)=>{
|
|
2648
|
+
if (documentRetry) options.onEvent?.({
|
|
2649
|
+
type: 'case-progress',
|
|
2650
|
+
outcome,
|
|
2651
|
+
documentAttemptIndex
|
|
2652
|
+
});
|
|
2653
|
+
else options.onEvent?.({
|
|
2654
|
+
type: 'case-end',
|
|
2655
|
+
outcome
|
|
2656
|
+
});
|
|
2657
|
+
if (outcome.attempts?.some((attempt)=>[
|
|
2658
|
+
...attempt.beforeEach,
|
|
2659
|
+
...attempt.steps,
|
|
2660
|
+
...attempt.afterEach
|
|
2661
|
+
].some((step)=>isProjectResourceFailure(step.error)) || attempt.teardownErrors?.some(isProjectResourceFailure))) options.markProjectFatal();
|
|
2662
|
+
if (!options.isolatedContext && caseHasFatalError(outcome)) options.markProjectFatal();
|
|
2663
|
+
if (!documentRetry && 'failed' === outcome.status) options.addFailedCases(1);
|
|
2664
|
+
},
|
|
2665
|
+
onDocumentResult: options.onDocumentResult
|
|
2666
|
+
})).catch(async (error)=>{
|
|
1495
2667
|
if (error instanceof test_runner_namespaceObject.WorkflowExecutionFailure) {
|
|
1496
2668
|
const partial = error.result;
|
|
1497
2669
|
const resourceOnly = error.errors.length > 0 && error.errors.every((failure)=>(0, test_runner_namespaceObject.isResourceRecoveryError)(failure) || partial.document.hostErrors?.some((host)=>'cleanup' === host.phase && (host.error === failure || host.error instanceof Error && host.error.cause === failure)));
|
|
@@ -1561,6 +2733,11 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1561
2733
|
shouldStop: options.shouldBail()
|
|
1562
2734
|
});
|
|
1563
2735
|
if (documentRetry) execution.document.retryDecision = decision;
|
|
2736
|
+
if (documentRetry && 'retry' === decision.decision) options.onEvent?.({
|
|
2737
|
+
type: 'document-retry',
|
|
2738
|
+
document: execution.document,
|
|
2739
|
+
maxAttempts: invocation.retry.count + 1
|
|
2740
|
+
});
|
|
1564
2741
|
attempts.push(execution);
|
|
1565
2742
|
sinks.cases.push(...execution.cases.map((outcome)=>({
|
|
1566
2743
|
...outcome,
|
|
@@ -1595,7 +2772,18 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1595
2772
|
});
|
|
1596
2773
|
const final = attempts.at(-1);
|
|
1597
2774
|
if (!final) return;
|
|
2775
|
+
if (documentRetry) for (const outcome of final.cases)options.onEvent?.({
|
|
2776
|
+
type: 'case-end',
|
|
2777
|
+
outcome,
|
|
2778
|
+
documentRetries: attempts.length - 1
|
|
2779
|
+
});
|
|
1598
2780
|
const failedCases = final.cases.filter((item)=>'failed' === item.status).length;
|
|
2781
|
+
options.onEvent?.({
|
|
2782
|
+
type: 'document-end',
|
|
2783
|
+
document: final.document,
|
|
2784
|
+
cases: final.cases,
|
|
2785
|
+
durationMs: attempts.reduce((total, attempt)=>total + attempt.document.durationMs, 0)
|
|
2786
|
+
});
|
|
1599
2787
|
if (documentRetry && failedCases) options.addFailedCases(failedCases);
|
|
1600
2788
|
else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
|
|
1601
2789
|
}
|
|
@@ -1635,11 +2823,45 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1635
2823
|
code: 'UNRESOLVED_FILE_RESOURCES'
|
|
1636
2824
|
}));
|
|
1637
2825
|
};
|
|
2826
|
+
const finalizeProject = (state)=>{
|
|
2827
|
+
const { prepared } = state;
|
|
2828
|
+
const completed = new Set(state.cases.map((item)=>item.caseId));
|
|
2829
|
+
const reason = invalid(state) ? 'project-preflight-failed' : state.setupFailed ? 'project-setup-failed' : options.signal.aborted ? 'interrupted' : state.fatal ? 'fatal-error' : 'bail';
|
|
2830
|
+
for (const document of prepared.documents)for (const item of document.cases)if (!completed.has(item.caseId)) state.cases.push({
|
|
2831
|
+
documentId: document.documentId,
|
|
2832
|
+
caseId: item.caseId,
|
|
2833
|
+
projectName: prepared.project.name,
|
|
2834
|
+
name: item.definition.name,
|
|
2835
|
+
sourcePath: item.sourcePath,
|
|
2836
|
+
caseIndex: item.caseIndex,
|
|
2837
|
+
status: 'not-run',
|
|
2838
|
+
notRunReason: reason
|
|
2839
|
+
});
|
|
2840
|
+
const order = new Map(prepared.documents.map((document, i)=>[
|
|
2841
|
+
document.documentId,
|
|
2842
|
+
i
|
|
2843
|
+
]));
|
|
2844
|
+
const position = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
2845
|
+
state.cases.sort((a, b)=>position(a.documentId) - position(b.documentId) || a.caseIndex - b.caseIndex);
|
|
2846
|
+
state.documents.sort((a, b)=>position(a.documentId) - position(b.documentId));
|
|
2847
|
+
};
|
|
1638
2848
|
const finishProject = async (state)=>{
|
|
1639
2849
|
if (!state.runtime || state.finished) return;
|
|
1640
2850
|
state.finished = true;
|
|
2851
|
+
try {
|
|
2852
|
+
options.onEvent?.({
|
|
2853
|
+
type: 'project-phase',
|
|
2854
|
+
projectName: state.prepared.project.name,
|
|
2855
|
+
phase: 'teardown'
|
|
2856
|
+
});
|
|
2857
|
+
} catch (error) {
|
|
2858
|
+
options.onInfrastructureError(error);
|
|
2859
|
+
}
|
|
1641
2860
|
const failed = state.fatal || options.signal.aborted || state.cases.some((item)=>'success' !== item.status) || state.documents.some((item)=>'success' !== item.status);
|
|
1642
|
-
state.lifecycle = await
|
|
2861
|
+
state.lifecycle = await (0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2862
|
+
projectName: state.prepared.project.name,
|
|
2863
|
+
phase: 'teardown'
|
|
2864
|
+
}, ()=>state.runtime.finish(failed ? 'failed' : 'success'));
|
|
1643
2865
|
const releaseProject = ()=>{
|
|
1644
2866
|
occupiedProjects--;
|
|
1645
2867
|
wake?.();
|
|
@@ -1653,7 +2875,11 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1653
2875
|
options.onInfrastructureError(error);
|
|
1654
2876
|
releaseProject();
|
|
1655
2877
|
});
|
|
1656
|
-
} else
|
|
2878
|
+
} else {
|
|
2879
|
+
releaseProject();
|
|
2880
|
+
finalizeProject(state);
|
|
2881
|
+
options.onProjectEnd?.(state);
|
|
2882
|
+
}
|
|
1657
2883
|
};
|
|
1658
2884
|
const admit = ()=>{
|
|
1659
2885
|
for(let offset = 0; offset < states.length; offset++){
|
|
@@ -1694,7 +2920,20 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1694
2920
|
});
|
|
1695
2921
|
(0, external_node_events_namespaceObject.setMaxListeners)(Math.max(10, Math.min(prepared.invocations.length, project.maxConcurrentFiles ?? options.maxConcurrentFiles) + 1), state.runtime.signal);
|
|
1696
2922
|
options.announceProject(prepared, index);
|
|
1697
|
-
|
|
2923
|
+
options.onEvent?.({
|
|
2924
|
+
type: 'project-phase',
|
|
2925
|
+
projectName: project.name,
|
|
2926
|
+
phase: 'setup'
|
|
2927
|
+
});
|
|
2928
|
+
await (0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2929
|
+
projectName: project.name,
|
|
2930
|
+
phase: 'setup'
|
|
2931
|
+
}, ()=>state.runtime.start());
|
|
2932
|
+
options.onEvent?.({
|
|
2933
|
+
type: 'project-phase',
|
|
2934
|
+
projectName: project.name,
|
|
2935
|
+
phase: 'running'
|
|
2936
|
+
});
|
|
1698
2937
|
state.initializing = false;
|
|
1699
2938
|
if (!state.runtime.canRun) {
|
|
1700
2939
|
state.fatal = true;
|
|
@@ -1722,8 +2961,9 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1722
2961
|
}));
|
|
1723
2962
|
},
|
|
1724
2963
|
addFailedCases: options.addFailedCases,
|
|
1725
|
-
onProgress: (message)=>options.onProgress(prepared, message),
|
|
2964
|
+
onProgress: (message)=>options.onProgress(prepared, options.maxConcurrentFiles > 1 ? ` [${invocation.document.sourcePath}]${message}` : message),
|
|
1726
2965
|
sinks: state,
|
|
2966
|
+
onEvent: options.onEvent,
|
|
1727
2967
|
isolatedContext: void 0 !== project.fileSetup,
|
|
1728
2968
|
isProjectResourceFailure: isSharedResourceFailure,
|
|
1729
2969
|
setupTimeoutMs: options.setupTimeoutMs,
|
|
@@ -1802,25 +3042,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1802
3042
|
for (const state of states){
|
|
1803
3043
|
const { prepared } = state;
|
|
1804
3044
|
if (!state.runtime) options.announceProject(prepared, states.indexOf(state));
|
|
1805
|
-
|
|
1806
|
-
const reason = invalid(state) ? 'project-preflight-failed' : state.setupFailed ? 'project-setup-failed' : options.signal.aborted ? 'interrupted' : state.fatal ? 'fatal-error' : 'bail';
|
|
1807
|
-
for (const document of prepared.documents)for (const item of document.cases)if (!completed.has(item.caseId)) state.cases.push({
|
|
1808
|
-
documentId: document.documentId,
|
|
1809
|
-
caseId: item.caseId,
|
|
1810
|
-
projectName: prepared.project.name,
|
|
1811
|
-
name: item.definition.name,
|
|
1812
|
-
sourcePath: item.sourcePath,
|
|
1813
|
-
caseIndex: item.caseIndex,
|
|
1814
|
-
status: 'not-run',
|
|
1815
|
-
notRunReason: reason
|
|
1816
|
-
});
|
|
1817
|
-
const order = new Map(prepared.documents.map((document, i)=>[
|
|
1818
|
-
document.documentId,
|
|
1819
|
-
i
|
|
1820
|
-
]));
|
|
1821
|
-
const position = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
1822
|
-
state.cases.sort((a, b)=>position(a.documentId) - position(b.documentId) || a.caseIndex - b.caseIndex);
|
|
1823
|
-
state.documents.sort((a, b)=>position(a.documentId) - position(b.documentId));
|
|
3045
|
+
finalizeProject(state);
|
|
1824
3046
|
}
|
|
1825
3047
|
return states;
|
|
1826
3048
|
}
|
|
@@ -1892,6 +3114,32 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1892
3114
|
const totalDocuments = preparedProjects.reduce((total, prepared)=>total + prepared.documents.length, 0);
|
|
1893
3115
|
const totalCases = preparedProjects.reduce((total, prepared)=>total + prepared.selectedCaseCount, 0);
|
|
1894
3116
|
const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
|
|
3117
|
+
plan.onEvent?.({
|
|
3118
|
+
type: 'run-start',
|
|
3119
|
+
configuredModels: configuredModelNames(),
|
|
3120
|
+
projects: preparedProjects.length,
|
|
3121
|
+
documents: totalDocuments + totalErrors,
|
|
3122
|
+
cases: totalCases,
|
|
3123
|
+
projectRoot,
|
|
3124
|
+
configPath,
|
|
3125
|
+
startedAt: startedAt.toISOString(),
|
|
3126
|
+
maxConcurrentProjects: definition.test.maxConcurrentProjects,
|
|
3127
|
+
maxConcurrentFiles: definition.test.maxConcurrentFiles,
|
|
3128
|
+
files: preparedProjects.flatMap((prepared)=>[
|
|
3129
|
+
...prepared.documents.map((document)=>({
|
|
3130
|
+
documentId: document.documentId,
|
|
3131
|
+
projectName: prepared.project.name,
|
|
3132
|
+
sourcePath: document.sourcePath,
|
|
3133
|
+
cases: document.cases.length
|
|
3134
|
+
})),
|
|
3135
|
+
...prepared.collectionErrors.map((error)=>({
|
|
3136
|
+
documentId: error.sourcePath,
|
|
3137
|
+
projectName: prepared.project.name,
|
|
3138
|
+
sourcePath: error.sourcePath,
|
|
3139
|
+
cases: 0
|
|
3140
|
+
}))
|
|
3141
|
+
])
|
|
3142
|
+
});
|
|
1895
3143
|
progress(`midscene-test: preflighted ${preparedProjects.length} projects, ${totalDocuments} documents, ${totalCases} cases, ${totalErrors} collection errors`);
|
|
1896
3144
|
const effectiveConcurrency = Math.min(definition.test.maxConcurrentFiles ?? 1 / 0, Math.max(1, totalDocuments));
|
|
1897
3145
|
await writeTestRunPlan(runDir, preparedProjects);
|
|
@@ -1994,7 +3242,12 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1994
3242
|
shouldBail: bailReached,
|
|
1995
3243
|
addFailedCases,
|
|
1996
3244
|
announceProject,
|
|
1997
|
-
onProgress: (prepared, message)=>runInfrastructureCallback(()=>progress(effectiveConcurrency > 1 ? `[${prepared.project.name}]${message}` : message)),
|
|
3245
|
+
onProgress: (prepared, message)=>runInfrastructureCallback(()=>progress(preparedProjects.length > 1 || effectiveConcurrency > 1 ? `[${prepared.project.name}]${message}` : message)),
|
|
3246
|
+
onEvent: (event)=>runInfrastructureCallback(()=>plan.onEvent?.(event)),
|
|
3247
|
+
onProjectEnd: (state)=>runInfrastructureCallback(()=>plan.onEvent?.({
|
|
3248
|
+
type: 'project-end',
|
|
3249
|
+
project: buildProjectResult(state.prepared, state.cases, state.documents, state.lifecycle)
|
|
3250
|
+
})),
|
|
1998
3251
|
onInfrastructureError: recordInfrastructureError
|
|
1999
3252
|
});
|
|
2000
3253
|
states.forEach((state, index)=>{
|
|
@@ -2075,6 +3328,10 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
2075
3328
|
buildRunnerDump: (index)=>{
|
|
2076
3329
|
reportIndex = index;
|
|
2077
3330
|
const dump = (0, test_runner_namespaceObject.buildTestRunReportDump)(completedResult, index);
|
|
3331
|
+
completedResult = {
|
|
3332
|
+
...completedResult,
|
|
3333
|
+
metrics: dump.metrics
|
|
3334
|
+
};
|
|
2078
3335
|
const warn = (0, logger_namespaceObject.getDebug)('test-runner:report-assembler', {
|
|
2079
3336
|
console: true
|
|
2080
3337
|
});
|
|
@@ -2092,6 +3349,16 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
2092
3349
|
await publication.publish(completedResult);
|
|
2093
3350
|
});
|
|
2094
3351
|
await publish('write-result', (0, external_node_path_.join)((0, external_node_path_.dirname)(summaryPath), 'result.md'), async ()=>{
|
|
3352
|
+
if (!completedResult.metrics) {
|
|
3353
|
+
const dump = (0, test_runner_namespaceObject.buildTestRunReportDump)(completedResult, reportIndex ?? {
|
|
3354
|
+
sources: [],
|
|
3355
|
+
metrics: (0, test_runner_namespaceObject.sumTestRunReportMetrics)([])
|
|
3356
|
+
});
|
|
3357
|
+
completedResult = {
|
|
3358
|
+
...completedResult,
|
|
3359
|
+
metrics: dump.metrics
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
2095
3362
|
const markdownPath = await writeTestResultMarkdown(completedResult, reportIndex);
|
|
2096
3363
|
completedResult = {
|
|
2097
3364
|
...completedResult,
|
|
@@ -2112,6 +3379,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
2112
3379
|
let configPath;
|
|
2113
3380
|
let resultDir;
|
|
2114
3381
|
const projectNames = [];
|
|
3382
|
+
let reporter;
|
|
2115
3383
|
for(let index = commandOffset; index < args.length; index += 1){
|
|
2116
3384
|
const token = args[index];
|
|
2117
3385
|
const equal = token.startsWith('-') ? token.indexOf('=') : -1;
|
|
@@ -2122,16 +3390,21 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
2122
3390
|
projectRoot = (0, external_node_path_.resolve)(cwd, arg);
|
|
2123
3391
|
continue;
|
|
2124
3392
|
}
|
|
2125
|
-
if ('--config' === arg || '--result-dir' === arg || '--project' === arg) {
|
|
3393
|
+
if ('--config' === arg || '--result-dir' === arg || '--project' === arg || '--reporter' === arg) {
|
|
2126
3394
|
const value = inlineValue ?? args[index + 1];
|
|
2127
3395
|
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
|
|
2128
3396
|
if ('--config' === arg) configPath = value;
|
|
2129
3397
|
else if ('--result-dir' === arg) resultDir = (0, external_node_path_.resolve)(cwd, value);
|
|
2130
|
-
else projectNames.push(value);
|
|
3398
|
+
else if ('--project' === arg) projectNames.push(value);
|
|
3399
|
+
else {
|
|
3400
|
+
if ('default' !== value && 'verbose' !== value) throw new Error(`Unknown reporter: ${value}. Expected default or verbose.`);
|
|
3401
|
+
reporter = value;
|
|
3402
|
+
}
|
|
2131
3403
|
if (void 0 === inlineValue) index += 1;
|
|
2132
3404
|
} else throw new Error(`Unknown option: ${token}`);
|
|
2133
3405
|
}
|
|
2134
3406
|
if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
|
|
3407
|
+
if ('nodes' === command && reporter) throw new Error('--reporter is not supported by nodes.');
|
|
2135
3408
|
if ('nodes' === command && projectNames.length > 1) throw new Error('nodes accepts only one --project name.');
|
|
2136
3409
|
return {
|
|
2137
3410
|
...command ? {
|
|
@@ -2143,13 +3416,27 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
2143
3416
|
resultDir,
|
|
2144
3417
|
...projectNames.length > 0 ? {
|
|
2145
3418
|
projectNames
|
|
3419
|
+
} : {},
|
|
3420
|
+
...reporter ? {
|
|
3421
|
+
reporter
|
|
2146
3422
|
} : {}
|
|
2147
3423
|
};
|
|
2148
3424
|
};
|
|
2149
3425
|
const defaultCliIO = {
|
|
2150
3426
|
log: console.log,
|
|
2151
3427
|
error: console.error,
|
|
2152
|
-
write:
|
|
3428
|
+
write: process.stdout.write.bind(process.stdout),
|
|
3429
|
+
isTTY: Boolean(process.stdout.isTTY && !process.env.CI),
|
|
3430
|
+
get columns () {
|
|
3431
|
+
return process.stdout.columns;
|
|
3432
|
+
},
|
|
3433
|
+
get rows () {
|
|
3434
|
+
return process.stdout.rows;
|
|
3435
|
+
},
|
|
3436
|
+
onResize (listener) {
|
|
3437
|
+
process.stdout.on('resize', listener);
|
|
3438
|
+
return ()=>process.stdout.off('resize', listener);
|
|
3439
|
+
}
|
|
2153
3440
|
};
|
|
2154
3441
|
const testCliHelp = `Midscene Test: run native cases/steps Test projects.
|
|
2155
3442
|
|
|
@@ -2164,6 +3451,7 @@ Options:
|
|
|
2164
3451
|
--config <path> Native TypeScript or JavaScript Test config
|
|
2165
3452
|
--project <name> Select a native execution Project (repeatable)
|
|
2166
3453
|
--result-dir <path> Store Test result files in this directory
|
|
3454
|
+
--reporter <default|verbose> Concise Case results or full step diagnostics
|
|
2167
3455
|
--help, -h Show this help
|
|
2168
3456
|
--version Show the package version
|
|
2169
3457
|
|
|
@@ -2224,11 +3512,17 @@ Legacy tasks/flow YAML and its CLI options belong to the midscene command.
|
|
|
2224
3512
|
io.log(`\nNode Spec generated: ${specPath}`);
|
|
2225
3513
|
}
|
|
2226
3514
|
};
|
|
2227
|
-
const reportTestProjectResult = (result, io, infrastructureErrors = result.errors ?? [])=>{
|
|
3515
|
+
const reportTestProjectResult = (result, io, reporter, projectRoot, infrastructureErrors = result.errors ?? [])=>{
|
|
3516
|
+
reporter.finish(result);
|
|
2228
3517
|
const reportedErrors = new Set();
|
|
2229
|
-
const reportError = (source, error)=>{
|
|
3518
|
+
const reportError = (source, error, sourcePath, context)=>{
|
|
2230
3519
|
if (error instanceof Error && reportedErrors.has(error)) return;
|
|
2231
|
-
|
|
3520
|
+
reporter.failure(source, error, {
|
|
3521
|
+
projectRoot,
|
|
3522
|
+
...context
|
|
3523
|
+
});
|
|
3524
|
+
const annotation = githubFailureAnnotation(source, formatConciseError(error), sourcePath ? (0, external_node_path_.resolve)(projectRoot, sourcePath) : void 0);
|
|
3525
|
+
if (annotation) io.log(annotation);
|
|
2232
3526
|
const pending = [
|
|
2233
3527
|
error
|
|
2234
3528
|
];
|
|
@@ -2240,7 +3534,33 @@ Legacy tasks/flow YAML and its CLI options belong to the midscene command.
|
|
|
2240
3534
|
if (current instanceof AggregateError) pending.push(...current.errors);
|
|
2241
3535
|
}
|
|
2242
3536
|
};
|
|
2243
|
-
for (const failure of result.collectionErrors)reportError(`${failure.projectName}/${failure.sourcePath}`, failure.error
|
|
3537
|
+
for (const failure of result.collectionErrors)reportError(`${failure.projectName}/${failure.sourcePath}`, failure.error, failure.sourcePath, {
|
|
3538
|
+
projectName: failure.projectName,
|
|
3539
|
+
sourcePath: failure.sourcePath,
|
|
3540
|
+
phase: 'collection'
|
|
3541
|
+
});
|
|
3542
|
+
const finalDocuments = new Map(result.documents.map((document)=>[
|
|
3543
|
+
document.documentId,
|
|
3544
|
+
document
|
|
3545
|
+
]));
|
|
3546
|
+
const documentContext = (projectName, documentId, sourcePath)=>{
|
|
3547
|
+
const project = result.projects.find((project)=>project.name === projectName);
|
|
3548
|
+
const document = finalDocuments.get(documentId);
|
|
3549
|
+
const plan = document?.plan ?? project?.plans?.find((plan)=>plan.documentId === documentId);
|
|
3550
|
+
const fileIndex = document?.attemptIndex ?? 0;
|
|
3551
|
+
return {
|
|
3552
|
+
plan,
|
|
3553
|
+
projectName,
|
|
3554
|
+
sourcePath,
|
|
3555
|
+
...plan?.retry.scope === 'document' ? {
|
|
3556
|
+
fileAttempt: {
|
|
3557
|
+
index: fileIndex,
|
|
3558
|
+
total: Math.max(fileIndex + 1, plan.retry.count + 1)
|
|
3559
|
+
}
|
|
3560
|
+
} : {},
|
|
3561
|
+
caseAttempts: plan?.retry.scope === 'document' ? 1 : (plan?.retry.count ?? project?.retry ?? 0) + 1
|
|
3562
|
+
};
|
|
3563
|
+
};
|
|
2244
3564
|
const finalCases = new Map(result.cases.map((outcome)=>[
|
|
2245
3565
|
outcome.caseId,
|
|
2246
3566
|
outcome
|
|
@@ -2249,43 +3569,101 @@ Legacy tasks/flow YAML and its CLI options belong to the midscene command.
|
|
|
2249
3569
|
if ('failed' !== outcome.status || !outcome.run) continue;
|
|
2250
3570
|
const run = outcome.run;
|
|
2251
3571
|
const source = `${run.projectName}/${run.sourcePath} / ${run.name}`;
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
...
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
3572
|
+
const { caseAttempts, plan, ...scope } = documentContext(run.projectName, outcome.documentId, run.sourcePath);
|
|
3573
|
+
const context = {
|
|
3574
|
+
...scope,
|
|
3575
|
+
caseName: run.name,
|
|
3576
|
+
attempt: {
|
|
3577
|
+
index: run.attemptIndex,
|
|
3578
|
+
total: Math.max(caseAttempts, run.attemptIndex + 1)
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
const casePlan = plan?.cases.find((item)=>item.caseId === outcome.caseId);
|
|
3582
|
+
for (const [phase, steps] of [
|
|
3583
|
+
[
|
|
3584
|
+
'beforeEach',
|
|
3585
|
+
run.beforeEach
|
|
3586
|
+
],
|
|
3587
|
+
[
|
|
3588
|
+
'steps',
|
|
3589
|
+
run.steps
|
|
3590
|
+
],
|
|
3591
|
+
[
|
|
3592
|
+
'afterEach',
|
|
3593
|
+
run.afterEach
|
|
3594
|
+
]
|
|
3595
|
+
])for (const step of steps)if (step.error) reportError(`${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}${run.attemptIndex ? ` (failed after ${run.attemptIndex + 1} attempts)` : ''}`, step.error, run.sourcePath, {
|
|
3596
|
+
...context,
|
|
3597
|
+
step,
|
|
3598
|
+
stepCount: casePlan?.[phase].length
|
|
3599
|
+
});
|
|
3600
|
+
for (const [phase, errors] of [
|
|
3601
|
+
[
|
|
3602
|
+
'execution',
|
|
3603
|
+
run.executionErrors
|
|
3604
|
+
],
|
|
3605
|
+
[
|
|
3606
|
+
'teardown',
|
|
3607
|
+
run.teardownErrors
|
|
3608
|
+
]
|
|
3609
|
+
])for (const error of errors ?? [])reportError(source, error, run.sourcePath, {
|
|
3610
|
+
...context,
|
|
3611
|
+
phase
|
|
3612
|
+
});
|
|
2261
3613
|
}
|
|
2262
|
-
const finalDocuments = new Map(result.documents.map((document)=>[
|
|
2263
|
-
document.documentId,
|
|
2264
|
-
document
|
|
2265
|
-
]));
|
|
2266
3614
|
for (const document of finalDocuments.values()){
|
|
2267
3615
|
if ('failed' !== document.status) continue;
|
|
2268
3616
|
const source = `${document.projectName}/${document.sourcePath}`;
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
3617
|
+
const { caseAttempts: _, plan, ...context } = documentContext(document.projectName, document.documentId, document.sourcePath);
|
|
3618
|
+
for (const [phase, steps] of [
|
|
3619
|
+
[
|
|
3620
|
+
'beforeAll',
|
|
3621
|
+
document.beforeAll
|
|
3622
|
+
],
|
|
3623
|
+
[
|
|
3624
|
+
'afterAll',
|
|
3625
|
+
document.afterAll
|
|
3626
|
+
]
|
|
3627
|
+
])for (const step of steps)if (step.error) reportError(`${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}`, step.error, document.sourcePath, {
|
|
3628
|
+
...context,
|
|
3629
|
+
step,
|
|
3630
|
+
stepCount: plan?.[phase].length
|
|
3631
|
+
});
|
|
3632
|
+
for (const [phase, errors] of [
|
|
3633
|
+
[
|
|
3634
|
+
'execution',
|
|
3635
|
+
document.executionErrors
|
|
3636
|
+
],
|
|
3637
|
+
[
|
|
3638
|
+
'teardown',
|
|
3639
|
+
document.teardownErrors
|
|
3640
|
+
]
|
|
3641
|
+
])for (const error of errors ?? [])reportError(source, error, document.sourcePath, {
|
|
3642
|
+
...context,
|
|
3643
|
+
phase
|
|
3644
|
+
});
|
|
3645
|
+
for (const failure of document.hostErrors ?? [])reportError(`${source} / host ${failure.phase}`, failure.error, document.sourcePath, {
|
|
3646
|
+
...context,
|
|
3647
|
+
phase: `host ${failure.phase}`
|
|
3648
|
+
});
|
|
2278
3649
|
}
|
|
2279
3650
|
for (const project of result.projects){
|
|
2280
|
-
if (project.lifecycle?.setupError) reportError(`${project.name} / setup`, project.lifecycle.setupError
|
|
2281
|
-
|
|
3651
|
+
if (project.lifecycle?.setupError) reportError(`${project.name} / setup`, project.lifecycle.setupError, void 0, {
|
|
3652
|
+
projectName: project.name,
|
|
3653
|
+
phase: 'setup'
|
|
3654
|
+
});
|
|
3655
|
+
for (const error of project.lifecycle?.teardownErrors ?? [])reportError(`${project.name} / teardown`, error, void 0, {
|
|
3656
|
+
projectName: project.name,
|
|
3657
|
+
phase: 'teardown'
|
|
3658
|
+
});
|
|
2282
3659
|
}
|
|
2283
3660
|
for (const error of infrastructureErrors)reportError('infrastructure', error);
|
|
2284
|
-
|
|
2285
|
-
io.log(`Results: ${result.resultDir}`);
|
|
3661
|
+
reporter.summary(result);
|
|
3662
|
+
if ('verbose' === reporter.mode) io.log(`Results: ${result.resultDir}`);
|
|
2286
3663
|
io.log(`Summary: ${result.summaryPath}`);
|
|
2287
|
-
if (result.markdownPath) io.log(`Markdown: ${result.markdownPath}`);
|
|
3664
|
+
if (result.markdownPath && 'verbose' === reporter.mode) io.log(`Markdown: ${result.markdownPath}`);
|
|
2288
3665
|
if (result.reportPath) io.log(`Report: ${result.reportPath}`);
|
|
3666
|
+
writeGitHubSummary(result);
|
|
2289
3667
|
return result.exitCode;
|
|
2290
3668
|
};
|
|
2291
3669
|
async function runTestCli(args, io = defaultCliIO) {
|
|
@@ -2315,18 +3693,32 @@ Legacy tasks/flow YAML and its CLI options belong to the midscene command.
|
|
|
2315
3693
|
await runNodesCommand(options, io);
|
|
2316
3694
|
return 0;
|
|
2317
3695
|
}
|
|
2318
|
-
|
|
3696
|
+
const reporter = new TerminalReporter(io, options.reporter);
|
|
3697
|
+
const restoreOutput = io === defaultCliIO && io.isTTY && 'verbose' !== options.reporter ? captureTerminalOutput((message, write)=>reporter.output(message, write)) : void 0;
|
|
3698
|
+
const projectRoot = (0, external_node_fs_.realpathSync)(options.projectRoot && (0, external_node_fs_.existsSync)(options.projectRoot) && (0, external_node_fs_.statSync)(options.projectRoot).isFile() ? (0, external_node_path_.dirname)(options.projectRoot) : options.projectRoot ?? options.cwd);
|
|
2319
3699
|
try {
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
3700
|
+
return await (0, logger_namespaceObject.runWithConsoleLogHandler)((entry)=>reporter.console(entry), async ()=>{
|
|
3701
|
+
let result;
|
|
3702
|
+
try {
|
|
3703
|
+
result = await runTestProject({
|
|
3704
|
+
...options,
|
|
3705
|
+
onProgress: (message)=>reporter.progress(message),
|
|
3706
|
+
onEvent: (event)=>reporter.event(event)
|
|
3707
|
+
});
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
if (!(error instanceof test_runner_namespaceObject.WorkflowExecutionFailure)) throw error;
|
|
3710
|
+
reportTestProjectResult(error.result, io, reporter, projectRoot, error.errors);
|
|
3711
|
+
return 1;
|
|
3712
|
+
}
|
|
3713
|
+
return reportTestProjectResult(result, io, reporter, projectRoot);
|
|
2323
3714
|
});
|
|
2324
|
-
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
3715
|
+
} finally{
|
|
3716
|
+
try {
|
|
3717
|
+
reporter.dispose();
|
|
3718
|
+
} finally{
|
|
3719
|
+
restoreOutput?.();
|
|
3720
|
+
}
|
|
2328
3721
|
}
|
|
2329
|
-
return reportTestProjectResult(result, io);
|
|
2330
3722
|
} catch (error) {
|
|
2331
3723
|
io.error(formatCliError(error));
|
|
2332
3724
|
return 1;
|