@itmetsam/samurai-framework 0.22.0 → 0.22.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/api.d.ts +3 -3
- package/dist/browser/page-content.d.ts +4 -0
- package/dist/browser/page-content.js +12 -0
- package/dist/browser/page-events.d.ts +11 -0
- package/dist/browser/page-events.js +23 -0
- package/dist/browser/page-logging.d.ts +24 -0
- package/dist/browser/page-logging.js +43 -0
- package/dist/browser/page-settings.d.ts +1 -1
- package/dist/browser/page-viewport.d.ts +21 -0
- package/dist/browser/page-viewport.js +33 -0
- package/dist/browser/page.d.ts +6 -9
- package/dist/browser/page.js +31 -63
- package/dist/browser/screenshot.d.ts +6 -0
- package/dist/browser/screenshot.js +14 -0
- package/dist/capabilities.d.ts +16 -2
- package/dist/capabilities.js +16 -2
- package/dist/config/config.js +2 -1
- package/dist/config/fixture-use.d.ts +19 -0
- package/dist/config/fixture-use.js +44 -0
- package/dist/config/group-use.d.ts +4 -2
- package/dist/config/group-use.js +30 -20
- package/dist/config/use-options.js +5 -1
- package/dist/recorder/capture.d.ts +3 -0
- package/dist/recorder/capture.js +18 -1
- package/dist/recorder/normalise.d.ts +12 -1
- package/dist/recorder/normalise.js +29 -1
- package/dist/recorder/page-messages.d.ts +6 -2
- package/dist/recorder/page-messages.js +29 -21
- package/dist/recorder/toolbar/inspect.js +10 -3
- package/dist/runner/fixture-defs.d.ts +2 -1
- package/dist/runner/fixture-defs.js +51 -18
- package/dist/runner/fixture-meta.d.ts +20 -0
- package/dist/runner/fixture-meta.js +109 -0
- package/dist/runner/fixture-options.d.ts +8 -0
- package/dist/runner/fixture-options.js +35 -0
- package/dist/runner/fixture-outputs.d.ts +11 -0
- package/dist/runner/fixture-outputs.js +28 -0
- package/dist/runner/fixture-registry.d.ts +35 -0
- package/dist/runner/fixture-registry.js +73 -0
- package/dist/runner/fixture-scope.d.ts +39 -9
- package/dist/runner/fixture-scope.js +139 -70
- package/dist/runner/fixture-start.d.ts +27 -0
- package/dist/runner/fixture-start.js +76 -0
- package/dist/runner/fixture-use.d.ts +23 -0
- package/dist/runner/fixture-use.js +64 -0
- package/dist/runner/fixture-worker.d.ts +26 -0
- package/dist/runner/fixture-worker.js +46 -0
- package/dist/runner/prepare-run.js +5 -0
- package/dist/runner/registry.js +15 -5
- package/dist/runner/reporter.d.ts +3 -1
- package/dist/runner/reporter.js +1 -0
- package/dist/runner/run-test.d.ts +3 -0
- package/dist/runner/run-test.js +16 -1
- package/dist/runner/run.d.ts +7 -0
- package/dist/runner/run.js +9 -0
- package/dist/runner/test-function.d.ts +2 -2
- package/dist/runner/test-runner.js +6 -1
- package/dist/runner/use-options.d.ts +16 -2
- package/dist/runner/use-options.js +28 -8
- package/dist/runner/worker-session.js +6 -0
- package/dist/steps/build.js +49 -25
- package/dist/steps/emit.js +64 -7
- package/dist/steps/index.d.ts +1 -1
- package/dist/steps/model.d.ts +60 -10
- package/dist/steps/parse/call.js +125 -39
- package/dist/steps/parse/expectation.d.ts +2 -1
- package/dist/steps/parse/expectation.js +30 -8
- package/dist/steps/parse/literals.d.ts +9 -0
- package/dist/steps/parse/literals.js +42 -0
- package/dist/steps/parse/step.js +28 -1
- package/dist/types/config.d.ts +35 -22
- package/dist/types/extend.d.ts +54 -2
- package/dist/types/test.d.ts +24 -0
- package/dist/types/use.d.ts +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { FIXTURES_KEY, mergeFixtureUse, pickFixtureUse, } from "../config/fixture-use.js";
|
|
2
|
+
import { peekRunSettings } from "../config/run-settings.js";
|
|
3
|
+
import { useLayers } from "./use-options.js";
|
|
4
|
+
/**
|
|
5
|
+
* The fixtures a test gets from `use`: config, then the run's environment, then the group, then `test.use()`, a
|
|
6
|
+
* later layer winning key by key. The environment's `false` is the exception: it wins over a `true` anywhere, so a
|
|
7
|
+
* fixture an environment turns off cannot be switched back on for it.
|
|
8
|
+
*/
|
|
9
|
+
export async function effectiveFixtureUse(test) {
|
|
10
|
+
const layers = await useLayers(test);
|
|
11
|
+
const merged = mergeFixtureUse([
|
|
12
|
+
layers.config,
|
|
13
|
+
layers.environment,
|
|
14
|
+
layers.group,
|
|
15
|
+
...layers.tests,
|
|
16
|
+
]);
|
|
17
|
+
const environment = peekRunSettings()?.environment ?? "";
|
|
18
|
+
const disabled = new Map();
|
|
19
|
+
for (const [name, on] of Object.entries(mergeFixtureUse([layers.environment]).fixtures))
|
|
20
|
+
if (!on)
|
|
21
|
+
disabled.set(name, `Fixture ${name} is turned off in ${environment}`);
|
|
22
|
+
return {
|
|
23
|
+
attach: Object.entries(merged.fixtures)
|
|
24
|
+
.filter(([, on]) => on)
|
|
25
|
+
.map(([name]) => name),
|
|
26
|
+
options: merged.options,
|
|
27
|
+
disabled,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const list = (names, none) => names.length > 0 ? names.join(", ") : none;
|
|
31
|
+
/** Throws when a `use` names a fixture or an option fixture that `catalog` does not have */
|
|
32
|
+
function checkPlace(place, use, catalog) {
|
|
33
|
+
const fixtures = catalog.map((entry) => entry.name);
|
|
34
|
+
const options = catalog.flatMap((entry) => entry.options?.name ?? []);
|
|
35
|
+
for (const [key, value] of Object.entries(pickFixtureUse(use))) {
|
|
36
|
+
if (key === FIXTURES_KEY) {
|
|
37
|
+
for (const name of Object.keys(value))
|
|
38
|
+
if (!fixtures.includes(name))
|
|
39
|
+
throw new Error(`${place}.fixtures: unknown fixture "${name}". ${fixtures.length > 0 ? `Known fixtures: ${list(fixtures, "")}` : "No fixtures are listed in config.fixtures"}`);
|
|
40
|
+
}
|
|
41
|
+
else if (!options.includes(key))
|
|
42
|
+
throw new Error(`${place}.${key}: unknown option fixture "${key}". ${options.length > 0 ? `Known option fixtures: ${list(options, "")}` : "No option fixtures are listed in config.fixtures"}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Refuses a config whose `use` (config, environments, groups) names a fixture that is not in `config.fixtures` */
|
|
46
|
+
export function checkFixtureUseNames(config, catalog) {
|
|
47
|
+
checkPlace("config.use", config.use, catalog);
|
|
48
|
+
for (const [name, environment] of Object.entries(config.environments ?? {}))
|
|
49
|
+
checkPlace(`config.environments.${name}.use`, environment.use, catalog);
|
|
50
|
+
config.groups?.forEach((group, index) => checkPlace(`config.groups[${index}].use`, group.use, catalog));
|
|
51
|
+
}
|
|
52
|
+
/** Refuses a `test.use()` that names a fixture the test's own `test.extend()` does not define */
|
|
53
|
+
export function checkTestFixtureUse(use, layers) {
|
|
54
|
+
for (const [key, value] of Object.entries(pickFixtureUse(use))) {
|
|
55
|
+
const known = [...(layers?.keys() ?? [])];
|
|
56
|
+
if (key === FIXTURES_KEY) {
|
|
57
|
+
for (const name of Object.keys(value))
|
|
58
|
+
if (!layers?.has(name))
|
|
59
|
+
throw new TypeError(`use(): unknown fixture "${name}". ${known.length > 0 ? `This test has: ${known.join(", ")}` : "This test has no fixtures; write it with test.extend()"}`);
|
|
60
|
+
}
|
|
61
|
+
else if (!layers?.get(key)?.slice(-1)[0]?.option)
|
|
62
|
+
throw new TypeError(`use(): unknown option fixture "${key}"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FixtureDef } from "./fixture-defs.js";
|
|
2
|
+
import { type Started } from "./fixture-start.js";
|
|
3
|
+
/** A worker fixture that is set up: its value, how to finish it, and the outputs the report lists */
|
|
4
|
+
export interface WorkerInstance {
|
|
5
|
+
value: unknown;
|
|
6
|
+
started: Started;
|
|
7
|
+
outputs?: Record<string, string | number | boolean> | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The worker-scoped fixtures of one worker. An instance is shared by the tests that need the same definition with
|
|
11
|
+
* the same key (the environment, the resolved options and the instances it uses), is set up on first use and is
|
|
12
|
+
* torn down when the worker is done, last set up first.
|
|
13
|
+
*/
|
|
14
|
+
export declare class WorkerFixtures {
|
|
15
|
+
private readonly limitMs;
|
|
16
|
+
private readonly byDef;
|
|
17
|
+
private readonly live;
|
|
18
|
+
/** `limitMs`: how long a teardown may take when the fixture has no `timeout` of its own */
|
|
19
|
+
constructor(limitMs: number);
|
|
20
|
+
/** The instance of `def` under `key`: the one already set up, else the result of `make`. A failed setup is not kept */
|
|
21
|
+
acquire(def: FixtureDef, key: string, make: () => Promise<WorkerInstance>): Promise<WorkerInstance & {
|
|
22
|
+
reused: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
/** Tears every instance down, last set up first, and returns the errors; a second call has nothing to do */
|
|
25
|
+
closeAll(): Promise<Error[]>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { finishWithin } from "./fixture-start.js";
|
|
2
|
+
/**
|
|
3
|
+
* The worker-scoped fixtures of one worker. An instance is shared by the tests that need the same definition with
|
|
4
|
+
* the same key (the environment, the resolved options and the instances it uses), is set up on first use and is
|
|
5
|
+
* torn down when the worker is done, last set up first.
|
|
6
|
+
*/
|
|
7
|
+
export class WorkerFixtures {
|
|
8
|
+
/** `limitMs`: how long a teardown may take when the fixture has no `timeout` of its own */
|
|
9
|
+
constructor(limitMs) {
|
|
10
|
+
this.limitMs = limitMs;
|
|
11
|
+
this.byDef = new Map();
|
|
12
|
+
this.live = [];
|
|
13
|
+
}
|
|
14
|
+
/** The instance of `def` under `key`: the one already set up, else the result of `make`. A failed setup is not kept */
|
|
15
|
+
async acquire(def, key, make) {
|
|
16
|
+
let keys = this.byDef.get(def);
|
|
17
|
+
if (!keys)
|
|
18
|
+
this.byDef.set(def, (keys = new Map()));
|
|
19
|
+
const existing = keys.get(key);
|
|
20
|
+
if (existing)
|
|
21
|
+
return { ...(await existing), reused: true };
|
|
22
|
+
const created = make().then((instance) => {
|
|
23
|
+
this.live.push(instance);
|
|
24
|
+
return instance;
|
|
25
|
+
});
|
|
26
|
+
keys.set(key, created);
|
|
27
|
+
try {
|
|
28
|
+
return { ...(await created), reused: false };
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
keys.delete(key);
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Tears every instance down, last set up first, and returns the errors; a second call has nothing to do */
|
|
36
|
+
async closeAll() {
|
|
37
|
+
const errors = [];
|
|
38
|
+
this.byDef.clear();
|
|
39
|
+
for (let one = this.live.pop(); one; one = this.live.pop()) {
|
|
40
|
+
const error = await finishWithin(one.started, this.limitMs);
|
|
41
|
+
if (error)
|
|
42
|
+
errors.push(error);
|
|
43
|
+
}
|
|
44
|
+
return errors;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -2,11 +2,16 @@ import { isShortSecret, registerSecret } from "../config/mask.js";
|
|
|
2
2
|
import { resolveRunSettings, setRunSettings, } from "../config/run-settings.js";
|
|
3
3
|
import { loadSecrets } from "../config/secrets.js";
|
|
4
4
|
import logger from "../logger/index.js";
|
|
5
|
+
import { fixtureCatalog } from "./fixture-registry.js";
|
|
6
|
+
import { checkFixtureUseNames } from "./fixture-use.js";
|
|
5
7
|
/**
|
|
6
8
|
* Starts a run: picks the environment, loads its secrets and registers every value for masking
|
|
7
9
|
* (all of them, not only the ones a test reads), then makes the settings active.
|
|
8
10
|
*/
|
|
9
11
|
export function prepareRun(config, overrides, options = {}) {
|
|
12
|
+
// A fixture name defined twice refuses the run before any test starts
|
|
13
|
+
const catalog = fixtureCatalog(config.fixtures ?? []);
|
|
14
|
+
checkFixtureUseNames(config, catalog);
|
|
10
15
|
const settings = resolveRunSettings(config, overrides);
|
|
11
16
|
const secrets = loadSecrets(settings.environment, options);
|
|
12
17
|
for (const [name, value] of secrets) {
|
package/dist/runner/registry.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import logger from "../logger/index.js";
|
|
3
3
|
import { extendLayers } from "./fixture-defs.js";
|
|
4
|
+
import { rememberLayers } from "./fixture-registry.js";
|
|
5
|
+
import { checkTestFixtureUse } from "./fixture-use.js";
|
|
4
6
|
import { validateUseOptions } from "../config/use-options.js";
|
|
5
7
|
import { currentStepLog, currentTestInfo, infoFor, skipFromCondition, } from "./test-info.js";
|
|
6
8
|
import { runStep } from "./steps.js";
|
|
@@ -150,16 +152,21 @@ export function beforeAll(hook) {
|
|
|
150
152
|
export function afterAll(hook) {
|
|
151
153
|
addHook(currentSuite(callerFile()), "afterAll", hook);
|
|
152
154
|
}
|
|
153
|
-
/** `test.use(options)`: checked now, applied when each test of the scope runs */
|
|
154
|
-
function
|
|
155
|
-
|
|
155
|
+
/** `test.use(options)` of a test with the fixtures `extension`: checked now, applied when each test of the scope runs */
|
|
156
|
+
function usingFor(extension) {
|
|
157
|
+
return (options) => {
|
|
158
|
+
const checked = validateUseOptions(options);
|
|
159
|
+
// `fixtures` and option fixtures may only name what this test's `test.extend()` defines
|
|
160
|
+
checkTestFixtureUse(checked, extension);
|
|
161
|
+
addUse(currentSuite(callerFile()), checked);
|
|
162
|
+
};
|
|
156
163
|
}
|
|
157
164
|
/**
|
|
158
165
|
* A `test` that registers its tests with the custom fixtures `extension`. The aliases are the same functions as
|
|
159
166
|
* the named exports: they find the spec file from the stack, so they must not be wrapped.
|
|
160
167
|
*/
|
|
161
168
|
function makeTest(extension) {
|
|
162
|
-
|
|
169
|
+
const made = Object.assign(registering("test", undefined, extension), {
|
|
163
170
|
skip: skipping("skip", extension),
|
|
164
171
|
fixme: skipping("fixme", extension),
|
|
165
172
|
fail: failing(extension),
|
|
@@ -168,7 +175,7 @@ function makeTest(extension) {
|
|
|
168
175
|
step: (title, body, options) => runStep(currentStepLog(), title, body, options),
|
|
169
176
|
only: registering("test.only", "only", extension),
|
|
170
177
|
info: currentTestInfo,
|
|
171
|
-
use,
|
|
178
|
+
use: usingFor(extension),
|
|
172
179
|
describe,
|
|
173
180
|
beforeEach,
|
|
174
181
|
afterEach,
|
|
@@ -176,6 +183,9 @@ function makeTest(extension) {
|
|
|
176
183
|
afterAll,
|
|
177
184
|
extend: (fixtures) => makeTest(extendLayers(extension, fixtures)),
|
|
178
185
|
});
|
|
186
|
+
if (extension)
|
|
187
|
+
rememberLayers(made, extension);
|
|
188
|
+
return made;
|
|
179
189
|
}
|
|
180
190
|
export const test = makeTest();
|
|
181
191
|
export function registeredTests() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TestAnnotation, TestStepEntry, TestAttachmentEntry, RegisteredTestCase, TestError, TestLogEntry, TestResult, TestRetries, TestSummary } from "../types/test.js";
|
|
1
|
+
import type { TestAnnotation, TestFixtureEntry, TestStepEntry, TestAttachmentEntry, RegisteredTestCase, TestError, TestLogEntry, TestResult, TestRetries, TestSummary } from "../types/test.js";
|
|
2
2
|
import type { SupportedBrowser } from "../types/browser.js";
|
|
3
3
|
import type { Shard } from "./shard.js";
|
|
4
4
|
/** What a run reports while it happens */
|
|
@@ -75,6 +75,8 @@ export interface TestEndExtras extends TestRetries {
|
|
|
75
75
|
annotations?: TestAnnotation[];
|
|
76
76
|
/** The steps the test ran with `test.step` */
|
|
77
77
|
steps?: TestStepEntry[];
|
|
78
|
+
/** The fixtures the test set up, when it set any up */
|
|
79
|
+
fixtures?: TestFixtureEntry[];
|
|
78
80
|
}
|
|
79
81
|
export default class TestReporter implements TestSink {
|
|
80
82
|
private testResults;
|
package/dist/runner/reporter.js
CHANGED
|
@@ -160,6 +160,7 @@ export default class TestReporter {
|
|
|
160
160
|
...(extras?.attachments && { attachments: extras.attachments }),
|
|
161
161
|
...(extras?.annotations && { annotations: extras.annotations }),
|
|
162
162
|
...(extras?.steps && { steps: extras.steps }),
|
|
163
|
+
...(extras?.fixtures?.length && { fixtures: extras.fixtures }),
|
|
163
164
|
...(extras?.retry && { retry: extras.retry }),
|
|
164
165
|
...(extras?.flaky && { flaky: true }),
|
|
165
166
|
...(extras?.previousAttempts?.length && {
|
|
@@ -4,6 +4,7 @@ import type { RunnerOptions } from "./options.js";
|
|
|
4
4
|
import type { AllHooks } from "./all-hooks.js";
|
|
5
5
|
import type { PreparedRun } from "./prepare-run.js";
|
|
6
6
|
import type { TestSink } from "./reporter.js";
|
|
7
|
+
import type { WorkerFixtures } from "./fixture-worker.js";
|
|
7
8
|
/** What a worker gives its tests that the main process does not: its own port and profile, and its number */
|
|
8
9
|
export interface WorkerResources {
|
|
9
10
|
/** The browser's debugging port. @default `options.port`, else DEFAULT_PORT */
|
|
@@ -16,6 +17,8 @@ export interface WorkerResources {
|
|
|
16
17
|
attempt?: number;
|
|
17
18
|
/** The `beforeAll` / `afterAll` hooks of this worker: kept across tests, closed when the worker is done */
|
|
18
19
|
allHooks?: AllHooks;
|
|
20
|
+
/** The worker-scoped fixtures of this worker: kept across tests, closed when the worker is done */
|
|
21
|
+
workerFixtures?: WorkerFixtures;
|
|
19
22
|
}
|
|
20
23
|
/** Runs one test in one browser and reports it: launch, run the body, judge the page's logs, close the browser */
|
|
21
24
|
export declare function runOneTest(test: RegisteredTestCase, browserName: SupportedBrowser, run: PreparedRun, options: RunnerOptions, srcDir: string, reporter: TestSink, resources?: WorkerResources): Promise<void>;
|
package/dist/runner/run-test.js
CHANGED
|
@@ -20,6 +20,7 @@ import { buildTestFixtures } from "./fixtures.js";
|
|
|
20
20
|
import { FixtureScope } from "./fixture-scope.js";
|
|
21
21
|
import { RequestFixture } from "./request-fixture.js";
|
|
22
22
|
import { effectiveUse } from "./use-options.js";
|
|
23
|
+
import { effectiveFixtureUse } from "./fixture-use.js";
|
|
23
24
|
import { beginOutputs, endOutputs } from "../wait/output-sink.js";
|
|
24
25
|
/**
|
|
25
26
|
* The state of one test run. A test ends once, whichever of finishing, failing, timing out, an abort or a
|
|
@@ -38,6 +39,8 @@ class TestOutcome {
|
|
|
38
39
|
this.browserClosed = false;
|
|
39
40
|
/** The test's inputs and outputs, as its report entry lists them when it ends */
|
|
40
41
|
this.io = () => ({});
|
|
42
|
+
/** The fixtures the test set up, as they stand when it ends */
|
|
43
|
+
this.fixtures = () => [];
|
|
41
44
|
}
|
|
42
45
|
get isEnded() {
|
|
43
46
|
return this.ended;
|
|
@@ -65,6 +68,7 @@ class TestOutcome {
|
|
|
65
68
|
screenshots: this.evidence?.screenshots,
|
|
66
69
|
...this.info?.reportExtras(),
|
|
67
70
|
...this.io(),
|
|
71
|
+
fixtures: this.fixtures(),
|
|
68
72
|
});
|
|
69
73
|
}
|
|
70
74
|
}
|
|
@@ -119,6 +123,9 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
|
|
|
119
123
|
// The `beforeAll` hooks come first, before a browser exists
|
|
120
124
|
await resources.allHooks?.ensure(test, browserName);
|
|
121
125
|
const use = await effectiveUse(test);
|
|
126
|
+
const fixtureUse = test.extension
|
|
127
|
+
? await effectiveFixtureUse(test)
|
|
128
|
+
: undefined;
|
|
122
129
|
const launched = await Browser.launch(browserName, {
|
|
123
130
|
port: resources.port ?? options.port ?? DEFAULT_PORT,
|
|
124
131
|
...(resources.profileDir && { profileDir: resources.profileDir }),
|
|
@@ -177,9 +184,17 @@ export async function runOneTest(test, browserName, run, options, srcDir, report
|
|
|
177
184
|
});
|
|
178
185
|
// A test written with `test.extend()` gets its custom fixtures set up on demand, and torn down below
|
|
179
186
|
const custom = test.extension
|
|
180
|
-
? new FixtureScope(test.extension, built
|
|
187
|
+
? new FixtureScope(test.extension, built, {
|
|
188
|
+
...fixtureUse,
|
|
189
|
+
environment: settings.environment,
|
|
190
|
+
...(resources.workerFixtures && {
|
|
191
|
+
workers: resources.workerFixtures,
|
|
192
|
+
}),
|
|
193
|
+
})
|
|
181
194
|
: undefined;
|
|
182
195
|
customFixtures = custom;
|
|
196
|
+
if (custom)
|
|
197
|
+
outcome.fixtures = () => custom.report();
|
|
183
198
|
const fixtures = custom?.fixtures ?? built;
|
|
184
199
|
let failure;
|
|
185
200
|
let failed = false;
|
package/dist/runner/run.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Modifier, TestSummary } from "../types/test.js";
|
|
2
2
|
import { type ProjectOptions } from "./project.js";
|
|
3
3
|
import { type ResolvedGroup } from "./groups.js";
|
|
4
|
+
import { type FixtureCatalogEntry } from "./fixture-registry.js";
|
|
4
5
|
import type { RunnerOptions } from "./options.js";
|
|
5
6
|
export interface RunTestsOptions extends RunnerOptions, ProjectOptions {
|
|
6
7
|
}
|
|
@@ -45,6 +46,12 @@ export interface FoundTest {
|
|
|
45
46
|
export declare function listTests(options?: RunTestsOptions): Promise<FoundTest[]>;
|
|
46
47
|
/** Imports the project's specs without running anything and lists the config's groups with the tests each holds */
|
|
47
48
|
export declare function listGroups(options?: RunTestsOptions): Promise<ResolvedGroup[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Lists the fixtures of the extended tests in the config's `fixtures`, with their label, icon, scope, option form
|
|
51
|
+
* and declared outputs, without running a test. Throws for a name defined twice or one that is not allowed.
|
|
52
|
+
*/
|
|
53
|
+
export declare function listFixtures(options?: ProjectOptions): Promise<FixtureCatalogEntry[]>;
|
|
54
|
+
export type { FixtureCatalogEntry } from "./fixture-registry.js";
|
|
48
55
|
export { BROWSER_NAMES, checkBrowserList } from "../config/browsers.js";
|
|
49
56
|
export { parseWorkers } from "../config/workers.js";
|
|
50
57
|
export { parseRetries } from "../config/retries.js";
|
package/dist/runner/run.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { withProject } from "./project.js";
|
|
3
3
|
import { testId, toPosix } from "./groups.js";
|
|
4
|
+
import { projectConfig } from "../config/config.js";
|
|
5
|
+
import { fixtureCatalog, } from "./fixture-registry.js";
|
|
4
6
|
import TestRunner from "./test-runner.js";
|
|
5
7
|
/**
|
|
6
8
|
* Sets the project up (config, environment, secrets), builds a runner from the rest of the options and hands both to
|
|
@@ -67,6 +69,13 @@ export async function listTests(options = {}) {
|
|
|
67
69
|
export async function listGroups(options = {}) {
|
|
68
70
|
return inProject(options, (runner) => runner.groups());
|
|
69
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Lists the fixtures of the extended tests in the config's `fixtures`, with their label, icon, scope, option form
|
|
74
|
+
* and declared outputs, without running a test. Throws for a name defined twice or one that is not allowed.
|
|
75
|
+
*/
|
|
76
|
+
export async function listFixtures(options = {}) {
|
|
77
|
+
return fixtureCatalog((await projectConfig(options)).fixtures ?? []);
|
|
78
|
+
}
|
|
70
79
|
export { BROWSER_NAMES, checkBrowserList } from "../config/browsers.js";
|
|
71
80
|
export { parseWorkers } from "../config/workers.js";
|
|
72
81
|
export { parseRetries } from "../config/retries.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AllHook, SuiteMode, TestCaseBase, TestDetails, TestInfo, TestOptions } from "../types/test.js";
|
|
2
|
-
import type { UseOptions } from "../types/use.js";
|
|
2
|
+
import type { FixtureUse, UseOptions } from "../types/use.js";
|
|
3
3
|
import type { FixtureDefinitions, FixturesOf, MergeFixtures } from "../types/extend.js";
|
|
4
4
|
/** What the registry keeps of a test body; the fixtures it gets are those of the test it was written for */
|
|
5
5
|
export type Body = TestCaseBase["function"];
|
|
@@ -46,7 +46,7 @@ export interface TestFunction<X = NoFixtures> {
|
|
|
46
46
|
* the whole file at the top level, whether before or after the tests. An inner block wins over an outer one, and
|
|
47
47
|
* both win over config `use`.
|
|
48
48
|
*/
|
|
49
|
-
use: (options: UseOptions) => void;
|
|
49
|
+
use: (options: UseOptions & FixtureUse) => void;
|
|
50
50
|
/** `describe`, under the name other frameworks use */
|
|
51
51
|
describe: DescribeFunction;
|
|
52
52
|
/** `beforeEach`, as `test.beforeEach` */
|
|
@@ -6,6 +6,7 @@ import { loadRawConfig, projectDir, readConfig } from "../config/config.js";
|
|
|
6
6
|
import { forbidsOnly } from "../config/forbid-only.js";
|
|
7
7
|
import logger from "../logger/index.js";
|
|
8
8
|
import { AllHooks } from "./all-hooks.js";
|
|
9
|
+
import { WorkerFixtures } from "./fixture-worker.js";
|
|
9
10
|
import { assertNoOnly, narrowToOnly } from "./focus.js";
|
|
10
11
|
import { DEFAULT_SRC_DIR, DEFAULT_TEST_MATCH } from "../config/defaults.js";
|
|
11
12
|
import { DEFAULT_PORT } from "./ports.js";
|
|
@@ -149,11 +150,13 @@ export default class TestRunner {
|
|
|
149
150
|
timeout: this.run.settings.timeout,
|
|
150
151
|
fixtures: (browser) => allHookFixtures(this.run, browser),
|
|
151
152
|
});
|
|
153
|
+
// The worker-scoped fixtures of the run: torn down when the last test is done
|
|
154
|
+
const workerFixtures = new WorkerFixtures(this.run.settings.timeout);
|
|
152
155
|
try {
|
|
153
156
|
for (const browser of browsers) {
|
|
154
157
|
const forBrowser = selected.filter((test) => runsIn(test, browser));
|
|
155
158
|
for (const chunk of chunkBySerialBlock(forBrowser)) {
|
|
156
|
-
const runTest = (test, sink, attempt) => runOneTest(test, browser, this.run, this.options, loaded.srcDir, sink, { allHooks, attempt });
|
|
159
|
+
const runTest = (test, sink, attempt) => runOneTest(test, browser, this.run, this.options, loaded.srcDir, sink, { allHooks, workerFixtures, attempt });
|
|
157
160
|
if (chunk.block) {
|
|
158
161
|
const { suite } = chunk.block;
|
|
159
162
|
await serialBlockRun({
|
|
@@ -192,6 +195,8 @@ export default class TestRunner {
|
|
|
192
195
|
finally {
|
|
193
196
|
for (const error of await allHooks.closeAll())
|
|
194
197
|
logger.error("%s", error.message);
|
|
198
|
+
for (const error of await workerFixtures.closeAll())
|
|
199
|
+
logger.error("%s", error.message);
|
|
195
200
|
}
|
|
196
201
|
return reporter.onEnd();
|
|
197
202
|
}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import type { GroupUse } from "../types/config.js";
|
|
2
2
|
import type { RegisteredTestCase } from "../types/test.js";
|
|
3
|
+
/** The `use` objects that apply to one test, as written, lowest layer first */
|
|
4
|
+
export interface UseLayers {
|
|
5
|
+
/** Config `use` */
|
|
6
|
+
config: object;
|
|
7
|
+
/** `environments.<name>.use` of the run's environment */
|
|
8
|
+
environment: object;
|
|
9
|
+
/** `use` of the group the test runs for */
|
|
10
|
+
group: object;
|
|
11
|
+
/** The `test.use()` calls around the test, outermost first */
|
|
12
|
+
tests: object[];
|
|
13
|
+
}
|
|
14
|
+
/** The `use` layers of a test: config, then the run's environment, then the group it runs for, then `test.use()` */
|
|
15
|
+
export declare function useLayers(test: RegisteredTestCase): Promise<UseLayers>;
|
|
3
16
|
/**
|
|
4
|
-
* The `use` options one test runs with: config `use`, then the `use` of the
|
|
5
|
-
* the `test.use()` calls around the test, outermost first, a later one
|
|
17
|
+
* The `use` options one test runs with: config `use`, then the `use` of the run's environment, then the `use` of
|
|
18
|
+
* the group the test runs for (if any), then the `test.use()` calls around the test, outermost first, a later one
|
|
19
|
+
* winning key by key. The fixture keys (`fixtures`, `orderOptions`) are for the fixtures: see `effectiveFixtureUse`.
|
|
6
20
|
*/
|
|
7
21
|
export declare function effectiveUse(test: RegisteredTestCase): Promise<GroupUse>;
|
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
import { readConfig } from "../config/config.js";
|
|
2
|
+
import { omitFixtureUse } from "../config/fixture-use.js";
|
|
2
3
|
import { groupUseOf } from "../config/group-use.js";
|
|
4
|
+
import { peekRunSettings } from "../config/run-settings.js";
|
|
3
5
|
import { mergeUse, useOptionsOf } from "../config/use-options.js";
|
|
4
6
|
import logger from "../logger/index.js";
|
|
5
7
|
import { useOf } from "./suite.js";
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
* the `test.use()` calls around the test, outermost first, a later one winning key by key
|
|
9
|
-
*/
|
|
10
|
-
export async function effectiveUse(test) {
|
|
8
|
+
/** The `use` layers of a test: config, then the run's environment, then the group it runs for, then `test.use()` */
|
|
9
|
+
export async function useLayers(test) {
|
|
11
10
|
let config;
|
|
12
11
|
let groups;
|
|
12
|
+
let environments;
|
|
13
13
|
try {
|
|
14
14
|
config = await readConfig("use");
|
|
15
15
|
groups = await readConfig("groups");
|
|
16
|
+
environments = await readConfig("environments");
|
|
16
17
|
}
|
|
17
18
|
catch (err) {
|
|
18
19
|
logger.debug("Could not read use config, using defaults", { err });
|
|
19
20
|
}
|
|
21
|
+
const environment = peekRunSettings()?.environment;
|
|
22
|
+
return {
|
|
23
|
+
config: config ?? {},
|
|
24
|
+
environment: (environment !== undefined &&
|
|
25
|
+
Object.prototype.hasOwnProperty.call(environments ?? {}, environment) &&
|
|
26
|
+
environments?.[environment]?.use) ||
|
|
27
|
+
{},
|
|
28
|
+
group: test.group === undefined ? {} : groupUseOf(groups, test.group),
|
|
29
|
+
tests: useOf(test.suite),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The `use` options one test runs with: config `use`, then the `use` of the run's environment, then the `use` of
|
|
34
|
+
* the group the test runs for (if any), then the `test.use()` calls around the test, outermost first, a later one
|
|
35
|
+
* winning key by key. The fixture keys (`fixtures`, `orderOptions`) are for the fixtures: see `effectiveFixtureUse`.
|
|
36
|
+
*/
|
|
37
|
+
export async function effectiveUse(test) {
|
|
38
|
+
const layers = await useLayers(test);
|
|
20
39
|
return mergeUse([
|
|
21
|
-
useOptionsOf(config),
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
useOptionsOf(layers.config),
|
|
41
|
+
omitFixtureUse(layers.environment),
|
|
42
|
+
omitFixtureUse(layers.group),
|
|
43
|
+
...layers.tests.map((layer) => omitFixtureUse(layer)),
|
|
24
44
|
]);
|
|
25
45
|
}
|
|
@@ -3,6 +3,7 @@ import logger from "../logger/index.js";
|
|
|
3
3
|
import { withProject } from "./project.js";
|
|
4
4
|
import TestReporter from "./reporter.js";
|
|
5
5
|
import { AllHooks } from "./all-hooks.js";
|
|
6
|
+
import { WorkerFixtures } from "./fixture-worker.js";
|
|
6
7
|
import { allHookFixtures } from "./fixtures.js";
|
|
7
8
|
import { runOneTest } from "./run-test.js";
|
|
8
9
|
import { runWithRetries } from "./retry.js";
|
|
@@ -37,6 +38,8 @@ export function openSession(init) {
|
|
|
37
38
|
timeout: run.settings.timeout,
|
|
38
39
|
fixtures: (browser) => allHookFixtures(run, browser, workerIndex),
|
|
39
40
|
});
|
|
41
|
+
// The worker-scoped fixtures this worker set up: torn down when it closes
|
|
42
|
+
const workerFixtures = new WorkerFixtures(run.settings.timeout);
|
|
40
43
|
resolve({
|
|
41
44
|
tests: loaded.all,
|
|
42
45
|
async runTest(test, browser, signal, onEvent, perTest = {}) {
|
|
@@ -63,12 +66,15 @@ export function openSession(init) {
|
|
|
63
66
|
profileDir: workerProfileDir(browser, workerIndex),
|
|
64
67
|
workerIndex,
|
|
65
68
|
allHooks,
|
|
69
|
+
workerFixtures,
|
|
66
70
|
attempt,
|
|
67
71
|
}), reporter);
|
|
68
72
|
},
|
|
69
73
|
async close() {
|
|
70
74
|
for (const error of await allHooks.closeAll())
|
|
71
75
|
logger.error("%s", error.message);
|
|
76
|
+
for (const error of await workerFixtures.closeAll())
|
|
77
|
+
logger.error("%s", error.message);
|
|
72
78
|
release();
|
|
73
79
|
await finished;
|
|
74
80
|
},
|
package/dist/steps/build.js
CHANGED
|
@@ -1,32 +1,56 @@
|
|
|
1
|
+
/** The RegExp or string a matcher stands for */
|
|
2
|
+
function textFromMatcher(matcher) {
|
|
3
|
+
return typeof matcher === "string"
|
|
4
|
+
? matcher
|
|
5
|
+
: new RegExp(matcher.regex.source, matcher.regex.flags);
|
|
6
|
+
}
|
|
7
|
+
/** `options` without the keys that are undefined, or undefined when nothing is left */
|
|
8
|
+
function defined(options) {
|
|
9
|
+
const entries = Object.entries(options).filter(([, v]) => v !== undefined);
|
|
10
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
11
|
+
}
|
|
12
|
+
function apply(target, call) {
|
|
13
|
+
switch (call.method) {
|
|
14
|
+
case "locator":
|
|
15
|
+
return target.locator(call.xpath);
|
|
16
|
+
case "getByCss":
|
|
17
|
+
return target.getByCss(call.css);
|
|
18
|
+
case "getByText":
|
|
19
|
+
case "getByLabel": {
|
|
20
|
+
const { exact, match, ignoreCase } = call;
|
|
21
|
+
return target[call.method](textFromMatcher(call.text), defined({ exact, match, ignoreCase }));
|
|
22
|
+
}
|
|
23
|
+
case "getByPlaceholder":
|
|
24
|
+
case "getByAltText":
|
|
25
|
+
case "getByTitle":
|
|
26
|
+
return target[call.method](textFromMatcher(call.text), defined({ exact: call.exact }));
|
|
27
|
+
case "getByRole": {
|
|
28
|
+
const { method, role, name, ...options } = call;
|
|
29
|
+
return target.getByRole(role, defined({
|
|
30
|
+
...options,
|
|
31
|
+
...(name !== undefined && { name: textFromMatcher(name) }),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
case "getByTestId":
|
|
35
|
+
return target.getByTestId(call.testId);
|
|
36
|
+
case "nth":
|
|
37
|
+
case "first":
|
|
38
|
+
case "last":
|
|
39
|
+
case "filter": {
|
|
40
|
+
if (!("nth" in target))
|
|
41
|
+
throw new Error(`${call.method} needs a locator to narrow`);
|
|
42
|
+
return call.method === "nth"
|
|
43
|
+
? target.nth(call.index)
|
|
44
|
+
: call.method === "filter"
|
|
45
|
+
? target.filter({ hasText: textFromMatcher(call.hasText) })
|
|
46
|
+
: target[call.method]();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
1
50
|
function fromCalls(page, calls) {
|
|
2
51
|
const [first, ...rest] = calls;
|
|
3
52
|
if (!first)
|
|
4
53
|
throw new Error("A locator needs at least one call");
|
|
5
|
-
const apply = (target, call) => {
|
|
6
|
-
const text = {
|
|
7
|
-
...(call.method !== "locator" &&
|
|
8
|
-
call.method !== "getByCss" &&
|
|
9
|
-
call.method !== "getByTestId" &&
|
|
10
|
-
call.method !== "getByRole" && {
|
|
11
|
-
...(call.match !== undefined && { match: call.match }),
|
|
12
|
-
...(call.ignoreCase !== undefined && { ignoreCase: call.ignoreCase }),
|
|
13
|
-
}),
|
|
14
|
-
};
|
|
15
|
-
switch (call.method) {
|
|
16
|
-
case "locator":
|
|
17
|
-
return target.locator(call.xpath);
|
|
18
|
-
case "getByCss":
|
|
19
|
-
return target.getByCss(call.css);
|
|
20
|
-
case "getByText":
|
|
21
|
-
return target.getByText(call.text, text);
|
|
22
|
-
case "getByLabel":
|
|
23
|
-
return target.getByLabel(call.text, text);
|
|
24
|
-
case "getByRole":
|
|
25
|
-
return target.getByRole(call.role, call.name === undefined ? undefined : { name: call.name });
|
|
26
|
-
case "getByTestId":
|
|
27
|
-
return target.getByTestId(call.testId);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
54
|
return rest.reduce((locator, call) => apply(locator, call), apply(page, first));
|
|
31
55
|
}
|
|
32
56
|
/** The locator a step's `LocatorSpec` stands for, with its fallbacks */
|