@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/README.md +101 -35
- package/dist/ambiguity.js +126 -0
- package/dist/appium.adapter.js +399 -0
- package/dist/checker.js +0 -1
- package/dist/index.d.ts +500 -101
- package/dist/index.js +587 -464
- package/dist/intercept.js +37 -84
- package/dist/match.js +12 -1
- package/dist/oxlint.cjs +420 -117
- package/dist/oxlint.d.cts +1 -1
- package/dist/oxlint.d.ts +1 -1
- package/dist/oxlint.js +420 -117
- package/dist/playwright.adapter.js +3 -127
- package/dist/queue.js +555 -0
- package/package.json +13 -4
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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
|
|
95
|
-
if (
|
|
49
|
+
async function registerContracts(contracts) {
|
|
50
|
+
if (contracts.length === 0) return {
|
|
96
51
|
cleanup: () => {},
|
|
97
52
|
violation: () => null
|
|
98
53
|
};
|
|
99
|
-
await
|
|
54
|
+
await ensureContractServer();
|
|
100
55
|
const { msw } = await loadMsw();
|
|
101
|
-
const queue = new
|
|
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
|
|
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
|
|
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
|
|
129
|
-
if (!
|
|
130
|
-
const response = typeof
|
|
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
|
|
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 {
|
|
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. */
|