@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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, join, relative } from 'node:path';
|
|
3
|
+
import { CucumberExpression, ParameterType, ParameterTypeRegistry, RegularExpression, } from '@cucumber/cucumber-expressions';
|
|
4
|
+
import { parseTagExpr } from '../config/tags.js';
|
|
5
|
+
import { coreStepNames, coreStepsDir, coreStepsPatterns } from '../steps/glob.js';
|
|
6
|
+
/**
|
|
7
|
+
* Ambiguous step definitions (#60).
|
|
8
|
+
*
|
|
9
|
+
* playwright-bdd refuses to generate specs when one scenario step matches more than one definition
|
|
10
|
+
* ("Multiple definitions matched scenario step"), and the core step libraries share one namespace
|
|
11
|
+
* with a project's own steps — so a phrasing a project wrote before a library shipped the same
|
|
12
|
+
* text breaks the whole suite at `sdods run`, while lint used to say "no findings".
|
|
13
|
+
*
|
|
14
|
+
* Definitions are read statically (no step file is executed, so lint stays fast and side-effect
|
|
15
|
+
* free) from exactly the files the runner hands bddgen: the core libraries minus
|
|
16
|
+
* `steps.core.exclude`, plus the project's `steps/**` and `pages/**`. They are matched against
|
|
17
|
+
* pickle step text (outline rows expanded) with the same Cucumber expression engine and the same
|
|
18
|
+
* rules bddgen uses: keywords are ignored, tag-scoped definitions are filtered by the scenario's
|
|
19
|
+
* tags, and `@skip` / `@fixme` scenarios are not matched. A pattern that is not a literal, or that
|
|
20
|
+
* names a parameter type lint cannot see, is left out rather than guessed at: this check must
|
|
21
|
+
* never fail a suite that bddgen would generate.
|
|
22
|
+
*/
|
|
23
|
+
const STEP_FNS = new Set(['Given', 'When', 'Then', 'Step']);
|
|
24
|
+
function walk(dir, out) {
|
|
25
|
+
if (!existsSync(dir))
|
|
26
|
+
return;
|
|
27
|
+
for (const name of readdirSync(dir).sort()) {
|
|
28
|
+
if (name === 'node_modules')
|
|
29
|
+
continue;
|
|
30
|
+
const p = join(dir, name);
|
|
31
|
+
if (statSync(p).isDirectory())
|
|
32
|
+
walk(p, out);
|
|
33
|
+
else if (name.endsWith('.ts') && !name.endsWith('.d.ts'))
|
|
34
|
+
out.push(p);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function literalRegExp(text) {
|
|
38
|
+
const m = /^\/(.*)\/([a-z]*)$/s.exec(text);
|
|
39
|
+
if (!m)
|
|
40
|
+
return undefined;
|
|
41
|
+
try {
|
|
42
|
+
return new RegExp(m[1], m[2]);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function extract(ts, file, library) {
|
|
49
|
+
const steps = [];
|
|
50
|
+
const params = [];
|
|
51
|
+
const text = readFileSync(file, 'utf8');
|
|
52
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, file.endsWith('.js') ? ts.ScriptKind.JS : ts.ScriptKind.TS);
|
|
53
|
+
const stringOf = (n) => {
|
|
54
|
+
if (!n)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n))
|
|
57
|
+
return n.text;
|
|
58
|
+
if (ts.isRegularExpressionLiteral(n))
|
|
59
|
+
return literalRegExp(n.text);
|
|
60
|
+
return undefined;
|
|
61
|
+
};
|
|
62
|
+
const visit = (node) => {
|
|
63
|
+
if (ts.isCallExpression(node)) {
|
|
64
|
+
const callee = node.expression;
|
|
65
|
+
const name = ts.isIdentifier(callee)
|
|
66
|
+
? callee.text
|
|
67
|
+
: ts.isPropertyAccessExpression(callee)
|
|
68
|
+
? callee.name.text
|
|
69
|
+
: undefined;
|
|
70
|
+
if (name && STEP_FNS.has(name) && node.arguments.length) {
|
|
71
|
+
const pattern = stringOf(node.arguments[0]);
|
|
72
|
+
if (pattern !== undefined) {
|
|
73
|
+
const def = {
|
|
74
|
+
pattern,
|
|
75
|
+
file,
|
|
76
|
+
line: sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1,
|
|
77
|
+
library,
|
|
78
|
+
};
|
|
79
|
+
const opts = node.arguments[1];
|
|
80
|
+
if (opts && ts.isObjectLiteralExpression(opts)) {
|
|
81
|
+
for (const prop of opts.properties) {
|
|
82
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
83
|
+
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
|
|
84
|
+
prop.name.text === 'tags') {
|
|
85
|
+
const tags = stringOf(prop.initializer);
|
|
86
|
+
def.tags = typeof tags === 'string' ? tags : null;
|
|
87
|
+
}
|
|
88
|
+
else if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === 'tags') {
|
|
89
|
+
def.tags = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
steps.push(def);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (name === 'defineParameterType' && node.arguments[0]) {
|
|
97
|
+
const arg = node.arguments[0];
|
|
98
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
99
|
+
let pname;
|
|
100
|
+
const regexps = [];
|
|
101
|
+
for (const prop of arg.properties) {
|
|
102
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
|
|
103
|
+
continue;
|
|
104
|
+
if (prop.name.text === 'name') {
|
|
105
|
+
const v = stringOf(prop.initializer);
|
|
106
|
+
if (typeof v === 'string')
|
|
107
|
+
pname = v;
|
|
108
|
+
}
|
|
109
|
+
else if (prop.name.text === 'regexp') {
|
|
110
|
+
const init = prop.initializer;
|
|
111
|
+
const items = ts.isArrayLiteralExpression(init) ? [...init.elements] : [init];
|
|
112
|
+
for (const item of items) {
|
|
113
|
+
const v = stringOf(item);
|
|
114
|
+
if (v !== undefined)
|
|
115
|
+
regexps.push(v);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (pname && regexps.length)
|
|
120
|
+
params.push({ name: pname, regexps });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
ts.forEachChild(node, visit);
|
|
125
|
+
};
|
|
126
|
+
visit(sf);
|
|
127
|
+
return { steps, params };
|
|
128
|
+
}
|
|
129
|
+
/** Gherkin step id → line, across backgrounds, scenarios and rules. */
|
|
130
|
+
function stepLines(doc) {
|
|
131
|
+
const lines = new Map();
|
|
132
|
+
const children = (list) => {
|
|
133
|
+
for (const c of list) {
|
|
134
|
+
const block = c.background ?? c.scenario;
|
|
135
|
+
for (const s of block?.steps ?? [])
|
|
136
|
+
lines.set(s.id, s.location.line);
|
|
137
|
+
if (c.rule)
|
|
138
|
+
children(c.rule.children);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
if (doc.feature)
|
|
142
|
+
children(doc.feature.children);
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
export async function checkStepAmbiguity(opts) {
|
|
146
|
+
const { project, features } = opts;
|
|
147
|
+
const errors = [];
|
|
148
|
+
const warnings = [];
|
|
149
|
+
const rel = (f) => relative(project.root, f).replace(/\\/g, '/');
|
|
150
|
+
const exclude = project.steps?.core?.exclude ?? [];
|
|
151
|
+
try {
|
|
152
|
+
coreStepsPatterns(exclude);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
errors.push({
|
|
156
|
+
severity: 'error',
|
|
157
|
+
rule: 'steps/core-exclude',
|
|
158
|
+
message: `${e.message} ${e.hint ?? ''}`.trim(),
|
|
159
|
+
file: 'sdods.project.yaml',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const { ts } = await import('ts-morph');
|
|
163
|
+
const dir = coreStepsDir();
|
|
164
|
+
const coreFile = (name) => [`${name}.ts`, `${name}.js`].map((f) => join(dir, f)).find((f) => existsSync(f));
|
|
165
|
+
const defs = [];
|
|
166
|
+
const paramDefs = [];
|
|
167
|
+
const add = (file, library) => {
|
|
168
|
+
if (!file)
|
|
169
|
+
return;
|
|
170
|
+
const r = extract(ts, file, library);
|
|
171
|
+
defs.push(...r.steps);
|
|
172
|
+
paramDefs.push(...r.params);
|
|
173
|
+
};
|
|
174
|
+
add(coreFile('params'));
|
|
175
|
+
for (const lib of coreStepNames()) {
|
|
176
|
+
if (!exclude.includes(lib))
|
|
177
|
+
add(coreFile(`${lib}.steps`), lib);
|
|
178
|
+
}
|
|
179
|
+
const projectFiles = [];
|
|
180
|
+
walk(join(project.root, 'steps'), projectFiles);
|
|
181
|
+
walk(join(project.root, 'pages'), projectFiles);
|
|
182
|
+
for (const f of projectFiles)
|
|
183
|
+
add(f);
|
|
184
|
+
const registry = new ParameterTypeRegistry();
|
|
185
|
+
for (const p of paramDefs) {
|
|
186
|
+
try {
|
|
187
|
+
registry.defineParameterType(new ParameterType(p.name, p.regexps, null, (s) => s, false, false));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* already defined (a project re-declaring a core type) — the first one wins */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const usable = defs.filter((d) => {
|
|
194
|
+
try {
|
|
195
|
+
d.expression =
|
|
196
|
+
typeof d.pattern === 'string'
|
|
197
|
+
? new CucumberExpression(d.pattern, registry)
|
|
198
|
+
: new RegularExpression(d.pattern, registry);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return false; // e.g. a parameter type defined dynamically — never guess
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
const tagExpr = new Map();
|
|
206
|
+
for (const d of usable) {
|
|
207
|
+
if (typeof d.tags === 'string' && !tagExpr.has(d.tags)) {
|
|
208
|
+
try {
|
|
209
|
+
tagExpr.set(d.tags, parseTagExpr(d.tags));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
d.tags = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const describe = (d) => d.library ? `${basename(d.file)}:${d.line}` : `${rel(d.file)}:${d.line}`;
|
|
217
|
+
const originOf = (d) => (d.library ? `core library "${d.library}"` : rel(d.file));
|
|
218
|
+
// text → definitions matching it by text alone (tags applied per scenario below)
|
|
219
|
+
const byText = new Map();
|
|
220
|
+
const matching = (text) => {
|
|
221
|
+
let hit = byText.get(text);
|
|
222
|
+
if (!hit) {
|
|
223
|
+
hit = usable.filter((d) => d.expression.match(text) !== null);
|
|
224
|
+
byText.set(text, hit);
|
|
225
|
+
}
|
|
226
|
+
return hit;
|
|
227
|
+
};
|
|
228
|
+
const groups = new Map();
|
|
229
|
+
const usedAmbiguous = new Set();
|
|
230
|
+
for (const feature of features) {
|
|
231
|
+
if (feature.errors.length)
|
|
232
|
+
continue;
|
|
233
|
+
const lines = stepLines(feature.document);
|
|
234
|
+
for (const pickle of feature.pickles) {
|
|
235
|
+
const tags = pickle.tags.map((t) => t.name);
|
|
236
|
+
if (tags.includes('@skip') || tags.includes('@fixme'))
|
|
237
|
+
continue;
|
|
238
|
+
for (const step of pickle.steps) {
|
|
239
|
+
let matched = matching(step.text);
|
|
240
|
+
if (matched.length < 2)
|
|
241
|
+
continue;
|
|
242
|
+
if (tags.length) {
|
|
243
|
+
const tagged = matched.filter((d) => typeof d.tags === 'string' && tagExpr.get(d.tags).evaluate(tags));
|
|
244
|
+
matched = tagged.length ? tagged : matched.filter((d) => d.tags === undefined);
|
|
245
|
+
}
|
|
246
|
+
// A tag option lint cannot read might be what separates them: say nothing.
|
|
247
|
+
if (matched.length < 2 || matched.some((d) => d.tags === null))
|
|
248
|
+
continue;
|
|
249
|
+
const origins = [...new Set(matched.map(originOf))].sort();
|
|
250
|
+
const key = origins.join('');
|
|
251
|
+
let g = groups.get(key);
|
|
252
|
+
if (!g) {
|
|
253
|
+
g = {
|
|
254
|
+
defs: [],
|
|
255
|
+
origins,
|
|
256
|
+
libraries: [...new Set(matched.flatMap((d) => (d.library ? [d.library] : [])))].sort(),
|
|
257
|
+
occurrences: new Map(),
|
|
258
|
+
};
|
|
259
|
+
groups.set(key, g);
|
|
260
|
+
}
|
|
261
|
+
const line = lines.get(step.astNodeIds[0]) ?? 0;
|
|
262
|
+
const loc = `${rel(feature.file)}:${line}`;
|
|
263
|
+
if (!g.occurrences.has(loc))
|
|
264
|
+
g.occurrences.set(loc, { file: rel(feature.file), line, text: step.text, defs: matched });
|
|
265
|
+
for (const d of matched)
|
|
266
|
+
usedAmbiguous.add(d);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const excludeHint = (libraries) => libraries.length
|
|
271
|
+
? ` Rename or remove the project definition, or drop the library with steps.core.exclude: [${libraries.join(', ')}] in sdods.project.yaml.`
|
|
272
|
+
: ' Remove or reword one of the definitions.';
|
|
273
|
+
for (const g of groups.values()) {
|
|
274
|
+
const occ = [...g.occurrences.values()];
|
|
275
|
+
const first = occ[0];
|
|
276
|
+
const examples = occ
|
|
277
|
+
.slice(0, 3)
|
|
278
|
+
.map((o) => `"${o.text}" (${o.file}:${o.line}) → ${o.defs.map(describe).join(', ')}`)
|
|
279
|
+
.join('; ');
|
|
280
|
+
const more = occ.length > 3 ? `; and ${occ.length - 3} more` : '';
|
|
281
|
+
const where = g.origins.length === 1
|
|
282
|
+
? `more than one definition in ${g.origins[0]}`
|
|
283
|
+
: `definitions in both ${g.origins.join(' and ')}`;
|
|
284
|
+
errors.push({
|
|
285
|
+
severity: 'error',
|
|
286
|
+
rule: 'steps/ambiguous',
|
|
287
|
+
message: `${occ.length} step(s) match ${where}; bddgen fails with "Multiple definitions matched scenario step". ` +
|
|
288
|
+
`${examples}${more}.${excludeHint(g.libraries)}`,
|
|
289
|
+
file: first.file,
|
|
290
|
+
line: first.line,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// The same phrasing defined twice with a project file involved, not used by any feature yet.
|
|
294
|
+
const dupes = new Map();
|
|
295
|
+
for (const d of usable) {
|
|
296
|
+
if (d.tags !== undefined)
|
|
297
|
+
continue;
|
|
298
|
+
const key = typeof d.pattern === 'string' ? `s:${d.pattern}` : `r:${d.pattern.source}`;
|
|
299
|
+
dupes.set(key, [...(dupes.get(key) ?? []), d]);
|
|
300
|
+
}
|
|
301
|
+
const dupGroups = new Map();
|
|
302
|
+
for (const set of dupes.values()) {
|
|
303
|
+
if (set.length < 2 || set.every((d) => d.library) || set.some((d) => usedAmbiguous.has(d)))
|
|
304
|
+
continue;
|
|
305
|
+
const origins = [...new Set(set.map(originOf))].sort();
|
|
306
|
+
const key = origins.join('');
|
|
307
|
+
const g = dupGroups.get(key) ?? {
|
|
308
|
+
origins,
|
|
309
|
+
libraries: [...new Set(set.flatMap((d) => (d.library ? [d.library] : [])))].sort(),
|
|
310
|
+
sets: [],
|
|
311
|
+
};
|
|
312
|
+
g.sets.push(set);
|
|
313
|
+
dupGroups.set(key, g);
|
|
314
|
+
}
|
|
315
|
+
for (const g of dupGroups.values()) {
|
|
316
|
+
const firstProject = g.sets[0].find((d) => !d.library);
|
|
317
|
+
const examples = g.sets
|
|
318
|
+
.slice(0, 3)
|
|
319
|
+
.map((set) => `"${String(set[0].pattern)}" at ${set.map(describe).join(', ')}`)
|
|
320
|
+
.join('; ');
|
|
321
|
+
const more = g.sets.length > 3 ? `; and ${g.sets.length - 3} more` : '';
|
|
322
|
+
warnings.push({
|
|
323
|
+
severity: 'warning',
|
|
324
|
+
rule: 'steps/duplicate',
|
|
325
|
+
message: `${g.sets.length} phrasing(s) defined in ${g.origins.join(' and ')}; the first feature to use one will fail generation. ` +
|
|
326
|
+
`${examples}${more}.${excludeHint(g.libraries)}`,
|
|
327
|
+
file: rel(firstProject.file),
|
|
328
|
+
line: firstProject.line,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return { errors, warnings };
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=steps.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function importProjectModule<T = Record<string, unknown>>(file: string): Promise<T>;
|
|
2
|
+
/**
|
|
3
|
+
* A named export from a module loaded by {@link importProjectModule}. A CommonJS-format file (no
|
|
4
|
+
* `"type": "module"` in the project) arrives with its exports under `default`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function namedExport<T>(mod: Record<string, unknown>, name: string): T | undefined;
|
|
7
|
+
//# sourceMappingURL=load-module.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
/**
|
|
3
|
+
* Import a TypeScript module that belongs to a project (`steps/auth.ts` and friends) from outside
|
|
4
|
+
* the Playwright runner.
|
|
5
|
+
*
|
|
6
|
+
* `sdods run` loads project files through Playwright's TS loader, which transpiles full
|
|
7
|
+
* TypeScript, remaps `import './helper.js'` to `helper.ts`, and treats a file as ESM or CommonJS
|
|
8
|
+
* by its package type. A bare `import()` from a CLI command hands the file to Node's native type
|
|
9
|
+
* stripping instead, which does none of the remapping — so a helper imported the conventional way
|
|
10
|
+
* worked under `sdods run` and crashed `sdods auth capture` with "Cannot find module .../helper.js"
|
|
11
|
+
* (#86). In the workspace the gap was invisible, because bin/sdods.js registers tsx globally.
|
|
12
|
+
*
|
|
13
|
+
* tsx gives the same three behaviours as Playwright's loader. Both its ESM and CommonJS hooks are
|
|
14
|
+
* registered — a project without `"type": "module"` loads `.ts` as CommonJS, and a CommonJS
|
|
15
|
+
* `require('./helper.js')` only goes through tsx's CJS hook — and removed again once the import
|
|
16
|
+
* settles, so nothing else in the process is affected. Registrations are reference-counted so
|
|
17
|
+
* overlapping imports do not unregister each other's hooks mid-load.
|
|
18
|
+
*/
|
|
19
|
+
let active = 0;
|
|
20
|
+
let hooks;
|
|
21
|
+
async function acquire() {
|
|
22
|
+
active++;
|
|
23
|
+
hooks ??= (async () => {
|
|
24
|
+
const [esm, cjs] = await Promise.all([import('tsx/esm/api'), import('tsx/cjs/api')]);
|
|
25
|
+
const offEsm = esm.register();
|
|
26
|
+
const offCjs = cjs.register();
|
|
27
|
+
return async () => {
|
|
28
|
+
offCjs();
|
|
29
|
+
await offEsm();
|
|
30
|
+
};
|
|
31
|
+
})();
|
|
32
|
+
try {
|
|
33
|
+
await hooks;
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
// Do not leave a rejected registration cached for every later import.
|
|
37
|
+
if (--active === 0)
|
|
38
|
+
hooks = undefined;
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function releaseOne() {
|
|
43
|
+
if (--active > 0 || !hooks)
|
|
44
|
+
return;
|
|
45
|
+
const off = hooks;
|
|
46
|
+
hooks = undefined;
|
|
47
|
+
await (await off)();
|
|
48
|
+
}
|
|
49
|
+
export async function importProjectModule(file) {
|
|
50
|
+
await acquire();
|
|
51
|
+
try {
|
|
52
|
+
return (await import(pathToFileURL(file).href));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await releaseOne();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A named export from a module loaded by {@link importProjectModule}. A CommonJS-format file (no
|
|
60
|
+
* `"type": "module"` in the project) arrives with its exports under `default`.
|
|
61
|
+
*/
|
|
62
|
+
export function namedExport(mod, name) {
|
|
63
|
+
if (mod[name] !== undefined)
|
|
64
|
+
return mod[name];
|
|
65
|
+
const def = mod.default;
|
|
66
|
+
if (def && typeof def === 'object' && def[name] !== undefined)
|
|
67
|
+
return def[name];
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=load-module.js.map
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'step' | 'debug';
|
|
2
|
+
export declare function isSecretKey(name: string): boolean;
|
|
2
3
|
export declare function setLogLevel(level: LogLevel): void;
|
|
3
4
|
export declare function setLogJson(enabled: boolean): void;
|
|
4
5
|
export declare function getLogLevel(): LogLevel;
|
package/dist/logger.js
CHANGED
|
@@ -9,7 +9,11 @@ const ORDER = {
|
|
|
9
9
|
};
|
|
10
10
|
let globalLevel = process.env.SDODS_LOG_LEVEL || 'info';
|
|
11
11
|
let jsonMode = process.env.SDODS_LOG_JSON === '1';
|
|
12
|
-
|
|
12
|
+
/** Key names whose string values are credentials: config fields, header names, log fields. */
|
|
13
|
+
const SECRET_KEY = /(passw(or)?d|secret|token|api[-_]?key|[-_]key$|authorization|cookie|credential)/i;
|
|
14
|
+
export function isSecretKey(name) {
|
|
15
|
+
return SECRET_KEY.test(name);
|
|
16
|
+
}
|
|
13
17
|
export function setLogLevel(level) {
|
|
14
18
|
globalLevel = level;
|
|
15
19
|
}
|
|
@@ -26,9 +30,11 @@ export function redact(value, depth = 0) {
|
|
|
26
30
|
return value.map((v) => redact(v, depth + 1));
|
|
27
31
|
if (value && typeof value === 'object') {
|
|
28
32
|
const out = {};
|
|
33
|
+
// Header auth (`{ type: header, name, value }`) keeps its secret under a neutral key.
|
|
34
|
+
const headerAuth = value.type === 'header';
|
|
29
35
|
for (const [k, v] of Object.entries(value)) {
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
const secret = SECRET_KEY.test(k) || (headerAuth && k === 'value');
|
|
37
|
+
out[k] = secret && typeof v === 'string' && v.length > 0 ? '***' : redact(v, depth + 1);
|
|
32
38
|
}
|
|
33
39
|
return out;
|
|
34
40
|
}
|
package/dist/steps/a11y.steps.js
CHANGED
|
@@ -3,7 +3,7 @@ import { expect } from '@playwright/test';
|
|
|
3
3
|
import { attachmentNames, scenarioFiles } from '@sdods/contracts';
|
|
4
4
|
import './params.js';
|
|
5
5
|
import { Then } from '../fixtures/test.js';
|
|
6
|
-
import {
|
|
6
|
+
import { renderStrict } from '../api/template.js';
|
|
7
7
|
import { SdodsError } from '../errors.js';
|
|
8
8
|
import { Logger } from '../logger.js';
|
|
9
9
|
/**
|
|
@@ -449,7 +449,7 @@ Then('the page should have no accessibility violations', async ({ page, scenario
|
|
|
449
449
|
* an empty context happily and reports zero violations, which is indistinguishable from a pass.
|
|
450
450
|
*/
|
|
451
451
|
Then('the page should have no accessibility violations within {string}', async ({ page, scenario, apiContext, env, $bddContext, $testInfo }, selector) => {
|
|
452
|
-
const sel =
|
|
452
|
+
const sel = renderStrict(selector, ...scopesOf(apiContext, env));
|
|
453
453
|
await requireRegion(page, sel);
|
|
454
454
|
const results = await runAxe(page, { include: sel });
|
|
455
455
|
attachA11y({ scenario, testInfo: $testInfo, stepIndex: $bddContext.stepIndex }, results);
|
|
@@ -462,7 +462,7 @@ Then('the page should have no accessibility violations within {string}', async (
|
|
|
462
462
|
* nothing — "or worse than banana" would otherwise be the greenest step in the suite.
|
|
463
463
|
*/
|
|
464
464
|
Then('the page should have no accessibility violations of impact {string} or worse', async ({ page, scenario, apiContext, env, $bddContext, $testInfo }, level) => {
|
|
465
|
-
const floor = parseImpactFloor(
|
|
465
|
+
const floor = parseImpactFloor(renderStrict(level, ...scopesOf(apiContext, env)));
|
|
466
466
|
const results = await runAxe(page, {});
|
|
467
467
|
attachA11y({ scenario, testInfo: $testInfo, stepIndex: $bddContext.stepIndex }, results);
|
|
468
468
|
assertAxeChecked(results, `on ${page.url()}`);
|
|
@@ -472,8 +472,8 @@ Then('the page should have no accessibility violations of impact {string} or wor
|
|
|
472
472
|
/** PROVES a region carries no violation at or above a severity — the scoped form of the above. */
|
|
473
473
|
Then('the page should have no accessibility violations of impact {string} or worse within {string}', async ({ page, scenario, apiContext, env, $bddContext, $testInfo }, level, selector) => {
|
|
474
474
|
const scopes = scopesOf(apiContext, env);
|
|
475
|
-
const floor = parseImpactFloor(
|
|
476
|
-
const sel =
|
|
475
|
+
const floor = parseImpactFloor(renderStrict(level, ...scopes));
|
|
476
|
+
const sel = renderStrict(selector, ...scopes);
|
|
477
477
|
await requireRegion(page, sel);
|
|
478
478
|
const results = await runAxe(page, { include: sel });
|
|
479
479
|
attachA11y({ scenario, testInfo: $testInfo, stepIndex: $bddContext.stepIndex }, results);
|
|
@@ -489,7 +489,7 @@ Then('the page should have no accessibility violations of impact {string} or wor
|
|
|
489
489
|
* step fails on either, because a rule that never ran cannot have passed.
|
|
490
490
|
*/
|
|
491
491
|
Then('the page should have no accessibility violations of rule {string}', async ({ page, scenario, apiContext, env, $bddContext, $testInfo }, ruleId) => {
|
|
492
|
-
const rule =
|
|
492
|
+
const rule = renderStrict(ruleId, ...scopesOf(apiContext, env));
|
|
493
493
|
const results = await runAxe(page, { rules: [rule] });
|
|
494
494
|
attachA11y({ scenario, testInfo: $testInfo, stepIndex: $bddContext.stepIndex }, results);
|
|
495
495
|
const bucket = assertRuleRan(results, rule, `on ${page.url()}`);
|
|
@@ -510,7 +510,7 @@ Then('the page should have no colour-contrast violations', async ({ page, scenar
|
|
|
510
510
|
});
|
|
511
511
|
/** PROVES one region's text meets AA contrast — for auditing a themed component in isolation. */
|
|
512
512
|
Then('the page should have no colour-contrast violations within {string}', async ({ page, scenario, apiContext, env, $bddContext, $testInfo }, selector) => {
|
|
513
|
-
const sel =
|
|
513
|
+
const sel = renderStrict(selector, ...scopesOf(apiContext, env));
|
|
514
514
|
await requireRegion(page, sel);
|
|
515
515
|
const results = await runAxe(page, { include: sel, rules: ['color-contrast'] });
|
|
516
516
|
attachA11y({ scenario, testInfo: $testInfo, stepIndex: $bddContext.stepIndex }, results);
|
|
@@ -556,7 +556,7 @@ Then('every image on the page should carry an alt attribute', async ({ page }) =
|
|
|
556
556
|
});
|
|
557
557
|
/** PROVES every image inside one region declares its alt intent — the scoped form of the above. */
|
|
558
558
|
Then('every image within {string} should carry an alt attribute', async ({ page, apiContext, env }, selector) => {
|
|
559
|
-
const sel =
|
|
559
|
+
const sel = renderStrict(selector, ...scopesOf(apiContext, env));
|
|
560
560
|
const gathered = await page.evaluate(gatherImages, sel);
|
|
561
561
|
const images = assertGathered(assertRegion(gathered, sel), 'img elements', `within "${sel}"`);
|
|
562
562
|
const missing = imagesWithoutAlt(images);
|
|
@@ -571,7 +571,7 @@ Then('every image within {string} should carry an alt attribute', async ({ page,
|
|
|
571
571
|
* itself — axe sees a non-empty name and passes it.
|
|
572
572
|
*/
|
|
573
573
|
Then('every icon-only control within {string} should expose an accessible name', async ({ page, apiContext, env }, selector) => {
|
|
574
|
-
const sel =
|
|
574
|
+
const sel = renderStrict(selector, ...scopesOf(apiContext, env));
|
|
575
575
|
const gathered = await page.evaluate(gatherControls, sel);
|
|
576
576
|
const controls = assertRegion(gathered, sel);
|
|
577
577
|
const iconOnly = assertGathered(iconOnlyControls(controls), 'icon-only controls', `within "${sel}" (${controls.length} control(s) there render text)`);
|
|
@@ -588,7 +588,7 @@ Then('every icon-only control within {string} should expose an accessible name',
|
|
|
588
588
|
* enter keyboard modality and never blurs between elements.
|
|
589
589
|
*/
|
|
590
590
|
Then('every focusable element within {string} should show a visible focus indicator', async ({ page, apiContext, env }, selector) => {
|
|
591
|
-
const sel =
|
|
591
|
+
const sel = renderStrict(selector, ...scopesOf(apiContext, env));
|
|
592
592
|
await page.keyboard.press('Tab');
|
|
593
593
|
const gathered = await page.evaluate(gatherFocusIndicators, sel);
|
|
594
594
|
const focusables = assertGathered(assertRegion(gathered, sel), 'focusable elements', `within "${sel}"`);
|