@jterrazz/test 15.1.1 → 15.2.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/dist/intercept.js CHANGED
@@ -1,101 +1,151 @@
1
1
  import { t as ContractQueue } from "./queue.js";
2
- //#region src/integrations/msw/intercept.ts
3
- let mswModule = null;
4
- let mswHttp = null;
5
- async function loadMsw() {
6
- if (!mswModule) {
7
- mswModule = await import("msw/node");
8
- mswHttp = await import("msw");
9
- }
10
- return {
11
- node: mswModule,
12
- msw: mswHttp
13
- };
14
- }
15
- let serverInstance = null;
2
+ //#region src/integrations/msw/handlers.ts
16
3
  /**
17
- * Start the MSW server (once per process).
4
+ * What a chain with zero contracts gets where the facet's scope is KNOWN but
5
+ * not total — the node engine, which shares a process with everything else the
6
+ * test does. Nothing was declared, so nothing is guarded and nothing has to be
7
+ * torn down (CONVENTIONS D7, known scope).
18
8
  */
19
- async function ensureContractServer() {
20
- if (serverInstance) return;
21
- const { node } = await loadMsw();
22
- serverInstance = node.setupServer();
23
- serverInstance.listen({ onUnhandledRequest: "bypass" });
24
- }
25
- /** Turn a contract response into the MSW reply, body kind by body kind. */
9
+ const NO_CONTRACTS = {
10
+ cleanup: () => {},
11
+ violation: () => null
12
+ };
13
+ /**
14
+ * A contract answers the request that was made, never a copy of an earlier one.
15
+ *
16
+ * In a page the reply travels through a real service worker and a real HTTP
17
+ * cache, so a second render of the same component can be served the FIRST
18
+ * test's answer and never reach the queue at all — a contract that was
19
+ * exhausted, or replaced, silently keeps answering. Stated as a default, so a
20
+ * spec whose subject IS caching can still say otherwise.
21
+ */
22
+ const NEVER_CACHED = { "cache-control": "no-store" };
23
+ /** Turn a contract response into the msw reply, body kind by body kind. */
26
24
  function toMswResponse(msw, response) {
27
25
  const { body, headers, status = 200 } = response;
26
+ if (response.transport === "network-error") return msw.HttpResponse.error();
28
27
  if (body === null || body === void 0) return new msw.HttpResponse(null, {
29
- headers,
28
+ headers: {
29
+ ...NEVER_CACHED,
30
+ ...headers
31
+ },
30
32
  status
31
33
  });
32
34
  if (typeof body === "string") return new msw.HttpResponse(body, {
33
35
  headers: {
36
+ ...NEVER_CACHED,
34
37
  "content-type": "text/plain; charset=utf-8",
35
38
  ...headers
36
39
  },
37
40
  status
38
41
  });
39
42
  return msw.HttpResponse.json(body, {
40
- headers,
43
+ headers: {
44
+ ...NEVER_CACHED,
45
+ ...headers
46
+ },
41
47
  status
42
48
  });
43
49
  }
44
50
  /**
45
- * Register a chain's contracts as MSW handlers. A trailing catch-all handler
46
- * records any request no contract accepted — the builder rethrows it as the
47
- * spec failure (rejecting the action promise, never an unhandled rejection).
51
+ * The handlers one chain's contracts become, plus the trailing catch-all that
52
+ * records any request no contract accepted (CONVENTIONS D7). The caller decides
53
+ * what to do with the recorded violation — the api chain rethrows it, the
54
+ * component chain fails the render. An EMPTY contract list is still a guard:
55
+ * the catch-all alone says "this subject has no network".
56
+ *
57
+ * `bypass` names the traffic that is not the subject's: under node there is
58
+ * none, in a page it is everything the runner fetches to BE a page.
48
59
  */
