@quiesco/jest 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +26 -2
- package/dist/environment.d.ts +16 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +34 -0
- package/dist/environment.js.map +1 -0
- package/dist/events.d.ts +29 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +81 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/shared.d.ts +22 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +123 -0
- package/dist/shared.js.map +1 -0
- package/dist/teardown.d.ts +2 -0
- package/dist/teardown.d.ts.map +1 -0
- package/dist/teardown.js +9 -0
- package/dist/teardown.js.map +1 -0
- package/jest-preset.cjs +7 -0
- package/package.json +65 -3
- package/src/environment.ts +46 -0
- package/src/events.ts +90 -0
- package/src/index.ts +30 -0
- package/src/shared.ts +127 -0
- package/src/teardown.ts +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Quiesco contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @quiesco/jest
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Quiesco for Jest: a preset whose test environment runs one session for the run, resets the app before
|
|
4
|
+
every test and gives tests the global `quiesco`.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm i -D @quiesco/jest @quiesco/core jest
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
// jest.config.js
|
|
12
|
+
module.exports = { preset: '@quiesco/jest', maxWorkers: 1, testTimeout: 120_000 };
|
|
13
|
+
|
|
14
|
+
// e2e/todo.test.js
|
|
15
|
+
test('a new todo appears at the top @ios', async () => {
|
|
16
|
+
await quiesco.app.tap('add-todo');
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Platform tags go in test or `describe` titles. Run with `npx quiesco test --platform ios --runner jest`.
|
|
21
|
+
Details: [using Quiesco without Cucumber](https://shift-14cc7601.mintlify.site/api#using-quiesco-without-cucumber).
|
|
22
|
+
|
|
23
|
+
Part of [Quiesco](https://github.com/h2nology/quiesco): gray-box end-to-end testing for Expo apps on iOS, Android and the web. Setup: `npx quiesco init` ([quick start](https://github.com/h2nology/quiesco#quick-start-web-about-10-minutes)).
|
|
24
|
+
|
|
25
|
+
## License
|
|
26
|
+
|
|
27
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest test environment (`testEnvironment: '@quiesco/jest/environment'`, set by the preset): Node's
|
|
3
|
+
* environment plus the run's Quiesco session, exposed to tests as the global `quiesco`
|
|
4
|
+
* (`quiesco.app`, `quiesco.session`).
|
|
5
|
+
*/
|
|
6
|
+
import type { EnvironmentContext, JestEnvironmentConfig } from '@jest/environment';
|
|
7
|
+
import { TestEnvironment } from 'jest-environment-node';
|
|
8
|
+
import type { EventLike } from './events.js';
|
|
9
|
+
export default class QuiescoEnvironment extends TestEnvironment {
|
|
10
|
+
private readonly worker;
|
|
11
|
+
private lifecycle;
|
|
12
|
+
constructor(config: JestEnvironmentConfig, context: EnvironmentContext);
|
|
13
|
+
setup(): Promise<void>;
|
|
14
|
+
handleTestEvent(event: EventLike): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=environment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../src/environment.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAEnF,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAKxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,eAAe;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,SAAS,CAA0B;gBAE/B,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,kBAAkB;IAKvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAe/B,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CAMvD"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { TestEnvironment } from 'jest-environment-node';
|
|
2
|
+
import { handleEvent, jestWorker } from './events.js';
|
|
3
|
+
import { inWorkerProcess, registerChildren, startShared } from './shared.js';
|
|
4
|
+
export default class QuiescoEnvironment extends TestEnvironment {
|
|
5
|
+
worker;
|
|
6
|
+
lifecycle = null;
|
|
7
|
+
constructor(config, context) {
|
|
8
|
+
super(config, context);
|
|
9
|
+
this.worker = jestWorker(config.globalConfig.maxWorkers, process.env, inWorkerProcess());
|
|
10
|
+
}
|
|
11
|
+
async setup() {
|
|
12
|
+
await super.setup();
|
|
13
|
+
const lifecycle = await startShared(this.worker);
|
|
14
|
+
this.lifecycle = lifecycle;
|
|
15
|
+
this.global['quiesco'] = {
|
|
16
|
+
get app() {
|
|
17
|
+
return lifecycle.current.driver;
|
|
18
|
+
},
|
|
19
|
+
get session() {
|
|
20
|
+
return lifecycle.current;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Structural event type: jest-circus passes its own Event; only `name` and `test` are read.
|
|
25
|
+
async handleTestEvent(event) {
|
|
26
|
+
if (this.lifecycle === null)
|
|
27
|
+
return;
|
|
28
|
+
await handleEvent(this.lifecycle, event);
|
|
29
|
+
// Children started during the test (the iOS system UI runner, a recorder) join the registry.
|
|
30
|
+
if (event.name === 'test_done' && inWorkerProcess())
|
|
31
|
+
registerChildren();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=environment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.js","sourceRoot":"","sources":["../src/environment.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7E,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,eAAe;IAC5C,MAAM,CAAa;IAC5B,SAAS,GAAqB,IAAI,CAAC;IAE3C,YAAY,MAA6B,EAAE,OAA2B;QACpE,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE,eAAe,EAAE,CAAC,CAAC;IAC3F,CAAC;IAEQ,KAAK,CAAC,KAAK;QAClB,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;QACpB,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG;YACvB,IAAI,GAAG;gBACL,OAAO,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC;YAClC,CAAC;YACD,IAAI,OAAO;gBACT,OAAO,SAAS,CAAC,OAAO,CAAC;YAC3B,CAAC;SACF,CAAC;IACJ,CAAC;IAED,4FAA4F;IAC5F,KAAK,CAAC,eAAe,CAAC,KAAgB;QACpC,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI;YAAE,OAAO;QACpC,MAAM,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACzC,6FAA6F;QAC7F,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,eAAe,EAAE;YAAE,gBAAgB,EAAE,CAAC;IAC1E,CAAC;CACF"}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Lifecycle } from '@quiesco/core';
|
|
2
|
+
export interface BlockLike {
|
|
3
|
+
name: string;
|
|
4
|
+
parent?: BlockLike | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface TestLike extends BlockLike {
|
|
7
|
+
mode?: string | undefined;
|
|
8
|
+
errors: unknown[];
|
|
9
|
+
}
|
|
10
|
+
export interface EventLike {
|
|
11
|
+
name: string;
|
|
12
|
+
test?: TestLike;
|
|
13
|
+
}
|
|
14
|
+
/** Title tags by level, outermost describe first. */
|
|
15
|
+
export declare function tagLevelsOf(test: TestLike): string[][];
|
|
16
|
+
/** "describe > test": the name of the test's artifacts directory. */
|
|
17
|
+
export declare function fullName(test: TestLike): string;
|
|
18
|
+
/** Adds where the evidence is to a failed test's first error (Jest prints the message from `stack`). */
|
|
19
|
+
export declare function addEvidence(test: TestLike, files: readonly string[]): void;
|
|
20
|
+
export declare function handleEvent(lifecycle: Lifecycle, event: EventLike): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* The worker this process is (Phase 6.12): Jest numbers its worker processes JEST_WORKER_ID = 1…maxWorkers.
|
|
23
|
+
* In-band runs (also chosen by Jest itself for fast suites, findings 6.12) are worker 0.
|
|
24
|
+
*/
|
|
25
|
+
export declare function jestWorker(maxWorkers: number | undefined, env: NodeJS.ProcessEnv, inWorkerProcess: boolean): {
|
|
26
|
+
index: number;
|
|
27
|
+
count: number;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAUD,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,EAAE,EAAE,CAEtD;AAED,qEAAqE;AACrE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAI/C;AAED,wGAAwG;AACxG,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAO1E;AAED,wBAAsB,WAAW,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBvF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAK7I"}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Jest side of the lifecycle (Phase 6.10, findings 6.10 batch B), on jest-circus events — structural
|
|
3
|
+
* types only, so it can be unit-tested without Jest:
|
|
4
|
+
*
|
|
5
|
+
* - `test_start` (dispatched before Jest decides to skip): a test tagged for another platform gets
|
|
6
|
+
* `mode = 'skip'`.
|
|
7
|
+
* - `test_started` (before the beforeEach hooks): reset the app.
|
|
8
|
+
* - `test_done` (after the afterEach hooks): end the test; on failure the evidence files are added to the
|
|
9
|
+
* first error's message. An app error reported after a passed test fails it.
|
|
10
|
+
*/
|
|
11
|
+
import { titleTags } from '@quiesco/core';
|
|
12
|
+
const ROOT = 'ROOT_DESCRIBE_BLOCK';
|
|
13
|
+
function chain(test) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (let n = test; n !== undefined; n = n.parent)
|
|
16
|
+
if (n.name !== ROOT)
|
|
17
|
+
out.unshift(n);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
/** Title tags by level, outermost describe first. */
|
|
21
|
+
export function tagLevelsOf(test) {
|
|
22
|
+
return chain(test).map((n) => titleTags(n.name));
|
|
23
|
+
}
|
|
24
|
+
/** "describe > test": the name of the test's artifacts directory. */
|
|
25
|
+
export function fullName(test) {
|
|
26
|
+
return chain(test)
|
|
27
|
+
.map((n) => n.name)
|
|
28
|
+
.join(' > ');
|
|
29
|
+
}
|
|
30
|
+
/** Adds where the evidence is to a failed test's first error (Jest prints the message from `stack`). */
|
|
31
|
+
export function addEvidence(test, files) {
|
|
32
|
+
const first = test.errors[0];
|
|
33
|
+
const err = (Array.isArray(first) ? first[0] : first);
|
|
34
|
+
if (err === undefined || err === null || typeof err !== 'object' || files.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const note = `\n\nQuiesco evidence:\n${files.map((f) => ` ${f}`).join('\n')}`;
|
|
37
|
+
if (typeof err.message === 'string')
|
|
38
|
+
err.message += note;
|
|
39
|
+
if (typeof err.stack === 'string')
|
|
40
|
+
err.stack = err.stack.replace(/\n|$/, `${note}\n`);
|
|
41
|
+
}
|
|
42
|
+
export async function handleEvent(lifecycle, event) {
|
|
43
|
+
const test = event.test;
|
|
44
|
+
if (test === undefined || !lifecycle.running)
|
|
45
|
+
return;
|
|
46
|
+
if (event.name === 'test_start') {
|
|
47
|
+
if (!lifecycle.runsHere(tagLevelsOf(test)))
|
|
48
|
+
test.mode = 'skip';
|
|
49
|
+
}
|
|
50
|
+
else if (event.name === 'test_started') {
|
|
51
|
+
try {
|
|
52
|
+
await lifecycle.before(tagLevelsOf(test), fullName(test));
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
test.errors.push(e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (event.name === 'test_done') {
|
|
59
|
+
const failed = test.errors.length > 0;
|
|
60
|
+
try {
|
|
61
|
+
const { files } = await lifecycle.after({ tags: tagLevelsOf(test).flat(), failed });
|
|
62
|
+
if (failed)
|
|
63
|
+
addEvidence(test, files);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
test.errors.push(e);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The worker this process is (Phase 6.12): Jest numbers its worker processes JEST_WORKER_ID = 1…maxWorkers.
|
|
72
|
+
* In-band runs (also chosen by Jest itself for fast suites, findings 6.12) are worker 0.
|
|
73
|
+
*/
|
|
74
|
+
export function jestWorker(maxWorkers, env, inWorkerProcess) {
|
|
75
|
+
const count = maxWorkers !== undefined && maxWorkers > 1 ? maxWorkers : 1;
|
|
76
|
+
const id = Number.parseInt(env['JEST_WORKER_ID'] ?? '', 10);
|
|
77
|
+
if (!inWorkerProcess || count === 1 || !(id >= 1))
|
|
78
|
+
return { index: 0, count };
|
|
79
|
+
return { index: Math.min(id, count) - 1, count };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @quiesco/jest (Phase 6.10): Quiesco for Jest.
|
|
3
|
+
*
|
|
4
|
+
* // jest.config.js
|
|
5
|
+
* module.exports = { preset: '@quiesco/jest', maxWorkers: 2, testTimeout: 60_000 };
|
|
6
|
+
*
|
|
7
|
+
* // e2e/todo.test.js — `quiesco` is a global, like Jest's own `test` / `expect`
|
|
8
|
+
* test('a new todo appears at the top @ios', async () => {
|
|
9
|
+
* await quiesco.app.tap('add-todo');
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* One session (device + app) per worker process (one device each; `maxWorkers: 1` runs serially), a
|
|
13
|
+
* reset before every test, a test tagged for another
|
|
14
|
+
* platform (in its or a describe's title; the innermost wins) is skipped, and a failure's message names
|
|
15
|
+
* the evidence files. TypeScript: add `"types": ["@quiesco/jest"]` for the `quiesco` global.
|
|
16
|
+
*/
|
|
17
|
+
import type { Driver, Session } from '@quiesco/core';
|
|
18
|
+
export interface QuiescoGlobal {
|
|
19
|
+
readonly app: Driver;
|
|
20
|
+
readonly session: Session;
|
|
21
|
+
}
|
|
22
|
+
declare global {
|
|
23
|
+
var quiesco: QuiescoGlobal;
|
|
24
|
+
}
|
|
25
|
+
export { addEvidence, fullName, handleEvent, jestWorker, tagLevelsOf } from './events.js';
|
|
26
|
+
export { registryDir, sweepRegistry } from './shared.js';
|
|
27
|
+
export type { BlockLike, EventLike, TestLike } from './events.js';
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,OAAO,EAAE,aAAa,CAAC;CAC5B;AAED,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Lifecycle } from '@quiesco/core';
|
|
2
|
+
import type { WorkerInfo } from '@quiesco/core';
|
|
3
|
+
/** Where workers of the Jest run with main process `mainPid` register their child processes. */
|
|
4
|
+
export declare function registryDir(mainPid: number, tmp?: string): string;
|
|
5
|
+
/** Whether this is a Jest worker process (forked with IPC), not the main process running in band. */
|
|
6
|
+
export declare function inWorkerProcess(): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* jest-worker's "end" message to a worker process (CHILD_MESSAGE_END, jest-worker 29 and 30). On it the
|
|
9
|
+
* worker only stops listening and waits for its event loop to drain, which an open session never lets
|
|
10
|
+
* happen: about 0.5 s later Jest sends SIGTERM and reports "A worker process has failed to exit
|
|
11
|
+
* gracefully" (findings 6.12).
|
|
12
|
+
*/
|
|
13
|
+
export declare const JEST_CHILD_MESSAGE_END = 2;
|
|
14
|
+
/** Write this worker's live child pids where globalTeardown finds them. */
|
|
15
|
+
export declare function registerChildren(dir?: string): void;
|
|
16
|
+
/** Starts the session once; a start that failed fails every later test file at once (no new attempt). */
|
|
17
|
+
export declare function startShared(worker: WorkerInfo): Promise<Lifecycle>;
|
|
18
|
+
/** globalTeardown: close an in-band session, then kill what the workers' registries still list. */
|
|
19
|
+
export declare function disposeShared(dir?: string): Promise<number>;
|
|
20
|
+
/** Kill registered child pids that are still alive; returns how many were. Removes the registry. */
|
|
21
|
+
export declare function sweepRegistry(dir: string, kill?: (pid: number, signal?: NodeJS.Signals | 0) => void): number;
|
|
22
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,SAAS,EAA4C,MAAM,eAAe,CAAC;AACpF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKhD,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAAc,GAAG,MAAM,CAEtE;AAED,qGAAqG;AACrG,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAID;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AA4BxC,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,SAA4B,GAAG,IAAI,CAOtE;AAED,yGAAyG;AACzG,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAWlE;AAED,mGAAmG;AACnG,wBAAsB,aAAa,CAAC,GAAG,SAA2B,GAAG,OAAO,CAAC,MAAM,CAAC,CAMnF;AAED,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,IAAiC,GAAG,MAAM,CA0BxI"}
|
package/dist/shared.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process's one Lifecycle. The test environment module is loaded once per process (outside the test
|
|
3
|
+
* files' sandboxes), so this module state outlives each test file (findings 6.10 batch B).
|
|
4
|
+
*
|
|
5
|
+
* Parallel runs (Phase 6.12, findings 6.12 batch A): with maxWorkers > 1 every Jest worker process has its
|
|
6
|
+
* own session (its own device). Jest ends a worker with SIGTERM and kills it about 0.5 s later, so the
|
|
7
|
+
* session's async dispose never finishes there: the worker kills the session's child processes
|
|
8
|
+
* synchronously instead, and writes their pids to a registry file that globalTeardown (in the main
|
|
9
|
+
* process) sweeps as a backstop. In-band runs close the session from globalTeardown as before.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { Lifecycle, killChildProcessesSync, trackedChildPids } from '@quiesco/core';
|
|
15
|
+
let lifecycle = null;
|
|
16
|
+
let starting = null;
|
|
17
|
+
/** Where workers of the Jest run with main process `mainPid` register their child processes. */
|
|
18
|
+
export function registryDir(mainPid, tmp = os.tmpdir()) {
|
|
19
|
+
return path.join(tmp, `quiesco-jest-${mainPid}`);
|
|
20
|
+
}
|
|
21
|
+
/** Whether this is a Jest worker process (forked with IPC), not the main process running in band. */
|
|
22
|
+
export function inWorkerProcess() {
|
|
23
|
+
return typeof process.send === 'function' && process.env['JEST_WORKER_ID'] !== undefined;
|
|
24
|
+
}
|
|
25
|
+
let exitHooked = false;
|
|
26
|
+
/**
|
|
27
|
+
* jest-worker's "end" message to a worker process (CHILD_MESSAGE_END, jest-worker 29 and 30). On it the
|
|
28
|
+
* worker only stops listening and waits for its event loop to drain, which an open session never lets
|
|
29
|
+
* happen: about 0.5 s later Jest sends SIGTERM and reports "A worker process has failed to exit
|
|
30
|
+
* gracefully" (findings 6.12).
|
|
31
|
+
*/
|
|
32
|
+
export const JEST_CHILD_MESSAGE_END = 2;
|
|
33
|
+
/** How long a worker's session may take to close before the worker exits anyway (Jest kills at ~0.5 s). */
|
|
34
|
+
const END_BUDGET_MS = 400;
|
|
35
|
+
/** In a worker process: close the session when Jest ends the worker, and never leave its children behind. */
|
|
36
|
+
function hookWorkerExit() {
|
|
37
|
+
if (exitHooked)
|
|
38
|
+
return;
|
|
39
|
+
exitHooked = true;
|
|
40
|
+
process.on('message', (m) => {
|
|
41
|
+
if (Array.isArray(m) && m[0] === JEST_CHILD_MESSAGE_END)
|
|
42
|
+
void endWorker();
|
|
43
|
+
});
|
|
44
|
+
process.once('SIGTERM', () => {
|
|
45
|
+
killChildProcessesSync();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
process.once('exit', () => killChildProcessesSync());
|
|
49
|
+
}
|
|
50
|
+
/** Close the session within the budget, then end the children left and the process. */
|
|
51
|
+
async function endWorker() {
|
|
52
|
+
const l = lifecycle;
|
|
53
|
+
lifecycle = null;
|
|
54
|
+
await Promise.race([l?.afterAll().catch(() => undefined), new Promise((r) => setTimeout(r, END_BUDGET_MS).unref())]);
|
|
55
|
+
killChildProcessesSync();
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
/** Write this worker's live child pids where globalTeardown finds them. */
|
|
59
|
+
export function registerChildren(dir = registryDir(process.ppid)) {
|
|
60
|
+
try {
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
fs.writeFileSync(path.join(dir, `${process.pid}.json`), JSON.stringify(trackedChildPids()));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// best effort: the synchronous kill on SIGTERM is the main mechanism
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Starts the session once; a start that failed fails every later test file at once (no new attempt). */
|
|
69
|
+
export function startShared(worker) {
|
|
70
|
+
starting ??= (async () => {
|
|
71
|
+
const worked = inWorkerProcess();
|
|
72
|
+
if (worked)
|
|
73
|
+
hookWorkerExit();
|
|
74
|
+
const l = new Lifecycle({ worker });
|
|
75
|
+
await l.beforeAll();
|
|
76
|
+
lifecycle = l;
|
|
77
|
+
if (worked)
|
|
78
|
+
registerChildren();
|
|
79
|
+
return l;
|
|
80
|
+
})();
|
|
81
|
+
return starting;
|
|
82
|
+
}
|
|
83
|
+
/** globalTeardown: close an in-band session, then kill what the workers' registries still list. */
|
|
84
|
+
export async function disposeShared(dir = registryDir(process.pid)) {
|
|
85
|
+
const l = lifecycle;
|
|
86
|
+
lifecycle = null;
|
|
87
|
+
starting = null;
|
|
88
|
+
await l?.afterAll();
|
|
89
|
+
return sweepRegistry(dir);
|
|
90
|
+
}
|
|
91
|
+
/** Kill registered child pids that are still alive; returns how many were. Removes the registry. */
|
|
92
|
+
export function sweepRegistry(dir, kill = process.kill.bind(process)) {
|
|
93
|
+
const read = (f, fallback) => {
|
|
94
|
+
try {
|
|
95
|
+
return f();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return fallback;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const files = read(() => fs.readdirSync(dir).filter((f) => f.endsWith('.json')), null);
|
|
102
|
+
if (files === null)
|
|
103
|
+
return 0;
|
|
104
|
+
let n = 0;
|
|
105
|
+
for (const f of files) {
|
|
106
|
+
const pids = read(() => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')), []);
|
|
107
|
+
for (const pid of Array.isArray(pids) ? pids : []) {
|
|
108
|
+
if (typeof pid !== 'number')
|
|
109
|
+
continue;
|
|
110
|
+
try {
|
|
111
|
+
kill(pid, 0);
|
|
112
|
+
kill(pid, 'SIGKILL');
|
|
113
|
+
n++;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// already gone
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
return n;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teardown.d.ts","sourceRoot":"","sources":["../src/teardown.ts"],"names":[],"mappings":"AAMA,wBAA8B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAEtD"}
|
package/dist/teardown.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest globalTeardown (set by the preset): closes an in-band run's session (device per config), and kills
|
|
3
|
+
* what parallel workers' sessions left running (Phase 6.12).
|
|
4
|
+
*/
|
|
5
|
+
import { disposeShared } from './shared.js';
|
|
6
|
+
export default async function teardown() {
|
|
7
|
+
await disposeShared();
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=teardown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teardown.js","sourceRoot":"","sources":["../src/teardown.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,QAAQ;IACpC,MAAM,aAAa,EAAE,CAAC;AACxB,CAAC"}
|
package/jest-preset.cjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// `preset: '@quiesco/jest'`: the Quiesco test environment and the teardown that closes the session.
|
|
2
|
+
// Set `maxWorkers` (1 = serial; N = N devices in parallel) and a `testTimeout` in your own config (Jest
|
|
3
|
+
// presets cannot set them).
|
|
4
|
+
module.exports = {
|
|
5
|
+
testEnvironment: require.resolve('./dist/environment.js'),
|
|
6
|
+
globalTeardown: require.resolve('./dist/teardown.js'),
|
|
7
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quiesco/jest",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Quiesco for Jest: a preset whose test environment runs one session for the run and gives tests `quiesco.app`",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "h2nology",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"quiesco",
|
|
9
|
+
"e2e",
|
|
10
|
+
"testing",
|
|
11
|
+
"expo",
|
|
12
|
+
"react-native",
|
|
13
|
+
"jest"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/h2nology/quiesco/tree/main/packages/jest#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/h2nology/quiesco.git",
|
|
19
|
+
"directory": "packages/jest"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/h2nology/quiesco/issues"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./environment": {
|
|
39
|
+
"types": "./dist/environment.d.ts",
|
|
40
|
+
"import": "./dist/environment.js"
|
|
41
|
+
},
|
|
42
|
+
"./teardown": {
|
|
43
|
+
"types": "./dist/teardown.d.ts",
|
|
44
|
+
"import": "./dist/teardown.js"
|
|
45
|
+
},
|
|
46
|
+
"./jest-preset": "./jest-preset.cjs"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist",
|
|
50
|
+
"src",
|
|
51
|
+
"jest-preset.cjs"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@quiesco/core": "0.1.1",
|
|
55
|
+
"jest-environment-node": "30.5.2"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@jest/environment": "30.5.2",
|
|
59
|
+
"@jest/types": "30.5.1"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"jest": ">=30"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsc -b",
|
|
66
|
+
"test": "vitest run"
|
|
67
|
+
}
|
|
6
68
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest test environment (`testEnvironment: '@quiesco/jest/environment'`, set by the preset): Node's
|
|
3
|
+
* environment plus the run's Quiesco session, exposed to tests as the global `quiesco`
|
|
4
|
+
* (`quiesco.app`, `quiesco.session`).
|
|
5
|
+
*/
|
|
6
|
+
import type { EnvironmentContext, JestEnvironmentConfig } from '@jest/environment';
|
|
7
|
+
import type { Lifecycle } from '@quiesco/core';
|
|
8
|
+
import { TestEnvironment } from 'jest-environment-node';
|
|
9
|
+
|
|
10
|
+
import type { WorkerInfo } from '@quiesco/core';
|
|
11
|
+
|
|
12
|
+
import { handleEvent, jestWorker } from './events.js';
|
|
13
|
+
import type { EventLike } from './events.js';
|
|
14
|
+
import { inWorkerProcess, registerChildren, startShared } from './shared.js';
|
|
15
|
+
|
|
16
|
+
export default class QuiescoEnvironment extends TestEnvironment {
|
|
17
|
+
private readonly worker: WorkerInfo;
|
|
18
|
+
private lifecycle: Lifecycle | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {
|
|
21
|
+
super(config, context);
|
|
22
|
+
this.worker = jestWorker(config.globalConfig.maxWorkers, process.env, inWorkerProcess());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override async setup(): Promise<void> {
|
|
26
|
+
await super.setup();
|
|
27
|
+
const lifecycle = await startShared(this.worker);
|
|
28
|
+
this.lifecycle = lifecycle;
|
|
29
|
+
this.global['quiesco'] = {
|
|
30
|
+
get app() {
|
|
31
|
+
return lifecycle.current.driver;
|
|
32
|
+
},
|
|
33
|
+
get session() {
|
|
34
|
+
return lifecycle.current;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Structural event type: jest-circus passes its own Event; only `name` and `test` are read.
|
|
40
|
+
async handleTestEvent(event: EventLike): Promise<void> {
|
|
41
|
+
if (this.lifecycle === null) return;
|
|
42
|
+
await handleEvent(this.lifecycle, event);
|
|
43
|
+
// Children started during the test (the iOS system UI runner, a recorder) join the registry.
|
|
44
|
+
if (event.name === 'test_done' && inWorkerProcess()) registerChildren();
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Jest side of the lifecycle (Phase 6.10, findings 6.10 batch B), on jest-circus events — structural
|
|
3
|
+
* types only, so it can be unit-tested without Jest:
|
|
4
|
+
*
|
|
5
|
+
* - `test_start` (dispatched before Jest decides to skip): a test tagged for another platform gets
|
|
6
|
+
* `mode = 'skip'`.
|
|
7
|
+
* - `test_started` (before the beforeEach hooks): reset the app.
|
|
8
|
+
* - `test_done` (after the afterEach hooks): end the test; on failure the evidence files are added to the
|
|
9
|
+
* first error's message. An app error reported after a passed test fails it.
|
|
10
|
+
*/
|
|
11
|
+
import { titleTags } from '@quiesco/core';
|
|
12
|
+
import type { Lifecycle } from '@quiesco/core';
|
|
13
|
+
|
|
14
|
+
export interface BlockLike {
|
|
15
|
+
name: string;
|
|
16
|
+
parent?: BlockLike | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TestLike extends BlockLike {
|
|
20
|
+
mode?: string | undefined;
|
|
21
|
+
errors: unknown[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EventLike {
|
|
25
|
+
name: string;
|
|
26
|
+
test?: TestLike;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ROOT = 'ROOT_DESCRIBE_BLOCK';
|
|
30
|
+
|
|
31
|
+
function chain(test: TestLike): BlockLike[] {
|
|
32
|
+
const out: BlockLike[] = [];
|
|
33
|
+
for (let n: BlockLike | undefined = test; n !== undefined; n = n.parent) if (n.name !== ROOT) out.unshift(n);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Title tags by level, outermost describe first. */
|
|
38
|
+
export function tagLevelsOf(test: TestLike): string[][] {
|
|
39
|
+
return chain(test).map((n) => titleTags(n.name));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** "describe > test": the name of the test's artifacts directory. */
|
|
43
|
+
export function fullName(test: TestLike): string {
|
|
44
|
+
return chain(test)
|
|
45
|
+
.map((n) => n.name)
|
|
46
|
+
.join(' > ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Adds where the evidence is to a failed test's first error (Jest prints the message from `stack`). */
|
|
50
|
+
export function addEvidence(test: TestLike, files: readonly string[]): void {
|
|
51
|
+
const first = test.errors[0];
|
|
52
|
+
const err = (Array.isArray(first) ? first[0] : first) as { message?: unknown; stack?: unknown } | undefined;
|
|
53
|
+
if (err === undefined || err === null || typeof err !== 'object' || files.length === 0) return;
|
|
54
|
+
const note = `\n\nQuiesco evidence:\n${files.map((f) => ` ${f}`).join('\n')}`;
|
|
55
|
+
if (typeof err.message === 'string') err.message += note;
|
|
56
|
+
if (typeof err.stack === 'string') err.stack = err.stack.replace(/\n|$/, `${note}\n`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function handleEvent(lifecycle: Lifecycle, event: EventLike): Promise<void> {
|
|
60
|
+
const test = event.test;
|
|
61
|
+
if (test === undefined || !lifecycle.running) return;
|
|
62
|
+
if (event.name === 'test_start') {
|
|
63
|
+
if (!lifecycle.runsHere(tagLevelsOf(test))) test.mode = 'skip';
|
|
64
|
+
} else if (event.name === 'test_started') {
|
|
65
|
+
try {
|
|
66
|
+
await lifecycle.before(tagLevelsOf(test), fullName(test));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
test.errors.push(e);
|
|
69
|
+
}
|
|
70
|
+
} else if (event.name === 'test_done') {
|
|
71
|
+
const failed = test.errors.length > 0;
|
|
72
|
+
try {
|
|
73
|
+
const { files } = await lifecycle.after({ tags: tagLevelsOf(test).flat(), failed });
|
|
74
|
+
if (failed) addEvidence(test, files);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
test.errors.push(e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The worker this process is (Phase 6.12): Jest numbers its worker processes JEST_WORKER_ID = 1…maxWorkers.
|
|
83
|
+
* In-band runs (also chosen by Jest itself for fast suites, findings 6.12) are worker 0.
|
|
84
|
+
*/
|
|
85
|
+
export function jestWorker(maxWorkers: number | undefined, env: NodeJS.ProcessEnv, inWorkerProcess: boolean): { index: number; count: number } {
|
|
86
|
+
const count = maxWorkers !== undefined && maxWorkers > 1 ? maxWorkers : 1;
|
|
87
|
+
const id = Number.parseInt(env['JEST_WORKER_ID'] ?? '', 10);
|
|
88
|
+
if (!inWorkerProcess || count === 1 || !(id >= 1)) return { index: 0, count };
|
|
89
|
+
return { index: Math.min(id, count) - 1, count };
|
|
90
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @quiesco/jest (Phase 6.10): Quiesco for Jest.
|
|
3
|
+
*
|
|
4
|
+
* // jest.config.js
|
|
5
|
+
* module.exports = { preset: '@quiesco/jest', maxWorkers: 2, testTimeout: 60_000 };
|
|
6
|
+
*
|
|
7
|
+
* // e2e/todo.test.js — `quiesco` is a global, like Jest's own `test` / `expect`
|
|
8
|
+
* test('a new todo appears at the top @ios', async () => {
|
|
9
|
+
* await quiesco.app.tap('add-todo');
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* One session (device + app) per worker process (one device each; `maxWorkers: 1` runs serially), a
|
|
13
|
+
* reset before every test, a test tagged for another
|
|
14
|
+
* platform (in its or a describe's title; the innermost wins) is skipped, and a failure's message names
|
|
15
|
+
* the evidence files. TypeScript: add `"types": ["@quiesco/jest"]` for the `quiesco` global.
|
|
16
|
+
*/
|
|
17
|
+
import type { Driver, Session } from '@quiesco/core';
|
|
18
|
+
|
|
19
|
+
export interface QuiescoGlobal {
|
|
20
|
+
readonly app: Driver;
|
|
21
|
+
readonly session: Session;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare global {
|
|
25
|
+
var quiesco: QuiescoGlobal;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { addEvidence, fullName, handleEvent, jestWorker, tagLevelsOf } from './events.js';
|
|
29
|
+
export { registryDir, sweepRegistry } from './shared.js';
|
|
30
|
+
export type { BlockLike, EventLike, TestLike } from './events.js';
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process's one Lifecycle. The test environment module is loaded once per process (outside the test
|
|
3
|
+
* files' sandboxes), so this module state outlives each test file (findings 6.10 batch B).
|
|
4
|
+
*
|
|
5
|
+
* Parallel runs (Phase 6.12, findings 6.12 batch A): with maxWorkers > 1 every Jest worker process has its
|
|
6
|
+
* own session (its own device). Jest ends a worker with SIGTERM and kills it about 0.5 s later, so the
|
|
7
|
+
* session's async dispose never finishes there: the worker kills the session's child processes
|
|
8
|
+
* synchronously instead, and writes their pids to a registry file that globalTeardown (in the main
|
|
9
|
+
* process) sweeps as a backstop. In-band runs close the session from globalTeardown as before.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { Lifecycle, killChildProcessesSync, trackedChildPids } from '@quiesco/core';
|
|
16
|
+
import type { WorkerInfo } from '@quiesco/core';
|
|
17
|
+
|
|
18
|
+
let lifecycle: Lifecycle | null = null;
|
|
19
|
+
let starting: Promise<Lifecycle> | null = null;
|
|
20
|
+
|
|
21
|
+
/** Where workers of the Jest run with main process `mainPid` register their child processes. */
|
|
22
|
+
export function registryDir(mainPid: number, tmp = os.tmpdir()): string {
|
|
23
|
+
return path.join(tmp, `quiesco-jest-${mainPid}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether this is a Jest worker process (forked with IPC), not the main process running in band. */
|
|
27
|
+
export function inWorkerProcess(): boolean {
|
|
28
|
+
return typeof process.send === 'function' && process.env['JEST_WORKER_ID'] !== undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let exitHooked = false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* jest-worker's "end" message to a worker process (CHILD_MESSAGE_END, jest-worker 29 and 30). On it the
|
|
35
|
+
* worker only stops listening and waits for its event loop to drain, which an open session never lets
|
|
36
|
+
* happen: about 0.5 s later Jest sends SIGTERM and reports "A worker process has failed to exit
|
|
37
|
+
* gracefully" (findings 6.12).
|
|
38
|
+
*/
|
|
39
|
+
export const JEST_CHILD_MESSAGE_END = 2;
|
|
40
|
+
|
|
41
|
+
/** How long a worker's session may take to close before the worker exits anyway (Jest kills at ~0.5 s). */
|
|
42
|
+
const END_BUDGET_MS = 400;
|
|
43
|
+
|
|
44
|
+
/** In a worker process: close the session when Jest ends the worker, and never leave its children behind. */
|
|
45
|
+
function hookWorkerExit(): void {
|
|
46
|
+
if (exitHooked) return;
|
|
47
|
+
exitHooked = true;
|
|
48
|
+
process.on('message', (m: unknown) => {
|
|
49
|
+
if (Array.isArray(m) && m[0] === JEST_CHILD_MESSAGE_END) void endWorker();
|
|
50
|
+
});
|
|
51
|
+
process.once('SIGTERM', () => {
|
|
52
|
+
killChildProcessesSync();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
});
|
|
55
|
+
process.once('exit', () => killChildProcessesSync());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Close the session within the budget, then end the children left and the process. */
|
|
59
|
+
async function endWorker(): Promise<void> {
|
|
60
|
+
const l = lifecycle;
|
|
61
|
+
lifecycle = null;
|
|
62
|
+
await Promise.race([l?.afterAll().catch(() => undefined), new Promise((r) => setTimeout(r, END_BUDGET_MS).unref())]);
|
|
63
|
+
killChildProcessesSync();
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Write this worker's live child pids where globalTeardown finds them. */
|
|
68
|
+
export function registerChildren(dir = registryDir(process.ppid)): void {
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
+
fs.writeFileSync(path.join(dir, `${process.pid}.json`), JSON.stringify(trackedChildPids()));
|
|
72
|
+
} catch {
|
|
73
|
+
// best effort: the synchronous kill on SIGTERM is the main mechanism
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Starts the session once; a start that failed fails every later test file at once (no new attempt). */
|
|
78
|
+
export function startShared(worker: WorkerInfo): Promise<Lifecycle> {
|
|
79
|
+
starting ??= (async () => {
|
|
80
|
+
const worked = inWorkerProcess();
|
|
81
|
+
if (worked) hookWorkerExit();
|
|
82
|
+
const l = new Lifecycle({ worker });
|
|
83
|
+
await l.beforeAll();
|
|
84
|
+
lifecycle = l;
|
|
85
|
+
if (worked) registerChildren();
|
|
86
|
+
return l;
|
|
87
|
+
})();
|
|
88
|
+
return starting;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** globalTeardown: close an in-band session, then kill what the workers' registries still list. */
|
|
92
|
+
export async function disposeShared(dir = registryDir(process.pid)): Promise<number> {
|
|
93
|
+
const l = lifecycle;
|
|
94
|
+
lifecycle = null;
|
|
95
|
+
starting = null;
|
|
96
|
+
await l?.afterAll();
|
|
97
|
+
return sweepRegistry(dir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Kill registered child pids that are still alive; returns how many were. Removes the registry. */
|
|
101
|
+
export function sweepRegistry(dir: string, kill: (pid: number, signal?: NodeJS.Signals | 0) => void = process.kill.bind(process)): number {
|
|
102
|
+
const read = <T>(f: () => T, fallback: T): T => {
|
|
103
|
+
try {
|
|
104
|
+
return f();
|
|
105
|
+
} catch {
|
|
106
|
+
return fallback;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const files = read(() => fs.readdirSync(dir).filter((f) => f.endsWith('.json')), null);
|
|
110
|
+
if (files === null) return 0;
|
|
111
|
+
let n = 0;
|
|
112
|
+
for (const f of files) {
|
|
113
|
+
const pids: unknown = read(() => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')) as unknown, []);
|
|
114
|
+
for (const pid of Array.isArray(pids) ? pids : []) {
|
|
115
|
+
if (typeof pid !== 'number') continue;
|
|
116
|
+
try {
|
|
117
|
+
kill(pid, 0);
|
|
118
|
+
kill(pid, 'SIGKILL');
|
|
119
|
+
n++;
|
|
120
|
+
} catch {
|
|
121
|
+
// already gone
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
126
|
+
return n;
|
|
127
|
+
}
|
package/src/teardown.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest globalTeardown (set by the preset): closes an in-band run's session (device per config), and kills
|
|
3
|
+
* what parallel workers' sessions left running (Phase 6.12).
|
|
4
|
+
*/
|
|
5
|
+
import { disposeShared } from './shared.js';
|
|
6
|
+
|
|
7
|
+
export default async function teardown(): Promise<void> {
|
|
8
|
+
await disposeShared();
|
|
9
|
+
}
|