@sdods/core 0.2.1 → 0.2.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/analyze/index.d.ts +1 -0
- package/dist/analyze/index.js +1 -0
- package/dist/analyze/modules.d.ts +74 -0
- package/dist/analyze/modules.js +353 -0
- package/dist/analyze/propose.js +28 -48
- package/dist/auth/capture.js +2 -1
- package/dist/auth/index.js +48 -10
- package/dist/config/playwright.d.ts +37 -0
- package/dist/config/playwright.js +262 -0
- package/dist/config/runner.js +3 -0
- package/dist/fixtures/auth.d.ts +9 -1
- package/dist/fixtures/auth.js +13 -5
- package/dist/fixtures/scenario.js +4 -1
- package/dist/har/api-har.d.ts +1 -0
- package/dist/har/api-har.js +1 -1
- package/dist/har/index.d.ts +1 -0
- package/dist/har/index.js +1 -0
- package/dist/har/scrub.d.ts +17 -0
- package/dist/har/scrub.js +60 -0
- package/dist/shots/hooks.js +10 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, resolve as resolvePath } from 'node:path';
|
|
3
|
+
import { devices } from '@playwright/test';
|
|
4
|
+
import { cucumberReporter, defineBddConfig } from 'playwright-bdd';
|
|
5
|
+
import { pwProjectName, runFiles } from '@sdods/contracts';
|
|
6
|
+
import { coreStepsGlob } from '../steps/glob.js';
|
|
7
|
+
import { combineTagExpr, normalizeTagExpr } from './tags.js';
|
|
8
|
+
const DEVICE_FOR_BROWSER = {
|
|
9
|
+
chromium: 'Desktop Chrome',
|
|
10
|
+
firefox: 'Desktop Firefox',
|
|
11
|
+
webkit: 'Desktop Safari',
|
|
12
|
+
'mobile-chrome': 'Pixel 7',
|
|
13
|
+
'mobile-safari': 'iPhone 15',
|
|
14
|
+
};
|
|
15
|
+
export const DASHBOARD_REPORTER = '@sdods/core/reporters/dashboard';
|
|
16
|
+
/** Convenience for `playwright.config.ts`: read the selection from SDODS_* env vars. */
|
|
17
|
+
export function selectionFromEnv(env = process.env) {
|
|
18
|
+
const list = (v) => v
|
|
19
|
+
? v
|
|
20
|
+
.split(',')
|
|
21
|
+
.map((s) => s.trim())
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
: undefined;
|
|
24
|
+
return {
|
|
25
|
+
project: env.SDODS_PROJECT || undefined,
|
|
26
|
+
env: env.SDODS_ENV || undefined,
|
|
27
|
+
layers: list(env.SDODS_LAYERS),
|
|
28
|
+
browsers: list(env.SDODS_BROWSERS),
|
|
29
|
+
tags: normalizeTagExpr(env.SDODS_TAGS),
|
|
30
|
+
runId: env.SDODS_RUN_ID || undefined,
|
|
31
|
+
lint: env.SDODS_LINT === '1',
|
|
32
|
+
allure: env.SDODS_ALLURE === '1',
|
|
33
|
+
reporters: list(env.SDODS_REPORTERS),
|
|
34
|
+
reporterMode: env.SDODS_REPORTER_MODE || 'default',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Names (and identity) of the Playwright projects a selection would produce, without side effects. */
|
|
38
|
+
export function listGeneratedProjects(registry, sel) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const entry of sel.project ? [registry.entry(sel.project)] : registry.entriesList()) {
|
|
41
|
+
const p = entry.config;
|
|
42
|
+
const layers = (sel.layers?.length ? p.layers.filter((l) => sel.layers.includes(l)) : p.layers);
|
|
43
|
+
const browsers = (sel.browsers?.length
|
|
44
|
+
? p.browsers.filter((b) => sel.browsers.includes(b))
|
|
45
|
+
: p.browsers);
|
|
46
|
+
for (const layer of layers) {
|
|
47
|
+
if (layer === 'api') {
|
|
48
|
+
out.push({ name: pwProjectName({ project: p.slug, layer }), project: p.slug, layer });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (layer === 'recorded' && !existsSync(join(entry.root, 'recorded')))
|
|
52
|
+
continue;
|
|
53
|
+
for (const browser of browsers) {
|
|
54
|
+
out.push({
|
|
55
|
+
name: pwProjectName({ project: p.slug, layer, browser }),
|
|
56
|
+
project: p.slug,
|
|
57
|
+
layer,
|
|
58
|
+
browser,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the Playwright config for a selection of projects × layers × browsers.
|
|
67
|
+
* One `defineBddConfig` per project × layer; browsers reuse the generated testDir.
|
|
68
|
+
*/
|
|
69
|
+
export function buildPlaywrightConfig(registry, sel = {}) {
|
|
70
|
+
const entries = sel.project ? [registry.entry(sel.project)] : registry.entriesList();
|
|
71
|
+
const projects = [];
|
|
72
|
+
let first;
|
|
73
|
+
let bddConfigs = 0;
|
|
74
|
+
const cliOverrides = sel.runId ? { runId: sel.runId } : undefined;
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const cfg = registry.resolve(entry.slug, sel.env, cliOverrides ? { ...parseEnvOverrides(), ...cliOverrides } : undefined);
|
|
77
|
+
first ??= cfg;
|
|
78
|
+
const p = cfg.project;
|
|
79
|
+
const layers = (sel.layers?.length ? p.layers.filter((l) => sel.layers.includes(l)) : p.layers);
|
|
80
|
+
const browsers = (sel.browsers?.length
|
|
81
|
+
? p.browsers.filter((b) => sel.browsers.includes(b))
|
|
82
|
+
: p.browsers);
|
|
83
|
+
const envUse = {
|
|
84
|
+
locale: cfg.env.use.locale,
|
|
85
|
+
timezoneId: cfg.env.use.timezoneId,
|
|
86
|
+
geolocation: cfg.env.use.geolocation,
|
|
87
|
+
permissions: cfg.env.use.permissions,
|
|
88
|
+
colorScheme: cfg.env.use.colorScheme,
|
|
89
|
+
ignoreHTTPSErrors: cfg.env.use.ignoreHTTPSErrors,
|
|
90
|
+
extraHTTPHeaders: cfg.env.use.extraHTTPHeaders,
|
|
91
|
+
httpCredentials: cfg.env.use.httpCredentials,
|
|
92
|
+
};
|
|
93
|
+
for (const k of Object.keys(envUse))
|
|
94
|
+
if (envUse[k] === undefined)
|
|
95
|
+
delete envUse[k];
|
|
96
|
+
for (const layer of layers) {
|
|
97
|
+
if (layer === 'recorded') {
|
|
98
|
+
const recordedDir = join(p.root, 'recorded');
|
|
99
|
+
if (!existsSync(recordedDir))
|
|
100
|
+
continue;
|
|
101
|
+
for (const browser of browsers) {
|
|
102
|
+
projects.push({
|
|
103
|
+
name: pwProjectName({ project: p.slug, layer, browser }),
|
|
104
|
+
testDir: recordedDir,
|
|
105
|
+
testMatch: '**/*.spec.ts',
|
|
106
|
+
snapshotPathTemplate: join(p.root, 'features', '__screenshots__', '{projectName}', '{platform}', '{arg}{ext}'),
|
|
107
|
+
use: {
|
|
108
|
+
...devices[DEVICE_FOR_BROWSER[browser]],
|
|
109
|
+
...(p.channel && (browser === 'chromium' || browser === 'mobile-chrome')
|
|
110
|
+
? { channel: p.channel }
|
|
111
|
+
: {}),
|
|
112
|
+
baseURL: cfg.env.ui.baseUrl,
|
|
113
|
+
testIdAttribute: p.testIdAttribute,
|
|
114
|
+
...envUse,
|
|
115
|
+
sdods: { project: p.slug, layer, browser },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const testDir = defineBddConfig({
|
|
122
|
+
features: `${toPosix(p.root)}/features/**/*.feature`,
|
|
123
|
+
steps: [
|
|
124
|
+
coreStepsGlob(),
|
|
125
|
+
`${toPosix(p.root)}/steps/**/*.ts`,
|
|
126
|
+
`${toPosix(p.root)}/pages/**/*.ts`,
|
|
127
|
+
],
|
|
128
|
+
// Each run generates into its own dir (cleaned by `sdods run`), lint/export into `.lint`,
|
|
129
|
+
// so concurrent runs and tooling never race on generated specs.
|
|
130
|
+
outputDir: `${toPosix(join(cfg.runtime.repoRoot, '.features-gen', sel.lint ? '.lint' : (sel.runId ?? 'adhoc'), p.slug, layer))}`,
|
|
131
|
+
featuresRoot: `${toPosix(p.root)}/features`,
|
|
132
|
+
// Explicit: scenarios that only use core steps cannot let bddgen guess the project test instance.
|
|
133
|
+
importTestFrom: `${toPosix(p.root)}/steps/fixtures.ts`,
|
|
134
|
+
disableWarnings: { importTestFrom: true },
|
|
135
|
+
tags: combineTagExpr(`@${layer}`, sel.tags),
|
|
136
|
+
examplesTitleFormat: 'Example #<_index_>',
|
|
137
|
+
missingSteps: sel.lint ? 'fail-on-gen' : 'fail-on-run',
|
|
138
|
+
aiFix: { promptAttachment: true },
|
|
139
|
+
quotes: 'single',
|
|
140
|
+
});
|
|
141
|
+
bddConfigs++;
|
|
142
|
+
if (layer === 'api') {
|
|
143
|
+
projects.push({
|
|
144
|
+
name: pwProjectName({ project: p.slug, layer }),
|
|
145
|
+
testDir,
|
|
146
|
+
use: { sdods: { project: p.slug, layer } },
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const browser of browsers) {
|
|
151
|
+
projects.push({
|
|
152
|
+
name: pwProjectName({ project: p.slug, layer, browser }),
|
|
153
|
+
testDir,
|
|
154
|
+
// Baselines live with the project (generated specs are per-run and deleted):
|
|
155
|
+
// projects/<slug>/features/__screenshots__/<pw project>/<platform>/<name>.png
|
|
156
|
+
snapshotPathTemplate: join(p.root, 'features', '__screenshots__', '{projectName}', '{platform}', '{arg}{ext}'),
|
|
157
|
+
use: {
|
|
158
|
+
...devices[DEVICE_FOR_BROWSER[browser]],
|
|
159
|
+
...(p.channel && (browser === 'chromium' || browser === 'mobile-chrome')
|
|
160
|
+
? { channel: p.channel }
|
|
161
|
+
: {}),
|
|
162
|
+
baseURL: cfg.env.ui.baseUrl,
|
|
163
|
+
testIdAttribute: p.testIdAttribute,
|
|
164
|
+
viewport: browser.startsWith('mobile') ? undefined : p.screenshots.viewport,
|
|
165
|
+
...envUse,
|
|
166
|
+
sdods: { project: p.slug, layer, browser },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const runDir = first?.runtime.runDir ?? resolvePath('.sdods/runs/adhoc');
|
|
173
|
+
const timeouts = first?.project.timeouts ?? {
|
|
174
|
+
test: 60_000,
|
|
175
|
+
expect: 10_000,
|
|
176
|
+
action: 15_000,
|
|
177
|
+
navigation: 30_000,
|
|
178
|
+
api: 15_000,
|
|
179
|
+
};
|
|
180
|
+
const workers = first?.runtime.workers;
|
|
181
|
+
const retries = first?.runtime.retries ?? 0;
|
|
182
|
+
const reporterMode = sel.reporterMode ?? 'default';
|
|
183
|
+
const reporter = [];
|
|
184
|
+
// `--reporter <name[=outputFile]>` ADDS reporters (e.g. `blob` for sharded CI) to the SDODS
|
|
185
|
+
// defaults, so the NDJSON, dashboard and HTML report always exist. Path-less `blob`/`json`/`junit`
|
|
186
|
+
// land inside the run directory.
|
|
187
|
+
const extra = (sel.reporters ?? []).map((r) => {
|
|
188
|
+
const [name, file] = r.split('=');
|
|
189
|
+
if (file)
|
|
190
|
+
return [name, name === 'blob' ? { outputDir: file } : { outputFile: file }];
|
|
191
|
+
if (name === 'blob')
|
|
192
|
+
return ['blob', { outputDir: join(runDir, 'blob-report') }];
|
|
193
|
+
if (name === 'json')
|
|
194
|
+
return ['json', { outputFile: join(runDir, 'pw-results.extra.json') }];
|
|
195
|
+
if (name === 'junit')
|
|
196
|
+
return ['junit', { outputFile: join(runDir, runFiles.junit) }];
|
|
197
|
+
return [name];
|
|
198
|
+
});
|
|
199
|
+
{
|
|
200
|
+
reporter.push(reporterMode === 'server' ? ['line'] : reporterMode === 'quiet' ? ['dot'] : ['list']);
|
|
201
|
+
reporter.push(['html', { outputFolder: join(runDir, runFiles.pwReport), open: 'never' }]);
|
|
202
|
+
// The cucumber reporter throws when no defineBddConfig() ran (recorded-only selections).
|
|
203
|
+
if (bddConfigs > 0)
|
|
204
|
+
reporter.push(cucumberReporter('message', {
|
|
205
|
+
outputFile: join(runDir, runFiles.messages),
|
|
206
|
+
}));
|
|
207
|
+
reporter.push([DASHBOARD_REPORTER, { outputDir: join(runDir, runFiles.dashboard) }]);
|
|
208
|
+
if (projects.some((p) => String(p.name).includes('--recorded--'))) {
|
|
209
|
+
reporter.push(['json', { outputFile: join(runDir, runFiles.pwResults) }]);
|
|
210
|
+
}
|
|
211
|
+
if (first?.project.reports.junit || first?.runtime.ci)
|
|
212
|
+
reporter.push(['junit', { outputFile: join(runDir, runFiles.junit) }]);
|
|
213
|
+
if (first?.project.reports.cucumberHtml)
|
|
214
|
+
reporter.push(cucumberReporter('html', {
|
|
215
|
+
outputFile: join(runDir, 'cucumber-report.html'),
|
|
216
|
+
}));
|
|
217
|
+
if (sel.allure || first?.project.reports.allure)
|
|
218
|
+
reporter.push(['allure-playwright', { resultsDir: join(runDir, 'allure-results') }]);
|
|
219
|
+
}
|
|
220
|
+
for (const r of extra) {
|
|
221
|
+
if (reporter.some((d) => d[0] === r[0]))
|
|
222
|
+
continue; // already emitted by defaults (e.g. junit in CI)
|
|
223
|
+
reporter.push(r);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
timeout: timeouts.test,
|
|
227
|
+
expect: { timeout: timeouts.expect },
|
|
228
|
+
retries,
|
|
229
|
+
workers,
|
|
230
|
+
fullyParallel: true,
|
|
231
|
+
outputDir: join(runDir, runFiles.pwOutput),
|
|
232
|
+
reporter,
|
|
233
|
+
use: {
|
|
234
|
+
screenshot: 'off',
|
|
235
|
+
video: 'retain-on-failure',
|
|
236
|
+
trace: 'on-first-retry',
|
|
237
|
+
actionTimeout: timeouts.action,
|
|
238
|
+
navigationTimeout: timeouts.navigation,
|
|
239
|
+
},
|
|
240
|
+
projects,
|
|
241
|
+
metadata: {
|
|
242
|
+
sdodsRunId: first?.runtime.runId,
|
|
243
|
+
sdodsEnv: first?.env.name,
|
|
244
|
+
sdodsRunDir: runDir,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function parseEnvOverrides() {
|
|
249
|
+
const raw = process.env.SDODS_CLI_OVERRIDES;
|
|
250
|
+
if (!raw)
|
|
251
|
+
return {};
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(raw);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function toPosix(p) {
|
|
260
|
+
return p.replace(/\\/g, '/');
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=playwright.js.map
|
package/dist/config/runner.js
CHANGED
|
@@ -236,6 +236,9 @@ export function buildRunnerConfig(registry, sel = {}) {
|
|
|
236
236
|
trace: 'on-first-retry',
|
|
237
237
|
actionTimeout: timeouts.action,
|
|
238
238
|
navigationTimeout: timeouts.navigation,
|
|
239
|
+
// Playwright's own `--headed` flag still wins on the CLI; this is what makes the documented
|
|
240
|
+
// SDODS_HEADED env var and the --headed CliOverride reach the browser at all.
|
|
241
|
+
headless: !first?.runtime.headed,
|
|
239
242
|
},
|
|
240
243
|
projects,
|
|
241
244
|
metadata: {
|
package/dist/fixtures/auth.d.ts
CHANGED
|
@@ -39,7 +39,15 @@ export declare class AuthStateCache {
|
|
|
39
39
|
private readState;
|
|
40
40
|
/** Return a fresh storageState path for the user, capturing it with the strategy when needed. */
|
|
41
41
|
ensure(user: PoolUserLike, auth: AuthStrategy, browser: Browser): Promise<string | undefined>;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Atomic save: temp file + rename, sidecar written last.
|
|
44
|
+
*
|
|
45
|
+
* Written owner-only. This file is a live session for the application under test — cookies and
|
|
46
|
+
* localStorage, replayable as-is — and it used to land world-readable (0644 in a 0755
|
|
47
|
+
* directory), so any other account on the machine could lift it. Encryption is not the control
|
|
48
|
+
* here: the state has to be decryptable to be replayed, so a key would have to sit on the same
|
|
49
|
+
* disk. Restricting access is what actually protects it.
|
|
50
|
+
*/
|
|
43
51
|
save(user: PoolUserLike, state: Record<string, unknown>): string;
|
|
44
52
|
/** Apply a cached state to a live context/page (step path: cookies + localStorage). */
|
|
45
53
|
apply(args: {
|
package/dist/fixtures/auth.js
CHANGED
|
@@ -92,15 +92,23 @@ export class AuthStateCache {
|
|
|
92
92
|
return this.save(user, state);
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Atomic save: temp file + rename, sidecar written last.
|
|
97
|
+
*
|
|
98
|
+
* Written owner-only. This file is a live session for the application under test — cookies and
|
|
99
|
+
* localStorage, replayable as-is — and it used to land world-readable (0644 in a 0755
|
|
100
|
+
* directory), so any other account on the machine could lift it. Encryption is not the control
|
|
101
|
+
* here: the state has to be decryptable to be replayed, so a key would have to sit on the same
|
|
102
|
+
* disk. Restricting access is what actually protects it.
|
|
103
|
+
*/
|
|
96
104
|
save(user, state) {
|
|
97
|
-
mkdirSync(this.dir, { recursive: true });
|
|
105
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
98
106
|
const file = this.fileFor(user);
|
|
99
107
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
100
|
-
writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
108
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
101
109
|
renameSync(tmp, file);
|
|
102
110
|
const sideTmp = `${this.sidecarFor(user)}.${process.pid}.tmp`;
|
|
103
|
-
writeFileSync(sideTmp, JSON.stringify({ capturedAt: new Date().toISOString(), user: user.username, role: user.role }, null, 2));
|
|
111
|
+
writeFileSync(sideTmp, JSON.stringify({ capturedAt: new Date().toISOString(), user: user.username, role: user.role }, null, 2), { mode: 0o600 });
|
|
104
112
|
renameSync(sideTmp, this.sidecarFor(user));
|
|
105
113
|
return file;
|
|
106
114
|
}
|
|
@@ -147,7 +155,7 @@ export class AuthStateCache {
|
|
|
147
155
|
* LOCK_STALE_MS is treated as abandoned by a crashed worker.
|
|
148
156
|
*/
|
|
149
157
|
async withLock(user, fn) {
|
|
150
|
-
mkdirSync(this.dir, { recursive: true });
|
|
158
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
151
159
|
const lock = this.lockFor(user);
|
|
152
160
|
const deadline = Date.now() + LOCK_STALE_MS * 2;
|
|
153
161
|
for (;;) {
|
|
@@ -37,7 +37,10 @@ export class ScenarioMeta {
|
|
|
37
37
|
pickleLine: init.pickleLine,
|
|
38
38
|
exampleIndex: null,
|
|
39
39
|
tags,
|
|
40
|
-
|
|
40
|
+
// `featureUri`, not `init.featureUri`: playwright-bdd hands us a repo-root-relative path,
|
|
41
|
+
// and moduleForFeature resolves its argument against project.root, so passing the raw
|
|
42
|
+
// value yields projects/<slug>/projects/<slug>/... and matches no module.
|
|
43
|
+
module: moduleForFeature(config.project, featureUri)?.name,
|
|
41
44
|
process: process.env.SDODS_PROCESS || undefined,
|
|
42
45
|
retry: testInfo.retry,
|
|
43
46
|
workerIndex: testInfo.workerIndex,
|
package/dist/har/api-har.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export interface HarFile {
|
|
|
63
63
|
entries: SdodsHarEntry[];
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
+
export declare const SECRET_HEADER: RegExp;
|
|
66
67
|
export declare function normalizeUrl(url: string): string;
|
|
67
68
|
export declare function bodyHash(body: unknown): string;
|
|
68
69
|
export declare function harKey(method: string, url: string, body?: unknown): string;
|
package/dist/har/api-har.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { redact } from '../logger.js';
|
|
5
5
|
import { VERSION } from '../version.js';
|
|
6
|
-
const SECRET_HEADER = /^(authorization|cookie|set-cookie|x-api-key|proxy-authorization)$/i;
|
|
6
|
+
export const SECRET_HEADER = /^(authorization|cookie|set-cookie|x-api-key|proxy-authorization)$/i;
|
|
7
7
|
const VOLATILE_QUERY = /^(_|t|ts|timestamp|nonce|cb|cache)$/i;
|
|
8
8
|
export function normalizeUrl(url) {
|
|
9
9
|
let u;
|
package/dist/har/index.d.ts
CHANGED
package/dist/har/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip credentials out of a HAR recorded by the browser.
|
|
3
|
+
*
|
|
4
|
+
* The API layer builds its own HAR and redacts secret headers as it goes. The browser layer does
|
|
5
|
+
* not: Playwright's `routeFromHAR({ update: true })` writes the file itself, verbatim, including
|
|
6
|
+
* the `Cookie`, `Set-Cookie` and `Authorization` headers of the application under test. HARs are
|
|
7
|
+
* meant to be committed — the demo project's are in git — so recording against a real application
|
|
8
|
+
* while signed in wrote a live session into a tracked file.
|
|
9
|
+
*
|
|
10
|
+
* Values are replaced rather than the headers removed, so replay still matches on their presence.
|
|
11
|
+
*/
|
|
12
|
+
export declare function scrubHar(har: unknown): {
|
|
13
|
+
changed: number;
|
|
14
|
+
};
|
|
15
|
+
/** Scrub a HAR file in place. Returns how many values were replaced, or -1 if it is not readable. */
|
|
16
|
+
export declare function scrubHarFile(file: string): number;
|
|
17
|
+
//# sourceMappingURL=scrub.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { SECRET_HEADER } from './api-har.js';
|
|
3
|
+
/**
|
|
4
|
+
* Strip credentials out of a HAR recorded by the browser.
|
|
5
|
+
*
|
|
6
|
+
* The API layer builds its own HAR and redacts secret headers as it goes. The browser layer does
|
|
7
|
+
* not: Playwright's `routeFromHAR({ update: true })` writes the file itself, verbatim, including
|
|
8
|
+
* the `Cookie`, `Set-Cookie` and `Authorization` headers of the application under test. HARs are
|
|
9
|
+
* meant to be committed — the demo project's are in git — so recording against a real application
|
|
10
|
+
* while signed in wrote a live session into a tracked file.
|
|
11
|
+
*
|
|
12
|
+
* Values are replaced rather than the headers removed, so replay still matches on their presence.
|
|
13
|
+
*/
|
|
14
|
+
export function scrubHar(har) {
|
|
15
|
+
let changed = 0;
|
|
16
|
+
const entries = har?.log?.entries;
|
|
17
|
+
if (!Array.isArray(entries))
|
|
18
|
+
return { changed };
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
for (const side of ['request', 'response']) {
|
|
21
|
+
const headers = entry[side]?.headers;
|
|
22
|
+
if (!Array.isArray(headers))
|
|
23
|
+
continue;
|
|
24
|
+
for (const h of headers) {
|
|
25
|
+
if (h && typeof h.name === 'string' && SECRET_HEADER.test(h.name) && h.value !== '***') {
|
|
26
|
+
h.value = '***';
|
|
27
|
+
changed++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Playwright also records cookies as structured arrays alongside the headers.
|
|
32
|
+
for (const side of ['request', 'response']) {
|
|
33
|
+
const cookies = entry[side]?.cookies;
|
|
34
|
+
if (!Array.isArray(cookies))
|
|
35
|
+
continue;
|
|
36
|
+
for (const c of cookies) {
|
|
37
|
+
if (c && typeof c.value === 'string' && c.value !== '***') {
|
|
38
|
+
c.value = '***';
|
|
39
|
+
changed++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { changed };
|
|
45
|
+
}
|
|
46
|
+
/** Scrub a HAR file in place. Returns how many values were replaced, or -1 if it is not readable. */
|
|
47
|
+
export function scrubHarFile(file) {
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
const { changed } = scrubHar(parsed);
|
|
56
|
+
if (changed > 0)
|
|
57
|
+
writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
58
|
+
return changed;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=scrub.js.map
|
package/dist/shots/hooks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachmentNames } from '@sdods/contracts';
|
|
1
2
|
import { AfterScenario, AfterStep, BeforeScenario, BeforeStep } from '../fixtures/test.js';
|
|
2
3
|
/**
|
|
3
4
|
* Screenshot narrative hooks. Tag-filtered so API scenarios never instantiate a page.
|
|
@@ -32,5 +33,14 @@ AfterScenario({ name: 'sdods:finalize' }, async ({ scenario, apiContext, heal, $
|
|
|
32
33
|
apiCalls: apiContext.history.length,
|
|
33
34
|
heals: heal.events.length,
|
|
34
35
|
});
|
|
36
|
+
// Publish the scenario identity to the report. Ingest already understands this attachment
|
|
37
|
+
// (parseAttachmentName -> kind 'meta') and prefers its `module` over the directory guess in
|
|
38
|
+
// moduleFromUri(), which is wrong whenever a module's `path` differs from its `name` -
|
|
39
|
+
// demo-shop's `posts-api` lives in features/api/, so the guess yields "api" and never joins
|
|
40
|
+
// to modules.name. Without this attach the meta branch only ever fired in tests.
|
|
41
|
+
await $testInfo.attach(attachmentNames.meta, {
|
|
42
|
+
body: JSON.stringify(scenario.data),
|
|
43
|
+
contentType: 'application/json',
|
|
44
|
+
});
|
|
35
45
|
});
|
|
36
46
|
//# sourceMappingURL=hooks.js.map
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.2";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
1
|
+
export const VERSION = '0.2.2';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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
6
|
"author": "SDODS <admin@sdods.com>",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"playwright-bdd": ">=9"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@sdods/contracts": "0.2.
|
|
45
|
+
"@sdods/contracts": "0.2.2",
|
|
46
46
|
"@cucumber/gherkin": "^42.0.1",
|
|
47
47
|
"@cucumber/messages": "^34.2.1",
|
|
48
48
|
"@faker-js/faker": "^10.6.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"ts-morph": "^28.0.0",
|
|
60
60
|
"yaml": "^2.9.0",
|
|
61
61
|
"zod": "^4.5.4",
|
|
62
|
-
"@sdods/db": "0.2.
|
|
62
|
+
"@sdods/db": "0.2.2"
|
|
63
63
|
},
|
|
64
64
|
"homepage": "https://sdods.com",
|
|
65
65
|
"bugs": {
|