test-inventory 0.7.0 → 0.9.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/CHANGELOG.md +180 -0
- package/README.md +16 -10
- package/dist/case-titles.d.ts +22 -0
- package/dist/case-titles.js +184 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +5 -5
- package/dist/config-dependencies.js +27 -3
- package/dist/config-runs.d.ts +117 -3
- package/dist/config-runs.js +1015 -19
- package/dist/config.d.ts +42 -1
- package/dist/config.js +287 -21
- package/dist/dependency-hash.d.ts +13 -5
- package/dist/dependency-hash.js +56 -12
- package/dist/diagnostics.js +16 -1
- package/dist/extract.d.ts +2 -0
- package/dist/extract.js +89 -29
- package/dist/files.js +4 -2
- package/dist/filter.js +1 -0
- package/dist/framework-version.d.ts +13 -2
- package/dist/framework-version.js +207 -7
- package/dist/frameworks/bindings.d.ts +14 -0
- package/dist/frameworks/bindings.js +99 -6
- package/dist/frameworks/bun.d.ts +42 -0
- package/dist/frameworks/bun.js +272 -0
- package/dist/frameworks/common.d.ts +3 -1
- package/dist/frameworks/common.js +18 -9
- package/dist/frameworks/cypress.d.ts +31 -0
- package/dist/frameworks/cypress.js +70 -0
- package/dist/frameworks/jasmine.d.ts +28 -0
- package/dist/frameworks/jasmine.js +218 -0
- package/dist/frameworks/jest.d.ts +46 -0
- package/dist/frameworks/jest.js +442 -0
- package/dist/frameworks/mocha.d.ts +50 -0
- package/dist/frameworks/mocha.js +306 -0
- package/dist/frameworks/node.d.ts +30 -0
- package/dist/frameworks/node.js +268 -0
- package/dist/frameworks/playwright.d.ts +13 -0
- package/dist/frameworks/playwright.js +57 -0
- package/dist/frameworks/test-functions.d.ts +194 -0
- package/dist/frameworks/test-functions.js +534 -0
- package/dist/frameworks/vitest.d.ts +15 -62
- package/dist/frameworks/vitest.js +69 -389
- package/dist/frameworks/webdriverio.d.ts +27 -0
- package/dist/frameworks/webdriverio.js +37 -0
- package/dist/imported-values.js +15 -1
- package/dist/model.d.ts +9 -2
- package/dist/node-scripts.d.ts +6 -0
- package/dist/node-scripts.js +85 -0
- package/dist/options.d.ts +16 -3
- package/dist/options.js +22 -3
- package/dist/parse.js +17 -2
- package/dist/records.d.ts +8 -2
- package/dist/records.js +94 -22
- package/dist/resolve.d.ts +21 -4
- package/dist/resolve.js +91 -52
- package/dist/scan-file.d.ts +8 -0
- package/dist/scan-file.js +21 -3
- package/dist/scan.js +62 -12
- package/dist/toml.d.ts +13 -0
- package/dist/toml.js +172 -0
- package/dist/types.d.ts +27 -11
- package/dist/values.d.ts +15 -0
- package/dist/values.js +120 -2
- package/dist/yaml.d.ts +12 -0
- package/dist/yaml.js +148 -0
- package/package.json +7 -6
- package/schema/v1.json +30 -8
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { isFunctionNode, unwrap } from "../ast.js";
|
|
2
|
+
import { bunCaseTitle } from "../case-titles.js";
|
|
3
|
+
import { snippet } from "../source.js";
|
|
4
|
+
import { requiredModule } from "./bindings.js";
|
|
5
|
+
import { atLeast, hookFunction, namedFunction, problem, readChain, readCondition, readCount, readOptions, readTitle, refused, text, unresolvedOptions, } from "./common.js";
|
|
6
|
+
import { SuiteAdapter } from "./test-functions.js";
|
|
7
|
+
const SOURCE = 'bun:test';
|
|
8
|
+
/** Bun rewrites imports of these to `bun:test` in its test files. */
|
|
9
|
+
const ALIASES = new Set(['@jest/globals', 'vitest']);
|
|
10
|
+
/** The modes a chain may hold one of each group of, as Bun throws on a second. */
|
|
11
|
+
const GROUPS = [new Set(['skip', 'todo', 'failing']), new Set(['only']), new Set(['concurrent', 'serial'])];
|
|
12
|
+
const MODES = new Set(GROUPS.flatMap((group) => [...group]));
|
|
13
|
+
const CONDITIONS = new Set(['if', 'skipIf', 'todoIf', 'failingIf', 'concurrentIf', 'serialIf']);
|
|
14
|
+
const MODIFIERS = new Set([...MODES, ...CONDITIONS, 'each']);
|
|
15
|
+
const BUN = {
|
|
16
|
+
name: 'Bun',
|
|
17
|
+
source: SOURCE,
|
|
18
|
+
aliases: ALIASES,
|
|
19
|
+
mismatched: new Set(['@playwright/test', 'playwright/test', 'mocha']),
|
|
20
|
+
starts: new Map([
|
|
21
|
+
['test', { kind: 'test', links: [] }],
|
|
22
|
+
['it', { kind: 'test', links: [] }],
|
|
23
|
+
['describe', { kind: 'describe', links: [] }],
|
|
24
|
+
['xtest', { kind: 'test', links: ['skip'] }],
|
|
25
|
+
['xit', { kind: 'test', links: ['skip'] }],
|
|
26
|
+
['xdescribe', { kind: 'describe', links: ['skip'] }],
|
|
27
|
+
]),
|
|
28
|
+
hooks: new Map(['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].map((name) => [name, name])),
|
|
29
|
+
derivations: new Set(),
|
|
30
|
+
modifiers: MODIFIERS,
|
|
31
|
+
carriesKinds: false,
|
|
32
|
+
isModule: (expression) => {
|
|
33
|
+
const source = requiredModule(expression);
|
|
34
|
+
return source === SOURCE || (source !== null && ALIASES.has(source));
|
|
35
|
+
},
|
|
36
|
+
defaultImport: null,
|
|
37
|
+
required: null,
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Bun's `bun test`, from 1.2.23, which runs its tests the way 1.3 and 1.4 do. Its functions are globals in test files,
|
|
41
|
+
* and imports of `bun:test`, `@jest/globals` and `vitest` give them too. A chain holds its modes in any order, one of
|
|
42
|
+
* each group, and the conditions `if`, `skipIf`, `todoIf`, `failingIf`, `concurrentIf` and `serialIf` as often as
|
|
43
|
+
* it likes; options go second or third.
|
|
44
|
+
*/
|
|
45
|
+
export class BunAdapter extends SuiteAdapter {
|
|
46
|
+
constructor(parsed, file, readModule) {
|
|
47
|
+
super(parsed, file, readModule, BUN, MODIFIERS);
|
|
48
|
+
}
|
|
49
|
+
classify(call, context) {
|
|
50
|
+
return this.hookCall(call, context) ?? this.chainCall(call, context);
|
|
51
|
+
}
|
|
52
|
+
isAliasLink(_kind, link) {
|
|
53
|
+
return MODIFIERS.has(link.name);
|
|
54
|
+
}
|
|
55
|
+
/** A hook; before Bun 1.3, one given more than its function fails the file. */
|
|
56
|
+
hook(call, name, context) {
|
|
57
|
+
const fn = hookFunction(this.file, context.scopes, call.arguments);
|
|
58
|
+
if (call.arguments.length > 1 && !atLeast(this.file.version, 1, 3)) {
|
|
59
|
+
const callee = snippet(text(this.file, call.callee));
|
|
60
|
+
const message = `${callee}() takes only its function before Bun 1.3, so Bun fails the file and runs none of its tests.`;
|
|
61
|
+
return { kind: 'invalid', problem: problem('invalid-chain', call, message, callee) };
|
|
62
|
+
}
|
|
63
|
+
return { kind: 'hook', name, fn, testContext: null, fixtures: [], takesAllFixtures: false };
|
|
64
|
+
}
|
|
65
|
+
chainCall(call, context) {
|
|
66
|
+
const chain = readChain(call.callee);
|
|
67
|
+
const resolved = chain ? this.resolve(chain, call.arguments, context) : null;
|
|
68
|
+
if (!resolved)
|
|
69
|
+
return null;
|
|
70
|
+
const { kind, links } = resolved;
|
|
71
|
+
const last = links.at(-1);
|
|
72
|
+
if ((last?.name === 'each' || CONDITIONS.has(last?.name ?? '')) && !last?.args)
|
|
73
|
+
return { kind: 'other' };
|
|
74
|
+
if (this.isValid(kind, links, context))
|
|
75
|
+
return this.declaration(call, kind, links, context);
|
|
76
|
+
const callee = snippet(text(this.file, call.callee));
|
|
77
|
+
const refusal = problem('invalid-chain', call, `${callee} is not part of Bun's API; the call throws, and Bun fails the file and runs none of its tests.`, callee);
|
|
78
|
+
return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether Bun takes a chain: one of `skip`, `todo` and `failing`, one `only`, one of `concurrent` and `serial`, and
|
|
82
|
+
* one table, where a condition that holds counts as its mode, as `if(false)` counts as `skip`; the conditions and
|
|
83
|
+
* the table called and the modes not; and no `failing` on a describe. A condition the scan can't work out counts as
|
|
84
|
+
* not holding (checked with Bun 1.2.23, 1.3.14 and 1.4.4).
|
|
85
|
+
*/
|
|
86
|
+
isValid(kind, links, context) {
|
|
87
|
+
if (!links.every((link) => MODIFIERS.has(link.name)))
|
|
88
|
+
return false;
|
|
89
|
+
const called = links.every((link) => (MODES.has(link.name) ? !link.args : link.args !== null) && !link.template);
|
|
90
|
+
if (!called)
|
|
91
|
+
return false;
|
|
92
|
+
const modes = links.flatMap((link) => {
|
|
93
|
+
if (!CONDITIONS.has(link.name))
|
|
94
|
+
return [link.name];
|
|
95
|
+
const holds = this.condition(link, context).applies === (link.name === 'if' ? 'never' : 'always');
|
|
96
|
+
return holds ? [link.name === 'if' ? 'skip' : link.name.slice(0, -2)] : [];
|
|
97
|
+
});
|
|
98
|
+
if ([...GROUPS, new Set(['each'])].some((group) => modes.filter((name) => group.has(name)).length > 1)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return kind !== 'describe' || !modes.includes('failing');
|
|
102
|
+
}
|
|
103
|
+
condition(link, context) {
|
|
104
|
+
return readCondition(this.file, context.scopes, link.args.at(0));
|
|
105
|
+
}
|
|
106
|
+
declaration(call, kind, links, context) {
|
|
107
|
+
const args = call.arguments;
|
|
108
|
+
const callee = snippet(text(this.file, call.callee));
|
|
109
|
+
const problems = [];
|
|
110
|
+
const { title, problem: titleProblem } = readTitle(this.file, context.scopes, args[0], {
|
|
111
|
+
stripTags: false,
|
|
112
|
+
functionNames: true,
|
|
113
|
+
});
|
|
114
|
+
if (titleProblem)
|
|
115
|
+
problems.push(titleProblem);
|
|
116
|
+
const spread = args.some((arg) => arg.type === 'SpreadElement');
|
|
117
|
+
// (title, fn, options) or (title, options, fn); a number is the timeout.
|
|
118
|
+
const [second, third] = [args.at(1), args.at(2)];
|
|
119
|
+
const fnSecond = third === undefined || isFunctionLike(second);
|
|
120
|
+
const body = spread ? undefined : fnSecond ? second : third;
|
|
121
|
+
const options = spread ? undefined : fnSecond ? third : second;
|
|
122
|
+
const fn = isFunctionNode(body) ? body : null;
|
|
123
|
+
const names = new Set(links.map((link) => link.name));
|
|
124
|
+
const line = this.file.source.line(call.start);
|
|
125
|
+
const states = this.states(links, line, context);
|
|
126
|
+
const table = links.find((link) => link.name === 'each') ?? null;
|
|
127
|
+
const run = this.options(options, problems, context);
|
|
128
|
+
const base = {
|
|
129
|
+
title,
|
|
130
|
+
states,
|
|
131
|
+
isOnly: names.has('only'),
|
|
132
|
+
mode: this.mode(links, context),
|
|
133
|
+
details: { tags: [], annotations: [], locks: [], meta: null, run },
|
|
134
|
+
fn,
|
|
135
|
+
hashed: fn ?? namedFunction(this.file, context.scopes, body),
|
|
136
|
+
isParameterized: table !== null,
|
|
137
|
+
caseCount: table ? this.caseCount(table, problems, context) : null,
|
|
138
|
+
cases: table ? this.tableCases(table, title, context) : null,
|
|
139
|
+
problems,
|
|
140
|
+
};
|
|
141
|
+
const declared = kind === 'describe'
|
|
142
|
+
? { kind: 'describe', ...base, isShuffled: false, passesDown: null }
|
|
143
|
+
: { kind: 'test', ...base, testContext: null, fixtures: [], takesAllFixtures: false, usesBench: false };
|
|
144
|
+
// Only a skipped or todo test may leave out its function.
|
|
145
|
+
const mayLeaveOut = kind === 'test' && (names.has('skip') || names.has('todo'));
|
|
146
|
+
const rows = table?.args?.at(0);
|
|
147
|
+
const resolvedRows = rows && rows.type !== 'SpreadElement' ? this.file.values.resolve(rows, context.scopes) : null;
|
|
148
|
+
if (resolvedRows?.ok && !Array.isArray(resolvedRows.value)) {
|
|
149
|
+
const source = snippet(text(this.file, rows));
|
|
150
|
+
const refusal = problem('invalid-chain', rows, `.each() takes an array, and ${source} is not one, so Bun fails the file and runs none of its tests.`, source);
|
|
151
|
+
return refused(call, refusal, context, () => declared);
|
|
152
|
+
}
|
|
153
|
+
// Bun wants a function second or third; a value that can't be one, as options alone, is none.
|
|
154
|
+
const missing = body === undefined || isValue(body);
|
|
155
|
+
if (missing && !spread && !mayLeaveOut) {
|
|
156
|
+
const refusal = problem('test-without-body', call, `${callee}() has no function, so Bun fails the file and runs none of its tests.`, callee);
|
|
157
|
+
return refused(call, refusal, context, () => declared);
|
|
158
|
+
}
|
|
159
|
+
// Bun refuses a retry above 0 with any repeats, 0 too.
|
|
160
|
+
if (run.retry && run.retry.count > 0 && run.repeats !== undefined && atLeast(this.file.version, 1, 3)) {
|
|
161
|
+
const refusal = problem('invalid-chain', options, `${callee}() sets both retry and repeats, which Bun refuses from 1.3, so it fails the file and runs none of its tests.`, snippet(text(this.file, options)));
|
|
162
|
+
return refused(call, refusal, context, () => declared);
|
|
163
|
+
}
|
|
164
|
+
return declared;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The states of a chain, as Bun ranks them: `skip` over `todo` over `failing`. `if(condition)` skips when the
|
|
168
|
+
* condition is false; `skipIf`, `todoIf` and `failingIf` set theirs when it is true.
|
|
169
|
+
*/
|
|
170
|
+
states(links, line, context) {
|
|
171
|
+
const setting = (state, condition) => ({
|
|
172
|
+
state,
|
|
173
|
+
condition,
|
|
174
|
+
reason: null,
|
|
175
|
+
hasDynamicReason: false,
|
|
176
|
+
line,
|
|
177
|
+
});
|
|
178
|
+
const states = [];
|
|
179
|
+
for (const link of links) {
|
|
180
|
+
if (link.name === 'skip')
|
|
181
|
+
states.push(setting('skip', null));
|
|
182
|
+
else if (link.name === 'todo')
|
|
183
|
+
states.push(setting('todo', null));
|
|
184
|
+
else if (link.name === 'failing')
|
|
185
|
+
states.push(setting('expectedToFail', null));
|
|
186
|
+
if (!['if', 'skipIf', 'todoIf', 'failingIf'].includes(link.name))
|
|
187
|
+
continue;
|
|
188
|
+
let condition = this.condition(link, context);
|
|
189
|
+
if (link.name === 'if')
|
|
190
|
+
condition = negated(condition);
|
|
191
|
+
if (condition.applies === 'never')
|
|
192
|
+
continue;
|
|
193
|
+
const state = link.name === 'todoIf' ? 'todo' : link.name === 'failingIf' ? 'expectedToFail' : 'skip';
|
|
194
|
+
states.push(setting(state, condition.applies === 'always' ? null : condition.text));
|
|
195
|
+
}
|
|
196
|
+
const rank = ['skip', 'todo', 'expectedToFail'];
|
|
197
|
+
return states.sort((a, b) => rank.indexOf(a.state) - rank.indexOf(b.state));
|
|
198
|
+
}
|
|
199
|
+
/** `concurrent` runs the test with others at once, and `serial` alone, also as `concurrentIf` and `serialIf` that hold. */
|
|
200
|
+
mode(links, context) {
|
|
201
|
+
for (const link of links) {
|
|
202
|
+
const holds = !link.name.endsWith('If') || this.condition(link, context).applies === 'always';
|
|
203
|
+
if (holds && (link.name === 'concurrent' || link.name === 'concurrentIf'))
|
|
204
|
+
return 'parallel';
|
|
205
|
+
if (holds && (link.name === 'serial' || link.name === 'serialIf'))
|
|
206
|
+
return 'default';
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
/** A number as the timeout, or an object with `timeout`, `retry` and `repeats`. */
|
|
211
|
+
options(node, problems, context) {
|
|
212
|
+
const run = {};
|
|
213
|
+
if (node === undefined || isFunctionLike(node))
|
|
214
|
+
return run;
|
|
215
|
+
const expression = unwrap(node);
|
|
216
|
+
const resolved = this.file.values.resolve(expression, context.scopes);
|
|
217
|
+
if (resolved.ok && typeof resolved.value === 'number') {
|
|
218
|
+
const value = readCount(this.file, 'timeout', { node: expression, resolved }, problems);
|
|
219
|
+
if (value !== null)
|
|
220
|
+
run.timeout = value;
|
|
221
|
+
return run;
|
|
222
|
+
}
|
|
223
|
+
const read = readOptions(this.file, context.scopes, node);
|
|
224
|
+
problems.push(...unresolvedOptions(this.file, read, 'its timeout, retry and repeats are missing'));
|
|
225
|
+
for (const key of ['timeout', 'retry', 'repeats']) {
|
|
226
|
+
const option = read.values.get(key);
|
|
227
|
+
const value = option ? readCount(this.file, key, option, problems) : null;
|
|
228
|
+
if (value === null)
|
|
229
|
+
continue;
|
|
230
|
+
if (key === 'retry')
|
|
231
|
+
run.retry = { count: value, delay: null, condition: null };
|
|
232
|
+
else
|
|
233
|
+
run[key] = value;
|
|
234
|
+
}
|
|
235
|
+
return run;
|
|
236
|
+
}
|
|
237
|
+
/** How many tests a table registers: one per item; null with a warning when the scan can't count them. */
|
|
238
|
+
caseCount(table, problems, context) {
|
|
239
|
+
const cases = table.args.at(0);
|
|
240
|
+
const length = cases ? this.arrayLength(cases, context) : null;
|
|
241
|
+
const resolved = cases && cases.type !== 'SpreadElement' ? this.file.values.resolve(cases, context.scopes) : null;
|
|
242
|
+
// A table that isn't an array fails the file, which is reported where the call is read.
|
|
243
|
+
if (length === null && cases && !resolved?.ok) {
|
|
244
|
+
const source = snippet(text(this.file, cases));
|
|
245
|
+
problems.push(problem('unresolved-cases', cases, 'The cases of .each() could not be counted; caseCount is null.', source));
|
|
246
|
+
}
|
|
247
|
+
return length;
|
|
248
|
+
}
|
|
249
|
+
/** The cases of a table the scan can work out: an item that is an array is spread into the arguments, another is one. */
|
|
250
|
+
tableCases(table, title, context) {
|
|
251
|
+
const first = table.args.at(0);
|
|
252
|
+
const resolved = first && first.type !== 'SpreadElement' ? this.file.values.resolve(first, context.scopes) : null;
|
|
253
|
+
if (!resolved?.ok || !Array.isArray(resolved.value))
|
|
254
|
+
return null;
|
|
255
|
+
return resolved.value.map((item, index) => {
|
|
256
|
+
const row = Array.isArray(item) ? item : [item];
|
|
257
|
+
return { title: title.isDynamic ? null : bunCaseTitle(title.text, row, index, this.file.version), args: row };
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function negated(condition) {
|
|
262
|
+
if (condition.applies === 'sometimes')
|
|
263
|
+
return { applies: 'sometimes', text: `!(${condition.text})` };
|
|
264
|
+
return { applies: condition.applies === 'always' ? 'never' : 'always', text: null };
|
|
265
|
+
}
|
|
266
|
+
function isFunctionLike(node) {
|
|
267
|
+
return node !== undefined && node.type !== 'SpreadElement' && isFunctionNode(unwrap(node));
|
|
268
|
+
}
|
|
269
|
+
/** Whether an argument is a value that can't be a function, as a literal or an object. */
|
|
270
|
+
function isValue(node) {
|
|
271
|
+
return ['Literal', 'ObjectExpression', 'ArrayExpression', 'TemplateLiteral'].includes(unwrap(node).type);
|
|
272
|
+
}
|
|
@@ -63,7 +63,9 @@ export declare function returnedFunction<T>(returns: Returns, shared: (a: T | nu
|
|
|
63
63
|
export declare function looksLikeDeclaration(args: readonly Argument[]): boolean;
|
|
64
64
|
/**
|
|
65
65
|
* A call the runner refuses. Shaped like a declaration outside any test or hook, it is also declared, without what
|
|
66
|
-
* makes it fail, so that its tests, and those of a describe, are listed as not loaded rather than missing.
|
|
66
|
+
* makes it fail, so that its tests, and those of a describe, are listed as not loaded rather than missing. Inside a
|
|
67
|
+
* test or hook body, a test or describe is nested, whatever its arguments: the runner refuses it when the test runs,
|
|
68
|
+
* and the file still loads.
|
|
67
69
|
*/
|
|
68
70
|
export declare function refused(call: CallExpression, refusal: Problem, context: CallContext, declare: () => ApiCall): ApiCall;
|
|
69
71
|
/**
|
|
@@ -132,10 +132,18 @@ export function looksLikeDeclaration(args) {
|
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
134
|
* A call the runner refuses. Shaped like a declaration outside any test or hook, it is also declared, without what
|
|
135
|
-
* makes it fail, so that its tests, and those of a describe, are listed as not loaded rather than missing.
|
|
135
|
+
* makes it fail, so that its tests, and those of a describe, are listed as not loaded rather than missing. Inside a
|
|
136
|
+
* test or hook body, a test or describe is nested, whatever its arguments: the runner refuses it when the test runs,
|
|
137
|
+
* and the file still loads.
|
|
136
138
|
*/
|
|
137
139
|
export function refused(call, refusal, context, declare) {
|
|
138
|
-
|
|
140
|
+
if (context.inBody) {
|
|
141
|
+
const nested = declare();
|
|
142
|
+
if (nested.kind === 'test' || nested.kind === 'describe')
|
|
143
|
+
return nested;
|
|
144
|
+
return { kind: 'invalid', problem: refusal };
|
|
145
|
+
}
|
|
146
|
+
const declared = looksLikeDeclaration(call.arguments) ? declare() : null;
|
|
139
147
|
// A declaration refused for a second reason as well, as for its options, is reported for the first.
|
|
140
148
|
const found = declared?.kind === 'invalid' ? declared.declaration : declared;
|
|
141
149
|
const declaration = found?.kind === 'test' || found?.kind === 'describe' ? found : undefined;
|
|
@@ -151,13 +159,7 @@ export function mayDeclareTests(name, links) {
|
|
|
151
159
|
}
|
|
152
160
|
/** A function declared in the file and passed by name, as in `test('fills an order', checkFill)`; null otherwise. */
|
|
153
161
|
export function namedFunction(file, scopes, node) {
|
|
154
|
-
|
|
155
|
-
return null;
|
|
156
|
-
const declaration = file.values.declarationOf(node.name, scopes);
|
|
157
|
-
if (declaration?.type === 'FunctionDeclaration')
|
|
158
|
-
return declaration;
|
|
159
|
-
const init = declaration?.type === 'VariableDeclarator' && declaration.init ? unwrap(declaration.init) : null;
|
|
160
|
-
return isFunctionNode(init) ? init : null;
|
|
162
|
+
return node?.type === 'Identifier' ? file.values.functionOf(node.name, scopes) : null;
|
|
161
163
|
}
|
|
162
164
|
export function findFunction(args, from = 0) {
|
|
163
165
|
for (const arg of args.slice(from)) {
|
|
@@ -185,6 +187,13 @@ export function readTitle(file, scopes, node, options) {
|
|
|
185
187
|
if (value === null && options.functionNames && expression?.type === 'Identifier') {
|
|
186
188
|
value = file.values.isFunction(expression.name, scopes) ? expression.name : null;
|
|
187
189
|
}
|
|
190
|
+
// A function or class written in place gives its name, as `describe(function OrderBook() {}, ...)` does.
|
|
191
|
+
const written = expression && unwrap(expression);
|
|
192
|
+
if (value === null &&
|
|
193
|
+
options.functionNames &&
|
|
194
|
+
(written?.type === 'FunctionExpression' || written?.type === 'ClassExpression')) {
|
|
195
|
+
value = written.id?.name ?? '';
|
|
196
|
+
}
|
|
188
197
|
if (value !== null) {
|
|
189
198
|
const withoutTags = options.stripTags ? value.replace(/@\S+/g, ' ').replace(/\s+/g, ' ').trim() : value;
|
|
190
199
|
return { title: { text: value, withoutTags, isDynamic: false }, problem: null };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Argument, CallExpression } from 'oxc-parser';
|
|
2
|
+
import type { CallContext, ModuleReader, SuiteDeclaration, TestDeclaration } from '../model.ts';
|
|
3
|
+
import type { ParsedFile } from '../parse.ts';
|
|
4
|
+
import { type FileContext } from './common.ts';
|
|
5
|
+
import { MochaAdapter } from './mocha.ts';
|
|
6
|
+
/**
|
|
7
|
+
* Cypress runs its specs on its own copy of Mocha's BDD interface. A test or describe takes Cypress's settings as an
|
|
8
|
+
* object before its function, as `it(title, { retries: 2 }, fn)`, and Cypress sets no Mocha timeout or retries from
|
|
9
|
+
* `this`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class CypressAdapter extends MochaAdapter {
|
|
12
|
+
protected readonly readsContextSettings = false;
|
|
13
|
+
protected readonly refusedEffect = "Cypress fails the spec and runs none of its tests";
|
|
14
|
+
constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
|
|
15
|
+
protected splitArguments(args: CallExpression['arguments']): {
|
|
16
|
+
body: Argument | undefined;
|
|
17
|
+
options: Argument | null;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Cypress's test config: `retries`, a count or `{ runMode, openMode }`, of which `cypress run` takes `runMode`;
|
|
21
|
+
* `browser`, which skips the test in the browsers it leaves out; `@cypress/grep`'s `tags`; and `env`, which Cypress
|
|
22
|
+
* 16 refuses in a test's config.
|
|
23
|
+
*/
|
|
24
|
+
protected withOptions(declared: TestDeclaration | SuiteDeclaration, options: Argument, context: CallContext): void;
|
|
25
|
+
/**
|
|
26
|
+
* A retry count, or `runMode` of `{ runMode, openMode }`, which `cypress run` uses; null when it is `null`, which
|
|
27
|
+
* Cypress takes as no retries set, or can't be worked out.
|
|
28
|
+
*/
|
|
29
|
+
private retryCount;
|
|
30
|
+
private count;
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { unwrap } from "../ast.js";
|
|
2
|
+
import { snippet } from "../source.js";
|
|
3
|
+
import { atLeast, problem, readCount, readOptions, readStrings, text, unresolvedOptions, } from "./common.js";
|
|
4
|
+
import { dialect, MochaAdapter } from "./mocha.js";
|
|
5
|
+
/**
|
|
6
|
+
* Cypress runs its specs on its own copy of Mocha's BDD interface. A test or describe takes Cypress's settings as an
|
|
7
|
+
* object before its function, as `it(title, { retries: 2 }, fn)`, and Cypress sets no Mocha timeout or retries from
|
|
8
|
+
* `this`.
|
|
9
|
+
*/
|
|
10
|
+
export class CypressAdapter extends MochaAdapter {
|
|
11
|
+
readsContextSettings = false;
|
|
12
|
+
refusedEffect = 'Cypress fails the spec and runs none of its tests';
|
|
13
|
+
constructor(parsed, file, readModule) {
|
|
14
|
+
super(parsed, file, readModule, 'bdd', { ...dialect('bdd'), name: 'Cypress', source: 'cypress' });
|
|
15
|
+
}
|
|
16
|
+
splitArguments(args) {
|
|
17
|
+
// Cypress takes the config only before a function: `it(title, { retries: 2 })` gives Mocha the object as the test's
|
|
18
|
+
// function, so the test runs, and fails.
|
|
19
|
+
if (args.length >= 3)
|
|
20
|
+
return { body: args.at(2), options: args.at(1) };
|
|
21
|
+
return { body: args.at(1), options: null };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Cypress's test config: `retries`, a count or `{ runMode, openMode }`, of which `cypress run` takes `runMode`;
|
|
25
|
+
* `browser`, which skips the test in the browsers it leaves out; `@cypress/grep`'s `tags`; and `env`, which Cypress
|
|
26
|
+
* 16 refuses in a test's config.
|
|
27
|
+
*/
|
|
28
|
+
withOptions(declared, options, context) {
|
|
29
|
+
const read = readOptions(this.file, context.scopes, options);
|
|
30
|
+
const { problems, details } = declared;
|
|
31
|
+
problems.push(...unresolvedOptions(this.file, read, 'its retries, tags and browser are missing'));
|
|
32
|
+
const retries = read.values.get('retries');
|
|
33
|
+
if (retries) {
|
|
34
|
+
const count = this.retryCount(retries, context, problems);
|
|
35
|
+
if (count !== null)
|
|
36
|
+
details.run = { ...details.run, retry: { count, delay: null, condition: null } };
|
|
37
|
+
}
|
|
38
|
+
const tags = read.values.get('tags');
|
|
39
|
+
if (tags)
|
|
40
|
+
details.tags.push(...readStrings(this.file, context.scopes, tags).values);
|
|
41
|
+
const browser = read.values.get('browser');
|
|
42
|
+
if (browser) {
|
|
43
|
+
const line = this.file.source.line(browser.node.start);
|
|
44
|
+
const condition = `browser: ${snippet(text(this.file, browser.node))}`;
|
|
45
|
+
declared.states.push({ state: 'skip', condition, reason: null, hasDynamicReason: false, line });
|
|
46
|
+
}
|
|
47
|
+
const env = read.values.get('env');
|
|
48
|
+
if (env && atLeast(this.file.version, 16, 0)) {
|
|
49
|
+
const source = snippet(text(this.file, env.node));
|
|
50
|
+
const message = "Cypress 16 no longer takes env in a test's or describe's config, and fails each test it applies to when the test runs; expose: { KEY: value } sets a value the browser reads.";
|
|
51
|
+
problems.push(problem('removed-api', env.node, message, source));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A retry count, or `runMode` of `{ runMode, openMode }`, which `cypress run` uses; null when it is `null`, which
|
|
56
|
+
* Cypress takes as no retries set, or can't be worked out.
|
|
57
|
+
*/
|
|
58
|
+
retryCount(option, context, problems) {
|
|
59
|
+
const node = unwrap(option.node);
|
|
60
|
+
if (node.type !== 'ObjectExpression')
|
|
61
|
+
return this.count('retries', option, problems);
|
|
62
|
+
const runMode = readOptions(this.file, context.scopes, node).values.get('runMode');
|
|
63
|
+
return runMode ? this.count('retries.runMode', runMode, problems) : null;
|
|
64
|
+
}
|
|
65
|
+
count(name, option, problems) {
|
|
66
|
+
if (option.resolved.ok && option.resolved.value === null)
|
|
67
|
+
return null;
|
|
68
|
+
return readCount(this.file, name, option, problems);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CallExpression } from 'oxc-parser';
|
|
2
|
+
import type { ApiCall, CallContext, ModuleReader } from '../model.ts';
|
|
3
|
+
import type { ParsedFile } from '../parse.ts';
|
|
4
|
+
import type { HookCounts } from '../types.ts';
|
|
5
|
+
import { type FileContext } from './common.ts';
|
|
6
|
+
import { SuiteAdapter } from './test-functions.ts';
|
|
7
|
+
export declare class JasmineAdapter extends SuiteAdapter {
|
|
8
|
+
/** What the runner does with a call it refuses at the top level of a file, as the end of a sentence. */
|
|
9
|
+
protected readonly refusedEffect: string;
|
|
10
|
+
/** Whether a number after a spec's timeout is a retry count, as WebdriverIO reads it. */
|
|
11
|
+
protected readonly trailingRetries: boolean;
|
|
12
|
+
constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
|
|
13
|
+
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
14
|
+
protected isAliasLink(): boolean;
|
|
15
|
+
protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
|
|
16
|
+
/**
|
|
17
|
+
* `pending(reason)` in a spec or its `beforeEach` or `afterEach` hook, and from Jasmine 7 `notApplicable(reason)`,
|
|
18
|
+
* which mark the spec as not run, with the reason (checked with Jasmine 4.6.0, 5.13.0 and 7.0.0).
|
|
19
|
+
*/
|
|
20
|
+
private pendingCall;
|
|
21
|
+
private chainCall;
|
|
22
|
+
private declaration;
|
|
23
|
+
/**
|
|
24
|
+
* A call Jasmine refuses: written at the top level of a file, it stops Jasmine before it runs any spec; inside a
|
|
25
|
+
* describe, Jasmine catches it, reports the describe as failed, and registers none of the specs written after it there.
|
|
26
|
+
*/
|
|
27
|
+
private refusal;
|
|
28
|
+
}
|