49
- async function registerContracts(contracts) {
50
- if (contracts.length === 0) return {
51
- cleanup: () => {},
52
- violation: () => null
53
- };
54
- await ensureContractServer();
55
- const { msw } = await loadMsw();
60
+ function buildContractHandlers(msw, contracts, bypass) {
56
61
  const queue = new ContractQueue(contracts);
57
62
  let violation = null;
58
63
  const recordViolation = (method, url) => {
59
64
  violation ??= queue.unmatchedError(method, url);
60
- return msw.HttpResponse.json({ error: `@jterrazz/test strict contracts: unmatched request ${method} ${url}` }, { status: 501 });
65
+ return msw.HttpResponse.json({ error: `@jterrazz/test strict contracts: unmatched request ${method} ${url}` }, {
66
+ headers: NEVER_CACHED,
67
+ status: 501
68
+ });
61
69
  };
62
70
  const handlers = [];
63
71
  for (const route of queue.routes) for (const method of route.methods) {
64
72
  const handlerFn = method === "*" ? msw.http.all : msw.http[method.toLowerCase()];
65
73
  if (!handlerFn) continue;
66
- const handler = handlerFn(route.url, async ({ request }) => {
67
- let body = null;
68
- const rawText = await request.clone().text();
69
- if (rawText) try {
70
- body = JSON.parse(rawText);
71
- } catch {
72
- body = rawText;
73
- }
74
- const headers = {};
75
- request.headers.forEach((value, key) => {
76
- headers[key.toLowerCase()] = value;
77
- });
78
- const observed = {
79
- body,
80
- headers,
81
- method: request.method.toUpperCase(),
82
- url: request.url
83
- };
74
+ handlers.push(handlerFn(route.url, async ({ request }) => {
75
+ const observed = await observe(request);
84
76
  const contract = queue.take(observed);
85
77
  if (!contract) return recordViolation(request.method, request.url);
86
78
  const response = typeof contract.response === "function" ? contract.response(observed) : contract.response;
87
- if (response.delay) await new Promise((r) => setTimeout(r, response.delay));
79
+ if (response.delay) await new Promise((resolve) => setTimeout(resolve, response.delay));
88
80
  return toMswResponse(msw, response);
89
- });
90
- handlers.push(handler);
81
+ }));
91
82
  }
92
- handlers.push(msw.http.all("*", ({ request }) => recordViolation(request.method, request.url)));
83
+ handlers.push(msw.http.all("*", ({ request }) => bypass?.(request.url) === true ? msw.passthrough() : recordViolation(request.method, request.url)));
84
+ return {
85
+ handlers,
86
+ violation: () => violation ?? queue.requiredError()
87
+ };
88
+ }
89
+ /** The request as the queue matches it — body parsed when it is JSON. */
90
+ async function observe(request) {
91
+ let body = null;
92
+ const rawText = await request.clone().text();
93
+ if (rawText) try {
94
+ body = JSON.parse(rawText);
95
+ } catch {
96
+ body = rawText;
97
+ }
98
+ const headers = {};
99
+ request.headers.forEach((value, key) => {
100
+ headers[key.toLowerCase()] = value;
101
+ });
102
+ return {
103
+ body,
104
+ headers,
105
+ method: request.method.toUpperCase(),
106
+ url: request.url
107
+ };
108
+ }
109
+ //#endregion
110
+ //#region src/integrations/msw/intercept.ts
111
+ let mswModule = null;
112
+ let mswHttp = null;
113
+ async function loadMsw() {
114
+ if (mswModule === null) {
115
+ mswModule = await import("msw/node");
116
+ mswHttp = await import("msw");
117
+ }
118
+ return {
119
+ msw: mswHttp,
120
+ node: mswModule
121
+ };
122
+ }
123
+ let serverInstance = null;
124
+ /**
125
+ * Start the MSW server (once per process).
126
+ */
127
+ async function ensureContractServer() {
128
+ if (serverInstance !== null) return;
129
+ const { node } = await loadMsw();
130
+ serverInstance = node.setupServer();
131
+ serverInstance.listen({ onUnhandledRequest: "bypass" });
132
+ }
133
+ /**
134
+ * Register a chain's contracts as MSW handlers. A trailing catch-all handler
135
+ * records any request no contract accepted — the builder rethrows it as the
136
+ * spec failure (rejecting the action promise, never an unhandled rejection).
137
+ */
138
+ async function registerContracts(contracts) {
139
+ if (contracts.length === 0) return NO_CONTRACTS;
140
+ await ensureContractServer();
141
+ const { msw } = await loadMsw();
142
+ const { handlers, violation } = buildContractHandlers(msw, contracts);
93
143
  serverInstance.use(...handlers);
94
144
  return {
95
145
  cleanup: () => {
96
146
  serverInstance.resetHandlers();
97
147
  },
98
- violation: () => violation ?? queue.requiredError()
148
+ violation
99
149
  };
100
150
  }
101
151
  //#endregion