@c9up/helix-plugin-ream 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The bridge between [Ream](https://github.com/C9up/ream) and [helix](https://github.com/C9up/helix), its test runner.
4
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`.
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 AdonisJS uses to join its own runner to the framework.
6
6
 
7
7
  ## Install
8
8
 
package/dist/index.d.ts CHANGED
@@ -6,10 +6,25 @@
6
6
  * client itself (`TestClient`, `createTestClient`) stays in `@c9up/ream/testing`
7
7
  * — it drives a Ream server and owes nothing to the runner.
8
8
  *
9
+ * The split follows AdonisJS: the plugin puts a CLIENT on the test context, and
10
+ * the SERVER is started by a suite hook, so a suite that does not declare the
11
+ * hook never starts one.
12
+ *
9
13
  * // tests/bootstrap.ts
10
14
  * import { configure } from '@c9up/helix'
11
15
  * import { apiClient } from '@c9up/helix-plugin-ream'
12
- * await configure({ plugins: [apiClient({ boot: () => bootApp() })] })
16
+ * import { createTestUtils } from '@c9up/ream/testing/utils'
17
+ *
18
+ * export const testUtils = createTestUtils((port) => bootApp(port))
19
+ *
20
+ * await configure({
21
+ * plugins: [apiClient({ testUtils })],
22
+ * configureSuite(suite) {
23
+ * if (['functional', 'e2e'].includes(suite.name)) {
24
+ * return suite.setup(() => testUtils.httpServer().start())
25
+ * }
26
+ * },
27
+ * })
13
28
  *
14
29
  * // a test
15
30
  * test('health', async ({ client }) => {
@@ -19,27 +34,55 @@
19
34
  import type { PluginApi } from "@c9up/helix";
20
35
  import type { AuthStrategy, RouteManifest } from "@c9up/ream/testing";
21
36
  import { TestClient } from "@c9up/ream/testing";
37
+ import type { BootServer, TestUtils } from "@c9up/ream/testing/utils";
22
38
  /** The slice of helix's `PluginApi` this plugin actually uses. */
23
39
  export type ClientHost = Pick<PluginApi, "context" | "cleanup">;
24
40
  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
- }>;
41
+ /**
42
+ * The utilities whose `httpServer()` a suite hook starts. The plugin reads
43
+ * the client of the server that hook started, so both point at one server.
44
+ */
45
+ testUtils?: TestUtils;
46
+ /**
47
+ * Boot the app directly, for a project that does not split its suites.
48
+ *
49
+ * A deviation from AdonisJS, where starting the server is always the suite
50
+ * hook's job. It exists because the server then starts on the client's FIRST
51
+ * REQUEST rather than at plugin time, so a file that issues none pays
52
+ * nothing — the same saving `testUtils` obtains by scoping, without having
53
+ * to split the suites first. Prefer `testUtils`; reach for this when the
54
+ * files are not separated.
55
+ */
56
+ boot?: BootServer;
30
57
  /** Warden auth strategy for `client.withAuth()`/`asUser()`. */
31
58
  auth?: AuthStrategy;
32
59
  /** Named-route manifest (`router.namedManifest()`) for `client.visit()`. */
33
60
  routes?: RouteManifest;
34
61
  }
35
62
  /**
36
- * Injects a booted {@link TestClient} on the test context as `ctx.client`.
63
+ * Injects a {@link TestClient} on the test context as `ctx.client`.
64
+ *
65
+ * With `testUtils`, the client is the one belonging to the server the suite
66
+ * hook started — so nothing is booted here, and a suite without the hook has no
67
+ * server at all. With `boot`, the client owns its server and starts it on the
68
+ * first request.
37
69
  *
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.
70
+ * Either way `api.cleanup` closes what this plugin started, which is nothing in
71
+ * the `testUtils` case the hook's own teardown owns that server.
41
72
  */
42
73
  export declare function apiClient(config: ApiClientConfig): (api: ClientHost) => Promise<void>;
74
+ /**
75
+ * The client this run is using, reachable without a test context.
76
+ *
77
+ * `ctx.client` covers a test body. A helper module, a fixture, or a
78
+ * `runnerHooks` setup has no context to read it from and had to be handed one
79
+ * through a parameter every call site then had to thread. Same instance as
80
+ * `ctx.client` — the AdonisJS `services/test_utils` idiom.
81
+ *
82
+ * Throws when no client is in play, which is a wiring mistake rather than a
83
+ * state to handle.
84
+ */
85
+ export declare function testClient(): TestClient;
43
86
  declare module "@c9up/helix" {
44
87
  interface TestContext {
45
88
  client: TestClient;
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;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;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAEtE,kEAAkE;AAClE,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,aAAa,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,SACrB,UAAU,KAAG,OAAO,CAAC,IAAI,CAAC,CAkDrD;AAiBD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAUvC;AAGD,OAAO,QAAQ,aAAa,CAAC;IAC5B,UAAU,WAAW;QACpB,MAAM,EAAE,UAAU,CAAC;KACnB;CACD"}
package/dist/index.js CHANGED
@@ -6,10 +6,25 @@
6
6
  * client itself (`TestClient`, `createTestClient`) stays in `@c9up/ream/testing`
7
7
  * — it drives a Ream server and owes nothing to the runner.
8
8
  *
9
+ * The split follows AdonisJS: the plugin puts a CLIENT on the test context, and
10
+ * the SERVER is started by a suite hook, so a suite that does not declare the
11
+ * hook never starts one.
12
+ *
9
13
  * // tests/bootstrap.ts
10
14
  * import { configure } from '@c9up/helix'
11
15
  * import { apiClient } from '@c9up/helix-plugin-ream'
12
- * await configure({ plugins: [apiClient({ boot: () => bootApp() })] })
16
+ * import { createTestUtils } from '@c9up/ream/testing/utils'
17
+ *
18
+ * export const testUtils = createTestUtils((port) => bootApp(port))
19
+ *
20
+ * await configure({
21
+ * plugins: [apiClient({ testUtils })],
22
+ * configureSuite(suite) {
23
+ * if (['functional', 'e2e'].includes(suite.name)) {
24
+ * return suite.setup(() => testUtils.httpServer().start())
25
+ * }
26
+ * },
27
+ * })
13
28
  *
14
29
  * // a test
15
30
  * test('health', async ({ client }) => {
@@ -18,25 +33,93 @@
18
33
  */
19
34
  import { TestClient } from "@c9up/ream/testing";
20
35
  /**
21
- * Injects a booted {@link TestClient} on the test context as `ctx.client`.
36
+ * Injects a {@link TestClient} on the test context as `ctx.client`.
22
37
  *
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.
38
+ * With `testUtils`, the client is the one belonging to the server the suite
39
+ * hook started so nothing is booted here, and a suite without the hook has no
40
+ * server at all. With `boot`, the client owns its server and starts it on the
41
+ * first request.
42
+ *
43
+ * Either way `api.cleanup` closes what this plugin started, which is nothing in
44
+ * the `testUtils` case — the hook's own teardown owns that server.
26
45
  */
27
46
  export function apiClient(config) {
28
47
  const plugin = async (api) => {
29
- const client = new TestClient(config.boot, {
30
- auth: config.auth,
31
- routes: config.routes,
48
+ if (!config.testUtils && !config.boot) {
49
+ throw new Error("[E_NO_SERVER] apiClient() needs either `testUtils` (the AdonisJS shape: " +
50
+ "a suite hook starts the server) or `boot` (the client starts one on its " +
51
+ "first request). It was given neither, so `ctx.client` would have had " +
52
+ "nothing to talk to.");
53
+ }
54
+ // Resolved per access, not once: with `testUtils` the server does not
55
+ // exist yet when the plugin runs — the suite's setup hook starts it.
56
+ const owned = config.boot
57
+ ? new TestClient(config.boot, {
58
+ auth: config.auth,
59
+ routes: config.routes,
60
+ })
61
+ : undefined;
62
+ const read = () => {
63
+ const client = owned ?? config.testUtils?.client();
64
+ if (!client) {
65
+ throw new Error("[E_SERVER_NOT_STARTED] No server is running for this suite. Add the " +
66
+ "hook that starts it:\n" +
67
+ " configureSuite(suite) {\n" +
68
+ " if (suite.name === 'functional') {\n" +
69
+ " return suite.setup(() => testUtils.httpServer().start())\n" +
70
+ " }\n" +
71
+ " }");
72
+ }
73
+ return client;
74
+ };
75
+ // The resolver, not a client: with `testUtils` there is nothing to record
76
+ // yet, and `testClient()` must not depend on someone having read
77
+ // `ctx.client` first — a fixture or a runner hook has no context to read
78
+ // it from, which is the reason the accessor exists.
79
+ setResolver(read);
80
+ api.context.getter("client", read);
81
+ api.cleanup(async () => {
82
+ if (owned)
83
+ await owned.close();
84
+ clearResolver(read);
32
85
  });
33
- await client.boot();
34
- api.context.macro("client", client);
35
- api.cleanup(() => client.close());
36
86
  };
37
87
  // A wider parameter than `PluginApi` stays assignable to `Plugin`, so the
38
88
  // plugin declares exactly what it touches and a caller can drive it with
39
89
  // nothing more than that.
40
90
  return plugin;
41
91
  }
92
+ let resolve;
93
+ /** @internal Record how to reach the client in play. */
94
+ function setResolver(read) {
95
+ resolve = read;
96
+ }
97
+ /**
98
+ * @internal Forget it IF it is still the one recorded — a second `configure()`
99
+ * in the same process must not have the first one's teardown clear its client.
100
+ */
101
+ function clearResolver(read) {
102
+ if (resolve === read)
103
+ resolve = undefined;
104
+ }
105
+ /**
106
+ * The client this run is using, reachable without a test context.
107
+ *
108
+ * `ctx.client` covers a test body. A helper module, a fixture, or a
109
+ * `runnerHooks` setup has no context to read it from and had to be handed one
110
+ * through a parameter every call site then had to thread. Same instance as
111
+ * `ctx.client` — the AdonisJS `services/test_utils` idiom.
112
+ *
113
+ * Throws when no client is in play, which is a wiring mistake rather than a
114
+ * state to handle.
115
+ */
116
+ export function testClient() {
117
+ if (!resolve) {
118
+ throw new Error("[E_NO_TEST_CLIENT] No client is in play. Add `apiClient()` to the plugins " +
119
+ "in tests/bootstrap.ts, and — with the `testUtils` shape — make sure the " +
120
+ "suite's setup hook has started the server. This reads the client of the " +
121
+ "CURRENT process.");
122
+ }
123
+ return resolve();
124
+ }
42
125
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AA6BhD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,SAAS,CAAC,MAAuB;IAChD,MAAM,MAAM,GAAG,KAAK,EAAE,GAAe,EAAiB,EAAE;QACvD,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACvC,MAAM,IAAI,KAAK,CACd,0EAA0E;gBACzE,0EAA0E;gBAC1E,uEAAuE;gBACvE,qBAAqB,CACtB,CAAC;QACH,CAAC;QAED,sEAAsE;QACtE,qEAAqE;QACrE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI;YACxB,CAAC,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE;gBAC5B,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,MAAM,EAAE,MAAM,CAAC,MAAM;aACrB,CAAC;YACH,CAAC,CAAC,SAAS,CAAC;QAEb,MAAM,IAAI,GAAG,GAAe,EAAE;YAC7B,MAAM,MAAM,GAAG,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACnD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CACd,sEAAsE;oBACrE,wBAAwB;oBACxB,6BAA6B;oBAC7B,0CAA0C;oBAC1C,kEAAkE;oBAClE,SAAS;oBACT,KAAK,CACN,CAAC;YACH,CAAC;YACD,OAAO,MAAM,CAAC;QACf,CAAC,CAAC;QAEF,0EAA0E;QAC1E,iEAAiE;QACjE,yEAAyE;QACzE,oDAAoD;QACpD,WAAW,CAAC,IAAI,CAAC,CAAC;QAClB,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACnC,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE;YACtB,IAAI,KAAK;gBAAE,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;YAC/B,aAAa,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;IACF,0EAA0E;IAC1E,yEAAyE;IACzE,0BAA0B;IAC1B,OAAO,MAAuB,CAAC;AAChC,CAAC;AAED,IAAI,OAAuC,CAAC;AAE5C,wDAAwD;AACxD,SAAS,WAAW,CAAC,IAAsB;IAC1C,OAAO,GAAG,IAAI,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,IAAsB;IAC5C,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,GAAG,SAAS,CAAC;AAC3C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACd,4EAA4E;YAC3E,0EAA0E;YAC1E,0EAA0E;YAC1E,kBAAkB,CACnB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,EAAE,CAAC;AAClB,CAAC"}
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The AdonisJS stratification, kept intact: the FRAMEWORK reads its rc file and
5
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
6
+ * `adonisrc.ts` and hands them to helix. The runner itself (helix) knows nothing
7
7
  * about ream, and ream owns no test execution — it only translates.
8
8
  *
9
9
  * // bin/test.ts
@@ -16,7 +16,7 @@
16
16
  * to be re-imported in each worker, so the runner needs the module's PATH, not
17
17
  * just the object it exported. `runTests` takes it as `configModule`, and
18
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
19
+ * deliver — in helix a declared `configure` runs, so a run that skipped it would
20
20
  * be green in a state its own config does not describe.
21
21
  */
22
22
  import type { TestsConfig } from "@c9up/ream";
@@ -57,7 +57,7 @@ export declare class UnknownSuiteError extends Error {
57
57
  /**
58
58
  * A suite declares `configure`, but this entry point cannot deliver it.
59
59
  *
60
- * Not a warning: Japa runs a declared `configure`, so carrying on would run the
60
+ * Not a warning: helix runs a declared `configure`, so carrying on would run the
61
61
  * suite in a state its own config does not describe — and a warning is exactly
62
62
  * what gets scrolled past in CI.
63
63
  */
@@ -67,7 +67,7 @@ export declare class SuiteConfigureUnreachableError extends Error {
67
67
  /**
68
68
  * The flags the worker processes are spawned with.
69
69
  *
70
- * Split out because it is a decision, not plumbing: the Japa alias loader
70
+ * Split out because it is a decision, not plumbing: the helix alias loader
71
71
  * redirects a package specifier, so it goes in only when a project asks. It
72
72
  * rides ALONGSIDE whatever loader is already there — the test files still need
73
73
  * theirs to read TypeScript.
@@ -1 +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,CAEV;AAkBD;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAC7B,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAyGjB;AAgBD;;;GAGG;AACH,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
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,CAEV;AAkBD;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAC7B,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAyGjB;AA2BD;;;GAGG;AACH,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
package/dist/runTests.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The AdonisJS stratification, kept intact: the FRAMEWORK reads its rc file and
5
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
6
+ * `adonisrc.ts` and hands them to helix. The runner itself (helix) knows nothing
7
7
  * about ream, and ream owns no test execution — it only translates.
8
8
  *
9
9
  * // bin/test.ts
@@ -16,7 +16,7 @@
16
16
  * to be re-imported in each worker, so the runner needs the module's PATH, not
17
17
  * just the object it exported. `runTests` takes it as `configModule`, and
18
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
19
+ * deliver — in helix a declared `configure` runs, so a run that skipped it would
20
20
  * be green in a state its own config does not describe.
21
21
  */
22
22
  import path from "node:path";
@@ -34,7 +34,7 @@ export class UnknownSuiteError extends Error {
34
34
  /**
35
35
  * A suite declares `configure`, but this entry point cannot deliver it.
36
36
  *
37
- * Not a warning: Japa runs a declared `configure`, so carrying on would run the
37
+ * Not a warning: helix runs a declared `configure`, so carrying on would run the
38
38
  * suite in a state its own config does not describe — and a warning is exactly
39
39
  * what gets scrolled past in CI.
40
40
  */
@@ -51,7 +51,7 @@ export class SuiteConfigureUnreachableError extends Error {
51
51
  /**
52
52
  * The flags the worker processes are spawned with.
53
53
  *
54
- * Split out because it is a decision, not plumbing: the Japa alias loader
54
+ * Split out because it is a decision, not plumbing: the helix alias loader
55
55
  * redirects a package specifier, so it goes in only when a project asks. It
56
56
  * rides ALONGSIDE whatever loader is already there — the test files still need
57
57
  * theirs to read TypeScript.
@@ -106,7 +106,7 @@ export async function runTests(tests, options = {}) {
106
106
  // should not pay for.
107
107
  const configuring = selected.filter((suite) => typeof suite.configure === "function");
108
108
  if (configuring.length > 0 && options.configModule === undefined) {
109
- // In Japa a declared `configure` RUNS. It cannot here — the callback needs
109
+ // In helix a declared `configure` RUNS. It cannot here — the callback needs
110
110
  // the module's path to be re-imported in each worker, and this entry was
111
111
  // handed the exported object. Running anyway would produce a green suite
112
112
  // configured differently from what the rc file says, so the run stops.
@@ -126,7 +126,7 @@ export async function runTests(tests, options = {}) {
126
126
  bail: options.bail,
127
127
  };
128
128
  // `runnerHooks` run ONCE around the whole run, here, and the workers skip
129
- // them — Japa's semantics, and the difference between migrating once and
129
+ // them — helix's semantics, and the difference between migrating once and
130
130
  // migrating once per test file.
131
131
  const dropGlobalHooks = await helix.runGlobalHooks(bootstrap);
132
132
  // No suites declared: run whatever the project's discovery finds, so an app
@@ -134,7 +134,7 @@ export async function runTests(tests, options = {}) {
134
134
  if (selected.length === 0) {
135
135
  try {
136
136
  const outcome = await helix.run(base);
137
- return finish(outcome.exitCode, forceExit);
137
+ return finish(outcome.exitCode, forceExit, helix.armDrainGuard);
138
138
  }
139
139
  finally {
140
140
  await dropGlobalHooks();
@@ -164,27 +164,34 @@ export async function runTests(tests, options = {}) {
164
164
  }
165
165
  if (steps.length === 0) {
166
166
  await dropGlobalHooks();
167
- return finish(0, forceExit);
167
+ return finish(0, forceExit, helix.armDrainGuard);
168
168
  }
169
169
  try {
170
170
  const outcome = await helix.runSuites(steps, base);
171
- return finish(outcome.exitCode, forceExit);
171
+ return finish(outcome.exitCode, forceExit, helix.armDrainGuard);
172
172
  }
173
173
  finally {
174
174
  await dropGlobalHooks();
175
175
  }
176
176
  }
177
177
  /**
178
- * Apply `tests.forceExit`. Japa does this inside its own run — sets the exit
178
+ * Apply `tests.forceExit`. helix does this inside its own run — sets the exit
179
179
  * code, then `process.exit()` — rather than leaving it to the caller, because
180
180
  * the whole point is to not wait for the event loop to drain. A run that
181
181
  * force-exits never returns here; the value is for every other run.
182
182
  */
183
- function finish(code, forceExit) {
183
+ function finish(code, forceExit, armDrainGuard) {
184
184
  if (forceExit) {
185
185
  process.exitCode = code;
186
186
  process.exit();
187
187
  }
188
+ // Without forceExit the loop is left to drain on its own, which is the right
189
+ // default — a leaked handle should be visible, not swallowed. But visible has
190
+ // to mean SAID: `ream test` used to print its summary and then hang with no
191
+ // further output, and under a CI timeout that is a non-zero exit for a run in
192
+ // which every test passed. helix's own CLI already said so; this entry point
193
+ // did not, and it is the one `ream test` goes through.
194
+ armDrainGuard(code);
188
195
  return code;
189
196
  }
190
197
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"runTests.js","sourceRoot":"","sources":["../src/runTests.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runTests.js","sourceRoot":"","sources":["../src/runTests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAiC9C,2DAA2D;AAC3D,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC3C,YAAY,IAAY,EAAE,QAAkB;QAC3C,KAAK,CACJ,QAAQ,CAAC,MAAM,KAAK,CAAC;YACpB,CAAC,CAAC,uBAAuB,IAAI,+BAA+B;YAC5D,CAAC,CAAC,uBAAuB,IAAI,gBAAgB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACpE,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IACjC,CAAC;CACD;AAED;;;;;;GAMG;AACH,MAAM,OAAO,8BAA+B,SAAQ,KAAK;IACxD,YAAY,KAAe;QAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QAChC,KAAK,CACJ,QAAQ,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YAC1E,UAAU,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,sDAAsD;YACjF,2EAA2E;YAC3E,oDAAoD,CACrD,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,gCAAgC,CAAC;IAC9C,CAAC;CACD;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC7B,KAA8B,EAC9B,OAAwB;IAExB,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,wEAAwE;AACxE,SAAS,MAAM,CACd,QAA2B,EAC3B,KAAe;IAEf,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IACxC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACrE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACzB,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,iBAAiB,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACvD,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC7B,KAA8B,EAC9B,UAA2B,EAAE;IAE7B,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,MAAM,CAAC;IAE9B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3C,8EAA8E;IAC9E,4EAA4E;IAC5E,YAAY,CAAC,aAAa,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1E,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAM,IAAI,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IAExD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAEjD,4EAA4E;IAC5E,6EAA6E;IAC7E,mBAAmB;IACnB,MAAM,SAAS,GAAG,KAAK,CAAC,gBAAgB,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CAAC,eAAe,GAAG,SAAS,IAAI,EAAE,CAAC;IAC9C,0EAA0E;IAC1E,4EAA4E;IAC5E,MAAM,SAAS,GAAG,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAEpD,8EAA8E;IAC9E,0EAA0E;IAC1E,sBAAsB;IACtB,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAClC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,CAAC,SAAS,KAAK,UAAU,CAChD,CAAC;IACF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QAClE,4EAA4E;QAC5E,yEAAyE;QACzE,yEAAyE;QACzE,uEAAuE;QACvE,MAAM,IAAI,8BAA8B,CACvC,WAAW,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CACtC,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,kBAAkB;QAC7B,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS;YAC3D,CAAC,CAAC,OAAO,CAAC,YAAY;YACtB,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,CAAC,GAAG,CAAC,sBAAsB,GAAG,cAAc,CAAC;IAEpD,MAAM,IAAI,GAAG;QACZ,IAAI;QACJ,QAAQ,EAAE,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC;QACxC,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,SAAS,EAAE,KAAK,EAAE,OAAO;QACzB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI;KAClB,CAAC;IAEF,0EAA0E;IAC1E,0EAA0E;IAC1E,gCAAgC;IAChC,MAAM,eAAe,GAAG,MAAM,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;IAE9D,4EAA4E;IAC5E,oEAAoE;IACpE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,IAAI,CAAC;YACJ,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACtC,OAAO,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;QACjE,CAAC;gBAAS,CAAC;YACV,MAAM,eAAe,EAAE,CAAC;QACzB,CAAC;IACF,CAAC;IAED,MAAM,KAAK,GAAG,EAAE,CAAC;IACjB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,iBAAiB,CAC1C;YACC,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO;SACtB,EACD,IAAI,EACJ,SAAS,CACT,CAAC;QACF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,KAAK,CAAC,IAAI,oBAAoB,CAAC,CAAC;YACrE,SAAS;QACV,CAAC;QACD,KAAK,CAAC,IAAI,CAAC;YACV,GAAG,EAAE;gBACJ,WAAW,EAAE,KAAK,CAAC,IAAI;gBACvB,kEAAkE;gBAClE,gCAAgC;gBAChC,aAAa,EAAE,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;aACvE;YACD,MAAM,EAAE,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,OAAO,IAAI,KAAK,EAAE,OAAO,EAAE;SACtE,CAAC,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,eAAe,EAAE,CAAC;QACxB,OAAO,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IAClD,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACnD,OAAO,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IACjE,CAAC;YAAS,CAAC;QACV,MAAM,eAAe,EAAE,CAAC;IACzB,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACH,SAAS,MAAM,CACd,IAAY,EACZ,SAAkB,EAClB,aAAqC;IAErC,IAAI,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;QACxB,OAAO,CAAC,IAAI,EAAE,CAAC;IAChB,CAAC;IACD,6EAA6E;IAC7E,8EAA8E;IAC9E,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,uDAAuD;IACvD,aAAa,CAAC,IAAI,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,MAAc,EACd,UAA2B,EAAE;IAE7B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QACvC,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9B,MAAM,QAAQ,GAAY,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC;IACrE,MAAM,EAAE,GACP,QAAQ,KAAK,IAAI,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAChD,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,QAAQ,CAAC;QAChD,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,KAAK,GACV,EAAE,KAAK,IAAI,IAAI,OAAO,EAAE,KAAK,QAAQ;QACpC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC;QAC1B,CAAC,CAAC,SAAS,CAAC;IACd,OAAO,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,EAAE;QACzD,GAAG,OAAO;QACV,IAAI;QACJ,YAAY,EAAE,QAAQ;KACtB,CAAC,CAAC;AACJ,CAAC;AAED,6DAA6D;AAC7D,SAAS,aAAa,CAAC,KAAc;IACpC,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC;AACpD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/helix-plugin-ream",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Helix plugin for Ream — boots a Ream app under test and injects a TestClient on the test context",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,13 +17,13 @@
17
17
  }
18
18
  },
19
19
  "peerDependencies": {
20
- "@c9up/helix": "^0.2.0",
21
- "@c9up/ream": "^0.2.0"
20
+ "@c9up/helix": "^0.2.3",
21
+ "@c9up/ream": "^0.2.7"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@biomejs/biome": "^2.4.10",
25
- "@c9up/helix": "^0.2.0",
26
- "@c9up/ream": "^0.2.0",
25
+ "@c9up/helix": "^0.2.3",
26
+ "@c9up/ream": "^0.2.7",
27
27
  "@types/node": "^22.19.15",
28
28
  "tsx": "^4",
29
29
  "typescript": "^6.0.2",
package/src/index.ts CHANGED
@@ -6,10 +6,25 @@
6
6
  * client itself (`TestClient`, `createTestClient`) stays in `@c9up/ream/testing`
7
7
  * — it drives a Ream server and owes nothing to the runner.
8
8
  *
9
+ * The split follows AdonisJS: the plugin puts a CLIENT on the test context, and
10
+ * the SERVER is started by a suite hook, so a suite that does not declare the
11
+ * hook never starts one.
12
+ *
9
13
  * // tests/bootstrap.ts
10
14
  * import { configure } from '@c9up/helix'
11
15
  * import { apiClient } from '@c9up/helix-plugin-ream'
12
- * await configure({ plugins: [apiClient({ boot: () => bootApp() })] })
16
+ * import { createTestUtils } from '@c9up/ream/testing/utils'
17
+ *
18
+ * export const testUtils = createTestUtils((port) => bootApp(port))
19
+ *
20
+ * await configure({
21
+ * plugins: [apiClient({ testUtils })],
22
+ * configureSuite(suite) {
23
+ * if (['functional', 'e2e'].includes(suite.name)) {
24
+ * return suite.setup(() => testUtils.httpServer().start())
25
+ * }
26
+ * },
27
+ * })
13
28
  *
14
29
  * // a test
15
30
  * test('health', async ({ client }) => {
@@ -20,15 +35,28 @@
20
35
  import type { Plugin, PluginApi } from "@c9up/helix";
21
36
  import type { AuthStrategy, RouteManifest } from "@c9up/ream/testing";
22
37
  import { TestClient } from "@c9up/ream/testing";
38
+ import type { BootServer, TestUtils } from "@c9up/ream/testing/utils";
23
39
 
24
40
  /** The slice of helix's `PluginApi` this plugin actually uses. */
25
41
  export type ClientHost = Pick<PluginApi, "context" | "cleanup">;
26
42
 
27
43
  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 }>;
44
+ /**
45
+ * The utilities whose `httpServer()` a suite hook starts. The plugin reads
46
+ * the client of the server that hook started, so both point at one server.
47
+ */
48
+ testUtils?: TestUtils;
49
+ /**
50
+ * Boot the app directly, for a project that does not split its suites.
51
+ *
52
+ * A deviation from AdonisJS, where starting the server is always the suite
53
+ * hook's job. It exists because the server then starts on the client's FIRST
54
+ * REQUEST rather than at plugin time, so a file that issues none pays
55
+ * nothing — the same saving `testUtils` obtains by scoping, without having
56
+ * to split the suites first. Prefer `testUtils`; reach for this when the
57
+ * files are not separated.
58
+ */
59
+ boot?: BootServer;
32
60
  /** Warden auth strategy for `client.withAuth()`/`asUser()`. */
33
61
  auth?: AuthStrategy;
34
62
  /** Named-route manifest (`router.namedManifest()`) for `client.visit()`. */
@@ -36,21 +64,62 @@ export interface ApiClientConfig {
36
64
  }
37
65
 
38
66
  /**
39
- * Injects a booted {@link TestClient} on the test context as `ctx.client`.
67
+ * Injects a {@link TestClient} on the test context as `ctx.client`.
68
+ *
69
+ * With `testUtils`, the client is the one belonging to the server the suite
70
+ * hook started — so nothing is booted here, and a suite without the hook has no
71
+ * server at all. With `boot`, the client owns its server and starts it on the
72
+ * first request.
40
73
  *
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.
74
+ * Either way `api.cleanup` closes what this plugin started, which is nothing in
75
+ * the `testUtils` case the hook's own teardown owns that server.
44
76
  */
45
77
  export function apiClient(config: ApiClientConfig) {
46
78
  const plugin = async (api: ClientHost): Promise<void> => {
47
- const client = new TestClient(config.boot, {
48
- auth: config.auth,
49
- routes: config.routes,
79
+ if (!config.testUtils && !config.boot) {
80
+ throw new Error(
81
+ "[E_NO_SERVER] apiClient() needs either `testUtils` (the AdonisJS shape: " +
82
+ "a suite hook starts the server) or `boot` (the client starts one on its " +
83
+ "first request). It was given neither, so `ctx.client` would have had " +
84
+ "nothing to talk to.",
85
+ );
86
+ }
87
+
88
+ // Resolved per access, not once: with `testUtils` the server does not
89
+ // exist yet when the plugin runs — the suite's setup hook starts it.
90
+ const owned = config.boot
91
+ ? new TestClient(config.boot, {
92
+ auth: config.auth,
93
+ routes: config.routes,
94
+ })
95
+ : undefined;
96
+
97
+ const read = (): TestClient => {
98
+ const client = owned ?? config.testUtils?.client();
99
+ if (!client) {
100
+ throw new Error(
101
+ "[E_SERVER_NOT_STARTED] No server is running for this suite. Add the " +
102
+ "hook that starts it:\n" +
103
+ " configureSuite(suite) {\n" +
104
+ " if (suite.name === 'functional') {\n" +
105
+ " return suite.setup(() => testUtils.httpServer().start())\n" +
106
+ " }\n" +
107
+ " }",
108
+ );
109
+ }
110
+ return client;
111
+ };
112
+
113
+ // The resolver, not a client: with `testUtils` there is nothing to record
114
+ // yet, and `testClient()` must not depend on someone having read
115
+ // `ctx.client` first — a fixture or a runner hook has no context to read
116
+ // it from, which is the reason the accessor exists.
117
+ setResolver(read);
118
+ api.context.getter("client", read);
119
+ api.cleanup(async () => {
120
+ if (owned) await owned.close();
121
+ clearResolver(read);
50
122
  });
51
- await client.boot();
52
- api.context.macro("client", client);
53
- api.cleanup(() => client.close());
54
123
  };
55
124
  // A wider parameter than `PluginApi` stays assignable to `Plugin`, so the
56
125
  // plugin declares exactly what it touches and a caller can drive it with
@@ -58,6 +127,44 @@ export function apiClient(config: ApiClientConfig) {
58
127
  return plugin satisfies Plugin;
59
128
  }
60
129
 
130
+ let resolve: (() => TestClient) | undefined;
131
+
132
+ /** @internal Record how to reach the client in play. */
133
+ function setResolver(read: () => TestClient): void {
134
+ resolve = read;
135
+ }
136
+
137
+ /**
138
+ * @internal Forget it IF it is still the one recorded — a second `configure()`
139
+ * in the same process must not have the first one's teardown clear its client.
140
+ */
141
+ function clearResolver(read: () => TestClient): void {
142
+ if (resolve === read) resolve = undefined;
143
+ }
144
+
145
+ /**
146
+ * The client this run is using, reachable without a test context.
147
+ *
148
+ * `ctx.client` covers a test body. A helper module, a fixture, or a
149
+ * `runnerHooks` setup has no context to read it from and had to be handed one
150
+ * through a parameter every call site then had to thread. Same instance as
151
+ * `ctx.client` — the AdonisJS `services/test_utils` idiom.
152
+ *
153
+ * Throws when no client is in play, which is a wiring mistake rather than a
154
+ * state to handle.
155
+ */
156
+ export function testClient(): TestClient {
157
+ if (!resolve) {
158
+ throw new Error(
159
+ "[E_NO_TEST_CLIENT] No client is in play. Add `apiClient()` to the plugins " +
160
+ "in tests/bootstrap.ts, and — with the `testUtils` shape — make sure the " +
161
+ "suite's setup hook has started the server. This reads the client of the " +
162
+ "CURRENT process.",
163
+ );
164
+ }
165
+ return resolve();
166
+ }
167
+
61
168
  // Typing side of the plugin — importing it augments the helix test context.
62
169
  declare module "@c9up/helix" {
63
170
  interface TestContext {
package/src/runTests.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The AdonisJS stratification, kept intact: the FRAMEWORK reads its rc file and
5
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
6
+ * `adonisrc.ts` and hands them to helix. The runner itself (helix) knows nothing
7
7
  * about ream, and ream owns no test execution — it only translates.
8
8
  *
9
9
  * // bin/test.ts
@@ -16,7 +16,7 @@
16
16
  * to be re-imported in each worker, so the runner needs the module's PATH, not
17
17
  * just the object it exported. `runTests` takes it as `configModule`, and
18
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
19
+ * deliver — in helix a declared `configure` runs, so a run that skipped it would
20
20
  * be green in a state its own config does not describe.
21
21
  */
22
22
 
@@ -71,7 +71,7 @@ export class UnknownSuiteError extends Error {
71
71
  /**
72
72
  * A suite declares `configure`, but this entry point cannot deliver it.
73
73
  *
74
- * Not a warning: Japa runs a declared `configure`, so carrying on would run the
74
+ * Not a warning: helix runs a declared `configure`, so carrying on would run the
75
75
  * suite in a state its own config does not describe — and a warning is exactly
76
76
  * what gets scrolled past in CI.
77
77
  */
@@ -91,7 +91,7 @@ export class SuiteConfigureUnreachableError extends Error {
91
91
  /**
92
92
  * The flags the worker processes are spawned with.
93
93
  *
94
- * Split out because it is a decision, not plumbing: the Japa alias loader
94
+ * Split out because it is a decision, not plumbing: the helix alias loader
95
95
  * redirects a package specifier, so it goes in only when a project asks. It
96
96
  * rides ALONGSIDE whatever loader is already there — the test files still need
97
97
  * theirs to read TypeScript.
@@ -162,7 +162,7 @@ export async function runTests(
162
162
  (suite) => typeof suite.configure === "function",
163
163
  );
164
164
  if (configuring.length > 0 && options.configModule === undefined) {
165
- // In Japa a declared `configure` RUNS. It cannot here — the callback needs
165
+ // In helix a declared `configure` RUNS. It cannot here — the callback needs
166
166
  // the module's path to be re-imported in each worker, and this entry was
167
167
  // handed the exported object. Running anyway would produce a green suite
168
168
  // configured differently from what the rc file says, so the run stops.
@@ -186,7 +186,7 @@ export async function runTests(
186
186
  };
187
187
 
188
188
  // `runnerHooks` run ONCE around the whole run, here, and the workers skip
189
- // them — Japa's semantics, and the difference between migrating once and
189
+ // them — helix's semantics, and the difference between migrating once and
190
190
  // migrating once per test file.
191
191
  const dropGlobalHooks = await helix.runGlobalHooks(bootstrap);
192
192
 
@@ -195,7 +195,7 @@ export async function runTests(
195
195
  if (selected.length === 0) {
196
196
  try {
197
197
  const outcome = await helix.run(base);
198
- return finish(outcome.exitCode, forceExit);
198
+ return finish(outcome.exitCode, forceExit, helix.armDrainGuard);
199
199
  } finally {
200
200
  await dropGlobalHooks();
201
201
  }
@@ -229,28 +229,39 @@ export async function runTests(
229
229
  }
230
230
  if (steps.length === 0) {
231
231
  await dropGlobalHooks();
232
- return finish(0, forceExit);
232
+ return finish(0, forceExit, helix.armDrainGuard);
233
233
  }
234
234
 
235
235
  try {
236
236
  const outcome = await helix.runSuites(steps, base);
237
- return finish(outcome.exitCode, forceExit);
237
+ return finish(outcome.exitCode, forceExit, helix.armDrainGuard);
238
238
  } finally {
239
239
  await dropGlobalHooks();
240
240
  }
241
241
  }
242
242
 
243
243
  /**
244
- * Apply `tests.forceExit`. Japa does this inside its own run — sets the exit
244
+ * Apply `tests.forceExit`. helix does this inside its own run — sets the exit
245
245
  * code, then `process.exit()` — rather than leaving it to the caller, because
246
246
  * the whole point is to not wait for the event loop to drain. A run that
247
247
  * force-exits never returns here; the value is for every other run.
248
248
  */
249
- function finish(code: number, forceExit: boolean): number {
249
+ function finish(
250
+ code: number,
251
+ forceExit: boolean,
252
+ armDrainGuard: (code: number) => void,
253
+ ): number {
250
254
  if (forceExit) {
251
255
  process.exitCode = code;
252
256
  process.exit();
253
257
  }
258
+ // Without forceExit the loop is left to drain on its own, which is the right
259
+ // default — a leaked handle should be visible, not swallowed. But visible has
260
+ // to mean SAID: `ream test` used to print its summary and then hang with no
261
+ // further output, and under a CI timeout that is a non-zero exit for a run in
262
+ // which every test passed. helix's own CLI already said so; this entry point
263
+ // did not, and it is the one `ream test` goes through.
264
+ armDrainGuard(code);
254
265
  return code;
255
266
  }
256
267