@c9up/helix-plugin-ream 0.1.3 → 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/dist/index.d.ts +53 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +94 -11
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +122 -15
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
|
-
*
|
|
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
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
*
|
|
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
|
|
36
|
+
* Injects a {@link TestClient} on the test context as `ctx.client`.
|
|
22
37
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/helix-plugin-ream",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@c9up/helix": "^0.2.3",
|
|
21
|
-
"@c9up/ream": "^0.2.
|
|
21
|
+
"@c9up/ream": "^0.2.7"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@biomejs/biome": "^2.4.10",
|
|
25
25
|
"@c9up/helix": "^0.2.3",
|
|
26
|
-
"@c9up/ream": "^0.2.
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
*
|
|
42
|
-
*
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 {
|