@sdods/core 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/auth/capture.js +4 -2
- package/dist/config/runner.d.ts +2 -0
- package/dist/config/runner.js +9 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lint/index.js +9 -3
- package/dist/recorder/codegen.js +4 -2
- package/dist/reporters/index.d.ts +2 -1
- package/dist/reporters/index.js +2 -1
- package/dist/reporters/terminal.d.ts +55 -0
- package/dist/reporters/terminal.js +232 -0
- package/dist/workspace-bin.d.ts +19 -0
- package/dist/workspace-bin.js +46 -0
- package/package.json +11 -9
package/dist/auth/capture.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Logger } from '../logger.js';
|
|
|
7
7
|
import { CompositeDataProvider } from '../data/provider.js';
|
|
8
8
|
import { AuthStateCache } from '../fixtures/auth.js';
|
|
9
9
|
import { defineAuth } from './index.js';
|
|
10
|
+
import { toolCommand } from '../workspace-bin.js';
|
|
10
11
|
const log = new Logger('auth');
|
|
11
12
|
/** Users of a role from the pool dataset (capped by env.users.poolSize), without leasing them. */
|
|
12
13
|
export async function poolUsers(config, role) {
|
|
@@ -80,7 +81,8 @@ async function defaultLaunch(name, headed) {
|
|
|
80
81
|
return pw[name].launch({ headless: !headed });
|
|
81
82
|
}
|
|
82
83
|
async function defaultInteractive(args) {
|
|
83
|
-
const
|
|
84
|
+
const [file, ...argv] = toolCommand(process.cwd(), args);
|
|
85
|
+
const res = await execa(file, argv, { stdio: 'inherit', reject: false });
|
|
84
86
|
return res.exitCode ?? 1;
|
|
85
87
|
}
|
|
86
88
|
/**
|
|
@@ -137,7 +139,7 @@ export async function captureAuth(opts) {
|
|
|
137
139
|
log.step(`interactive login for ${user.username} (${user.role}) → complete the login, then close the window`);
|
|
138
140
|
const code = await (opts.runInteractive ?? defaultInteractive)(args);
|
|
139
141
|
if (code !== 0 && !existsSync(file)) {
|
|
140
|
-
result.skipped = `
|
|
142
|
+
result.skipped = `the login window closed with code ${code} before a session was saved`;
|
|
141
143
|
}
|
|
142
144
|
else if (existsSync(file)) {
|
|
143
145
|
cache.save(user, JSON.parse(readFileSync(file, 'utf8')));
|
package/dist/config/runner.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface SdodsUseOption {
|
|
|
22
22
|
browser?: BrowserName;
|
|
23
23
|
}
|
|
24
24
|
export declare const DASHBOARD_REPORTER = "@sdods/core/reporters/dashboard";
|
|
25
|
+
/** The console output of a run: scenario names and `sdods` follow-ups, not spec paths. */
|
|
26
|
+
export declare const TERMINAL_REPORTER = "@sdods/core/reporters/terminal";
|
|
25
27
|
/** Convenience for `sdods.runner.config.ts`: read the selection from SDODS_* env vars. */
|
|
26
28
|
export declare function selectionFromEnv(env?: NodeJS.ProcessEnv): RunnerSelection;
|
|
27
29
|
export interface GeneratedProject {
|
package/dist/config/runner.js
CHANGED
|
@@ -48,6 +48,8 @@ function browserUse(browser, projectChannel) {
|
|
|
48
48
|
return { ...devices[DEVICE_FOR_BROWSER[browser]], ...(channel ? { channel } : {}) };
|
|
49
49
|
}
|
|
50
50
|
export const DASHBOARD_REPORTER = '@sdods/core/reporters/dashboard';
|
|
51
|
+
/** The console output of a run: scenario names and `sdods` follow-ups, not spec paths. */
|
|
52
|
+
export const TERMINAL_REPORTER = '@sdods/core/reporters/terminal';
|
|
51
53
|
/** Convenience for `sdods.runner.config.ts`: read the selection from SDODS_* env vars. */
|
|
52
54
|
export function selectionFromEnv(env = process.env) {
|
|
53
55
|
const list = (v) => v
|
|
@@ -268,7 +270,13 @@ export function buildRunnerConfig(registry, sel = {}) {
|
|
|
268
270
|
return [name];
|
|
269
271
|
});
|
|
270
272
|
{
|
|
271
|
-
reporter.push(
|
|
273
|
+
reporter.push([
|
|
274
|
+
TERMINAL_REPORTER,
|
|
275
|
+
{
|
|
276
|
+
mode: reporterMode === 'server' ? 'line' : reporterMode === 'quiet' ? 'dot' : 'list',
|
|
277
|
+
rootDir: first?.runtime.repoRoot ?? process.cwd(),
|
|
278
|
+
},
|
|
279
|
+
]);
|
|
272
280
|
reporter.push(['html', { outputFolder: join(runDir, runFiles.htmlReport), open: 'never' }]);
|
|
273
281
|
// The cucumber reporter throws when no defineBddConfig() ran (recorded-only selections).
|
|
274
282
|
if (bddConfigs > 0)
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/lint/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { parseFeatureFile, scenariosOf } from './gherkin.js';
|
|
|
8
8
|
import { projectRequirements } from '../analyze/traceability.js';
|
|
9
9
|
import { checkStepAmbiguity } from './steps.js';
|
|
10
10
|
import { ROLES_MATRIX_FILE, planMatrixExpansion } from '../matrix/index.js';
|
|
11
|
+
import { toolCommand } from '../workspace-bin.js';
|
|
11
12
|
export * from './gherkin.js';
|
|
12
13
|
export function listFeatureFiles(root) {
|
|
13
14
|
const out = [];
|
|
@@ -332,7 +333,12 @@ export async function detectUndefinedSteps(project, repoRoot) {
|
|
|
332
333
|
SDODS_LINT: '1',
|
|
333
334
|
SDODS_ARTIFACTS_DIR: join(repoRoot, '.sdods', 'lint'),
|
|
334
335
|
};
|
|
335
|
-
const
|
|
336
|
+
const [file, ...argv] = toolCommand(repoRoot, [
|
|
337
|
+
'bddgen',
|
|
338
|
+
'-c',
|
|
339
|
+
join(repoRoot, 'sdods.runner.config.ts'),
|
|
340
|
+
]);
|
|
341
|
+
const result = await execa(file, argv, {
|
|
336
342
|
cwd: repoRoot,
|
|
337
343
|
env,
|
|
338
344
|
reject: false,
|
|
@@ -350,7 +356,7 @@ export async function detectUndefinedSteps(project, repoRoot) {
|
|
|
350
356
|
if (m) {
|
|
351
357
|
const stepLine = lines[i + 1]?.trim() ?? '';
|
|
352
358
|
out.push({
|
|
353
|
-
message: `Undefined step: ${stepLine || '(see
|
|
359
|
+
message: `Undefined step: ${stepLine || '(see the generator output)'}`,
|
|
354
360
|
file: relative(project.root, m[1]).replace(/\\/g, '/'),
|
|
355
361
|
line: Number(m[2]),
|
|
356
362
|
});
|
|
@@ -390,7 +396,7 @@ export async function detectUndefinedSteps(project, repoRoot) {
|
|
|
390
396
|
.join(' | ');
|
|
391
397
|
out.push({
|
|
392
398
|
rule: 'steps/bddgen',
|
|
393
|
-
message: `
|
|
399
|
+
message: `Could not generate specs from the features (exit ${result.exitCode}): ${tail || '(no output)'}`,
|
|
394
400
|
});
|
|
395
401
|
}
|
|
396
402
|
return out;
|
package/dist/recorder/codegen.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import { SdodsError } from '../errors.js';
|
|
5
5
|
import { harPath, safeHarName } from '../har/paths.js';
|
|
6
|
+
import { toolCommand } from '../workspace-bin.js';
|
|
6
7
|
export function recordedDir(config) {
|
|
7
8
|
return join(config.project.root, 'recorded');
|
|
8
9
|
}
|
|
@@ -64,7 +65,8 @@ export async function runCodegen(opts) {
|
|
|
64
65
|
mkdirSync(recordedDir(opts.config), { recursive: true });
|
|
65
66
|
if (harFile)
|
|
66
67
|
mkdirSync(join(harFile, '..'), { recursive: true });
|
|
67
|
-
const
|
|
68
|
+
const [file, ...argv] = toolCommand(opts.config.runtime.repoRoot, args);
|
|
69
|
+
const res = await execa(file, argv, {
|
|
68
70
|
cwd: opts.config.runtime.repoRoot,
|
|
69
71
|
stdio: 'inherit',
|
|
70
72
|
reject: false,
|
|
@@ -73,7 +75,7 @@ export async function runCodegen(opts) {
|
|
|
73
75
|
const exitCode = res.exitCode ?? 1;
|
|
74
76
|
const produced = existsSync(outputFile);
|
|
75
77
|
if (exitCode !== 0 && !produced) {
|
|
76
|
-
throw new SdodsError('RUN_FAILED', `
|
|
78
|
+
throw new SdodsError('RUN_FAILED', `The recorder closed with code ${exitCode} before saving a spec.`, {
|
|
77
79
|
hint: 'Codegen needs a display. On a headless machine, record on your workstation or import an existing spec into recorded/.',
|
|
78
80
|
exitCode: 1,
|
|
79
81
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { default as DashboardReporter, type DashboardOptions } from './dashboard.js';
|
|
2
|
-
export {
|
|
2
|
+
export { default as TerminalReporter, type TerminalMode, type TerminalOptions, } from './terminal.js';
|
|
3
|
+
export { DASHBOARD_REPORTER, TERMINAL_REPORTER } from '../config/runner.js';
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/reporters/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { default as DashboardReporter } from './dashboard.js';
|
|
2
|
-
export {
|
|
2
|
+
export { default as TerminalReporter, } from './terminal.js';
|
|
3
|
+
export { DASHBOARD_REPORTER, TERMINAL_REPORTER } from '../config/runner.js';
|
|
3
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestError, TestResult } from '@playwright/test/reporter';
|
|
2
|
+
export type TerminalMode = 'list' | 'line' | 'dot';
|
|
3
|
+
export interface TerminalOptions {
|
|
4
|
+
/** `list` (default, a line per scenario), `line` (the same, for logs and the server), `dot`. */
|
|
5
|
+
mode?: TerminalMode;
|
|
6
|
+
/** Where paths are shown relative to. Defaults to the working directory. */
|
|
7
|
+
rootDir?: string;
|
|
8
|
+
/** Output sink; tests inject one. Defaults to process.stdout. */
|
|
9
|
+
write?: (text: string) => void;
|
|
10
|
+
/** List the scenarios instead of reporting a run (`sdods run --list`; SDODS_LIST_ONLY=1). */
|
|
11
|
+
listOnly?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The console output of `sdods run`, in SDODS terms.
|
|
15
|
+
*
|
|
16
|
+
* Replaces the runner's list/line/dot reporters, which print generated spec paths
|
|
17
|
+
* (`.sdods/generated/<run>/…/x.feature.spec.js`) as test names and end failures with
|
|
18
|
+
* `npx playwright show-trace`. Here a scenario is named by its feature and scenario title, the
|
|
19
|
+
* layer and browser come from the SDODS run-target name, and every follow-up is an `sdods` command.
|
|
20
|
+
*
|
|
21
|
+
* `[n/total]` stays on every scenario line: MCP `run_tests` reads progress from it.
|
|
22
|
+
*/
|
|
23
|
+
export default class TerminalReporter implements Reporter {
|
|
24
|
+
private readonly mode;
|
|
25
|
+
private readonly rootDir;
|
|
26
|
+
private readonly write;
|
|
27
|
+
private total;
|
|
28
|
+
private done;
|
|
29
|
+
private dots;
|
|
30
|
+
private readonly failures;
|
|
31
|
+
private readonly flaky;
|
|
32
|
+
private readonly counts;
|
|
33
|
+
private readonly errors;
|
|
34
|
+
constructor(options?: TerminalOptions);
|
|
35
|
+
private readonly listOnly;
|
|
36
|
+
/** This reporter owns the console, so the runner adds no reporter of its own. */
|
|
37
|
+
printsToStdio(): boolean;
|
|
38
|
+
onBegin(config: FullConfig, suite: Suite): void;
|
|
39
|
+
onStdOut(chunk: string | Buffer, test?: TestCase): void;
|
|
40
|
+
onStdErr(chunk: string | Buffer, test?: TestCase): void;
|
|
41
|
+
onTestEnd(test: TestCase, result: TestResult): void;
|
|
42
|
+
onError(error: TestError): void;
|
|
43
|
+
onEnd(result: FullResult): void;
|
|
44
|
+
private describeFailure;
|
|
45
|
+
}
|
|
46
|
+
/** Feature › Scenario, without the runner's project and generated-file segments. */
|
|
47
|
+
export declare function title(test: TestCase): string;
|
|
48
|
+
/** `[api]`, `[ui · chromium]` — from the SDODS run-target name, not the raw project name. */
|
|
49
|
+
export declare function target(test: TestCase): string;
|
|
50
|
+
/**
|
|
51
|
+
* `<project>: features/<dir>/x.feature` for a scenario generated from Gherkin, read back from the
|
|
52
|
+
* generated spec path `.sdods/generated/<run>/<project>/<layer>/<dir>/x.feature.spec.js`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function featureOf(test: TestCase): string | undefined;
|
|
55
|
+
//# sourceMappingURL=terminal.d.ts.map
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { parseRunnerProjectName } from '@sdods/contracts';
|
|
4
|
+
/**
|
|
5
|
+
* The console output of `sdods run`, in SDODS terms.
|
|
6
|
+
*
|
|
7
|
+
* Replaces the runner's list/line/dot reporters, which print generated spec paths
|
|
8
|
+
* (`.sdods/generated/<run>/…/x.feature.spec.js`) as test names and end failures with
|
|
9
|
+
* `npx playwright show-trace`. Here a scenario is named by its feature and scenario title, the
|
|
10
|
+
* layer and browser come from the SDODS run-target name, and every follow-up is an `sdods` command.
|
|
11
|
+
*
|
|
12
|
+
* `[n/total]` stays on every scenario line: MCP `run_tests` reads progress from it.
|
|
13
|
+
*/
|
|
14
|
+
export default class TerminalReporter {
|
|
15
|
+
mode;
|
|
16
|
+
rootDir;
|
|
17
|
+
write;
|
|
18
|
+
total = 0;
|
|
19
|
+
done = 0;
|
|
20
|
+
dots = 0;
|
|
21
|
+
failures = [];
|
|
22
|
+
flaky = [];
|
|
23
|
+
counts = { passed: 0, failed: 0, skipped: 0, flaky: 0 };
|
|
24
|
+
errors = [];
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.mode = options.mode ?? 'list';
|
|
27
|
+
this.rootDir = options.rootDir ?? process.cwd();
|
|
28
|
+
this.write = options.write ?? ((text) => process.stdout.write(text));
|
|
29
|
+
this.listOnly = options.listOnly ?? process.env.SDODS_LIST_ONLY === '1';
|
|
30
|
+
}
|
|
31
|
+
listOnly;
|
|
32
|
+
/** This reporter owns the console, so the runner adds no reporter of its own. */
|
|
33
|
+
printsToStdio() {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
onBegin(config, suite) {
|
|
37
|
+
this.total = suite.allTests().length;
|
|
38
|
+
if (this.listOnly) {
|
|
39
|
+
const features = new Set();
|
|
40
|
+
for (const t of suite.allTests()) {
|
|
41
|
+
features.add(featureOf(t) ?? t.location.file);
|
|
42
|
+
this.write(` ${title(t)} ${pc.dim(target(t))}\n`);
|
|
43
|
+
}
|
|
44
|
+
this.write(`\n${this.total} scenario${this.total === 1 ? '' : 's'} in ${features.size} file${features.size === 1 ? '' : 's'}\n`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const workers = config.workers;
|
|
48
|
+
this.write(pc.dim(`Running ${this.total} scenario${this.total === 1 ? '' : 's'} with ${workers} worker${workers === 1 ? '' : 's'}\n`));
|
|
49
|
+
if (this.mode !== 'dot')
|
|
50
|
+
this.write('\n');
|
|
51
|
+
}
|
|
52
|
+
onStdOut(chunk, test) {
|
|
53
|
+
if (this.mode === 'dot')
|
|
54
|
+
return;
|
|
55
|
+
this.write(test ? pc.dim(String(chunk)) : String(chunk));
|
|
56
|
+
}
|
|
57
|
+
onStdErr(chunk, test) {
|
|
58
|
+
if (this.mode === 'dot')
|
|
59
|
+
return;
|
|
60
|
+
process.stderr.write(test ? pc.dim(String(chunk)) : String(chunk));
|
|
61
|
+
}
|
|
62
|
+
onTestEnd(test, result) {
|
|
63
|
+
if (this.listOnly)
|
|
64
|
+
return;
|
|
65
|
+
const final = result.status === 'passed' ||
|
|
66
|
+
result.status === 'skipped' ||
|
|
67
|
+
result.status === 'interrupted' ||
|
|
68
|
+
result.retry >= test.retries;
|
|
69
|
+
if (!final) {
|
|
70
|
+
if (this.mode !== 'dot')
|
|
71
|
+
this.write(` ${pc.yellow('↻')} ${title(test)} ${pc.dim(`${target(test)} · retry ${result.retry + 1} of ${test.retries}`)}\n`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.done++;
|
|
75
|
+
const flaky = result.status === 'passed' && result.retry > 0;
|
|
76
|
+
let mark;
|
|
77
|
+
let dot;
|
|
78
|
+
if (flaky) {
|
|
79
|
+
this.counts.flaky++;
|
|
80
|
+
this.flaky.push(test);
|
|
81
|
+
mark = pc.yellow('±');
|
|
82
|
+
dot = pc.yellow('±');
|
|
83
|
+
}
|
|
84
|
+
else if (result.status === 'passed') {
|
|
85
|
+
this.counts.passed++;
|
|
86
|
+
mark = pc.green('✔');
|
|
87
|
+
dot = pc.green('·');
|
|
88
|
+
}
|
|
89
|
+
else if (result.status === 'skipped') {
|
|
90
|
+
this.counts.skipped++;
|
|
91
|
+
mark = pc.dim('↷');
|
|
92
|
+
dot = pc.dim('°');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.counts.failed++;
|
|
96
|
+
this.failures.push({ test, result });
|
|
97
|
+
mark = pc.red('✖');
|
|
98
|
+
dot = pc.red('F');
|
|
99
|
+
}
|
|
100
|
+
if (this.mode === 'dot') {
|
|
101
|
+
this.write(dot);
|
|
102
|
+
if (++this.dots % 80 === 0)
|
|
103
|
+
this.write('\n');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const progress = pc.dim(`[${this.done}/${this.total}]`);
|
|
107
|
+
const time = pc.dim(`(${seconds(result.duration)})`);
|
|
108
|
+
const status = result.status === 'timedOut'
|
|
109
|
+
? pc.red(' timed out')
|
|
110
|
+
: result.status === 'interrupted'
|
|
111
|
+
? pc.yellow(' interrupted')
|
|
112
|
+
: '';
|
|
113
|
+
this.write(`${progress} ${mark} ${title(test)} ${pc.dim(target(test))}${status} ${time}\n`);
|
|
114
|
+
}
|
|
115
|
+
onError(error) {
|
|
116
|
+
this.errors.push(error);
|
|
117
|
+
this.write(`\n${pc.red('✖ Error outside a scenario')}\n${indent(formatError(error, this.rootDir), 2)}\n`);
|
|
118
|
+
}
|
|
119
|
+
onEnd(result) {
|
|
120
|
+
if (this.listOnly)
|
|
121
|
+
return;
|
|
122
|
+
if (this.mode === 'dot' && this.dots % 80 !== 0)
|
|
123
|
+
this.write('\n');
|
|
124
|
+
if (this.failures.length) {
|
|
125
|
+
this.write(`\n${pc.bold(pc.red(`Failed (${this.failures.length})`))}\n`);
|
|
126
|
+
this.failures.forEach((f, i) => this.write(this.describeFailure(f, i + 1)));
|
|
127
|
+
}
|
|
128
|
+
if (this.flaky.length) {
|
|
129
|
+
this.write(`\n${pc.bold(pc.yellow(`Flaky: passed on a retry (${this.flaky.length})`))}\n`);
|
|
130
|
+
for (const t of this.flaky)
|
|
131
|
+
this.write(` ${title(t)} ${pc.dim(target(t))}\n`);
|
|
132
|
+
}
|
|
133
|
+
const parts = [
|
|
134
|
+
pc.green(`${this.counts.passed} passed`),
|
|
135
|
+
this.counts.failed ? pc.red(`${this.counts.failed} failed`) : '',
|
|
136
|
+
this.counts.flaky ? pc.yellow(`${this.counts.flaky} flaky`) : '',
|
|
137
|
+
this.counts.skipped ? pc.dim(`${this.counts.skipped} skipped`) : '',
|
|
138
|
+
// Never started: a setup scenario they depend on failed, --max-failures stopped the run, or
|
|
139
|
+
// it was interrupted. They have no result, so they would otherwise vanish from the count.
|
|
140
|
+
this.total > this.done ? pc.yellow(`${this.total - this.done} did not run`) : '',
|
|
141
|
+
].filter(Boolean);
|
|
142
|
+
this.write(`\n ${parts.join(', ')} ${pc.dim(`(${seconds(result.duration)})`)}\n`);
|
|
143
|
+
}
|
|
144
|
+
describeFailure({ test, result }, n) {
|
|
145
|
+
const lines = [];
|
|
146
|
+
const where = test.location ? relative(this.rootDir, test.location.file) : '';
|
|
147
|
+
lines.push(`\n ${pc.red(`${n})`)} ${pc.bold(title(test))} ${pc.dim(target(test))}`);
|
|
148
|
+
const feature = featureOf(test);
|
|
149
|
+
if (feature || where)
|
|
150
|
+
lines.push(pc.dim(` ${feature ?? `${where}:${test.location.line}`}`));
|
|
151
|
+
const errors = result.errors.length ? result.errors : result.error ? [result.error] : [];
|
|
152
|
+
// A generated spec is SDODS's translation of the feature, not code anyone wrote: its snippet
|
|
153
|
+
// and line point at the wrong file, and the feature path above is the one to open.
|
|
154
|
+
const generated = feature !== undefined;
|
|
155
|
+
for (const e of errors)
|
|
156
|
+
lines.push('', indent(formatError(e, this.rootDir, generated), 5));
|
|
157
|
+
const files = result.attachments.filter((a) => a.path);
|
|
158
|
+
const shots = files.filter((a) => a.contentType.startsWith('image/'));
|
|
159
|
+
// The before/after shots are in the dashboard; the console names the one at the failure.
|
|
160
|
+
const shot = shots.find((a) => /failure|failed/i.test(a.path)) ?? shots[shots.length - 1];
|
|
161
|
+
const video = files.find((a) => a.name === 'video');
|
|
162
|
+
const trace = files.find((a) => a.name === 'trace');
|
|
163
|
+
if (shot || video || trace)
|
|
164
|
+
lines.push('');
|
|
165
|
+
if (shot)
|
|
166
|
+
lines.push(` ${pc.dim('screenshot')} ${relative(this.rootDir, shot.path)}`);
|
|
167
|
+
if (video)
|
|
168
|
+
lines.push(` ${pc.dim('video ')} ${relative(this.rootDir, video.path)}`);
|
|
169
|
+
if (trace)
|
|
170
|
+
lines.push(` ${pc.dim('trace ')} ${pc.cyan(`sdods trace ${quote(relative(this.rootDir, trace.path))}`)}`);
|
|
171
|
+
return lines.join('\n') + '\n';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Feature › Scenario, without the runner's project and generated-file segments. */
|
|
175
|
+
export function title(test) {
|
|
176
|
+
// titlePath(): ['', <run target>, <spec file>, ...describe blocks, <test title>]
|
|
177
|
+
const path = test.titlePath().slice(3).filter(Boolean);
|
|
178
|
+
return (path.length ? path : [test.title]).join(' › ');
|
|
179
|
+
}
|
|
180
|
+
/** `[api]`, `[ui · chromium]` — from the SDODS run-target name, not the raw project name. */
|
|
181
|
+
export function target(test) {
|
|
182
|
+
const name = test.parent?.project()?.name ?? '';
|
|
183
|
+
if (!name)
|
|
184
|
+
return '';
|
|
185
|
+
const parts = parseRunnerProjectName(name);
|
|
186
|
+
if (!parts)
|
|
187
|
+
return `[${name}]`;
|
|
188
|
+
const bits = [
|
|
189
|
+
parts.layer,
|
|
190
|
+
parts.layer === 'api' ? undefined : parts.browser,
|
|
191
|
+
parts.phase, // `setup`: the gate tier other scenarios depend on
|
|
192
|
+
].filter(Boolean);
|
|
193
|
+
return `[${bits.join(' · ')}]`;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* `<project>: features/<dir>/x.feature` for a scenario generated from Gherkin, read back from the
|
|
197
|
+
* generated spec path `.sdods/generated/<run>/<project>/<layer>/<dir>/x.feature.spec.js`.
|
|
198
|
+
*/
|
|
199
|
+
export function featureOf(test) {
|
|
200
|
+
const file = (test.location?.file ?? '').replace(/\\/g, '/');
|
|
201
|
+
const m = /\.sdods\/generated\/[^/]+\/([^/]+)\/[^/]+\/(.+\.feature)\.spec\.[cm]?js$/.exec(file);
|
|
202
|
+
return m ? `${m[1]}: features/${m[2]}` : undefined;
|
|
203
|
+
}
|
|
204
|
+
function formatError(error, rootDir, generated = false) {
|
|
205
|
+
const out = [];
|
|
206
|
+
let message = (error.message ?? error.value ?? 'Unknown error').trimEnd();
|
|
207
|
+
// The launch log (every browser flag, display warnings) follows "Browser logs:" and is in the
|
|
208
|
+
// trace; in the console it buries the one line that says what went wrong.
|
|
209
|
+
const logs = message.indexOf('\nBrowser logs:');
|
|
210
|
+
if (logs >= 0)
|
|
211
|
+
message = message.slice(0, logs).trimEnd() + pc.dim('\n(browser log: see the trace)');
|
|
212
|
+
out.push(message);
|
|
213
|
+
if (error.snippet && !generated)
|
|
214
|
+
out.push('', error.snippet);
|
|
215
|
+
if (error.location && !generated)
|
|
216
|
+
out.push(pc.dim(`at ${relative(rootDir, error.location.file)}:${error.location.line}`));
|
|
217
|
+
return out.join('\n');
|
|
218
|
+
}
|
|
219
|
+
function indent(text, n) {
|
|
220
|
+
const pad = ' '.repeat(n);
|
|
221
|
+
return text
|
|
222
|
+
.split('\n')
|
|
223
|
+
.map((l) => (l ? pad + l : l))
|
|
224
|
+
.join('\n');
|
|
225
|
+
}
|
|
226
|
+
function seconds(ms) {
|
|
227
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
228
|
+
}
|
|
229
|
+
function quote(p) {
|
|
230
|
+
return /[\s'"$`\\]/.test(p) ? `'${p.replace(/'/g, `'\\''`)}'` : p;
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=terminal.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How to run a package's CLI from the workspace: `[node, <its bin script>]` when the package is
|
|
3
|
+
* installed there, else `['npx', <bin>]`.
|
|
4
|
+
*
|
|
5
|
+
* `npx <bin>` only finds a tool through a `node_modules/.bin` shim. Some layouts have the package
|
|
6
|
+
* but no shim npx recognises (bun on Windows writes `.bunx` shims), and npx then downloads an
|
|
7
|
+
* unrelated registry package of the same name: `npx bddgen` fetched `bddgen@1.0.5` and every run
|
|
8
|
+
* failed with "bddgen failed to generate specs". Resolving the package and running its bin with
|
|
9
|
+
* this Node works in every layout.
|
|
10
|
+
*/
|
|
11
|
+
export declare function workspaceBin(rootDir: string, pkg: string, bin: string): [string, ...string[]];
|
|
12
|
+
/**
|
|
13
|
+
* `[file, ...argv]` for a runner tool invocation written as `['playwright', 'test', …]` or
|
|
14
|
+
* `['bddgen', …]`, resolved through {@link workspaceBin}. Every SDODS command that drives the
|
|
15
|
+
* runner goes through here rather than a bare `npx`, which can prompt to download a package or
|
|
16
|
+
* pick up an unrelated one of the same name.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toolCommand(rootDir: string, args: readonly string[]): [string, ...string[]];
|
|
19
|
+
//# sourceMappingURL=workspace-bin.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* How to run a package's CLI from the workspace: `[node, <its bin script>]` when the package is
|
|
6
|
+
* installed there, else `['npx', <bin>]`.
|
|
7
|
+
*
|
|
8
|
+
* `npx <bin>` only finds a tool through a `node_modules/.bin` shim. Some layouts have the package
|
|
9
|
+
* but no shim npx recognises (bun on Windows writes `.bunx` shims), and npx then downloads an
|
|
10
|
+
* unrelated registry package of the same name: `npx bddgen` fetched `bddgen@1.0.5` and every run
|
|
11
|
+
* failed with "bddgen failed to generate specs". Resolving the package and running its bin with
|
|
12
|
+
* this Node works in every layout.
|
|
13
|
+
*/
|
|
14
|
+
export function workspaceBin(rootDir, pkg, bin) {
|
|
15
|
+
try {
|
|
16
|
+
const manifest = createRequire(join(rootDir, 'package.json')).resolve(`${pkg}/package.json`);
|
|
17
|
+
const { bin: bins } = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
18
|
+
const rel = typeof bins === 'string' ? bins : bins?.[bin];
|
|
19
|
+
if (rel)
|
|
20
|
+
return [process.execPath, join(dirname(manifest), rel)];
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// not resolvable from here: fall back to npx
|
|
24
|
+
}
|
|
25
|
+
return ['npx', bin];
|
|
26
|
+
}
|
|
27
|
+
/** The packages behind the runner tools SDODS drives, keyed by the bin name. */
|
|
28
|
+
const TOOL_PACKAGES = {
|
|
29
|
+
playwright: '@playwright/test',
|
|
30
|
+
bddgen: 'playwright-bdd',
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* `[file, ...argv]` for a runner tool invocation written as `['playwright', 'test', …]` or
|
|
34
|
+
* `['bddgen', …]`, resolved through {@link workspaceBin}. Every SDODS command that drives the
|
|
35
|
+
* runner goes through here rather than a bare `npx`, which can prompt to download a package or
|
|
36
|
+
* pick up an unrelated one of the same name.
|
|
37
|
+
*/
|
|
38
|
+
export function toolCommand(rootDir, args) {
|
|
39
|
+
const [bin, ...rest] = args;
|
|
40
|
+
const pkg = bin ? TOOL_PACKAGES[bin] : undefined;
|
|
41
|
+
if (!bin || !pkg)
|
|
42
|
+
return ['npx', ...args];
|
|
43
|
+
const [file, ...pre] = workspaceBin(rootDir, pkg, bin);
|
|
44
|
+
return [file, ...pre, ...rest];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=workspace-bin.js.map
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "SDODS runtime: configuration, project registry, fixtures, step libraries, data providers, screenshot narratives and self-healing locators.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"funding": "https://sdods.com/sponsor/",
|
|
6
7
|
"author": "SDODS <admin@sdods.com>",
|
|
7
8
|
"type": "module",
|
|
8
9
|
"exports": {
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"./auth": "./dist/auth/index.js",
|
|
26
27
|
"./insights": "./dist/insights/index.js",
|
|
27
28
|
"./reporters/dashboard": "./dist/reporters/dashboard.js",
|
|
29
|
+
"./reporters/terminal": "./dist/reporters/terminal.js",
|
|
28
30
|
"./reporters": "./dist/reporters/index.js",
|
|
29
31
|
"./logger": "./dist/logger.js",
|
|
30
32
|
"./auth/capture": "./dist/auth/capture.js"
|
|
@@ -44,25 +46,25 @@
|
|
|
44
46
|
"playwright-bdd": ">=9"
|
|
45
47
|
},
|
|
46
48
|
"dependencies": {
|
|
47
|
-
"@sdods/contracts": "0.
|
|
49
|
+
"@sdods/contracts": "0.13.0",
|
|
48
50
|
"@cucumber/cucumber-expressions": "19.0.0",
|
|
49
51
|
"@cucumber/gherkin": "^42.0.1",
|
|
50
52
|
"@cucumber/messages": "^34.2.1",
|
|
51
53
|
"@faker-js/faker": "^10.6.0",
|
|
52
|
-
"@scalar/openapi-parser": "^0.29.
|
|
54
|
+
"@scalar/openapi-parser": "^0.29.12",
|
|
53
55
|
"ajv": "^8.20.0",
|
|
54
56
|
"ajv-formats": "^3.0.1",
|
|
55
|
-
"csv-parse": "^7.0.
|
|
57
|
+
"csv-parse": "^7.0.3",
|
|
56
58
|
"cucumber-tag-expressions": "^2.0.3",
|
|
57
|
-
"dotenv": "^
|
|
58
|
-
"execa": "^10.0
|
|
59
|
-
"jsonpath-plus": "^
|
|
59
|
+
"dotenv": "^18.0.7",
|
|
60
|
+
"execa": "^10.1.0",
|
|
61
|
+
"jsonpath-plus": "^11.2.0",
|
|
60
62
|
"picocolors": "^1.1.1",
|
|
61
63
|
"ts-morph": "^28.0.0",
|
|
62
|
-
"tsx": "^4.23.
|
|
64
|
+
"tsx": "^4.23.15",
|
|
63
65
|
"yaml": "^2.9.1",
|
|
64
66
|
"zod": "^4.6.5",
|
|
65
|
-
"@sdods/db": "0.
|
|
67
|
+
"@sdods/db": "0.13.0"
|
|
66
68
|
},
|
|
67
69
|
"homepage": "https://sdods.com",
|
|
68
70
|
"bugs": {
|