@sdods/core 0.3.2 → 0.5.2
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/api/client.d.ts +62 -1
- package/dist/api/client.js +189 -27
- package/dist/api/template.d.ts +15 -2
- package/dist/api/template.js +31 -13
- package/dist/auth/capture.js +4 -3
- package/dist/auth/index.js +3 -18
- package/dist/config/defaults.js +2 -0
- package/dist/config/resolve.d.ts +5 -1
- package/dist/config/resolve.js +16 -3
- package/dist/config/runner.d.ts +26 -1
- package/dist/config/runner.js +155 -24
- package/dist/config/tags.d.ts +14 -2
- package/dist/config/tags.js +27 -7
- package/dist/evidence/index.d.ts +3 -0
- package/dist/evidence/index.js +3 -0
- package/dist/evidence/trace-redact.d.ts +74 -0
- package/dist/evidence/trace-redact.js +297 -0
- package/dist/evidence/zip.d.ts +36 -0
- package/dist/evidence/zip.js +148 -0
- package/dist/fixtures/api-context.d.ts +7 -0
- package/dist/fixtures/api-context.js +7 -0
- package/dist/fixtures/auth.js +72 -5
- package/dist/fixtures/test.js +9 -2
- package/dist/heal/healer.d.ts +12 -1
- package/dist/heal/healer.js +80 -34
- package/dist/heal/strategies.d.ts +12 -2
- package/dist/heal/strategies.js +42 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lint/index.d.ts +1 -0
- package/dist/lint/index.js +44 -3
- package/dist/lint/steps.d.ts +13 -0
- package/dist/lint/steps.js +333 -0
- package/dist/load-module.d.ts +7 -0
- package/dist/load-module.js +70 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +9 -3
- package/dist/steps/a11y.steps.js +10 -10
- package/dist/steps/api.steps.js +64 -27
- package/dist/steps/browser.steps.js +34 -33
- package/dist/steps/clock.steps.js +5 -5
- package/dist/steps/data.steps.js +61 -8
- package/dist/steps/db.steps.js +8 -8
- package/dist/steps/dom.steps.js +49 -49
- package/dist/steps/hybrid.steps.js +9 -5
- package/dist/steps/iframe.steps.js +9 -9
- package/dist/steps/net.steps.d.ts +12 -7
- package/dist/steps/net.steps.js +78 -83
- package/dist/steps/perf.steps.js +6 -6
- package/dist/steps/tabs.steps.js +6 -6
- package/dist/steps/ui.steps.js +94 -45
- package/dist/steps/webhook.steps.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +15 -1
- package/package.json +6 -4
package/dist/config/runner.js
CHANGED
|
@@ -2,16 +2,51 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { join, resolve as resolvePath } from 'node:path';
|
|
3
3
|
import { devices } from '@playwright/test';
|
|
4
4
|
import { cucumberReporter, defineBddConfig } from 'playwright-bdd';
|
|
5
|
-
import { runnerProjectName, runFiles } from '@sdods/contracts';
|
|
5
|
+
import { EVIDENCE_DEFAULTS, RUNNER_SETUP_PHASE, runnerProjectName, runFiles, } from '@sdods/contracts';
|
|
6
6
|
import { coreStepsPatterns } from '../steps/glob.js';
|
|
7
7
|
import { combineTagExpr, normalizeTagExpr } from './tags.js';
|
|
8
8
|
const DEVICE_FOR_BROWSER = {
|
|
9
9
|
chromium: 'Desktop Chrome',
|
|
10
|
+
edge: 'Desktop Edge',
|
|
10
11
|
firefox: 'Desktop Firefox',
|
|
11
12
|
webkit: 'Desktop Safari',
|
|
12
13
|
'mobile-chrome': 'Pixel 7',
|
|
13
14
|
'mobile-safari': 'iPhone 15',
|
|
14
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Browsers that are a branded channel of an engine rather than the bundled build.
|
|
18
|
+
*
|
|
19
|
+
* `devices['Desktop Edge']` only sets an Edge user-agent — its `defaultBrowserType` is still
|
|
20
|
+
* 'chromium' — so without an explicit channel Playwright launches the bundled Chromium and it
|
|
21
|
+
* merely claims to be Edge. The channel is what actually starts the browser the user installed.
|
|
22
|
+
*/
|
|
23
|
+
const CHANNEL_FOR_BROWSER = { edge: 'msedge' };
|
|
24
|
+
/** The Playwright channel family a project-level `channel:` belongs to, for compatibility checks. */
|
|
25
|
+
function channelFamily(channel) {
|
|
26
|
+
return channel.startsWith('msedge') ? 'msedge' : 'chrome';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The channel a run target launches with.
|
|
30
|
+
*
|
|
31
|
+
* A project-level `channel:` refines a branded browser, it does not redirect it: `channel: chrome`
|
|
32
|
+
* on a project that also runs `edge` must not make the `--edge` target launch Google Chrome, which
|
|
33
|
+
* would put a browser in the results under another browser's name.
|
|
34
|
+
*/
|
|
35
|
+
function channelFor(browser, projectChannel) {
|
|
36
|
+
const branded = CHANNEL_FOR_BROWSER[browser];
|
|
37
|
+
if (!branded) {
|
|
38
|
+
// Chromium-family engines take whatever the project asked for; other engines have no channels.
|
|
39
|
+
return browser === 'chromium' || browser === 'mobile-chrome' ? projectChannel : undefined;
|
|
40
|
+
}
|
|
41
|
+
if (projectChannel && channelFamily(projectChannel) === channelFamily(branded))
|
|
42
|
+
return projectChannel;
|
|
43
|
+
return branded;
|
|
44
|
+
}
|
|
45
|
+
/** The `use` keys that select the browser binary for a run target. */
|
|
46
|
+
function browserUse(browser, projectChannel) {
|
|
47
|
+
const channel = channelFor(browser, projectChannel);
|
|
48
|
+
return { ...devices[DEVICE_FOR_BROWSER[browser]], ...(channel ? { channel } : {}) };
|
|
49
|
+
}
|
|
15
50
|
export const DASHBOARD_REPORTER = '@sdods/core/reporters/dashboard';
|
|
16
51
|
/** Convenience for `sdods.runner.config.ts`: read the selection from SDODS_* env vars. */
|
|
17
52
|
export function selectionFromEnv(env = process.env) {
|
|
@@ -32,6 +67,7 @@ export function selectionFromEnv(env = process.env) {
|
|
|
32
67
|
allure: env.SDODS_ALLURE === '1',
|
|
33
68
|
reporters: list(env.SDODS_REPORTERS),
|
|
34
69
|
reporterMode: env.SDODS_REPORTER_MODE || 'default',
|
|
70
|
+
process: env.SDODS_PROCESS || undefined,
|
|
35
71
|
};
|
|
36
72
|
}
|
|
37
73
|
/** Names (and identity) of the Playwright projects a selection would produce, without side effects. */
|
|
@@ -93,32 +129,40 @@ export function buildRunnerConfig(registry, sel = {}) {
|
|
|
93
129
|
for (const k of Object.keys(envUse))
|
|
94
130
|
if (envUse[k] === undefined)
|
|
95
131
|
delete envUse[k];
|
|
132
|
+
const byTag = p.retries?.byTag ?? {};
|
|
133
|
+
const pushProject = (project) => projects.push(...withRetriesByTag(project, byTag));
|
|
134
|
+
const { fullyParallel, setup } = runSettings(registry, entry.slug, p, sel.process);
|
|
135
|
+
// Playwright's own artifacts, per target so each project in a multi-project run keeps its own.
|
|
136
|
+
const evidenceUse = {
|
|
137
|
+
screenshot: p.evidence.screenshot,
|
|
138
|
+
video: p.evidence.video,
|
|
139
|
+
trace: p.evidence.trace,
|
|
140
|
+
};
|
|
96
141
|
for (const layer of layers) {
|
|
97
142
|
if (layer === 'recorded') {
|
|
98
143
|
const recordedDir = join(p.root, 'recorded');
|
|
99
144
|
if (!existsSync(recordedDir))
|
|
100
145
|
continue;
|
|
101
146
|
for (const browser of browsers) {
|
|
102
|
-
|
|
147
|
+
pushProject({
|
|
103
148
|
name: runnerProjectName({ project: p.slug, layer, browser }),
|
|
104
149
|
testDir: recordedDir,
|
|
105
150
|
testMatch: '**/*.spec.ts',
|
|
151
|
+
fullyParallel,
|
|
106
152
|
snapshotPathTemplate: join(p.root, 'features', '__screenshots__', '{projectName}', '{platform}', '{arg}{ext}'),
|
|
107
153
|
use: {
|
|
108
|
-
...
|
|
109
|
-
...(p.channel && (browser === 'chromium' || browser === 'mobile-chrome')
|
|
110
|
-
? { channel: p.channel }
|
|
111
|
-
: {}),
|
|
154
|
+
...browserUse(browser, p.channel),
|
|
112
155
|
baseURL: cfg.env.ui.baseUrl,
|
|
113
156
|
testIdAttribute: p.testIdAttribute,
|
|
114
157
|
...envUse,
|
|
158
|
+
...evidenceUse,
|
|
115
159
|
sdods: { project: p.slug, layer, browser },
|
|
116
160
|
},
|
|
117
161
|
});
|
|
118
162
|
}
|
|
119
163
|
continue;
|
|
120
164
|
}
|
|
121
|
-
const
|
|
165
|
+
const generate = (tags, outDir) => defineBddConfig({
|
|
122
166
|
features: `${toPosix(p.root)}/features/**/*.feature`,
|
|
123
167
|
steps: [
|
|
124
168
|
...coreStepsPatterns(cfg.project.steps?.core?.exclude ?? []),
|
|
@@ -127,42 +171,66 @@ export function buildRunnerConfig(registry, sel = {}) {
|
|
|
127
171
|
],
|
|
128
172
|
// Each run generates into its own dir (cleaned by `sdods run`), lint/export into `.lint`,
|
|
129
173
|
// so concurrent runs and tooling never race on generated specs.
|
|
130
|
-
outputDir: `${toPosix(join(cfg.runtime.repoRoot, '.sdods/generated', sel.lint ? '.lint' : (sel.runId ?? 'adhoc'), p.slug,
|
|
174
|
+
outputDir: `${toPosix(join(cfg.runtime.repoRoot, '.sdods/generated', sel.lint ? '.lint' : (sel.runId ?? 'adhoc'), p.slug, outDir))}`,
|
|
131
175
|
featuresRoot: `${toPosix(p.root)}/features`,
|
|
132
176
|
// Explicit: scenarios that only use core steps cannot let bddgen guess the project test instance.
|
|
133
177
|
importTestFrom: `${toPosix(p.root)}/steps/fixtures.ts`,
|
|
134
178
|
disableWarnings: { importTestFrom: true },
|
|
135
|
-
tags
|
|
179
|
+
tags,
|
|
136
180
|
examplesTitleFormat: 'Example #<_index_>',
|
|
137
181
|
missingSteps: sel.lint ? 'fail-on-gen' : 'fail-on-run',
|
|
138
182
|
aiFix: { promptAttachment: true },
|
|
139
183
|
quotes: 'single',
|
|
140
184
|
});
|
|
185
|
+
const layerExpr = combineTagExpr(`@${layer}`, sel.tags);
|
|
186
|
+
// Setup scenarios are generated on their own: whatever --tags selects, the gate still runs,
|
|
187
|
+
// and a scenario that is setup never runs a second time inside the target it gates.
|
|
188
|
+
const testDir = generate(setup ? `(${layerExpr}) and not (${setup.tags})` : layerExpr, layer);
|
|
141
189
|
bddConfigs++;
|
|
190
|
+
const setupTestDir = setup
|
|
191
|
+
? generate(combineTagExpr(`@${layer}`, setup.tags), `${layer}--${RUNNER_SETUP_PHASE}`)
|
|
192
|
+
: undefined;
|
|
193
|
+
if (setupTestDir)
|
|
194
|
+
bddConfigs++;
|
|
195
|
+
/** Push a target, preceded by its setup companion when the project has a setup tier. */
|
|
196
|
+
const pushTarget = (parts, target) => {
|
|
197
|
+
const name = runnerProjectName({ project: p.slug, ...parts });
|
|
198
|
+
if (setupTestDir) {
|
|
199
|
+
const setupName = runnerProjectName({
|
|
200
|
+
project: p.slug,
|
|
201
|
+
...parts,
|
|
202
|
+
phase: RUNNER_SETUP_PHASE,
|
|
203
|
+
});
|
|
204
|
+
projects.push({ ...target, name: setupName, testDir: setupTestDir });
|
|
205
|
+
pushProject({ ...target, name, testDir, dependencies: [setupName] });
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
pushProject({ ...target, name, testDir });
|
|
209
|
+
}
|
|
210
|
+
};
|
|
142
211
|
if (layer === 'api') {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
212
|
+
pushTarget({ layer }, {
|
|
213
|
+
fullyParallel,
|
|
214
|
+
use: {
|
|
215
|
+
...evidenceUse,
|
|
216
|
+
sdods: { project: p.slug, layer },
|
|
217
|
+
},
|
|
147
218
|
});
|
|
148
219
|
continue;
|
|
149
220
|
}
|
|
150
221
|
for (const browser of browsers) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
testDir,
|
|
222
|
+
pushTarget({ layer, browser }, {
|
|
223
|
+
fullyParallel,
|
|
154
224
|
// Baselines live with the project (generated specs are per-run and deleted):
|
|
155
225
|
// projects/<slug>/features/__screenshots__/<pw project>/<platform>/<name>.png
|
|
156
226
|
snapshotPathTemplate: join(p.root, 'features', '__screenshots__', '{projectName}', '{platform}', '{arg}{ext}'),
|
|
157
227
|
use: {
|
|
158
|
-
...
|
|
159
|
-
...(p.channel && (browser === 'chromium' || browser === 'mobile-chrome')
|
|
160
|
-
? { channel: p.channel }
|
|
161
|
-
: {}),
|
|
228
|
+
...browserUse(browser, p.channel),
|
|
162
229
|
baseURL: cfg.env.ui.baseUrl,
|
|
163
230
|
testIdAttribute: p.testIdAttribute,
|
|
164
231
|
viewport: browser.startsWith('mobile') ? undefined : p.screenshots.viewport,
|
|
165
232
|
...envUse,
|
|
233
|
+
...evidenceUse,
|
|
166
234
|
sdods: { project: p.slug, layer, browser },
|
|
167
235
|
},
|
|
168
236
|
});
|
|
@@ -227,13 +295,16 @@ export function buildRunnerConfig(registry, sel = {}) {
|
|
|
227
295
|
expect: { timeout: timeouts.expect },
|
|
228
296
|
retries,
|
|
229
297
|
workers,
|
|
230
|
-
|
|
298
|
+
// Each generated project sets its own; this is the fallback for anything added by hand.
|
|
299
|
+
fullyParallel: first
|
|
300
|
+
? runSettings(registry, first.project.slug, first.project, sel.process).fullyParallel
|
|
301
|
+
: true,
|
|
231
302
|
outputDir: join(runDir, runFiles.output),
|
|
232
303
|
reporter,
|
|
233
304
|
use: {
|
|
234
|
-
screenshot:
|
|
235
|
-
video:
|
|
236
|
-
trace:
|
|
305
|
+
screenshot: first?.project.evidence.screenshot ?? EVIDENCE_DEFAULTS.screenshot,
|
|
306
|
+
video: first?.project.evidence.video ?? EVIDENCE_DEFAULTS.video,
|
|
307
|
+
trace: first?.project.evidence.trace ?? EVIDENCE_DEFAULTS.trace,
|
|
237
308
|
actionTimeout: timeouts.action,
|
|
238
309
|
navigationTimeout: timeouts.navigation,
|
|
239
310
|
// Playwright's own `--headed` flag still wins on the CLI; this is what makes the documented
|
|
@@ -248,6 +319,66 @@ export function buildRunnerConfig(registry, sel = {}) {
|
|
|
248
319
|
},
|
|
249
320
|
};
|
|
250
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* `retries.byTag` as runner projects.
|
|
324
|
+
*
|
|
325
|
+
* Retries are fixed when a test is collected: Playwright takes them from the enclosing
|
|
326
|
+
* `test.describe.configure` or the project, and offers no runtime setter; bddgen has no hook to
|
|
327
|
+
* add `@retries:N` to a scenario it did not read from the feature file. What Playwright does
|
|
328
|
+
* offer is project-level `grep`, matched against the title AND the tags. So each byTag entry
|
|
329
|
+
* becomes a sibling of the project — same name, so `--project <name>`, reports and dashboards are
|
|
330
|
+
* unchanged — that greps for the tag and carries its retries, and the base project inverts every
|
|
331
|
+
* byTag grep. Entries are ordered by retries, highest first, and each sibling also inverts the
|
|
332
|
+
* ones before it, so a scenario with two such tags runs exactly once, with the larger count.
|
|
333
|
+
*
|
|
334
|
+
* Explicit `--retries` on the CLI still overrides every project, these included. A scenario's own
|
|
335
|
+
* `@retries:N` tag (playwright-bdd) wins over all of it.
|
|
336
|
+
*/
|
|
337
|
+
export function withRetriesByTag(project, byTag) {
|
|
338
|
+
const entries = Object.entries(byTag)
|
|
339
|
+
.map(([tag, retries]) => ({ tag: tag.startsWith('@') ? tag : `@${tag}`, retries }))
|
|
340
|
+
.sort((a, b) => b.retries - a.retries);
|
|
341
|
+
if (!entries.length)
|
|
342
|
+
return [project];
|
|
343
|
+
// Whitespace-delimited: Playwright joins the title path and tags with spaces, so `@flaky`
|
|
344
|
+
// must not match `@flakyish`. (A scenario TITLE containing the literal tag also matches.)
|
|
345
|
+
const tagRe = (tag) => new RegExp(`(^|\\s)${tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$)`);
|
|
346
|
+
const seen = [];
|
|
347
|
+
const out = [];
|
|
348
|
+
for (const { tag, retries } of entries) {
|
|
349
|
+
const re = tagRe(tag);
|
|
350
|
+
out.push({
|
|
351
|
+
...project,
|
|
352
|
+
grep: re,
|
|
353
|
+
...(seen.length ? { grepInvert: [...seen] } : {}),
|
|
354
|
+
retries,
|
|
355
|
+
});
|
|
356
|
+
seen.push(re);
|
|
357
|
+
}
|
|
358
|
+
return [{ ...project, grepInvert: seen }, ...out];
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* `fullyParallel` and the setup tier for one project, with a named process taking precedence.
|
|
362
|
+
* A process that is not defined for this project (a multi-project run) leaves the project's own.
|
|
363
|
+
*/
|
|
364
|
+
function runSettings(registry, slug, project, processName) {
|
|
365
|
+
const proc = processName
|
|
366
|
+
? registry.processesOf(slug).find((x) => x.name === processName)
|
|
367
|
+
: undefined;
|
|
368
|
+
return {
|
|
369
|
+
fullyParallel: proc?.fullyParallel ?? project.fullyParallel,
|
|
370
|
+
setup: setupTierOf(project, proc),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The setup tier a run uses: the process's `setup` (`false` turns it off), else the project's.
|
|
375
|
+
* Tags are normalised like `--tags`, so `setup: { tags: setup }` means `@setup`.
|
|
376
|
+
*/
|
|
377
|
+
export function setupTierOf(project, proc) {
|
|
378
|
+
const setup = proc?.setup === false ? undefined : (proc?.setup ?? project.setup);
|
|
379
|
+
const tags = normalizeTagExpr(setup?.tags);
|
|
380
|
+
return tags ? { tags } : undefined;
|
|
381
|
+
}
|
|
251
382
|
function parseEnvOverrides() {
|
|
252
383
|
const raw = process.env.SDODS_CLI_OVERRIDES;
|
|
253
384
|
if (!raw)
|
package/dist/config/tags.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ProjectConfig } from '@sdods/contracts';
|
|
2
2
|
export declare const LAYER_TAGS: readonly ["@ui", "@api", "@hybrid"];
|
|
3
3
|
/** Control tags the runner interprets itself; lint passes them through untouched. */
|
|
4
4
|
export declare const RUNNER_SPECIAL_TAGS: RegExp;
|
|
@@ -6,7 +6,11 @@ export declare const RUNNER_SPECIAL_TAGS: RegExp;
|
|
|
6
6
|
export declare const PLAYWRIGHT_BDD_SPECIAL: RegExp;
|
|
7
7
|
export declare const VALUE_TAG: RegExp;
|
|
8
8
|
export declare const KNOWN_VALUE_TAGS: readonly ["env", "user", "data", "har", "jira", "github", "skip", "title", "flag"];
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Values `@skip:<browser>` accepts. Derived from the schema rather than copied, so adding a browser
|
|
11
|
+
* to `BrowserSchema` is the only edit needed — and the lint message lists the new name for free.
|
|
12
|
+
*/
|
|
13
|
+
export declare const BROWSERS_FOR_SKIP: ("chromium" | "edge" | "firefox" | "webkit" | "mobile-chrome" | "mobile-safari")[];
|
|
10
14
|
export interface TagTaxonomy {
|
|
11
15
|
layers: readonly string[];
|
|
12
16
|
suites: string[];
|
|
@@ -24,6 +28,14 @@ export declare function suiteOfTags(tags: readonly string[], suites: readonly st
|
|
|
24
28
|
export declare function combineTagExpr(layerTag: string, userExpr?: string): string;
|
|
25
29
|
/** `--tags @smoke` shorthand also accepts comma lists ("@smoke,@sanity" → "@smoke or @sanity"). */
|
|
26
30
|
export declare function normalizeTagExpr(input?: string): string | undefined;
|
|
31
|
+
export interface TagExpr {
|
|
32
|
+
evaluate(tags: readonly string[]): boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse a Cucumber tag expression, or fail as a configuration error (exit 2) that quotes it.
|
|
36
|
+
* Every command that selects scenarios by tag goes through here so they agree on what matches.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseTagExpr(expr: string): TagExpr;
|
|
27
39
|
export interface TagGateContext {
|
|
28
40
|
/** `config.env.name` — the environment this run is actually pointed at. */
|
|
29
41
|
env: string;
|
package/dist/config/tags.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import parseInfix from 'cucumber-tag-expressions';
|
|
2
|
+
import { BrowserSchema } from '@sdods/contracts';
|
|
3
|
+
import { SdodsError } from '../errors.js';
|
|
1
4
|
export const LAYER_TAGS = ['@ui', '@api', '@hybrid'];
|
|
2
5
|
/** Control tags the runner interprets itself; lint passes them through untouched. */
|
|
3
6
|
export const RUNNER_SPECIAL_TAGS = /^@(only|skip|fixme|fail|slow|timeout:\d+|retries:\d+|mode:(parallel|serial|default))$/;
|
|
@@ -15,13 +18,11 @@ export const KNOWN_VALUE_TAGS = [
|
|
|
15
18
|
'title',
|
|
16
19
|
'flag',
|
|
17
20
|
];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'mobile-safari',
|
|
24
|
-
];
|
|
21
|
+
/**
|
|
22
|
+
* Values `@skip:<browser>` accepts. Derived from the schema rather than copied, so adding a browser
|
|
23
|
+
* to `BrowserSchema` is the only edit needed — and the lint message lists the new name for free.
|
|
24
|
+
*/
|
|
25
|
+
export const BROWSERS_FOR_SKIP = BrowserSchema.options;
|
|
25
26
|
export function taxonomyFromProject(project) {
|
|
26
27
|
return {
|
|
27
28
|
layers: LAYER_TAGS,
|
|
@@ -75,6 +76,25 @@ export function normalizeTagExpr(input) {
|
|
|
75
76
|
.map((t) => (t.startsWith('@') ? t : `@${t}`));
|
|
76
77
|
return parts.length > 1 ? parts.join(' or ') : parts[0];
|
|
77
78
|
}
|
|
79
|
+
// The package is CommonJS with `exports.default`; ESM interop may hand back the module object.
|
|
80
|
+
const parseInfixFn = (parseInfix.default ??
|
|
81
|
+
parseInfix);
|
|
82
|
+
/**
|
|
83
|
+
* Parse a Cucumber tag expression, or fail as a configuration error (exit 2) that quotes it.
|
|
84
|
+
* Every command that selects scenarios by tag goes through here so they agree on what matches.
|
|
85
|
+
*/
|
|
86
|
+
export function parseTagExpr(expr) {
|
|
87
|
+
try {
|
|
88
|
+
const node = parseInfixFn(expr);
|
|
89
|
+
return { evaluate: (tags) => node.evaluate([...tags]) };
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
throw new SdodsError('CONFIG_INVALID', `Invalid tag expression "${expr}": ${e.message}`, {
|
|
93
|
+
hint: 'Combine tags with and / or / not and balanced parentheses, e.g. "@ui and (@smoke or @sanity)".',
|
|
94
|
+
exitCode: 2,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
78
98
|
/**
|
|
79
99
|
* Why this scenario should not run here, or undefined to run it.
|
|
80
100
|
*
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type ZipEntry } from './zip.js';
|
|
2
|
+
/**
|
|
3
|
+
* Strip credentials out of Playwright traces (#101).
|
|
4
|
+
*
|
|
5
|
+
* A trace records every request and response header, the context's options (`extraHTTPHeaders`,
|
|
6
|
+
* `httpCredentials`, `storageState`), every `storageState()` / `addCookies()` call with its result,
|
|
7
|
+
* DOM snapshots and the arguments of every action. For a project that signs in with pool accounts
|
|
8
|
+
* that is the session cookie, the `Authorization` bearer and — for Firebase — the long-lived
|
|
9
|
+
* refresh token in localStorage/IndexedDB.
|
|
10
|
+
*
|
|
11
|
+
* What is replaced with `[redacted]`:
|
|
12
|
+
* - credential headers (`Cookie`, `Set-Cookie`, `Authorization`, `Proxy-Authorization`,
|
|
13
|
+
* `X-Api-Key` and similar) wherever a `{ name, value }` header or a header map appears —
|
|
14
|
+
* `.network` request/response entries, context options, route and fetch parameters;
|
|
15
|
+
* - every cookie value, localStorage/sessionStorage value and IndexedDB record;
|
|
16
|
+
* - `password` / `passphrase` fields (`httpCredentials`, `proxy`, client certificates);
|
|
17
|
+
* - the value of password inputs in DOM snapshots, and values typed by `fill`/`type` into a field
|
|
18
|
+
* whose locator names a password, secret, token or one-time code (best effort).
|
|
19
|
+
*
|
|
20
|
+
* Not covered: response and request bodies (`resources/`), page text and screenshots. A redacted
|
|
21
|
+
* trace is safer to keep, not safe to publish.
|
|
22
|
+
*
|
|
23
|
+
* Only `*.trace` and `*.network` entries are rewritten; everything else (sources, screenshots,
|
|
24
|
+
* resources, stacks) is copied through unchanged, so the trace viewer opens the result as before.
|
|
25
|
+
*/
|
|
26
|
+
export declare const REDACTED = "[redacted]";
|
|
27
|
+
export declare const TRACE_SECRET_HEADER: RegExp;
|
|
28
|
+
/**
|
|
29
|
+
* Redact one parsed trace event in place. Returns the number of values replaced.
|
|
30
|
+
*
|
|
31
|
+
* `typed` carries values redacted from typing actions across the events of one stream, by call id,
|
|
32
|
+
* so the action's `log` lines (`fill("…")`) lose the value too.
|
|
33
|
+
*/
|
|
34
|
+
export declare function redactTraceEvent(event: unknown, typed?: Map<string, string>): number;
|
|
35
|
+
/** Redact every JSON line of a `.trace` / `.network` entry. Unparseable lines are kept as they are. */
|
|
36
|
+
export declare function redactTraceJsonl(text: string): {
|
|
37
|
+
text: string;
|
|
38
|
+
changed: number;
|
|
39
|
+
};
|
|
40
|
+
/** True when the buffer is a zip holding a Playwright trace (or a zip that nests one). */
|
|
41
|
+
export declare function isTraceZip(entries: ZipEntry[]): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Redact a trace zip held in memory. Zips nested inside (blob shard reports keep attachments as
|
|
44
|
+
* `resources/<sha>.zip`) are redacted too. Non-trace zips come back untouched with `trace: false`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function redactTraceZip(buf: Buffer): {
|
|
47
|
+
buffer: Buffer;
|
|
48
|
+
changed: number;
|
|
49
|
+
trace: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** Redact a trace zip on disk in place. Returns values replaced (0 when not a trace). */
|
|
52
|
+
export declare function redactTraceFile(file: string): number;
|
|
53
|
+
/**
|
|
54
|
+
* Cucumber messages embed each trace as a BASE64 attachment body, and ingest writes `trace.zip`
|
|
55
|
+
* from those bytes — so the copy in `messages*.ndjson` is redacted as well.
|
|
56
|
+
*/
|
|
57
|
+
export declare function redactMessagesFile(file: string): number;
|
|
58
|
+
export interface RunTraceRedaction {
|
|
59
|
+
/** trace zips and message files that were rewritten */
|
|
60
|
+
files: string[];
|
|
61
|
+
/** values replaced across all of them */
|
|
62
|
+
values: number;
|
|
63
|
+
/** files that could not be read or rewritten, with the reason */
|
|
64
|
+
errors: Array<{
|
|
65
|
+
file: string;
|
|
66
|
+
error: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Redact every trace a run left behind: `runner-output/<test>/trace.zip`, the HTML report's copies
|
|
71
|
+
* under `html-report/data/`, blob shard reports, and the BASE64 bodies in `messages*.ndjson`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function redactRunTraces(runDir: string, maxDepth?: number): RunTraceRedaction;
|
|
74
|
+
//# sourceMappingURL=trace-redact.d.ts.map
|