@jterrazz/test 10.0.0 → 11.0.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,3 +1,4 @@
1
+ import { t as ContractQueue } from "./queue.js";
1
2
  //#region src/integrations/msw/intercept.ts
2
3
  let mswModule = null;
3
4
  let mswHttp = null;
@@ -15,100 +16,54 @@ let serverInstance = null;
15
16
  /**
16
17
  * Start the MSW server (once per process).
17
18
  */
18
- async function ensureInterceptServer() {
19
+ async function ensureContractServer() {
19
20
  if (serverInstance) return;
20
21
  const { node } = await loadMsw();
21
22
  serverInstance = node.setupServer();
22
23
  serverInstance.listen({ onUnhandledRequest: "bypass" });
23
24
  }
24
- function describeTrigger(entry) {
25
- return `${entry.trigger.method === "*" ? "ANY" : entry.trigger.method} ${String(entry.trigger.url)}`;
26
- }
27
- /**
28
- * Pure FIFO queue over the chain's intercept entries. Each observed request
29
- * consumes the first unconsumed entry whose URL, method, and optional body
30
- * matcher accept it. Exported for unit tests — MSW never reaches this class.
31
- */
32
- var InterceptQueue = class {
33
- consumed;
34
- entries;
35
- constructor(entries) {
36
- this.entries = entries;
37
- this.consumed = entries.map(() => false);
38
- }
39
- /**
40
- * Unique trigger URLs, preserving RegExp/string identity for MSW routing.
41
- * Deduped by string form — two RegExp objects with the same source are
42
- * one trigger (and get one handler).
43
- */
44
- get urls() {
45
- const urls = /* @__PURE__ */ new Map();
46
- for (const entry of this.entries) {
47
- const key = String(entry.trigger.url);
48
- if (!urls.has(key)) urls.set(key, entry.trigger.url);
49
- }
50
- return [...urls.values()];
51
- }
52
- /** Methods declared for a given trigger URL. */
53
- methodsFor(url) {
54
- const methods = /* @__PURE__ */ new Set();
55
- for (const entry of this.entries) if (sameUrl(entry.trigger.url, url)) methods.add(entry.trigger.method);
56
- return [...methods];
57
- }
58
- /**
59
- * Consume and return the first unconsumed entry registered for
60
- * `handlerUrl`/`method` that accepts the request, or null when the queue
61
- * for that trigger is exhausted (or no matcher accepts).
62
- */
63
- take(handlerUrl, method, request) {
64
- for (let i = 0; i < this.entries.length; i++) {
65
- if (this.consumed[i]) continue;
66
- const entry = this.entries[i];
67
- if (!sameUrl(entry.trigger.url, handlerUrl)) continue;
68
- if (entry.trigger.method !== method && entry.trigger.method !== "*") continue;
69
- if (entry.trigger.match && !entry.trigger.match(request)) continue;
70
- this.consumed[i] = true;
71
- return entry;
72
- }
73
- return null;
74
- }
75
- /**
76
- * Build the strict-intercept failure for a request that matched no
77
- * registered intercept (CONVENTIONS D7): method + URL of the offending
78
- * request, plus every registered trigger and its consumption state.
79
- */
80
- unmatchedError(method, url) {
81
- const triggers = this.entries.length === 0 ? " (no intercepts registered)" : this.entries.map((entry, i) => ` - ${describeTrigger(entry)}${this.consumed[i] ? " (already consumed)" : ""}`).join("\n");
82
- return /* @__PURE__ */ new Error(`Unmatched outgoing HTTP request during spec: ${method} ${url}\nRegistered intercepts:\n${triggers}\nEvery outgoing request of a chain that declares intercepts must match one — add an .intercept() for it (or one more if its queue is exhausted).`);
83
- }
84
- };
85
- function sameUrl(a, b) {
86
- return a === b || String(a) === String(b);
25
+ /** Turn a contract response into the MSW reply, body kind by body kind. */
26
+ function toMswResponse(msw, response) {
27
+ const { body, headers, status = 200 } = response;
28
+ if (body === null || body === void 0) return new msw.HttpResponse(null, {
29
+ headers,
30
+ status
31
+ });
32
+ if (typeof body === "string") return new msw.HttpResponse(body, {
33
+ headers: {
34
+ "content-type": "text/plain; charset=utf-8",
35
+ ...headers
36
+ },
37
+ status
38
+ });
39
+ return msw.HttpResponse.json(body, {
40
+ headers,
41
+ status
42
+ });
87
43
  }
88
44
  /**
89
- * Register intercept entries as MSW handlers for one chain. A trailing
90
- * catch-all handler records any request that no entry accepted — the
91
- * builder rethrows it as the spec failure (rejecting the action promise,
92
- * never an unhandled rejection).
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).
93
48
  */
94
- async function registerIntercepts(entries) {
95
- if (entries.length === 0) return {
49
+ async function registerContracts(contracts) {
50
+ if (contracts.length === 0) return {
96
51
  cleanup: () => {},
97
52
  violation: () => null
98
53
  };
99
- await ensureInterceptServer();
54
+ await ensureContractServer();
100
55
  const { msw } = await loadMsw();
101
- const queue = new InterceptQueue(entries);
56
+ const queue = new ContractQueue(contracts);
102
57
  let violation = null;
103
58
  const recordViolation = (method, url) => {
104
59
  violation ??= queue.unmatchedError(method, url);
105
- return msw.HttpResponse.json({ error: `@jterrazz/test strict intercepts: unmatched request ${method} ${url}` }, { status: 501 });
60
+ return msw.HttpResponse.json({ error: `@jterrazz/test strict contracts: unmatched request ${method} ${url}` }, { status: 501 });
106
61
  };
107
62
  const handlers = [];
108
- for (const url of queue.urls) for (const method of queue.methodsFor(url)) {
63
+ for (const route of queue.routes) for (const method of route.methods) {
109
64
  const handlerFn = method === "*" ? msw.http.all : msw.http[method.toLowerCase()];
110
65
  if (!handlerFn) continue;
111
- const handler = handlerFn(url, async ({ request }) => {
66
+ const handler = handlerFn(route.url, async ({ request }) => {
112
67
  let body = null;
113
68
  const rawText = await request.clone().text();
114
69
  if (rawText) try {
@@ -123,16 +78,14 @@ async function registerIntercepts(entries) {
123
78
  const observed = {
124
79
  body,
125
80
  headers,
81
+ method: request.method.toUpperCase(),
126
82
  url: request.url
127
83
  };
128
- const entry = queue.take(url, request.method, observed);
129
- if (!entry) return recordViolation(request.method, request.url);
130
- const response = typeof entry.response === "function" ? entry.response(observed) : entry.response;
84
+ const contract = queue.take(observed);
85
+ if (!contract) return recordViolation(request.method, request.url);
86
+ const response = typeof contract.response === "function" ? contract.response(observed) : contract.response;
131
87
  if (response.delay) await new Promise((r) => setTimeout(r, response.delay));
132
- return msw.HttpResponse.json(response.body, {
133
- status: response.status ?? 200,
134
- headers: response.headers
135
- });
88
+ return toMswResponse(msw, response);
136
89
  });
137
90
  handlers.push(handler);
138
91
  }
@@ -142,8 +95,8 @@ async function registerIntercepts(entries) {
142
95
  cleanup: () => {
143
96
  serverInstance.resetHandlers();
144
97
  },
145
- violation: () => violation
98
+ violation: () => violation ?? queue.requiredError()
146
99
  };
147
100
  }
148
101
  //#endregion
149
- export { registerIntercepts };
102
+ export { registerContracts };
package/dist/match.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region src/core/matching/match.ts
2
- /** Every kind usable as a `{{token}}` in fixture files (all but ref/regex). */
2
+ /** Every kind usable as a `{{token}}` in fixture files (all but ref/regex/includes). */
3
3
  const TOKEN_KINDS = [
4
4
  "any",
5
5
  "base64",
@@ -28,6 +28,8 @@ const TOKEN_KINDS = [
28
28
  * constructed directly by user code.
29
29
  */
30
30
  var Matcher = class {
31
+ /** For kind 'includes': the substring the actual value must contain. */
32
+ includes;
31
33
  kind;
32
34
  /** For kind 'ref': the capture to assert inequality against. */
33
35
  notRef;
@@ -36,6 +38,7 @@ var Matcher = class {
36
38
  /** For kind 'regex': the pattern the actual value must match. */
37
39
  regex;
38
40
  constructor(kind, options = {}) {
41
+ this.includes = options.includes;
39
42
  this.kind = kind;
40
43
  this.notRef = options.notRef;
41
44
  this.refName = options.refName;
@@ -44,6 +47,7 @@ var Matcher = class {
44
47
  /** Placeholder-style rendering used in failure diffs and serialization. */
45
48
  toString() {
46
49
  switch (this.kind) {
50
+ case "includes": return `{{includes:${this.includes}}}`;
47
51
  case "ref": return this.notRef ? `{{ref#${this.refName}!${this.notRef}}}` : `{{ref#${this.refName}}}`;
48
52
  case "regex": return `{{regex:${this.regex?.source}}}`;
49
53
  default: return this.refName ? `{{${this.kind}#${this.refName}}}` : `{{${this.kind}}}`;
@@ -83,6 +87,13 @@ const match = {
83
87
  float: typed("float"),
84
88
  /** Matches a hexadecimal string. */
85
89
  hex: typed("hex"),
90
+ /**
91
+ * Matches a string CONTAINING the given substring. Code-only, like
92
+ * {@link match.regex} — the `{{token}}` fixture vocabulary does not grow.
93
+ * The explicit escape hatch now that contract string filters
94
+ * (`openai.chat({ user })`) mean exact equality.
95
+ */
96
+ includes: (substring) => new Matcher("includes", { includes: substring }),
86
97
  /** Matches an integer (or an integer string in text contexts). */
87
98
  int: typed("int"),
88
99
  /** Matches an IPv4 address. */