@glubean/sdk 0.1.39 → 0.2.1
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/configure.d.ts +4 -21
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +19 -145
- package/dist/configure.js.map +1 -1
- package/dist/contract-core.d.ts +98 -0
- package/dist/contract-core.d.ts.map +1 -0
- package/dist/contract-core.js +749 -0
- package/dist/contract-core.js.map +1 -0
- package/dist/contract-http/adapter.d.ts +52 -0
- package/dist/contract-http/adapter.d.ts.map +1 -0
- package/dist/contract-http/adapter.js +650 -0
- package/dist/contract-http/adapter.js.map +1 -0
- package/dist/contract-http/factory.d.ts +32 -0
- package/dist/contract-http/factory.d.ts.map +1 -0
- package/dist/contract-http/factory.js +83 -0
- package/dist/contract-http/factory.js.map +1 -0
- package/dist/contract-http/flow-helpers.d.ts +12 -0
- package/dist/contract-http/flow-helpers.d.ts.map +1 -0
- package/dist/contract-http/flow-helpers.js +34 -0
- package/dist/contract-http/flow-helpers.js.map +1 -0
- package/dist/contract-http/index.d.ts +16 -0
- package/dist/contract-http/index.d.ts.map +1 -0
- package/dist/contract-http/index.js +15 -0
- package/dist/contract-http/index.js.map +1 -0
- package/dist/contract-http/markdown.d.ts +10 -0
- package/dist/contract-http/markdown.d.ts.map +1 -0
- package/dist/contract-http/markdown.js +21 -0
- package/dist/contract-http/markdown.js.map +1 -0
- package/dist/contract-http/openapi.d.ts +15 -0
- package/dist/contract-http/openapi.d.ts.map +1 -0
- package/dist/contract-http/openapi.js +38 -0
- package/dist/contract-http/openapi.js.map +1 -0
- package/dist/contract-http/types.d.ts +252 -0
- package/dist/contract-http/types.d.ts.map +1 -0
- package/dist/contract-http/types.js +13 -0
- package/dist/contract-http/types.js.map +1 -0
- package/dist/contract-types.d.ts +420 -467
- package/dist/contract-types.d.ts.map +1 -1
- package/dist/contract-types.js +16 -4
- package/dist/contract-types.js.map +1 -1
- package/dist/each-builder.d.ts +244 -0
- package/dist/each-builder.d.ts.map +1 -0
- package/dist/each-builder.js +268 -0
- package/dist/each-builder.js.map +1 -0
- package/dist/index.d.ts +30 -513
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -826
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -1
- package/dist/runtime-carrier.d.ts +142 -0
- package/dist/runtime-carrier.d.ts.map +1 -0
- package/dist/runtime-carrier.js +148 -0
- package/dist/runtime-carrier.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -1
- package/dist/session.js.map +1 -1
- package/dist/test-builder.d.ts +249 -0
- package/dist/test-builder.d.ts.map +1 -0
- package/dist/test-builder.js +265 -0
- package/dist/test-builder.js.map +1 -0
- package/dist/test-extend.d.ts +59 -0
- package/dist/test-extend.d.ts.map +1 -0
- package/dist/test-extend.js +111 -0
- package/dist/test-extend.js.map +1 -0
- package/dist/test-utils.d.ts +39 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +91 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/types.d.ts +41 -122
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/contract.d.ts +0 -64
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js +0 -793
- package/dist/contract.js.map +0 -1
package/dist/contract.js
DELETED
|
@@ -1,793 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* contract.http() — declarative API contract testing.
|
|
3
|
-
*
|
|
4
|
-
* Spec in, Test[] out. Each case becomes a runnable Test.
|
|
5
|
-
* The return value extends Array<Test> so runner/resolve handles it natively.
|
|
6
|
-
*/
|
|
7
|
-
import { registerTest } from "./internal.js";
|
|
8
|
-
// =============================================================================
|
|
9
|
-
// HttpContractImpl — extends Array<Test> with contract-level methods
|
|
10
|
-
// =============================================================================
|
|
11
|
-
/**
|
|
12
|
-
* Create an HttpContract — a Test[] with contract-level properties.
|
|
13
|
-
* Uses Object.assign on a plain array to avoid class-extends-Array pitfalls.
|
|
14
|
-
*/
|
|
15
|
-
function createHttpContract(id, endpoint, tests, request, description, feature, caseSchemas, instanceName, security, deprecated, extensions, requestContentType, requestHeaders, requestExample, requestExamples) {
|
|
16
|
-
const arr = [...tests];
|
|
17
|
-
const asSteps = () => {
|
|
18
|
-
return (b) => {
|
|
19
|
-
for (const t of arr) {
|
|
20
|
-
if (t.fn) {
|
|
21
|
-
b.step(t.meta.name ?? t.meta.id, async (ctx) => {
|
|
22
|
-
await t.fn(ctx);
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return b;
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
const asStep = (caseKey) => {
|
|
30
|
-
const target = caseKey
|
|
31
|
-
? arr.find((t) => t.meta.id.endsWith(`.${caseKey}`))
|
|
32
|
-
: arr.find((t) => t.fn !== undefined && !t.meta.deferred && !t.meta.deprecated);
|
|
33
|
-
if (!target)
|
|
34
|
-
throw new Error(`Case "${caseKey ?? "default"}" not found in contract "${id}"`);
|
|
35
|
-
return (b) => {
|
|
36
|
-
b.step(target.meta.name ?? target.meta.id, async (ctx) => {
|
|
37
|
-
await target.fn(ctx);
|
|
38
|
-
});
|
|
39
|
-
return b;
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
return Object.assign(arr, {
|
|
43
|
-
id, endpoint, request, description, feature,
|
|
44
|
-
instanceName, security, deprecated, extensions,
|
|
45
|
-
requestContentType, requestHeaders, requestExample, requestExamples,
|
|
46
|
-
_caseSchemas: caseSchemas,
|
|
47
|
-
asSteps, asStep,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
// =============================================================================
|
|
51
|
-
// Parse endpoint string
|
|
52
|
-
// =============================================================================
|
|
53
|
-
function parseEndpoint(endpoint) {
|
|
54
|
-
const spaceIdx = endpoint.indexOf(" ");
|
|
55
|
-
if (spaceIdx === -1)
|
|
56
|
-
return { method: "GET", path: endpoint };
|
|
57
|
-
return {
|
|
58
|
-
method: endpoint.slice(0, spaceIdx).toUpperCase(),
|
|
59
|
-
path: endpoint.slice(spaceIdx + 1),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Extract the string value from a ParamValue (string or { value } object).
|
|
64
|
-
*/
|
|
65
|
-
function extractParamValue(v) {
|
|
66
|
-
if (typeof v === "string")
|
|
67
|
-
return v;
|
|
68
|
-
if (v && typeof v === "object" && "value" in v) {
|
|
69
|
-
return String(v.value);
|
|
70
|
-
}
|
|
71
|
-
return String(v);
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Convert a params/query Record<string, ParamValue> to Record<string, string>
|
|
75
|
-
* by extracting `.value` from ParamValue objects.
|
|
76
|
-
*/
|
|
77
|
-
function flattenParamValues(params) {
|
|
78
|
-
if (!params)
|
|
79
|
-
return undefined;
|
|
80
|
-
const result = {};
|
|
81
|
-
for (const [key, value] of Object.entries(params)) {
|
|
82
|
-
result[key] = extractParamValue(value);
|
|
83
|
-
}
|
|
84
|
-
return result;
|
|
85
|
-
}
|
|
86
|
-
function resolveParams(path, params) {
|
|
87
|
-
if (!params)
|
|
88
|
-
return path;
|
|
89
|
-
let resolved = path;
|
|
90
|
-
for (const [key, value] of Object.entries(params)) {
|
|
91
|
-
resolved = resolved.replace(`:${key}`, encodeURIComponent(value));
|
|
92
|
-
}
|
|
93
|
-
return resolved;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Field names on the structured RequestSpec form. If any of these exist on `req`,
|
|
97
|
-
* we treat it as structured; otherwise we treat it as a bare SchemaLike body shorthand.
|
|
98
|
-
* This is the authoritative disambiguator — do NOT rely on probing SchemaLike methods
|
|
99
|
-
* (safeParse/parse), because SchemaLike allows either one to be present.
|
|
100
|
-
*/
|
|
101
|
-
const STRUCTURED_REQUEST_FIELDS = ["body", "contentType", "headers", "example", "examples"];
|
|
102
|
-
/**
|
|
103
|
-
* Normalize RequestSpec to a structured form { body, contentType, headers, example, examples }.
|
|
104
|
-
* Accepts either bare SchemaLike (treated as JSON body) or already-structured object.
|
|
105
|
-
*
|
|
106
|
-
* Disambiguation rule: structured form is recognized by presence of any known
|
|
107
|
-
* structured field. Otherwise the input is a SchemaLike (either safeParse-only
|
|
108
|
-
* or parse-only — both are valid).
|
|
109
|
-
*/
|
|
110
|
-
function normalizeRequest(req) {
|
|
111
|
-
if (!req || typeof req !== "object")
|
|
112
|
-
return undefined;
|
|
113
|
-
// Structured form: has any of the known structured fields
|
|
114
|
-
const hasStructuredField = STRUCTURED_REQUEST_FIELDS.some((f) => f in req);
|
|
115
|
-
if (hasStructuredField) {
|
|
116
|
-
return req;
|
|
117
|
-
}
|
|
118
|
-
// Otherwise treat as SchemaLike (safeParse-only or parse-only)
|
|
119
|
-
return { body: req };
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Build request options based on content type. Supports:
|
|
123
|
-
* - application/json (default) — body → requestOptions.json
|
|
124
|
-
* - multipart/form-data — body (FormData | object) → requestOptions.body
|
|
125
|
-
* - application/x-www-form-urlencoded — body (URLSearchParams | object) → requestOptions.body
|
|
126
|
-
* - text/plain, application/octet-stream — body → requestOptions.body (string/binary)
|
|
127
|
-
*/
|
|
128
|
-
function buildRequestBodyOptions(body, contentType) {
|
|
129
|
-
if (body === undefined)
|
|
130
|
-
return {};
|
|
131
|
-
const ct = (contentType ?? "application/json").toLowerCase();
|
|
132
|
-
if (ct.startsWith("application/json")) {
|
|
133
|
-
return { json: body };
|
|
134
|
-
}
|
|
135
|
-
if (ct.startsWith("multipart/form-data")) {
|
|
136
|
-
// If body is already FormData, pass through. Otherwise, convert object to FormData.
|
|
137
|
-
if (typeof FormData !== "undefined" && body instanceof FormData) {
|
|
138
|
-
return { body };
|
|
139
|
-
}
|
|
140
|
-
if (body && typeof body === "object") {
|
|
141
|
-
const fd = new FormData();
|
|
142
|
-
for (const [k, v] of Object.entries(body)) {
|
|
143
|
-
if (v instanceof Blob || v instanceof File) {
|
|
144
|
-
fd.append(k, v);
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
fd.append(k, String(v));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return { body: fd };
|
|
151
|
-
}
|
|
152
|
-
return { body };
|
|
153
|
-
}
|
|
154
|
-
if (ct.startsWith("application/x-www-form-urlencoded")) {
|
|
155
|
-
if (body instanceof URLSearchParams)
|
|
156
|
-
return { body };
|
|
157
|
-
if (body && typeof body === "object") {
|
|
158
|
-
const params = new URLSearchParams();
|
|
159
|
-
for (const [k, v] of Object.entries(body)) {
|
|
160
|
-
params.append(k, String(v));
|
|
161
|
-
}
|
|
162
|
-
return { body: params };
|
|
163
|
-
}
|
|
164
|
-
return { body };
|
|
165
|
-
}
|
|
166
|
-
// text/plain, application/octet-stream, other — raw pass-through
|
|
167
|
-
// Set Content-Type header if not already present (handled via headers in caller)
|
|
168
|
-
return { body };
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Normalize response headers to lowercase keys + preserve multi-value shape.
|
|
172
|
-
* HTTP spec: header names are case-insensitive. Some headers (Set-Cookie) can have multiple values.
|
|
173
|
-
*/
|
|
174
|
-
function normalizeResponseHeaders(headers) {
|
|
175
|
-
const result = {};
|
|
176
|
-
if (!headers)
|
|
177
|
-
return result;
|
|
178
|
-
// Handle Headers object (fetch / ky response headers)
|
|
179
|
-
if (typeof headers.forEach === "function" && typeof headers.get === "function") {
|
|
180
|
-
// Use .forEach which handles multi-value headers by concatenation
|
|
181
|
-
headers.forEach((value, key) => {
|
|
182
|
-
const lowerKey = key.toLowerCase();
|
|
183
|
-
// Set-Cookie is the common multi-value case; split comma for best-effort
|
|
184
|
-
if (lowerKey === "set-cookie") {
|
|
185
|
-
const existing = result[lowerKey];
|
|
186
|
-
const newValue = typeof existing === "string" ? [existing, value] : [...(existing ?? []), value];
|
|
187
|
-
result[lowerKey] = newValue;
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
result[lowerKey] = value;
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
return result;
|
|
194
|
-
}
|
|
195
|
-
// Handle plain object
|
|
196
|
-
if (typeof headers === "object") {
|
|
197
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
198
|
-
const lowerKey = key.toLowerCase();
|
|
199
|
-
if (Array.isArray(value)) {
|
|
200
|
-
result[lowerKey] = value.map(String);
|
|
201
|
-
}
|
|
202
|
-
else if (value != null) {
|
|
203
|
-
result[lowerKey] = String(value);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return result;
|
|
208
|
-
}
|
|
209
|
-
// =============================================================================
|
|
210
|
-
// Build a Test from a single case
|
|
211
|
-
// =============================================================================
|
|
212
|
-
function buildCaseTest(contractId, caseKey, endpoint, c, spec, instanceName, security) {
|
|
213
|
-
const { method, path } = parseEndpoint(endpoint);
|
|
214
|
-
const contractTags = spec.tags ? (Array.isArray(spec.tags) ? spec.tags : [spec.tags]) : [];
|
|
215
|
-
const caseTags = c.tags ?? [];
|
|
216
|
-
const allTags = [...contractTags, ...caseTags];
|
|
217
|
-
const testId = `${contractId}.${caseKey}`;
|
|
218
|
-
const testName = `${contractId} — ${caseKey}`;
|
|
219
|
-
// Contract-level deprecated propagates to cases (case value wins if both set)
|
|
220
|
-
const effectiveDeprecated = c.deprecated ?? spec.deprecated;
|
|
221
|
-
// Auto-imply: non-headless requires → defaultRun: "opt-in"
|
|
222
|
-
const requires = c.requires ?? "headless";
|
|
223
|
-
const defaultRun = c.defaultRun ?? (requires !== "headless" ? "opt-in" : "always");
|
|
224
|
-
// Auto-tag: requires:browser, requires:out-of-band, default-run:opt-in
|
|
225
|
-
const runtimeTags = [];
|
|
226
|
-
if (requires !== "headless")
|
|
227
|
-
runtimeTags.push(`requires:${requires}`);
|
|
228
|
-
if (defaultRun === "opt-in")
|
|
229
|
-
runtimeTags.push("default-run:opt-in");
|
|
230
|
-
const finalTags = [...allTags, ...runtimeTags];
|
|
231
|
-
const meta = {
|
|
232
|
-
id: testId,
|
|
233
|
-
name: testName,
|
|
234
|
-
description: c.description,
|
|
235
|
-
tags: finalTags.length > 0 ? finalTags : undefined,
|
|
236
|
-
deferred: c.deferred,
|
|
237
|
-
deprecated: effectiveDeprecated,
|
|
238
|
-
requires,
|
|
239
|
-
defaultRun,
|
|
240
|
-
};
|
|
241
|
-
const fn = async (ctx) => {
|
|
242
|
-
// 1. Deprecated → skip (takes precedence over deferred)
|
|
243
|
-
if (effectiveDeprecated) {
|
|
244
|
-
ctx.skip(`deprecated: ${effectiveDeprecated}`);
|
|
245
|
-
}
|
|
246
|
-
// 2. Deferred → skip
|
|
247
|
-
if (c.deferred) {
|
|
248
|
-
ctx.skip(c.deferred);
|
|
249
|
-
}
|
|
250
|
-
// 2. Setup
|
|
251
|
-
const state = c.setup ? await c.setup(ctx) : undefined;
|
|
252
|
-
try {
|
|
253
|
-
// 3. Resolve client
|
|
254
|
-
const client = (c.client ?? spec.client);
|
|
255
|
-
if (!client) {
|
|
256
|
-
throw new Error(`No HTTP client provided for case "${caseKey}". ` +
|
|
257
|
-
`Set "client" on the case or on the contract spec.`);
|
|
258
|
-
}
|
|
259
|
-
// 4. Resolve params and path (flatten ParamValue objects to strings)
|
|
260
|
-
const rawParams = typeof c.params === "function" ? c.params(state) : c.params;
|
|
261
|
-
const params = flattenParamValues(rawParams);
|
|
262
|
-
const resolvedPath = resolveParams(path, params);
|
|
263
|
-
// 5. Build request options
|
|
264
|
-
const requestOptions = {};
|
|
265
|
-
const body = typeof c.body === "function" ? c.body(state) : c.body;
|
|
266
|
-
// Resolve content type: case override > contract default > application/json
|
|
267
|
-
const normalizedRequest = normalizeRequest(spec.request);
|
|
268
|
-
const effectiveContentType = c.contentType ?? normalizedRequest?.contentType ?? "application/json";
|
|
269
|
-
// Dispatch body serialization based on content type
|
|
270
|
-
if (body !== undefined) {
|
|
271
|
-
Object.assign(requestOptions, buildRequestBodyOptions(body, effectiveContentType));
|
|
272
|
-
}
|
|
273
|
-
const headers = typeof c.headers === "function" ? c.headers(state) : c.headers;
|
|
274
|
-
if (headers)
|
|
275
|
-
requestOptions.headers = headers;
|
|
276
|
-
if (c.query) {
|
|
277
|
-
const rawQuery = typeof c.query === "function" ? c.query(state) : c.query;
|
|
278
|
-
requestOptions.searchParams = flattenParamValues(rawQuery);
|
|
279
|
-
}
|
|
280
|
-
requestOptions.throwHttpErrors = false;
|
|
281
|
-
// 6. Send request
|
|
282
|
-
const methodLower = method.toLowerCase();
|
|
283
|
-
let res;
|
|
284
|
-
try {
|
|
285
|
-
res = await client[methodLower](resolvedPath, requestOptions);
|
|
286
|
-
}
|
|
287
|
-
catch (err) {
|
|
288
|
-
// Enhance timeout errors with configured timeout value
|
|
289
|
-
if (err instanceof Error && err.name === "TimeoutError") {
|
|
290
|
-
const timeoutMs = client._configuredTimeout ?? 10000;
|
|
291
|
-
throw new Error(`${err.message} (timeout: ${timeoutMs}ms)`);
|
|
292
|
-
}
|
|
293
|
-
throw err;
|
|
294
|
-
}
|
|
295
|
-
// 7. Assert status
|
|
296
|
-
ctx.expect(res).toHaveStatus(c.expect.status);
|
|
297
|
-
// 7b. Validate response headers (if schema provided)
|
|
298
|
-
if (c.expect.headers) {
|
|
299
|
-
const normalizedHeaders = normalizeResponseHeaders(res.headers);
|
|
300
|
-
ctx.validate(normalizedHeaders, c.expect.headers, `${testId} response headers`);
|
|
301
|
-
}
|
|
302
|
-
// 8. Parse response + validate schema
|
|
303
|
-
let parsed;
|
|
304
|
-
if (c.expect.schema) {
|
|
305
|
-
const body = await res.json();
|
|
306
|
-
const validated = ctx.validate(body, c.expect.schema, `${testId} response`);
|
|
307
|
-
parsed = (validated !== undefined ? validated : body);
|
|
308
|
-
}
|
|
309
|
-
else if (c.verify) {
|
|
310
|
-
// No schema but verify needs the body — parse as raw JSON
|
|
311
|
-
parsed = (await res.json());
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
parsed = undefined;
|
|
315
|
-
}
|
|
316
|
-
// 9. Verify callback
|
|
317
|
-
if (c.verify) {
|
|
318
|
-
await c.verify(ctx, parsed);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
finally {
|
|
322
|
-
// 10. Teardown (always)
|
|
323
|
-
if (c.teardown) {
|
|
324
|
-
await c.teardown(ctx, state);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
// Lifecycle normalization: deprecated > deferred > active
|
|
329
|
-
const lifecycle = c.deprecated ? "deprecated" :
|
|
330
|
-
c.deferred ? "deferred" :
|
|
331
|
-
"active";
|
|
332
|
-
const severity = c.severity ?? "warning";
|
|
333
|
-
// Register to global registry with protocol-agnostic contract metadata
|
|
334
|
-
const registryMeta = {
|
|
335
|
-
target: endpoint,
|
|
336
|
-
protocol: "http",
|
|
337
|
-
caseKey,
|
|
338
|
-
lifecycle,
|
|
339
|
-
severity,
|
|
340
|
-
hasSchema: !!c.expect.schema,
|
|
341
|
-
instanceName,
|
|
342
|
-
protocolMeta: {
|
|
343
|
-
...(security != null ? { security } : {}),
|
|
344
|
-
expect: { status: c.expect.status },
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
registerTest({
|
|
348
|
-
id: testId,
|
|
349
|
-
name: testName,
|
|
350
|
-
type: "simple",
|
|
351
|
-
tags: finalTags.length > 0 ? finalTags : undefined,
|
|
352
|
-
groupId: contractId,
|
|
353
|
-
requires,
|
|
354
|
-
defaultRun,
|
|
355
|
-
contract: registryMeta,
|
|
356
|
-
});
|
|
357
|
-
return { meta, type: "simple", fn };
|
|
358
|
-
}
|
|
359
|
-
// =============================================================================
|
|
360
|
-
// contract.http()
|
|
361
|
-
// =============================================================================
|
|
362
|
-
function contractHttp(id, spec, instanceName, security) {
|
|
363
|
-
const tests = [];
|
|
364
|
-
const caseSchemas = {};
|
|
365
|
-
for (const [caseKey, caseSpec] of Object.entries(spec.cases)) {
|
|
366
|
-
tests.push(buildCaseTest(id, caseKey, spec.endpoint, caseSpec, spec, instanceName, security));
|
|
367
|
-
// Resolve effective requires/defaultRun (same logic as buildCaseTest)
|
|
368
|
-
const effectiveRequires = caseSpec.requires ?? "headless";
|
|
369
|
-
const effectiveDefaultRun = caseSpec.defaultRun ?? (effectiveRequires !== "headless" ? "opt-in" : "always");
|
|
370
|
-
// Effective deprecated: case wins, fall back to contract-level
|
|
371
|
-
const effectiveDeprecated = caseSpec.deprecated ?? spec.deprecated;
|
|
372
|
-
// Lifecycle normalization: deprecated > deferred > active
|
|
373
|
-
const lifecycle = effectiveDeprecated ? "deprecated" :
|
|
374
|
-
caseSpec.deferred ? "deferred" :
|
|
375
|
-
"active";
|
|
376
|
-
// Extract per-param schemas (only from object-shaped ParamValues, not string shorthand)
|
|
377
|
-
const paramSchemas = extractParamMetaSchemas(caseSpec.params);
|
|
378
|
-
const querySchemas = extractParamMetaSchemas(caseSpec.query);
|
|
379
|
-
// Merge case-level extensions over contract-level (spec.extensions is already defaults+contract merged)
|
|
380
|
-
const caseExtensions = mergeExtensions(spec.extensions, caseSpec.extensions);
|
|
381
|
-
caseSchemas[caseKey] = {
|
|
382
|
-
expectStatus: caseSpec.expect.status,
|
|
383
|
-
responseSchema: caseSpec.expect.schema,
|
|
384
|
-
responseHeaders: caseSpec.expect.headers,
|
|
385
|
-
responseContentType: caseSpec.expect.contentType,
|
|
386
|
-
example: caseSpec.expect.example,
|
|
387
|
-
examples: caseSpec.expect.examples,
|
|
388
|
-
paramSchemas,
|
|
389
|
-
querySchemas,
|
|
390
|
-
description: caseSpec.description,
|
|
391
|
-
deferred: caseSpec.deferred,
|
|
392
|
-
deprecated: effectiveDeprecated,
|
|
393
|
-
severity: caseSpec.severity,
|
|
394
|
-
lifecycle,
|
|
395
|
-
requires: effectiveRequires,
|
|
396
|
-
defaultRun: effectiveDefaultRun,
|
|
397
|
-
extensions: caseExtensions,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
// Normalize request (SchemaLike or RequestSpec object → structured fields)
|
|
401
|
-
const normalizedReq = normalizeRequest(spec.request);
|
|
402
|
-
return createHttpContract(id, spec.endpoint, tests, normalizedReq?.body, spec.description, spec.feature, caseSchemas, instanceName, security, spec.deprecated, spec.extensions, normalizedReq?.contentType, normalizedReq?.headers, normalizedReq?.example, normalizedReq?.examples);
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Extract per-param schema metadata from a params/query object.
|
|
406
|
-
* Only collects entries where the ParamValue is an object (has `.value` + optional schema/description).
|
|
407
|
-
* String shorthand values produce no metadata.
|
|
408
|
-
*/
|
|
409
|
-
function extractParamMetaSchemas(params) {
|
|
410
|
-
if (!params || typeof params === "function")
|
|
411
|
-
return undefined;
|
|
412
|
-
const result = {};
|
|
413
|
-
let hasAny = false;
|
|
414
|
-
for (const [key, val] of Object.entries(params)) {
|
|
415
|
-
if (val && typeof val === "object" && !Array.isArray(val) && "value" in val) {
|
|
416
|
-
const pv = val;
|
|
417
|
-
if (pv.schema !== undefined || pv.description !== undefined || pv.required !== undefined || pv.deprecated !== undefined) {
|
|
418
|
-
result[key] = {
|
|
419
|
-
schema: pv.schema,
|
|
420
|
-
description: pv.description,
|
|
421
|
-
required: pv.required,
|
|
422
|
-
deprecated: pv.deprecated,
|
|
423
|
-
};
|
|
424
|
-
hasAny = true;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return hasAny ? result : undefined;
|
|
429
|
-
}
|
|
430
|
-
class FlowBuilder {
|
|
431
|
-
_id;
|
|
432
|
-
_defaultClient;
|
|
433
|
-
_tags;
|
|
434
|
-
_requires;
|
|
435
|
-
_defaultRun;
|
|
436
|
-
_steps = [];
|
|
437
|
-
_setupFn;
|
|
438
|
-
_teardownFn;
|
|
439
|
-
constructor(id, options) {
|
|
440
|
-
this._id = id;
|
|
441
|
-
this._defaultClient = options?.client;
|
|
442
|
-
this._tags = options?.tags;
|
|
443
|
-
this._requires = options?.requires;
|
|
444
|
-
this._defaultRun = options?.defaultRun;
|
|
445
|
-
}
|
|
446
|
-
setup(fn) {
|
|
447
|
-
this._setupFn = fn;
|
|
448
|
-
return this;
|
|
449
|
-
}
|
|
450
|
-
teardown(fn) {
|
|
451
|
-
this._teardownFn = fn;
|
|
452
|
-
return this;
|
|
453
|
-
}
|
|
454
|
-
http(name, spec) {
|
|
455
|
-
const { method, path } = parseEndpoint(spec.endpoint);
|
|
456
|
-
const expectedStatus = spec.expect.status;
|
|
457
|
-
const stepFn = async (ctx, state) => {
|
|
458
|
-
// Resolve client
|
|
459
|
-
const client = (spec.client ?? this._defaultClient);
|
|
460
|
-
if (!client) {
|
|
461
|
-
throw new Error(`No HTTP client for flow step "${name}". Set client on the step or flow.`);
|
|
462
|
-
}
|
|
463
|
-
// Resolve params/query/body from state
|
|
464
|
-
const params = typeof spec.params === "function" ? spec.params(state) : spec.params;
|
|
465
|
-
const resolvedPath = resolveParams(path, params);
|
|
466
|
-
const query = typeof spec.query === "function" ? spec.query(state) : spec.query;
|
|
467
|
-
const body = typeof spec.body === "function" ? spec.body(state) : spec.body;
|
|
468
|
-
// Build request options
|
|
469
|
-
const opts = { throwHttpErrors: false };
|
|
470
|
-
if (body !== undefined)
|
|
471
|
-
opts.json = body;
|
|
472
|
-
const headers = typeof spec.headers === "function" ? spec.headers(state) : spec.headers;
|
|
473
|
-
if (headers)
|
|
474
|
-
opts.headers = headers;
|
|
475
|
-
if (query)
|
|
476
|
-
opts.searchParams = query;
|
|
477
|
-
// Send request
|
|
478
|
-
const methodLower = method.toLowerCase();
|
|
479
|
-
let res;
|
|
480
|
-
try {
|
|
481
|
-
res = await client[methodLower](resolvedPath, opts);
|
|
482
|
-
}
|
|
483
|
-
catch (err) {
|
|
484
|
-
if (err instanceof Error && err.name === "TimeoutError") {
|
|
485
|
-
const timeoutMs = client._configuredTimeout ?? 10000;
|
|
486
|
-
throw new Error(`${err.message} (timeout: ${timeoutMs}ms)`);
|
|
487
|
-
}
|
|
488
|
-
throw err;
|
|
489
|
-
}
|
|
490
|
-
// Assert status
|
|
491
|
-
ctx.expect(res).toHaveStatus(expectedStatus);
|
|
492
|
-
// Validate schema + parse response
|
|
493
|
-
let parsed;
|
|
494
|
-
if (spec.expect.schema) {
|
|
495
|
-
const jsonBody = await res.json();
|
|
496
|
-
const validated = ctx.validate(jsonBody, spec.expect.schema, `${this._id}/${name} response`);
|
|
497
|
-
parsed = validated !== undefined ? validated : jsonBody;
|
|
498
|
-
}
|
|
499
|
-
else if (spec.verify || spec.returns) {
|
|
500
|
-
parsed = await res.json();
|
|
501
|
-
}
|
|
502
|
-
// Verify callback
|
|
503
|
-
if (spec.verify) {
|
|
504
|
-
await spec.verify(ctx, parsed);
|
|
505
|
-
}
|
|
506
|
-
// State evolution
|
|
507
|
-
if (spec.returns) {
|
|
508
|
-
return spec.returns(parsed, state);
|
|
509
|
-
}
|
|
510
|
-
return state;
|
|
511
|
-
};
|
|
512
|
-
this._steps.push({
|
|
513
|
-
meta: { name, endpoint: spec.endpoint, expectStatus: expectedStatus },
|
|
514
|
-
fn: stepFn,
|
|
515
|
-
});
|
|
516
|
-
return this;
|
|
517
|
-
}
|
|
518
|
-
build() {
|
|
519
|
-
// Auto-imply: non-headless requires → defaultRun: "opt-in"
|
|
520
|
-
const requires = this._requires ?? "headless";
|
|
521
|
-
const defaultRun = this._defaultRun ?? (requires !== "headless" ? "opt-in" : "always");
|
|
522
|
-
// Auto-tag
|
|
523
|
-
const baseTags = this._tags ?? [];
|
|
524
|
-
const runtimeTags = [];
|
|
525
|
-
if (requires !== "headless")
|
|
526
|
-
runtimeTags.push(`requires:${requires}`);
|
|
527
|
-
if (defaultRun === "opt-in")
|
|
528
|
-
runtimeTags.push("default-run:opt-in");
|
|
529
|
-
const finalTags = [...baseTags, ...runtimeTags];
|
|
530
|
-
const test = {
|
|
531
|
-
meta: {
|
|
532
|
-
id: this._id,
|
|
533
|
-
name: this._id,
|
|
534
|
-
tags: finalTags.length > 0 ? finalTags : undefined,
|
|
535
|
-
requires,
|
|
536
|
-
defaultRun,
|
|
537
|
-
},
|
|
538
|
-
type: "steps",
|
|
539
|
-
setup: this._setupFn,
|
|
540
|
-
teardown: this._teardownFn,
|
|
541
|
-
steps: this._steps.map((s) => ({
|
|
542
|
-
meta: { name: s.meta.name },
|
|
543
|
-
fn: s.fn,
|
|
544
|
-
})),
|
|
545
|
-
};
|
|
546
|
-
const flowStepsMeta = this._steps.map((s) => s.meta);
|
|
547
|
-
// Register to global registry with flow metadata
|
|
548
|
-
registerTest({
|
|
549
|
-
id: this._id,
|
|
550
|
-
name: this._id,
|
|
551
|
-
type: "steps",
|
|
552
|
-
tags: finalTags.length > 0 ? finalTags : undefined,
|
|
553
|
-
requires,
|
|
554
|
-
defaultRun,
|
|
555
|
-
steps: this._steps.map((s) => ({ name: s.meta.name })),
|
|
556
|
-
hasSetup: !!this._setupFn,
|
|
557
|
-
hasTeardown: !!this._teardownFn,
|
|
558
|
-
flow: {
|
|
559
|
-
steps: flowStepsMeta,
|
|
560
|
-
},
|
|
561
|
-
});
|
|
562
|
-
const asSteps = () => {
|
|
563
|
-
const steps = test.steps;
|
|
564
|
-
const setupFn = this._setupFn;
|
|
565
|
-
const teardownFn = this._teardownFn;
|
|
566
|
-
return (b) => {
|
|
567
|
-
// Note: setup and teardown are injected as regular steps.
|
|
568
|
-
// This means teardown does NOT have finally semantics —
|
|
569
|
-
// if an earlier step fails, teardown won't run.
|
|
570
|
-
// For guaranteed cleanup, use standalone flow or add
|
|
571
|
-
// .teardown() on the outer test builder.
|
|
572
|
-
if (setupFn) {
|
|
573
|
-
b.step(`${this._id} [setup]`, async (ctx) => {
|
|
574
|
-
return setupFn(ctx);
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
// Inject flow steps
|
|
578
|
-
for (const s of steps) {
|
|
579
|
-
b.step(s.meta.name, async (ctx, state) => {
|
|
580
|
-
return s.fn(ctx, state);
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
// Inject teardown as last step if flow has one
|
|
584
|
-
if (teardownFn) {
|
|
585
|
-
b.step(`${this._id} [teardown]`, async (ctx, state) => {
|
|
586
|
-
await teardownFn(ctx, state);
|
|
587
|
-
return state;
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
return b;
|
|
591
|
-
};
|
|
592
|
-
};
|
|
593
|
-
return Object.assign(test, { flowId: this._id, flowSteps: flowStepsMeta, asSteps });
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
function contractFlow(id, options) {
|
|
597
|
-
return new FlowBuilder(id, options);
|
|
598
|
-
}
|
|
599
|
-
// =============================================================================
|
|
600
|
-
// contract namespace + register()
|
|
601
|
-
// =============================================================================
|
|
602
|
-
const _adapters = new Map();
|
|
603
|
-
// =============================================================================
|
|
604
|
-
// HTTP contract factory — contract.http.with("name", defaults)
|
|
605
|
-
// =============================================================================
|
|
606
|
-
/**
|
|
607
|
-
* Merge instance defaults with per-contract spec.
|
|
608
|
-
* Tags are additive (concat), other fields: spec overrides defaults.
|
|
609
|
-
*/
|
|
610
|
-
function mergeHttpDefaults(defaults, spec) {
|
|
611
|
-
if (!defaults)
|
|
612
|
-
return spec;
|
|
613
|
-
const mergedTags = [
|
|
614
|
-
...(defaults.tags ?? []),
|
|
615
|
-
...(spec.tags ?? []),
|
|
616
|
-
];
|
|
617
|
-
// Merge extensions: defaults < contract (contract key overrides defaults key)
|
|
618
|
-
const mergedExtensions = mergeExtensions(defaults.extensions, spec.extensions);
|
|
619
|
-
return {
|
|
620
|
-
...spec,
|
|
621
|
-
client: spec.client ?? defaults.client,
|
|
622
|
-
feature: spec.feature ?? defaults.feature,
|
|
623
|
-
tags: mergedTags.length > 0 ? mergedTags : undefined,
|
|
624
|
-
extensions: mergedExtensions,
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Merge two Extensions records. Right wins on key conflict.
|
|
629
|
-
* Returns undefined if result would be empty.
|
|
630
|
-
*/
|
|
631
|
-
function mergeExtensions(base, override) {
|
|
632
|
-
if (!base && !override)
|
|
633
|
-
return undefined;
|
|
634
|
-
const merged = { ...(base ?? {}), ...(override ?? {}) };
|
|
635
|
-
return Object.keys(merged).length > 0
|
|
636
|
-
? merged
|
|
637
|
-
: undefined;
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* Create an HTTP contract factory with `.with()` support.
|
|
641
|
-
* The factory is callable (same signature as contractHttp) and chainable.
|
|
642
|
-
*/
|
|
643
|
-
/**
|
|
644
|
-
* Create an HTTP contract factory. If `defaults` is provided (via .with()),
|
|
645
|
-
* the factory is callable. If not, only .with() is available — direct
|
|
646
|
-
* contract.http("id", spec) is not supported.
|
|
647
|
-
*/
|
|
648
|
-
function createHttpFactory(defaults) {
|
|
649
|
-
const factory = (id, spec) => {
|
|
650
|
-
if (!defaults?._name) {
|
|
651
|
-
throw new Error(`contract.http("${id}", spec) is not supported. ` +
|
|
652
|
-
`Use contract.http.with("name", { client }) first to create a scoped instance, ` +
|
|
653
|
-
`then call instance("${id}", spec).`);
|
|
654
|
-
}
|
|
655
|
-
const merged = mergeHttpDefaults(defaults, spec);
|
|
656
|
-
return contractHttp(id, merged, defaults._name, defaults.security);
|
|
657
|
-
};
|
|
658
|
-
factory.with = (name, more) => {
|
|
659
|
-
const mergedTags = [...(defaults?.tags ?? []), ...(more.tags ?? [])];
|
|
660
|
-
const mergedExtensions = mergeExtensions(defaults?.extensions, more.extensions);
|
|
661
|
-
return createHttpFactory({
|
|
662
|
-
...defaults,
|
|
663
|
-
...more,
|
|
664
|
-
tags: mergedTags.length > 0 ? mergedTags : undefined,
|
|
665
|
-
extensions: mergedExtensions,
|
|
666
|
-
_name: name,
|
|
667
|
-
});
|
|
668
|
-
};
|
|
669
|
-
return factory;
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* The contract namespace.
|
|
673
|
-
*
|
|
674
|
-
* - `contract.http.with("name", defaults)` — create scoped HTTP factory
|
|
675
|
-
* - `contract.flow(id, options)` — declarative flow builder
|
|
676
|
-
* - `contract.register(protocol, adapter)` — plugin extension point
|
|
677
|
-
* - `contract[protocol](id, spec)` — available after register()
|
|
678
|
-
*/
|
|
679
|
-
export const contract = {
|
|
680
|
-
http: createHttpFactory(),
|
|
681
|
-
flow: contractFlow,
|
|
682
|
-
register(protocol, adapter) {
|
|
683
|
-
if (protocol === "http" || protocol === "flow" || protocol === "register") {
|
|
684
|
-
throw new Error(`Cannot register reserved protocol "${protocol}"`);
|
|
685
|
-
}
|
|
686
|
-
_adapters.set(protocol, adapter);
|
|
687
|
-
// Dynamically attach contract[protocol]()
|
|
688
|
-
contract[protocol] = (id, spec) => {
|
|
689
|
-
// Get projection from adapter v2
|
|
690
|
-
const projection = adapter.project(spec);
|
|
691
|
-
// Validate: no duplicate keys in projected cases
|
|
692
|
-
const projKeyList = projection.cases.map(c => c.key);
|
|
693
|
-
const projKeySet = new Set(projKeyList);
|
|
694
|
-
if (projKeySet.size !== projKeyList.length) {
|
|
695
|
-
const dupes = projKeyList.filter((k, i) => projKeyList.indexOf(k) !== i);
|
|
696
|
-
throw new Error(`contract.register("${protocol}"): project() returned duplicate case key(s): ${[...new Set(dupes)].join(", ")}. ` +
|
|
697
|
-
`Each projected case key must be unique.`);
|
|
698
|
-
}
|
|
699
|
-
// Validate 1:1 key invariant between projection and spec.cases
|
|
700
|
-
const specKeys = new Set(Object.keys(spec.cases ?? {}));
|
|
701
|
-
for (const key of projKeySet) {
|
|
702
|
-
if (!specKeys.has(key)) {
|
|
703
|
-
throw new Error(`contract.register("${protocol}"): project() returned case "${key}" not present in spec.cases. ` +
|
|
704
|
-
`Projected cases must 1:1 match spec.cases keys.`);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
for (const key of specKeys) {
|
|
708
|
-
if (!projKeySet.has(key)) {
|
|
709
|
-
throw new Error(`contract.register("${protocol}"): spec.cases has "${key}" but project() did not return it. ` +
|
|
710
|
-
`Projected cases must 1:1 match spec.cases keys.`);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
const cases = spec.cases ?? {};
|
|
714
|
-
const contractTags = spec.tags ? (Array.isArray(spec.tags) ? spec.tags : [spec.tags]) : [];
|
|
715
|
-
// Build a lookup from projection cases by key
|
|
716
|
-
const projCaseMap = new Map(projection.cases.map(c => [c.key, c]));
|
|
717
|
-
const tests = Object.entries(cases).map(([caseKey, caseSpec]) => {
|
|
718
|
-
const testId = `${id}.${caseKey}`;
|
|
719
|
-
const testName = `${id} — ${caseKey}`;
|
|
720
|
-
const caseTags = caseSpec.tags ?? [];
|
|
721
|
-
const allTags = [...contractTags, ...caseTags];
|
|
722
|
-
const projCase = projCaseMap.get(caseKey);
|
|
723
|
-
// Derive runtime requires/defaultRun from projection (authoritative)
|
|
724
|
-
// with case spec as fallback, mirroring HTTP path logic
|
|
725
|
-
const requires = projCase.requires ?? caseSpec.requires ?? "headless";
|
|
726
|
-
const defaultRun = projCase.defaultRun ?? caseSpec.defaultRun ?? (requires !== "headless" ? "opt-in" : "always");
|
|
727
|
-
// Runtime tags from requires/defaultRun
|
|
728
|
-
const runtimeTags = [];
|
|
729
|
-
if (requires !== "headless")
|
|
730
|
-
runtimeTags.push(`requires:${requires}`);
|
|
731
|
-
if (defaultRun === "opt-in")
|
|
732
|
-
runtimeTags.push("default-run:opt-in");
|
|
733
|
-
const finalTags = [...allTags, ...runtimeTags];
|
|
734
|
-
// Derive skip from projected lifecycle (authoritative), not just caseSpec fields
|
|
735
|
-
const skipDeprecated = projCase.lifecycle === "deprecated"
|
|
736
|
-
? `deprecated: ${projCase.deprecatedReason ?? caseSpec.deprecated ?? "deprecated"}`
|
|
737
|
-
: caseSpec.deprecated ? `deprecated: ${caseSpec.deprecated}` : undefined;
|
|
738
|
-
const skipDeferred = projCase.lifecycle === "deferred"
|
|
739
|
-
? (projCase.deferredReason ?? caseSpec.deferred ?? "deferred")
|
|
740
|
-
: caseSpec.deferred;
|
|
741
|
-
const testDef = {
|
|
742
|
-
meta: {
|
|
743
|
-
id: testId,
|
|
744
|
-
name: testName,
|
|
745
|
-
description: projCase.description,
|
|
746
|
-
tags: finalTags.length > 0 ? finalTags : undefined,
|
|
747
|
-
deferred: skipDeferred,
|
|
748
|
-
deprecated: skipDeprecated ? (projCase.deprecatedReason ?? caseSpec.deprecated ?? "deprecated") : undefined,
|
|
749
|
-
requires,
|
|
750
|
-
defaultRun,
|
|
751
|
-
},
|
|
752
|
-
type: "simple",
|
|
753
|
-
fn: async (ctx) => {
|
|
754
|
-
if (skipDeprecated)
|
|
755
|
-
ctx.skip(skipDeprecated);
|
|
756
|
-
if (skipDeferred)
|
|
757
|
-
ctx.skip(skipDeferred);
|
|
758
|
-
await adapter.execute(ctx, caseSpec, spec);
|
|
759
|
-
},
|
|
760
|
-
};
|
|
761
|
-
registerTest({
|
|
762
|
-
id: testId,
|
|
763
|
-
name: testName,
|
|
764
|
-
type: "simple",
|
|
765
|
-
tags: finalTags.length > 0 ? finalTags : undefined,
|
|
766
|
-
groupId: id,
|
|
767
|
-
requires,
|
|
768
|
-
defaultRun,
|
|
769
|
-
contract: {
|
|
770
|
-
target: projection.target,
|
|
771
|
-
protocol: projection.protocol,
|
|
772
|
-
caseKey,
|
|
773
|
-
lifecycle: projCase.lifecycle,
|
|
774
|
-
severity: projCase.severity,
|
|
775
|
-
hasSchema: projCase.responseSchema != null,
|
|
776
|
-
instanceName: projection.instanceName,
|
|
777
|
-
protocolMeta: {
|
|
778
|
-
...(projection.protocolMeta ?? {}),
|
|
779
|
-
...(projCase.protocolExpect ? { expect: projCase.protocolExpect } : {}),
|
|
780
|
-
...(projCase.protocolMeta ?? {}),
|
|
781
|
-
},
|
|
782
|
-
},
|
|
783
|
-
});
|
|
784
|
-
return testDef;
|
|
785
|
-
});
|
|
786
|
-
// Inject contract id into projection (adapter doesn't know the user-supplied id)
|
|
787
|
-
const enrichedProjection = { ...projection, id };
|
|
788
|
-
// Return ProtocolContract — Test[] with _projection carrier
|
|
789
|
-
return Object.assign(tests, { _projection: enrichedProjection });
|
|
790
|
-
};
|
|
791
|
-
},
|
|
792
|
-
};
|
|
793
|
-
//# sourceMappingURL=contract.js.map
|