@c9up/helix-plugin-ream 0.1.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/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/runTests.d.ts +93 -0
- package/dist/runTests.d.ts.map +1 -0
- package/dist/runTests.js +222 -0
- package/dist/runTests.js.map +1 -0
- package/package.json +55 -0
- package/src/index.ts +66 -0
- package/src/runTests.ts +294 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 C9up
|
|
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
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @c9up/helix-plugin-ream
|
|
2
|
+
|
|
3
|
+
The bridge between [Ream](https://github.com/C9up/ream) and [helix](https://github.com/C9up/helix), its test runner.
|
|
4
|
+
|
|
5
|
+
Ream knows nothing about helix, helix knows nothing about Ream. The plugin that joins them lives here and declares both sides as peers — the same shape as `@japa/plugin-adonisjs`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add -D @c9up/helix-plugin-ream @c9up/helix
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Inject a test client
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// tests/bootstrap.ts
|
|
17
|
+
import { configure } from '@c9up/helix'
|
|
18
|
+
import { apiClient } from '@c9up/helix-plugin-ream'
|
|
19
|
+
|
|
20
|
+
await configure({ plugins: [apiClient({ boot: () => bootApp() })] })
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
test('health', async ({ client }) => {
|
|
25
|
+
await client.get('/health').assertOk()
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The server boots once at `configure()` time, is shared across the run, and closes through `api.cleanup` when the run ends.
|
|
30
|
+
|
|
31
|
+
## Run the suites declared in the rc file
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { runTestsFromRcFile } from '@c9up/helix-plugin-ream/runner'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This is what `ream test` calls.
|
|
38
|
+
|
|
39
|
+
## What stays in Ream
|
|
40
|
+
|
|
41
|
+
`TestClient`, `createTestClient`, `RequestBuilder` and `ApiResponse` live in `@c9up/ream/testing`. They drive a Ream server over HTTP and owe nothing to the runner, so they need no plugin.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@c9up/helix-plugin-ream` — the bridge between Ream and its test runner.
|
|
3
|
+
*
|
|
4
|
+
* Ream knows nothing about helix, helix knows nothing about Ream: the plugin
|
|
5
|
+
* that joins them lives here, and declares both sides as peers. The HTTP test
|
|
6
|
+
* client itself (`TestClient`, `createTestClient`) stays in `@c9up/ream/testing`
|
|
7
|
+
* — it drives a Ream server and owes nothing to the runner.
|
|
8
|
+
*
|
|
9
|
+
* // tests/bootstrap.ts
|
|
10
|
+
* import { configure } from '@c9up/helix'
|
|
11
|
+
* import { apiClient } from '@c9up/helix-plugin-ream'
|
|
12
|
+
* await configure({ plugins: [apiClient({ boot: () => bootApp() })] })
|
|
13
|
+
*
|
|
14
|
+
* // a test
|
|
15
|
+
* test('health', async ({ client }) => {
|
|
16
|
+
* await client.get('/health').assertOk()
|
|
17
|
+
* })
|
|
18
|
+
*/
|
|
19
|
+
import type { PluginApi } from "@c9up/helix";
|
|
20
|
+
import type { AuthStrategy, RouteManifest } from "@c9up/ream/testing";
|
|
21
|
+
import { TestClient } from "@c9up/ream/testing";
|
|
22
|
+
/** The slice of helix's `PluginApi` this plugin actually uses. */
|
|
23
|
+
export type ClientHost = Pick<PluginApi, "context" | "cleanup">;
|
|
24
|
+
export interface ApiClientConfig {
|
|
25
|
+
/** Boot the app under test on the given port; return the port + a close fn. */
|
|
26
|
+
boot: (port: number) => Promise<{
|
|
27
|
+
port: number;
|
|
28
|
+
close: () => Promise<void> | void;
|
|
29
|
+
}>;
|
|
30
|
+
/** Warden auth strategy for `client.withAuth()`/`asUser()`. */
|
|
31
|
+
auth?: AuthStrategy;
|
|
32
|
+
/** Named-route manifest (`router.namedManifest()`) for `client.visit()`. */
|
|
33
|
+
routes?: RouteManifest;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Injects a booted {@link TestClient} on the test context as `ctx.client`.
|
|
37
|
+
*
|
|
38
|
+
* The server is booted once at `configure()` time, shared across the run, and
|
|
39
|
+
* closed via `api.cleanup` after the run finishes — a proper lifecycle, with no
|
|
40
|
+
* reliance on process exit.
|
|
41
|
+
*/
|
|
42
|
+
export declare function apiClient(config: ApiClientConfig): (api: ClientHost) => Promise<void>;
|
|
43
|
+
declare module "@c9up/helix" {
|
|
44
|
+
interface TestContext {
|
|
45
|
+
client: TestClient;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAU,SAAS,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,kEAAkE;AAClE,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC/B,+EAA+E;IAC/E,IAAI,EAAE,CACL,IAAI,EAAE,MAAM,KACR,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAClE,+DAA+D;IAC/D,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,aAAa,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,SACrB,UAAU,KAAG,OAAO,CAAC,IAAI,CAAC,CAarD;AAGD,OAAO,QAAQ,aAAa,CAAC;IAC5B,UAAU,WAAW;QACpB,MAAM,EAAE,UAAU,CAAC;KACnB;CACD"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@c9up/helix-plugin-ream` — the bridge between Ream and its test runner.
|
|
3
|
+
*
|
|
4
|
+
* Ream knows nothing about helix, helix knows nothing about Ream: the plugin
|
|
5
|
+
* that joins them lives here, and declares both sides as peers. The HTTP test
|
|
6
|
+
* client itself (`TestClient`, `createTestClient`) stays in `@c9up/ream/testing`
|
|
7
|
+
* — it drives a Ream server and owes nothing to the runner.
|
|
8
|
+
*
|
|
9
|
+
* // tests/bootstrap.ts
|
|
10
|
+
* import { configure } from '@c9up/helix'
|
|
11
|
+
* import { apiClient } from '@c9up/helix-plugin-ream'
|
|
12
|
+
* await configure({ plugins: [apiClient({ boot: () => bootApp() })] })
|
|
13
|
+
*
|
|
14
|
+
* // a test
|
|
15
|
+
* test('health', async ({ client }) => {
|
|
16
|
+
* await client.get('/health').assertOk()
|
|
17
|
+
* })
|
|
18
|
+
*/
|
|
19
|
+
import { TestClient } from "@c9up/ream/testing";
|
|
20
|
+
/**
|
|
21
|
+
* Injects a booted {@link TestClient} on the test context as `ctx.client`.
|
|
22
|
+
*
|
|
23
|
+
* The server is booted once at `configure()` time, shared across the run, and
|
|
24
|
+
* closed via `api.cleanup` after the run finishes — a proper lifecycle, with no
|
|
25
|
+
* reliance on process exit.
|
|
26
|
+
*/
|
|
27
|
+
export function apiClient(config) {
|
|
28
|
+
const plugin = async (api) => {
|
|
29
|
+
const client = new TestClient(config.boot, {
|
|
30
|
+
auth: config.auth,
|
|
31
|
+
routes: config.routes,
|
|
32
|
+
});
|
|
33
|
+
await client.boot();
|
|
34
|
+
api.context.macro("client", client);
|
|
35
|
+
api.cleanup(() => client.close());
|
|
36
|
+
};
|
|
37
|
+
// A wider parameter than `PluginApi` stays assignable to `Plugin`, so the
|
|
38
|
+
// plugin declares exactly what it touches and a caller can drive it with
|
|
39
|
+
// nothing more than that.
|
|
40
|
+
return plugin;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAgBhD;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,MAAuB;IAChD,MAAM,MAAM,GAAG,KAAK,EAAE,GAAe,EAAiB,EAAE;QACvD,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE;YAC1C,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,MAAM,EAAE,MAAM,CAAC,MAAM;SACrB,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACpC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IACnC,CAAC,CAAC;IACF,0EAA0E;IAC1E,yEAAyE;IACzE,0BAA0B;IAC1B,OAAO,MAAuB,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream test` — run the suites declared in the rc file.
|
|
3
|
+
*
|
|
4
|
+
* The AdonisJS stratification, kept intact: the FRAMEWORK reads its rc file and
|
|
5
|
+
* hands the suites to the runner, exactly as `@adonisjs/core` reads
|
|
6
|
+
* `adonisrc.ts` and hands them to Japa. The runner itself (helix) knows nothing
|
|
7
|
+
* about ream, and ream owns no test execution — it only translates.
|
|
8
|
+
*
|
|
9
|
+
* // bin/test.ts
|
|
10
|
+
* import { runTestsFromRcFile } from '@c9up/helix-plugin-ream/runner'
|
|
11
|
+
* process.exitCode = await runTestsFromRcFile('./reamrc.ts', {
|
|
12
|
+
* suites: process.argv.slice(2),
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* Prefer that over `runTests(rc.tests, …)`: a `suites[].configure` callback has
|
|
16
|
+
* to be re-imported in each worker, so the runner needs the module's PATH, not
|
|
17
|
+
* just the object it exported. `runTests` takes it as `configModule`, and
|
|
18
|
+
* REFUSES to run when a suite declares a callback it was given no way to
|
|
19
|
+
* deliver — in Japa a declared `configure` runs, so a run that skipped it would
|
|
20
|
+
* be green in a state its own config does not describe.
|
|
21
|
+
*/
|
|
22
|
+
import type { TestsConfig } from "@c9up/ream";
|
|
23
|
+
/** What a caller may override on top of the rc file. */
|
|
24
|
+
export interface RunTestsOptions {
|
|
25
|
+
/** Project root the suites resolve against. Defaults to `process.cwd()`. */
|
|
26
|
+
root?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Suite names to run. Empty (the default) runs every declared suite, in
|
|
29
|
+
* order — the AdonisJS behaviour for `ream test` with no argument.
|
|
30
|
+
*/
|
|
31
|
+
suites?: string[];
|
|
32
|
+
/** Concurrent worker processes. */
|
|
33
|
+
threads?: number;
|
|
34
|
+
/** Reporter name, or several (`["spec", "json"]`). */
|
|
35
|
+
reporters?: string[];
|
|
36
|
+
/** Stop at the first failure. */
|
|
37
|
+
bail?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The module that declared the suites, so a `suites[].configure` callback can
|
|
40
|
+
* be re-imported in each worker — a function does not cross a process
|
|
41
|
+
* boundary. Set for you by {@link runTestsFromRcFile}.
|
|
42
|
+
*/
|
|
43
|
+
configModule?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Flags the worker processes are spawned with. Defaults to this process's own
|
|
46
|
+
* (`process.execArgv`), so the workers load TypeScript through whatever
|
|
47
|
+
* loader the parent was started with — `--import @swc-node/register/esm-register`
|
|
48
|
+
* for `ream test`, tsx for a project that prefers it. Nothing to detect: the
|
|
49
|
+
* workers simply run under the same loader as their parent.
|
|
50
|
+
*/
|
|
51
|
+
nodeArgs?: string[];
|
|
52
|
+
}
|
|
53
|
+
/** A suite name that was asked for but is not declared. */
|
|
54
|
+
export declare class UnknownSuiteError extends Error {
|
|
55
|
+
constructor(name: string, declared: string[]);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A suite declares `configure`, but this entry point cannot deliver it.
|
|
59
|
+
*
|
|
60
|
+
* Not a warning: Japa runs a declared `configure`, so carrying on would run the
|
|
61
|
+
* suite in a state its own config does not describe — and a warning is exactly
|
|
62
|
+
* what gets scrolled past in CI.
|
|
63
|
+
*/
|
|
64
|
+
export declare class SuiteConfigureUnreachableError extends Error {
|
|
65
|
+
constructor(names: string[]);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The flags the worker processes are spawned with.
|
|
69
|
+
*
|
|
70
|
+
* Split out because it is a decision, not plumbing: the Japa alias loader
|
|
71
|
+
* redirects a package specifier, so it goes in only when a project asks. It
|
|
72
|
+
* rides ALONGSIDE whatever loader is already there — the test files still need
|
|
73
|
+
* theirs to read TypeScript.
|
|
74
|
+
*/
|
|
75
|
+
export declare function workerNodeArgs(tests: TestsConfig | undefined, options: RunTestsOptions): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Run the rc file's test suites. Returns the process exit code; the caller
|
|
78
|
+
* decides what to do with it, so this stays usable from a `bin/test.ts`, from a
|
|
79
|
+
* console command, or from a test of its own.
|
|
80
|
+
*
|
|
81
|
+
* `NODE_ENV=test` is set first, then the `.env` files are loaded — so `.env.test`
|
|
82
|
+
* wins over `.env` and `.env.local` is skipped, the AdonisJS test-env rules. It
|
|
83
|
+
* happens HERE, in the process that spawns the workers, so every worker
|
|
84
|
+
* inherits the result: an app gets its test environment without writing a
|
|
85
|
+
* single hook, which is what "loaded automatically" has to mean.
|
|
86
|
+
*/
|
|
87
|
+
export declare function runTests(tests: TestsConfig | undefined, options?: RunTestsOptions): Promise<number>;
|
|
88
|
+
/**
|
|
89
|
+
* Load an rc file and run its suites — the one-liner a `bin/test.ts` needs.
|
|
90
|
+
* `rcPath` is resolved against `root`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function runTestsFromRcFile(rcPath: string, options?: RunTestsOptions): Promise<number>;
|
|
93
|
+
//# sourceMappingURL=runTests.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runTests.d.ts","sourceRoot":"","sources":["../src/runTests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAmB,WAAW,EAAE,MAAM,YAAY,CAAC;AAG/D,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC/B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,iCAAiC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,2DAA2D;AAC3D,qBAAa,iBAAkB,SAAQ,KAAK;gBAC/B,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE;CAQ5C;AAED;;;;;;GAMG;AACH,qBAAa,8BAA+B,SAAQ,KAAK;gBAC5C,KAAK,EAAE,MAAM,EAAE;CAU3B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC7B,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,OAAO,EAAE,eAAe,GACtB,MAAM,EAAE,CAMV;AAkBD;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAC7B,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAAC,MAAM,CAAC,CA2GjB;AAgBD;;;GAGG;AACH,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
|
package/dist/runTests.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream test` — run the suites declared in the rc file.
|
|
3
|
+
*
|
|
4
|
+
* The AdonisJS stratification, kept intact: the FRAMEWORK reads its rc file and
|
|
5
|
+
* hands the suites to the runner, exactly as `@adonisjs/core` reads
|
|
6
|
+
* `adonisrc.ts` and hands them to Japa. The runner itself (helix) knows nothing
|
|
7
|
+
* about ream, and ream owns no test execution — it only translates.
|
|
8
|
+
*
|
|
9
|
+
* // bin/test.ts
|
|
10
|
+
* import { runTestsFromRcFile } from '@c9up/helix-plugin-ream/runner'
|
|
11
|
+
* process.exitCode = await runTestsFromRcFile('./reamrc.ts', {
|
|
12
|
+
* suites: process.argv.slice(2),
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* Prefer that over `runTests(rc.tests, …)`: a `suites[].configure` callback has
|
|
16
|
+
* to be re-imported in each worker, so the runner needs the module's PATH, not
|
|
17
|
+
* just the object it exported. `runTests` takes it as `configModule`, and
|
|
18
|
+
* REFUSES to run when a suite declares a callback it was given no way to
|
|
19
|
+
* deliver — in Japa a declared `configure` runs, so a run that skipped it would
|
|
20
|
+
* be green in a state its own config does not describe.
|
|
21
|
+
*/
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { pathToFileURL } from "node:url";
|
|
24
|
+
import { loadEnvFiles } from "@c9up/ream/env";
|
|
25
|
+
/** A suite name that was asked for but is not declared. */
|
|
26
|
+
export class UnknownSuiteError extends Error {
|
|
27
|
+
constructor(name, declared) {
|
|
28
|
+
super(declared.length === 0
|
|
29
|
+
? `Unknown test suite "${name}": the rc file declares none.`
|
|
30
|
+
: `Unknown test suite "${name}". Declared: ${declared.join(", ")}.`);
|
|
31
|
+
this.name = "UnknownSuiteError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A suite declares `configure`, but this entry point cannot deliver it.
|
|
36
|
+
*
|
|
37
|
+
* Not a warning: Japa runs a declared `configure`, so carrying on would run the
|
|
38
|
+
* suite in a state its own config does not describe — and a warning is exactly
|
|
39
|
+
* what gets scrolled past in CI.
|
|
40
|
+
*/
|
|
41
|
+
export class SuiteConfigureUnreachableError extends Error {
|
|
42
|
+
constructor(names) {
|
|
43
|
+
const plural = names.length > 1;
|
|
44
|
+
super(`Suite${plural ? "s" : ""} ${names.map((name) => `"${name}"`).join(", ")} ` +
|
|
45
|
+
`declare${plural ? "" : "s"} \`configure\`, which has to be re-imported in each ` +
|
|
46
|
+
"worker and therefore needs the rc file PATH, not the object it exported. " +
|
|
47
|
+
"Call runTestsFromRcFile(), or pass `configModule`.");
|
|
48
|
+
this.name = "SuiteConfigureUnreachableError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The flags the worker processes are spawned with.
|
|
53
|
+
*
|
|
54
|
+
* Split out because it is a decision, not plumbing: the Japa alias loader
|
|
55
|
+
* redirects a package specifier, so it goes in only when a project asks. It
|
|
56
|
+
* rides ALONGSIDE whatever loader is already there — the test files still need
|
|
57
|
+
* theirs to read TypeScript.
|
|
58
|
+
*/
|
|
59
|
+
export function workerNodeArgs(tests, options) {
|
|
60
|
+
const args = [...(options.nodeArgs ?? process.execArgv)];
|
|
61
|
+
if (tests?.japaPlugins === true) {
|
|
62
|
+
args.push("--import", import.meta.resolve("@c9up/helix/japa-alias"));
|
|
63
|
+
}
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
/** The suites to run, in declaration order, for the given selection. */
|
|
67
|
+
function select(declared, asked) {
|
|
68
|
+
if (asked.length === 0)
|
|
69
|
+
return declared;
|
|
70
|
+
const byName = new Map(declared.map((suite) => [suite.name, suite]));
|
|
71
|
+
return asked.map((name) => {
|
|
72
|
+
const suite = byName.get(name);
|
|
73
|
+
if (suite === undefined) {
|
|
74
|
+
throw new UnknownSuiteError(name, [...byName.keys()]);
|
|
75
|
+
}
|
|
76
|
+
return suite;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run the rc file's test suites. Returns the process exit code; the caller
|
|
81
|
+
* decides what to do with it, so this stays usable from a `bin/test.ts`, from a
|
|
82
|
+
* console command, or from a test of its own.
|
|
83
|
+
*
|
|
84
|
+
* `NODE_ENV=test` is set first, then the `.env` files are loaded — so `.env.test`
|
|
85
|
+
* wins over `.env` and `.env.local` is skipped, the AdonisJS test-env rules. It
|
|
86
|
+
* happens HERE, in the process that spawns the workers, so every worker
|
|
87
|
+
* inherits the result: an app gets its test environment without writing a
|
|
88
|
+
* single hook, which is what "loaded automatically" has to mean.
|
|
89
|
+
*/
|
|
90
|
+
export async function runTests(tests, options = {}) {
|
|
91
|
+
process.env.NODE_ENV = "test";
|
|
92
|
+
const root = options.root ?? process.cwd();
|
|
93
|
+
// Skipping `.env.local` is deliberate (and what the Ignitor does for the test
|
|
94
|
+
// environment): a developer's local overrides must not decide what CI runs.
|
|
95
|
+
loadEnvFiles(pathToFileURL(`${root}${path.sep}`), { skipEnvLocal: true });
|
|
96
|
+
const declared = tests?.suites ?? [];
|
|
97
|
+
const selected = select(declared, options.suites ?? []);
|
|
98
|
+
const helix = await import("@c9up/helix/runner");
|
|
99
|
+
// The bootstrap module is the app's, not the runner's — helix imports it in
|
|
100
|
+
// every worker, so a plugin's context extensions exist before the first test
|
|
101
|
+
// declares itself.
|
|
102
|
+
const bootstrap = helix.resolveBootstrap(root, tests?.bootstrap);
|
|
103
|
+
process.env.HELIX_BOOTSTRAP = bootstrap ?? "";
|
|
104
|
+
// Assigned either way: a second call in the same process must not inherit
|
|
105
|
+
// the first one's flag. A plugin reads it back off `api.cliArgs.forceExit`.
|
|
106
|
+
const forceExit = tests?.forceExit === true;
|
|
107
|
+
process.env.HELIX_FORCE_EXIT = forceExit ? "1" : "";
|
|
108
|
+
// Only named when a suite actually declares `configure`: pointing at it makes
|
|
109
|
+
// every worker import the rc file, which a project not using the callback
|
|
110
|
+
// should not pay for.
|
|
111
|
+
const configuring = selected.filter((suite) => typeof suite.configure === "function");
|
|
112
|
+
if (configuring.length > 0 && options.configModule === undefined) {
|
|
113
|
+
// In Japa a declared `configure` RUNS. It cannot here — the callback needs
|
|
114
|
+
// the module's path to be re-imported in each worker, and this entry was
|
|
115
|
+
// handed the exported object. Running anyway would produce a green suite
|
|
116
|
+
// configured differently from what the rc file says, so the run stops.
|
|
117
|
+
throw new SuiteConfigureUnreachableError(configuring.map((suite) => suite.name));
|
|
118
|
+
}
|
|
119
|
+
process.env.HELIX_SUITE_CONFIG =
|
|
120
|
+
configuring.length > 0 && options.configModule !== undefined
|
|
121
|
+
? options.configModule
|
|
122
|
+
: "";
|
|
123
|
+
process.env.HELIX_SUITE_CONFIG_KEY = "tests.suites";
|
|
124
|
+
const base = {
|
|
125
|
+
root,
|
|
126
|
+
nodeArgs: workerNodeArgs(tests, options),
|
|
127
|
+
threads: options.threads,
|
|
128
|
+
timeoutMs: tests?.timeout,
|
|
129
|
+
reporters: options.reporters,
|
|
130
|
+
bail: options.bail,
|
|
131
|
+
};
|
|
132
|
+
// `runnerHooks` run ONCE around the whole run, here, and the workers skip
|
|
133
|
+
// them — Japa's semantics, and the difference between migrating once and
|
|
134
|
+
// migrating once per test file.
|
|
135
|
+
const dropGlobalHooks = await helix.runGlobalHooks(bootstrap, {
|
|
136
|
+
japaPlugins: tests?.japaPlugins === true,
|
|
137
|
+
});
|
|
138
|
+
// No suites declared: run whatever the project's discovery finds, so an app
|
|
139
|
+
// with a plain `tests/` directory works without declaring anything.
|
|
140
|
+
if (selected.length === 0) {
|
|
141
|
+
try {
|
|
142
|
+
const outcome = await helix.run(base);
|
|
143
|
+
return finish(outcome.exitCode, forceExit);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
await dropGlobalHooks();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const steps = [];
|
|
150
|
+
for (const suite of selected) {
|
|
151
|
+
const files = await helix.resolveSuiteFiles({
|
|
152
|
+
name: suite.name,
|
|
153
|
+
files: suite.files,
|
|
154
|
+
timeout: suite.timeout,
|
|
155
|
+
retries: suite.retries,
|
|
156
|
+
}, root, undefined);
|
|
157
|
+
if (files.length === 0) {
|
|
158
|
+
process.stderr.write(`ream: suite "${suite.name}": no test files\n`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
steps.push({
|
|
162
|
+
env: {
|
|
163
|
+
HELIX_SUITE: suite.name,
|
|
164
|
+
// Empty means unset, so a suite that declares no retries does not
|
|
165
|
+
// inherit the previous suite's.
|
|
166
|
+
HELIX_RETRIES: suite.retries === undefined ? "" : String(suite.retries),
|
|
167
|
+
},
|
|
168
|
+
config: { ...base, files, timeoutMs: suite.timeout ?? tests?.timeout },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (steps.length === 0) {
|
|
172
|
+
await dropGlobalHooks();
|
|
173
|
+
return finish(0, forceExit);
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const outcome = await helix.runSuites(steps, base);
|
|
177
|
+
return finish(outcome.exitCode, forceExit);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await dropGlobalHooks();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Apply `tests.forceExit`. Japa does this inside its own run — sets the exit
|
|
185
|
+
* code, then `process.exit()` — rather than leaving it to the caller, because
|
|
186
|
+
* the whole point is to not wait for the event loop to drain. A run that
|
|
187
|
+
* force-exits never returns here; the value is for every other run.
|
|
188
|
+
*/
|
|
189
|
+
function finish(code, forceExit) {
|
|
190
|
+
if (forceExit) {
|
|
191
|
+
process.exitCode = code;
|
|
192
|
+
process.exit();
|
|
193
|
+
}
|
|
194
|
+
return code;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Load an rc file and run its suites — the one-liner a `bin/test.ts` needs.
|
|
198
|
+
* `rcPath` is resolved against `root`.
|
|
199
|
+
*/
|
|
200
|
+
export async function runTestsFromRcFile(rcPath, options = {}) {
|
|
201
|
+
const root = options.root ?? process.cwd();
|
|
202
|
+
const absolute = path.isAbsolute(rcPath)
|
|
203
|
+
? rcPath
|
|
204
|
+
: path.resolve(root, rcPath);
|
|
205
|
+
const imported = await import(pathToFileURL(absolute).href);
|
|
206
|
+
const rc = imported !== null && typeof imported === "object"
|
|
207
|
+
? (Reflect.get(imported, "default") ?? imported)
|
|
208
|
+
: undefined;
|
|
209
|
+
const tests = rc !== null && typeof rc === "object"
|
|
210
|
+
? Reflect.get(rc, "tests")
|
|
211
|
+
: undefined;
|
|
212
|
+
return runTests(isTestsConfig(tests) ? tests : undefined, {
|
|
213
|
+
...options,
|
|
214
|
+
root,
|
|
215
|
+
configModule: absolute,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Narrow an rc file's `tests` value without trusting it. */
|
|
219
|
+
function isTestsConfig(value) {
|
|
220
|
+
return value !== null && typeof value === "object";
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=runTests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runTests.js","sourceRoot":"","sources":["../src/runTests.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c9up/helix-plugin-ream",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Helix plugin for Ream — boots a Ream app under test and injects a TestClient on the test context",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./runner": {
|
|
15
|
+
"types": "./dist/runTests.d.ts",
|
|
16
|
+
"import": "./dist/runTests.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@c9up/helix": "^0.1.9",
|
|
21
|
+
"@c9up/ream": "^0.1.27"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@biomejs/biome": "^2.4.10",
|
|
25
|
+
"@c9up/helix": "^0.1.9",
|
|
26
|
+
"@c9up/ream": "^0.1.27",
|
|
27
|
+
"@types/node": "^22.19.15",
|
|
28
|
+
"tsx": "^4",
|
|
29
|
+
"typescript": "^6.0.2",
|
|
30
|
+
"vitest": "^4.1.2"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"src",
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/C9up/helix-plugin-ream.git"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"lint": "biome check src/",
|
|
52
|
+
"test:coverage": "vitest run --coverage",
|
|
53
|
+
"typecheck": "tsc --noEmit"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@c9up/helix-plugin-ream` — the bridge between Ream and its test runner.
|
|
3
|
+
*
|
|
4
|
+
* Ream knows nothing about helix, helix knows nothing about Ream: the plugin
|
|
5
|
+
* that joins them lives here, and declares both sides as peers. The HTTP test
|
|
6
|
+
* client itself (`TestClient`, `createTestClient`) stays in `@c9up/ream/testing`
|
|
7
|
+
* — it drives a Ream server and owes nothing to the runner.
|
|
8
|
+
*
|
|
9
|
+
* // tests/bootstrap.ts
|
|
10
|
+
* import { configure } from '@c9up/helix'
|
|
11
|
+
* import { apiClient } from '@c9up/helix-plugin-ream'
|
|
12
|
+
* await configure({ plugins: [apiClient({ boot: () => bootApp() })] })
|
|
13
|
+
*
|
|
14
|
+
* // a test
|
|
15
|
+
* test('health', async ({ client }) => {
|
|
16
|
+
* await client.get('/health').assertOk()
|
|
17
|
+
* })
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Plugin, PluginApi } from "@c9up/helix";
|
|
21
|
+
import type { AuthStrategy, RouteManifest } from "@c9up/ream/testing";
|
|
22
|
+
import { TestClient } from "@c9up/ream/testing";
|
|
23
|
+
|
|
24
|
+
/** The slice of helix's `PluginApi` this plugin actually uses. */
|
|
25
|
+
export type ClientHost = Pick<PluginApi, "context" | "cleanup">;
|
|
26
|
+
|
|
27
|
+
export interface ApiClientConfig {
|
|
28
|
+
/** Boot the app under test on the given port; return the port + a close fn. */
|
|
29
|
+
boot: (
|
|
30
|
+
port: number,
|
|
31
|
+
) => Promise<{ port: number; close: () => Promise<void> | void }>;
|
|
32
|
+
/** Warden auth strategy for `client.withAuth()`/`asUser()`. */
|
|
33
|
+
auth?: AuthStrategy;
|
|
34
|
+
/** Named-route manifest (`router.namedManifest()`) for `client.visit()`. */
|
|
35
|
+
routes?: RouteManifest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Injects a booted {@link TestClient} on the test context as `ctx.client`.
|
|
40
|
+
*
|
|
41
|
+
* The server is booted once at `configure()` time, shared across the run, and
|
|
42
|
+
* closed via `api.cleanup` after the run finishes — a proper lifecycle, with no
|
|
43
|
+
* reliance on process exit.
|
|
44
|
+
*/
|
|
45
|
+
export function apiClient(config: ApiClientConfig) {
|
|
46
|
+
const plugin = async (api: ClientHost): Promise<void> => {
|
|
47
|
+
const client = new TestClient(config.boot, {
|
|
48
|
+
auth: config.auth,
|
|
49
|
+
routes: config.routes,
|
|
50
|
+
});
|
|
51
|
+
await client.boot();
|
|
52
|
+
api.context.macro("client", client);
|
|
53
|
+
api.cleanup(() => client.close());
|
|
54
|
+
};
|
|
55
|
+
// A wider parameter than `PluginApi` stays assignable to `Plugin`, so the
|
|
56
|
+
// plugin declares exactly what it touches and a caller can drive it with
|
|
57
|
+
// nothing more than that.
|
|
58
|
+
return plugin satisfies Plugin;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Typing side of the plugin — importing it augments the helix test context.
|
|
62
|
+
declare module "@c9up/helix" {
|
|
63
|
+
interface TestContext {
|
|
64
|
+
client: TestClient;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/runTests.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream test` — run the suites declared in the rc file.
|
|
3
|
+
*
|
|
4
|
+
* The AdonisJS stratification, kept intact: the FRAMEWORK reads its rc file and
|
|
5
|
+
* hands the suites to the runner, exactly as `@adonisjs/core` reads
|
|
6
|
+
* `adonisrc.ts` and hands them to Japa. The runner itself (helix) knows nothing
|
|
7
|
+
* about ream, and ream owns no test execution — it only translates.
|
|
8
|
+
*
|
|
9
|
+
* // bin/test.ts
|
|
10
|
+
* import { runTestsFromRcFile } from '@c9up/helix-plugin-ream/runner'
|
|
11
|
+
* process.exitCode = await runTestsFromRcFile('./reamrc.ts', {
|
|
12
|
+
* suites: process.argv.slice(2),
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* Prefer that over `runTests(rc.tests, …)`: a `suites[].configure` callback has
|
|
16
|
+
* to be re-imported in each worker, so the runner needs the module's PATH, not
|
|
17
|
+
* just the object it exported. `runTests` takes it as `configModule`, and
|
|
18
|
+
* REFUSES to run when a suite declares a callback it was given no way to
|
|
19
|
+
* deliver — in Japa a declared `configure` runs, so a run that skipped it would
|
|
20
|
+
* be green in a state its own config does not describe.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
import type { TestSuiteConfig, TestsConfig } from "@c9up/ream";
|
|
26
|
+
import { loadEnvFiles } from "@c9up/ream/env";
|
|
27
|
+
|
|
28
|
+
/** What a caller may override on top of the rc file. */
|
|
29
|
+
export interface RunTestsOptions {
|
|
30
|
+
/** Project root the suites resolve against. Defaults to `process.cwd()`. */
|
|
31
|
+
root?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Suite names to run. Empty (the default) runs every declared suite, in
|
|
34
|
+
* order — the AdonisJS behaviour for `ream test` with no argument.
|
|
35
|
+
*/
|
|
36
|
+
suites?: string[];
|
|
37
|
+
/** Concurrent worker processes. */
|
|
38
|
+
threads?: number;
|
|
39
|
+
/** Reporter name, or several (`["spec", "json"]`). */
|
|
40
|
+
reporters?: string[];
|
|
41
|
+
/** Stop at the first failure. */
|
|
42
|
+
bail?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The module that declared the suites, so a `suites[].configure` callback can
|
|
45
|
+
* be re-imported in each worker — a function does not cross a process
|
|
46
|
+
* boundary. Set for you by {@link runTestsFromRcFile}.
|
|
47
|
+
*/
|
|
48
|
+
configModule?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Flags the worker processes are spawned with. Defaults to this process's own
|
|
51
|
+
* (`process.execArgv`), so the workers load TypeScript through whatever
|
|
52
|
+
* loader the parent was started with — `--import @swc-node/register/esm-register`
|
|
53
|
+
* for `ream test`, tsx for a project that prefers it. Nothing to detect: the
|
|
54
|
+
* workers simply run under the same loader as their parent.
|
|
55
|
+
*/
|
|
56
|
+
nodeArgs?: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A suite name that was asked for but is not declared. */
|
|
60
|
+
export class UnknownSuiteError extends Error {
|
|
61
|
+
constructor(name: string, declared: string[]) {
|
|
62
|
+
super(
|
|
63
|
+
declared.length === 0
|
|
64
|
+
? `Unknown test suite "${name}": the rc file declares none.`
|
|
65
|
+
: `Unknown test suite "${name}". Declared: ${declared.join(", ")}.`,
|
|
66
|
+
);
|
|
67
|
+
this.name = "UnknownSuiteError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A suite declares `configure`, but this entry point cannot deliver it.
|
|
73
|
+
*
|
|
74
|
+
* Not a warning: Japa runs a declared `configure`, so carrying on would run the
|
|
75
|
+
* suite in a state its own config does not describe — and a warning is exactly
|
|
76
|
+
* what gets scrolled past in CI.
|
|
77
|
+
*/
|
|
78
|
+
export class SuiteConfigureUnreachableError extends Error {
|
|
79
|
+
constructor(names: string[]) {
|
|
80
|
+
const plural = names.length > 1;
|
|
81
|
+
super(
|
|
82
|
+
`Suite${plural ? "s" : ""} ${names.map((name) => `"${name}"`).join(", ")} ` +
|
|
83
|
+
`declare${plural ? "" : "s"} \`configure\`, which has to be re-imported in each ` +
|
|
84
|
+
"worker and therefore needs the rc file PATH, not the object it exported. " +
|
|
85
|
+
"Call runTestsFromRcFile(), or pass `configModule`.",
|
|
86
|
+
);
|
|
87
|
+
this.name = "SuiteConfigureUnreachableError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The flags the worker processes are spawned with.
|
|
93
|
+
*
|
|
94
|
+
* Split out because it is a decision, not plumbing: the Japa alias loader
|
|
95
|
+
* redirects a package specifier, so it goes in only when a project asks. It
|
|
96
|
+
* rides ALONGSIDE whatever loader is already there — the test files still need
|
|
97
|
+
* theirs to read TypeScript.
|
|
98
|
+
*/
|
|
99
|
+
export function workerNodeArgs(
|
|
100
|
+
tests: TestsConfig | undefined,
|
|
101
|
+
options: RunTestsOptions,
|
|
102
|
+
): string[] {
|
|
103
|
+
const args = [...(options.nodeArgs ?? process.execArgv)];
|
|
104
|
+
if (tests?.japaPlugins === true) {
|
|
105
|
+
args.push("--import", import.meta.resolve("@c9up/helix/japa-alias"));
|
|
106
|
+
}
|
|
107
|
+
return args;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The suites to run, in declaration order, for the given selection. */
|
|
111
|
+
function select(
|
|
112
|
+
declared: TestSuiteConfig[],
|
|
113
|
+
asked: string[],
|
|
114
|
+
): TestSuiteConfig[] {
|
|
115
|
+
if (asked.length === 0) return declared;
|
|
116
|
+
const byName = new Map(declared.map((suite) => [suite.name, suite]));
|
|
117
|
+
return asked.map((name) => {
|
|
118
|
+
const suite = byName.get(name);
|
|
119
|
+
if (suite === undefined) {
|
|
120
|
+
throw new UnknownSuiteError(name, [...byName.keys()]);
|
|
121
|
+
}
|
|
122
|
+
return suite;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Run the rc file's test suites. Returns the process exit code; the caller
|
|
128
|
+
* decides what to do with it, so this stays usable from a `bin/test.ts`, from a
|
|
129
|
+
* console command, or from a test of its own.
|
|
130
|
+
*
|
|
131
|
+
* `NODE_ENV=test` is set first, then the `.env` files are loaded — so `.env.test`
|
|
132
|
+
* wins over `.env` and `.env.local` is skipped, the AdonisJS test-env rules. It
|
|
133
|
+
* happens HERE, in the process that spawns the workers, so every worker
|
|
134
|
+
* inherits the result: an app gets its test environment without writing a
|
|
135
|
+
* single hook, which is what "loaded automatically" has to mean.
|
|
136
|
+
*/
|
|
137
|
+
export async function runTests(
|
|
138
|
+
tests: TestsConfig | undefined,
|
|
139
|
+
options: RunTestsOptions = {},
|
|
140
|
+
): Promise<number> {
|
|
141
|
+
process.env.NODE_ENV = "test";
|
|
142
|
+
|
|
143
|
+
const root = options.root ?? process.cwd();
|
|
144
|
+
// Skipping `.env.local` is deliberate (and what the Ignitor does for the test
|
|
145
|
+
// environment): a developer's local overrides must not decide what CI runs.
|
|
146
|
+
loadEnvFiles(pathToFileURL(`${root}${path.sep}`), { skipEnvLocal: true });
|
|
147
|
+
const declared = tests?.suites ?? [];
|
|
148
|
+
const selected = select(declared, options.suites ?? []);
|
|
149
|
+
|
|
150
|
+
const helix = await import("@c9up/helix/runner");
|
|
151
|
+
|
|
152
|
+
// The bootstrap module is the app's, not the runner's — helix imports it in
|
|
153
|
+
// every worker, so a plugin's context extensions exist before the first test
|
|
154
|
+
// declares itself.
|
|
155
|
+
const bootstrap = helix.resolveBootstrap(root, tests?.bootstrap);
|
|
156
|
+
process.env.HELIX_BOOTSTRAP = bootstrap ?? "";
|
|
157
|
+
// Assigned either way: a second call in the same process must not inherit
|
|
158
|
+
// the first one's flag. A plugin reads it back off `api.cliArgs.forceExit`.
|
|
159
|
+
const forceExit = tests?.forceExit === true;
|
|
160
|
+
process.env.HELIX_FORCE_EXIT = forceExit ? "1" : "";
|
|
161
|
+
|
|
162
|
+
// Only named when a suite actually declares `configure`: pointing at it makes
|
|
163
|
+
// every worker import the rc file, which a project not using the callback
|
|
164
|
+
// should not pay for.
|
|
165
|
+
const configuring = selected.filter(
|
|
166
|
+
(suite) => typeof suite.configure === "function",
|
|
167
|
+
);
|
|
168
|
+
if (configuring.length > 0 && options.configModule === undefined) {
|
|
169
|
+
// In Japa a declared `configure` RUNS. It cannot here — the callback needs
|
|
170
|
+
// the module's path to be re-imported in each worker, and this entry was
|
|
171
|
+
// handed the exported object. Running anyway would produce a green suite
|
|
172
|
+
// configured differently from what the rc file says, so the run stops.
|
|
173
|
+
throw new SuiteConfigureUnreachableError(
|
|
174
|
+
configuring.map((suite) => suite.name),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
process.env.HELIX_SUITE_CONFIG =
|
|
178
|
+
configuring.length > 0 && options.configModule !== undefined
|
|
179
|
+
? options.configModule
|
|
180
|
+
: "";
|
|
181
|
+
process.env.HELIX_SUITE_CONFIG_KEY = "tests.suites";
|
|
182
|
+
|
|
183
|
+
const base = {
|
|
184
|
+
root,
|
|
185
|
+
nodeArgs: workerNodeArgs(tests, options),
|
|
186
|
+
threads: options.threads,
|
|
187
|
+
timeoutMs: tests?.timeout,
|
|
188
|
+
reporters: options.reporters,
|
|
189
|
+
bail: options.bail,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// `runnerHooks` run ONCE around the whole run, here, and the workers skip
|
|
193
|
+
// them — Japa's semantics, and the difference between migrating once and
|
|
194
|
+
// migrating once per test file.
|
|
195
|
+
const dropGlobalHooks = await helix.runGlobalHooks(bootstrap, {
|
|
196
|
+
japaPlugins: tests?.japaPlugins === true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// No suites declared: run whatever the project's discovery finds, so an app
|
|
200
|
+
// with a plain `tests/` directory works without declaring anything.
|
|
201
|
+
if (selected.length === 0) {
|
|
202
|
+
try {
|
|
203
|
+
const outcome = await helix.run(base);
|
|
204
|
+
return finish(outcome.exitCode, forceExit);
|
|
205
|
+
} finally {
|
|
206
|
+
await dropGlobalHooks();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const steps = [];
|
|
211
|
+
for (const suite of selected) {
|
|
212
|
+
const files = await helix.resolveSuiteFiles(
|
|
213
|
+
{
|
|
214
|
+
name: suite.name,
|
|
215
|
+
files: suite.files,
|
|
216
|
+
timeout: suite.timeout,
|
|
217
|
+
retries: suite.retries,
|
|
218
|
+
},
|
|
219
|
+
root,
|
|
220
|
+
undefined,
|
|
221
|
+
);
|
|
222
|
+
if (files.length === 0) {
|
|
223
|
+
process.stderr.write(`ream: suite "${suite.name}": no test files\n`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
steps.push({
|
|
227
|
+
env: {
|
|
228
|
+
HELIX_SUITE: suite.name,
|
|
229
|
+
// Empty means unset, so a suite that declares no retries does not
|
|
230
|
+
// inherit the previous suite's.
|
|
231
|
+
HELIX_RETRIES: suite.retries === undefined ? "" : String(suite.retries),
|
|
232
|
+
},
|
|
233
|
+
config: { ...base, files, timeoutMs: suite.timeout ?? tests?.timeout },
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (steps.length === 0) {
|
|
237
|
+
await dropGlobalHooks();
|
|
238
|
+
return finish(0, forceExit);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const outcome = await helix.runSuites(steps, base);
|
|
243
|
+
return finish(outcome.exitCode, forceExit);
|
|
244
|
+
} finally {
|
|
245
|
+
await dropGlobalHooks();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Apply `tests.forceExit`. Japa does this inside its own run — sets the exit
|
|
251
|
+
* code, then `process.exit()` — rather than leaving it to the caller, because
|
|
252
|
+
* the whole point is to not wait for the event loop to drain. A run that
|
|
253
|
+
* force-exits never returns here; the value is for every other run.
|
|
254
|
+
*/
|
|
255
|
+
function finish(code: number, forceExit: boolean): number {
|
|
256
|
+
if (forceExit) {
|
|
257
|
+
process.exitCode = code;
|
|
258
|
+
process.exit();
|
|
259
|
+
}
|
|
260
|
+
return code;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Load an rc file and run its suites — the one-liner a `bin/test.ts` needs.
|
|
265
|
+
* `rcPath` is resolved against `root`.
|
|
266
|
+
*/
|
|
267
|
+
export async function runTestsFromRcFile(
|
|
268
|
+
rcPath: string,
|
|
269
|
+
options: RunTestsOptions = {},
|
|
270
|
+
): Promise<number> {
|
|
271
|
+
const root = options.root ?? process.cwd();
|
|
272
|
+
const absolute = path.isAbsolute(rcPath)
|
|
273
|
+
? rcPath
|
|
274
|
+
: path.resolve(root, rcPath);
|
|
275
|
+
const imported: unknown = await import(pathToFileURL(absolute).href);
|
|
276
|
+
const rc =
|
|
277
|
+
imported !== null && typeof imported === "object"
|
|
278
|
+
? (Reflect.get(imported, "default") ?? imported)
|
|
279
|
+
: undefined;
|
|
280
|
+
const tests =
|
|
281
|
+
rc !== null && typeof rc === "object"
|
|
282
|
+
? Reflect.get(rc, "tests")
|
|
283
|
+
: undefined;
|
|
284
|
+
return runTests(isTestsConfig(tests) ? tests : undefined, {
|
|
285
|
+
...options,
|
|
286
|
+
root,
|
|
287
|
+
configModule: absolute,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Narrow an rc file's `tests` value without trusting it. */
|
|
292
|
+
function isTestsConfig(value: unknown): value is TestsConfig {
|
|
293
|
+
return value !== null && typeof value === "object";
|
|
294
|
+
}
|