@glubean/sdk 0.1.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/configure.d.ts +141 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +535 -0
- package/dist/configure.js.map +1 -0
- package/dist/data.d.ts +232 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +543 -0
- package/dist/data.js.map +1 -0
- package/dist/expect.d.ts +511 -0
- package/dist/expect.d.ts.map +1 -0
- package/dist/expect.js +763 -0
- package/dist/expect.js.map +1 -0
- package/dist/index.d.ts +718 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1015 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +39 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +52 -0
- package/dist/internal.js.map +1 -0
- package/dist/plugin.d.ts +56 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +57 -0
- package/dist/plugin.js.map +1 -0
- package/dist/types.d.ts +1971 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +54 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
package/dist/expect.js
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluent assertion API for Glubean tests.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Jest/Vitest `expect()` but designed for API testing:
|
|
5
|
+
* - **Soft-by-default**: Failed assertions emit events but do NOT throw.
|
|
6
|
+
* All assertions run and all failures are collected.
|
|
7
|
+
* - **`.orFail()` guard**: Opt-in hard failure for when subsequent code
|
|
8
|
+
* depends on the assertion passing.
|
|
9
|
+
* - **`.not` negation**: Negate any assertion via a getter.
|
|
10
|
+
*
|
|
11
|
+
* The class is framework-agnostic — it accepts an `emitter` callback
|
|
12
|
+
* that routes assertion results into the runner's event pipeline.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Produce a short, human-readable representation of a value for error messages.
|
|
21
|
+
* Truncates long output to keep assertion messages scannable.
|
|
22
|
+
*/
|
|
23
|
+
export function inspect(value, maxLen = 64) {
|
|
24
|
+
if (value === null)
|
|
25
|
+
return "null";
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return "undefined";
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
const escaped = JSON.stringify(value);
|
|
30
|
+
if (escaped.length <= maxLen)
|
|
31
|
+
return escaped;
|
|
32
|
+
// Truncate: keep opening quote, trim content, append ..."
|
|
33
|
+
// Result: "aaa..." which is maxLen chars total
|
|
34
|
+
return escaped.slice(0, maxLen - 4) + '..."';
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "bigint")
|
|
40
|
+
return `${value}n`;
|
|
41
|
+
if (typeof value === "symbol")
|
|
42
|
+
return value.toString();
|
|
43
|
+
if (typeof value === "function") {
|
|
44
|
+
return `[Function: ${value.name || "anonymous"}]`;
|
|
45
|
+
}
|
|
46
|
+
if (value instanceof RegExp)
|
|
47
|
+
return value.toString();
|
|
48
|
+
if (value instanceof Date)
|
|
49
|
+
return value.toISOString();
|
|
50
|
+
try {
|
|
51
|
+
const json = JSON.stringify(value);
|
|
52
|
+
return json.length > maxLen ? json.slice(0, maxLen - 3) + "..." : json;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Deep equality check.
|
|
60
|
+
* Handles primitives, arrays, plain objects, Date, RegExp, Map, Set, null, undefined.
|
|
61
|
+
* Safely handles circular references via a seen-pairs set.
|
|
62
|
+
*/
|
|
63
|
+
export function deepEqual(a, b, seen) {
|
|
64
|
+
if (Object.is(a, b))
|
|
65
|
+
return true;
|
|
66
|
+
if (a === null ||
|
|
67
|
+
b === null ||
|
|
68
|
+
typeof a !== "object" ||
|
|
69
|
+
typeof b !== "object") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Circular reference guard: if we've already started comparing this exact
|
|
73
|
+
// pair of object references, treat them as equal to avoid infinite recursion.
|
|
74
|
+
if (!seen)
|
|
75
|
+
seen = new Set();
|
|
76
|
+
const sentinel = { a, b };
|
|
77
|
+
for (const s of seen) {
|
|
78
|
+
const pair = s;
|
|
79
|
+
if (pair.a === a && pair.b === b)
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
seen.add(sentinel);
|
|
83
|
+
// Date
|
|
84
|
+
if (a instanceof Date && b instanceof Date) {
|
|
85
|
+
return a.getTime() === b.getTime();
|
|
86
|
+
}
|
|
87
|
+
// RegExp
|
|
88
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
89
|
+
return a.source === b.source && a.flags === b.flags;
|
|
90
|
+
}
|
|
91
|
+
// Map
|
|
92
|
+
if (a instanceof Map && b instanceof Map) {
|
|
93
|
+
if (a.size !== b.size)
|
|
94
|
+
return false;
|
|
95
|
+
for (const [key, val] of a) {
|
|
96
|
+
if (!b.has(key) || !deepEqual(val, b.get(key), seen))
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
// Set
|
|
102
|
+
if (a instanceof Set && b instanceof Set) {
|
|
103
|
+
if (a.size !== b.size)
|
|
104
|
+
return false;
|
|
105
|
+
for (const val of a) {
|
|
106
|
+
if (!b.has(val))
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Array
|
|
112
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
113
|
+
if (a.length !== b.length)
|
|
114
|
+
return false;
|
|
115
|
+
for (let i = 0; i < a.length; i++) {
|
|
116
|
+
if (!deepEqual(a[i], b[i], seen))
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
// Plain objects
|
|
122
|
+
const keysA = Object.keys(a);
|
|
123
|
+
const keysB = Object.keys(b);
|
|
124
|
+
if (keysA.length !== keysB.length)
|
|
125
|
+
return false;
|
|
126
|
+
for (const key of keysA) {
|
|
127
|
+
if (!Object.prototype.hasOwnProperty.call(b, key) ||
|
|
128
|
+
!deepEqual(a[key], b[key], seen)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check whether `subset` is a subset-match of `obj` (partial deep equality).
|
|
136
|
+
* Every key in `subset` must exist in `obj` and deeply equal the value.
|
|
137
|
+
*/
|
|
138
|
+
function matchesObject(obj, subset) {
|
|
139
|
+
for (const key of Object.keys(subset)) {
|
|
140
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key))
|
|
141
|
+
return false;
|
|
142
|
+
const sv = subset[key];
|
|
143
|
+
const ov = obj[key];
|
|
144
|
+
if (typeof sv === "object" &&
|
|
145
|
+
sv !== null &&
|
|
146
|
+
!Array.isArray(sv) &&
|
|
147
|
+
typeof ov === "object" &&
|
|
148
|
+
ov !== null &&
|
|
149
|
+
!Array.isArray(ov)) {
|
|
150
|
+
if (!matchesObject(ov, sv)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (!deepEqual(ov, sv)) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolve a dot-separated property path on an object.
|
|
162
|
+
* Returns `{ found: true, value }` or `{ found: false }`.
|
|
163
|
+
*/
|
|
164
|
+
function resolvePath(obj, path) {
|
|
165
|
+
const parts = path.split(".");
|
|
166
|
+
let current = obj;
|
|
167
|
+
for (const part of parts) {
|
|
168
|
+
if (current === null ||
|
|
169
|
+
current === undefined ||
|
|
170
|
+
typeof current !== "object") {
|
|
171
|
+
return { found: false };
|
|
172
|
+
}
|
|
173
|
+
if (!Object.prototype.hasOwnProperty.call(current, part)) {
|
|
174
|
+
return { found: false };
|
|
175
|
+
}
|
|
176
|
+
current = current[part];
|
|
177
|
+
}
|
|
178
|
+
return { found: true, value: current };
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Sentinel error for .orFail()
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
/**
|
|
184
|
+
* Error thrown by `.orFail()` when the preceding assertion failed.
|
|
185
|
+
* Caught by the harness as a hard failure.
|
|
186
|
+
*/
|
|
187
|
+
export class ExpectFailError extends Error {
|
|
188
|
+
constructor(message) {
|
|
189
|
+
super(message);
|
|
190
|
+
this.name = "ExpectFailError";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Fluent assertion object returned by `ctx.expect(actual)`.
|
|
195
|
+
*
|
|
196
|
+
* Every terminal method (e.g. `toBe`, `toContain`) emits an assertion event
|
|
197
|
+
* and returns `this` for optional chaining with `.orFail()`.
|
|
198
|
+
*
|
|
199
|
+
* **Soft-by-default**: failed assertions are recorded but execution continues.
|
|
200
|
+
*
|
|
201
|
+
* **Custom matchers**: Use `Expectation.extend()` to add domain-specific
|
|
202
|
+
* assertions. Combine with `CustomMatchers` declaration merging for full
|
|
203
|
+
* type safety — see {@link CustomMatchers} for details.
|
|
204
|
+
*
|
|
205
|
+
* **Assertion messages**: Every matcher accepts an optional `message` string as
|
|
206
|
+
* its last argument. When provided, it is prepended to the auto-generated
|
|
207
|
+
* message: `"GET /users status: expected 401 to be 200"`. This makes failures
|
|
208
|
+
* far more actionable in Trace Viewer, CI logs, and MCP tool output.
|
|
209
|
+
*
|
|
210
|
+
* @example Basic assertions
|
|
211
|
+
* ```ts
|
|
212
|
+
* ctx.expect(res.status).toBe(200);
|
|
213
|
+
* ctx.expect(body.users).toHaveLength(3);
|
|
214
|
+
* ctx.expect(body).toMatchObject({ success: true });
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* @example With descriptive messages (recommended)
|
|
218
|
+
* ```ts
|
|
219
|
+
* ctx.expect(res.status).toBe(200, "GET /users status");
|
|
220
|
+
* ctx.expect(body.users).toHaveLength(3, "user list count");
|
|
221
|
+
* ctx.expect(res).toHaveHeader("content-type", /json/, "response content type");
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @example Guard + message
|
|
225
|
+
* ```ts
|
|
226
|
+
* ctx.expect(res.status).toBe(200, "POST /orders").orFail();
|
|
227
|
+
* const body = await res.json(); // safe — status was 200
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
export class Expectation {
|
|
231
|
+
/**
|
|
232
|
+
* The actual value being asserted on.
|
|
233
|
+
* `protected` (not `#private`) so that `Expectation.extend()` prototype
|
|
234
|
+
* patching can access it at runtime.
|
|
235
|
+
*/
|
|
236
|
+
_actual;
|
|
237
|
+
_negated;
|
|
238
|
+
_emit;
|
|
239
|
+
/** Tracks whether the last assertion in this chain passed. */
|
|
240
|
+
_lastPassed = true;
|
|
241
|
+
_lastMessage = "";
|
|
242
|
+
constructor(actual, emit, negated = false) {
|
|
243
|
+
this._actual = actual;
|
|
244
|
+
this._emit = emit;
|
|
245
|
+
this._negated = negated;
|
|
246
|
+
}
|
|
247
|
+
// -------------------------------------------------------------------------
|
|
248
|
+
// Modifiers
|
|
249
|
+
// -------------------------------------------------------------------------
|
|
250
|
+
/**
|
|
251
|
+
* Negate the next assertion.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ctx.expect(body.banned).not.toBe(true);
|
|
255
|
+
* ctx.expect(body.roles).not.toContain("superadmin");
|
|
256
|
+
*/
|
|
257
|
+
get not() {
|
|
258
|
+
return new Expectation(this._actual, this._emit, !this._negated);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* If the preceding assertion failed, throw immediately to abort the test.
|
|
262
|
+
* Use this for "guard" assertions where subsequent code depends on the value.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ctx.expect(res.status).toBe(200).orFail();
|
|
266
|
+
* const body = await res.json(); // safe — status was 200
|
|
267
|
+
*/
|
|
268
|
+
orFail() {
|
|
269
|
+
if (!this._lastPassed) {
|
|
270
|
+
throw new ExpectFailError(this._lastMessage);
|
|
271
|
+
}
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
// -------------------------------------------------------------------------
|
|
275
|
+
// Custom matchers
|
|
276
|
+
// -------------------------------------------------------------------------
|
|
277
|
+
/**
|
|
278
|
+
* Register custom assertion matchers on the `Expectation` prototype.
|
|
279
|
+
*
|
|
280
|
+
* Each matcher is a pure function that receives the actual value and any
|
|
281
|
+
* extra arguments, and returns a `MatcherResult`. The SDK automatically
|
|
282
|
+
* handles `.not` negation and `.orFail()` chaining.
|
|
283
|
+
*
|
|
284
|
+
* **Isolation**: Each test file runs in its own Deno subprocess, so
|
|
285
|
+
* prototype mutations from `Expectation.extend()` do not leak between files.
|
|
286
|
+
*
|
|
287
|
+
* **Type safety**: To get full TypeScript support for custom matchers,
|
|
288
|
+
* augment the {@link CustomMatchers} interface via declaration merging.
|
|
289
|
+
* See {@link CustomMatchers} for a complete example.
|
|
290
|
+
*
|
|
291
|
+
* @param matchers Record of matcher name → matcher function
|
|
292
|
+
* @throws If a matcher name conflicts with an existing method
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```ts
|
|
296
|
+
* // Step 1: Declare types (in your test file or shared .d.ts)
|
|
297
|
+
* declare module "@glubean/sdk/expect" {
|
|
298
|
+
* interface CustomMatchers<T> {
|
|
299
|
+
* toBeEven(): Expectation<T>;
|
|
300
|
+
* toBeWithinRange(min: number, max: number): Expectation<T>;
|
|
301
|
+
* }
|
|
302
|
+
* }
|
|
303
|
+
*
|
|
304
|
+
* // Step 2: Register runtime implementations
|
|
305
|
+
* import { Expectation } from "@glubean/sdk/expect";
|
|
306
|
+
*
|
|
307
|
+
* Expectation.extend({
|
|
308
|
+
* toBeEven: (actual) => ({
|
|
309
|
+
* passed: typeof actual === "number" && actual % 2 === 0,
|
|
310
|
+
* message: "to be even",
|
|
311
|
+
* actual,
|
|
312
|
+
* }),
|
|
313
|
+
* toBeWithinRange: (actual, min, max) => ({
|
|
314
|
+
* passed: typeof actual === "number" && actual >= (min as number) && actual <= (max as number),
|
|
315
|
+
* message: `to be within [${min}, ${max}]`,
|
|
316
|
+
* actual,
|
|
317
|
+
* expected: `[${min}, ${max}]`,
|
|
318
|
+
* }),
|
|
319
|
+
* });
|
|
320
|
+
*
|
|
321
|
+
* // Step 3: Use — fully typed
|
|
322
|
+
* ctx.expect(count).toBeEven();
|
|
323
|
+
* ctx.expect(count).not.toBeEven();
|
|
324
|
+
* ctx.expect(score).toBeWithinRange(0, 100).orFail();
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
static extend(matchers) {
|
|
328
|
+
for (const [name, fn] of Object.entries(matchers)) {
|
|
329
|
+
if (name in Expectation.prototype) {
|
|
330
|
+
throw new Error(`Matcher "${name}" already exists on Expectation. ` +
|
|
331
|
+
`Choose a different name to avoid conflicts with built-in matchers.`);
|
|
332
|
+
}
|
|
333
|
+
Expectation.prototype[name] = function (...args) {
|
|
334
|
+
// Access protected members via the instance.
|
|
335
|
+
const self = this;
|
|
336
|
+
const result = fn(self._actual, ...args);
|
|
337
|
+
return self._report(result.passed, result.message, result.actual, result.expected);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// -------------------------------------------------------------------------
|
|
342
|
+
// Internal emit helper
|
|
343
|
+
// -------------------------------------------------------------------------
|
|
344
|
+
/**
|
|
345
|
+
* Emit an assertion result.
|
|
346
|
+
* `protected` so that `Expectation.extend()` prototype patching can call it.
|
|
347
|
+
*
|
|
348
|
+
* @param label Optional human-readable context prepended to the message
|
|
349
|
+
* (e.g. `"GET /users status"` → `"GET /users status: expected 401 to be 200"`).
|
|
350
|
+
*/
|
|
351
|
+
_report(rawPassed, message, actual, expected, label) {
|
|
352
|
+
const passed = this._negated ? !rawPassed : rawPassed;
|
|
353
|
+
const prefix = this._negated ? "not " : "";
|
|
354
|
+
const autoMessage = `expected ${inspect(this._actual)} ${prefix}${message}`;
|
|
355
|
+
const fullMessage = label ? `${label}: ${autoMessage}` : autoMessage;
|
|
356
|
+
this._lastPassed = passed;
|
|
357
|
+
this._lastMessage = fullMessage;
|
|
358
|
+
this._emit({ passed, message: fullMessage, actual, expected });
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
// -------------------------------------------------------------------------
|
|
362
|
+
// Equality
|
|
363
|
+
// -------------------------------------------------------------------------
|
|
364
|
+
/**
|
|
365
|
+
* Strict equality (`Object.is`).
|
|
366
|
+
*
|
|
367
|
+
* @param expected The value to compare against
|
|
368
|
+
* @param message Optional context prepended to the assertion message
|
|
369
|
+
*
|
|
370
|
+
* @example ctx.expect(res.status).toBe(200, "GET /users status");
|
|
371
|
+
*/
|
|
372
|
+
toBe(expected, message) {
|
|
373
|
+
return this._report(Object.is(this._actual, expected), `to be ${inspect(expected)}`, this._actual, expected, message);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Deep equality.
|
|
377
|
+
*
|
|
378
|
+
* @param expected The value to compare against
|
|
379
|
+
* @param message Optional context prepended to the assertion message
|
|
380
|
+
*
|
|
381
|
+
* @example ctx.expect(body).toEqual({ id: 1, name: "Alice" }, "response body");
|
|
382
|
+
*/
|
|
383
|
+
toEqual(expected, message) {
|
|
384
|
+
return this._report(deepEqual(this._actual, expected), `to equal ${inspect(expected)}`, this._actual, expected, message);
|
|
385
|
+
}
|
|
386
|
+
// -------------------------------------------------------------------------
|
|
387
|
+
// Type / truthiness
|
|
388
|
+
// -------------------------------------------------------------------------
|
|
389
|
+
/**
|
|
390
|
+
* Check the runtime type via `typeof`.
|
|
391
|
+
*
|
|
392
|
+
* @param expected The expected type string
|
|
393
|
+
* @param message Optional context prepended to the assertion message
|
|
394
|
+
*
|
|
395
|
+
* @example ctx.expect(body.name).toBeType("string", "user name type");
|
|
396
|
+
*/
|
|
397
|
+
toBeType(expected, message) {
|
|
398
|
+
const actualType = typeof this._actual;
|
|
399
|
+
return this._report(actualType === expected, `to be type ${inspect(expected)}`, actualType, expected, message);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Check that the value is truthy.
|
|
403
|
+
*
|
|
404
|
+
* @param message Optional context prepended to the assertion message
|
|
405
|
+
*
|
|
406
|
+
* @example ctx.expect(body.active).toBeTruthy("user active flag");
|
|
407
|
+
*/
|
|
408
|
+
toBeTruthy(message) {
|
|
409
|
+
return this._report(!!this._actual, "to be truthy", this._actual, undefined, message);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Check that the value is falsy.
|
|
413
|
+
*
|
|
414
|
+
* @param message Optional context prepended to the assertion message
|
|
415
|
+
*
|
|
416
|
+
* @example ctx.expect(body.deleted).toBeFalsy("user should not be deleted");
|
|
417
|
+
*/
|
|
418
|
+
toBeFalsy(message) {
|
|
419
|
+
return this._report(!this._actual, "to be falsy", this._actual, undefined, message);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Check that the value is `null`.
|
|
423
|
+
*
|
|
424
|
+
* @param message Optional context prepended to the assertion message
|
|
425
|
+
*
|
|
426
|
+
* @example ctx.expect(body.avatar).toBeNull("deleted user avatar");
|
|
427
|
+
*/
|
|
428
|
+
toBeNull(message) {
|
|
429
|
+
return this._report(this._actual === null, "to be null", this._actual, null, message);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Check that the value is `undefined`.
|
|
433
|
+
*
|
|
434
|
+
* @param message Optional context prepended to the assertion message
|
|
435
|
+
*
|
|
436
|
+
* @example ctx.expect(body.nickname).toBeUndefined();
|
|
437
|
+
*/
|
|
438
|
+
toBeUndefined(message) {
|
|
439
|
+
return this._report(this._actual === undefined, "to be undefined", this._actual, undefined, message);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Check that the value is not `undefined`.
|
|
443
|
+
*
|
|
444
|
+
* @param message Optional context prepended to the assertion message
|
|
445
|
+
*
|
|
446
|
+
* @example ctx.expect(body.id).toBeDefined("response should include id");
|
|
447
|
+
*/
|
|
448
|
+
toBeDefined(message) {
|
|
449
|
+
return this._report(this._actual !== undefined, "to be defined", this._actual, undefined, message);
|
|
450
|
+
}
|
|
451
|
+
// -------------------------------------------------------------------------
|
|
452
|
+
// Numeric comparisons
|
|
453
|
+
// -------------------------------------------------------------------------
|
|
454
|
+
/**
|
|
455
|
+
* Check that the value is greater than `n`.
|
|
456
|
+
*
|
|
457
|
+
* @param n The lower bound (exclusive)
|
|
458
|
+
* @param message Optional context prepended to the assertion message
|
|
459
|
+
*
|
|
460
|
+
* @example ctx.expect(body.age).toBeGreaterThan(0, "user age");
|
|
461
|
+
*/
|
|
462
|
+
toBeGreaterThan(n, message) {
|
|
463
|
+
return this._report(this._actual > n, `to be greater than ${n}`, this._actual, `> ${n}`, message);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Check that the value is greater than or equal to `n`.
|
|
467
|
+
*
|
|
468
|
+
* @param n The lower bound (inclusive)
|
|
469
|
+
* @param message Optional context prepended to the assertion message
|
|
470
|
+
*
|
|
471
|
+
* @example ctx.expect(body.items.length).toBeGreaterThanOrEqual(1, "result count");
|
|
472
|
+
*/
|
|
473
|
+
toBeGreaterThanOrEqual(n, message) {
|
|
474
|
+
return this._report(this._actual >= n, `to be greater than or equal to ${n}`, this._actual, `>= ${n}`, message);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Check that the value is less than `n`.
|
|
478
|
+
*
|
|
479
|
+
* @param n The upper bound (exclusive)
|
|
480
|
+
* @param message Optional context prepended to the assertion message
|
|
481
|
+
*
|
|
482
|
+
* @example ctx.expect(latency).toBeLessThan(500, "response latency ms");
|
|
483
|
+
*/
|
|
484
|
+
toBeLessThan(n, message) {
|
|
485
|
+
return this._report(this._actual < n, `to be less than ${n}`, this._actual, `< ${n}`, message);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Check that the value is less than or equal to `n`.
|
|
489
|
+
*
|
|
490
|
+
* @param n The upper bound (inclusive)
|
|
491
|
+
* @param message Optional context prepended to the assertion message
|
|
492
|
+
*
|
|
493
|
+
* @example ctx.expect(res.status).toBeLessThanOrEqual(299, "GET /users status range");
|
|
494
|
+
*/
|
|
495
|
+
toBeLessThanOrEqual(n, message) {
|
|
496
|
+
return this._report(this._actual <= n, `to be less than or equal to ${n}`, this._actual, `<= ${n}`, message);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Check that the value is within `[min, max]` (inclusive).
|
|
500
|
+
*
|
|
501
|
+
* @param min Lower bound (inclusive)
|
|
502
|
+
* @param max Upper bound (inclusive)
|
|
503
|
+
* @param message Optional context prepended to the assertion message
|
|
504
|
+
*
|
|
505
|
+
* @example ctx.expect(body.score).toBeWithin(0, 100, "user score range");
|
|
506
|
+
*/
|
|
507
|
+
toBeWithin(min, max, message) {
|
|
508
|
+
const val = this._actual;
|
|
509
|
+
return this._report(val >= min && val <= max, `to be within [${min}, ${max}]`, this._actual, `[${min}, ${max}]`, message);
|
|
510
|
+
}
|
|
511
|
+
// -------------------------------------------------------------------------
|
|
512
|
+
// Collection / string
|
|
513
|
+
// -------------------------------------------------------------------------
|
|
514
|
+
/**
|
|
515
|
+
* Check that the value has the expected `length`.
|
|
516
|
+
*
|
|
517
|
+
* @param expected The expected length
|
|
518
|
+
* @param message Optional context prepended to the assertion message
|
|
519
|
+
*
|
|
520
|
+
* @example ctx.expect(body.users).toHaveLength(3, "user list count");
|
|
521
|
+
*/
|
|
522
|
+
toHaveLength(expected, message) {
|
|
523
|
+
const actual = this._actual?.length;
|
|
524
|
+
return this._report(actual === expected, `to have length ${expected}`, actual, expected, message);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Check that an array or string contains the given item/substring.
|
|
528
|
+
*
|
|
529
|
+
* @param item The item or substring to search for
|
|
530
|
+
* @param message Optional context prepended to the assertion message
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* ctx.expect(body.roles).toContain("admin", "user roles");
|
|
534
|
+
* ctx.expect(body.name).toContain("Ali");
|
|
535
|
+
*/
|
|
536
|
+
toContain(item, message) {
|
|
537
|
+
let found;
|
|
538
|
+
if (Array.isArray(this._actual)) {
|
|
539
|
+
found = this._actual.some((el) => deepEqual(el, item));
|
|
540
|
+
}
|
|
541
|
+
else if (typeof this._actual === "string") {
|
|
542
|
+
found = this._actual.includes(item);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
found = false;
|
|
546
|
+
}
|
|
547
|
+
return this._report(found, `to contain ${inspect(item)}`, this._actual, item, message);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Check that a string matches a regex or includes a substring.
|
|
551
|
+
*
|
|
552
|
+
* @param pattern Regex or substring to match
|
|
553
|
+
* @param message Optional context prepended to the assertion message
|
|
554
|
+
*
|
|
555
|
+
* @example
|
|
556
|
+
* ctx.expect(body.email).toMatch(/@example\.com$/, "email domain");
|
|
557
|
+
* ctx.expect(body.name).toMatch("Alice");
|
|
558
|
+
*/
|
|
559
|
+
toMatch(pattern, message) {
|
|
560
|
+
const actual = this._actual;
|
|
561
|
+
const passed = pattern instanceof RegExp ? pattern.test(actual) : actual?.includes(pattern);
|
|
562
|
+
return this._report(!!passed, `to match ${inspect(pattern)}`, this._actual, pattern instanceof RegExp ? pattern.toString() : pattern, message);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Check that a string starts with the given prefix.
|
|
566
|
+
*
|
|
567
|
+
* @param prefix The expected prefix
|
|
568
|
+
* @param message Optional context prepended to the assertion message
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* ctx.expect(body.id).toStartWith("usr_", "user id prefix");
|
|
572
|
+
* ctx.expect(url).toStartWith("https://");
|
|
573
|
+
*/
|
|
574
|
+
toStartWith(prefix, message) {
|
|
575
|
+
const actual = this._actual;
|
|
576
|
+
return this._report(typeof actual === "string" && actual.startsWith(prefix), `to start with ${inspect(prefix)}`, this._actual, prefix, message);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Check that a string ends with the given suffix.
|
|
580
|
+
*
|
|
581
|
+
* @param suffix The expected suffix
|
|
582
|
+
* @param message Optional context prepended to the assertion message
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* ctx.expect(body.email).toEndWith("@example.com", "email format");
|
|
586
|
+
* ctx.expect(body.filename).toEndWith(".json");
|
|
587
|
+
*/
|
|
588
|
+
toEndWith(suffix, message) {
|
|
589
|
+
const actual = this._actual;
|
|
590
|
+
return this._report(typeof actual === "string" && actual.endsWith(suffix), `to end with ${inspect(suffix)}`, this._actual, suffix, message);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Partial deep match — every key in `subset` must exist and match in the actual value.
|
|
594
|
+
*
|
|
595
|
+
* @param subset The expected subset of properties
|
|
596
|
+
* @param message Optional context prepended to the assertion message
|
|
597
|
+
*
|
|
598
|
+
* @example ctx.expect(body).toMatchObject({ success: true }, "create user response");
|
|
599
|
+
*/
|
|
600
|
+
toMatchObject(subset, message) {
|
|
601
|
+
const passed = typeof this._actual === "object" &&
|
|
602
|
+
this._actual !== null &&
|
|
603
|
+
matchesObject(this._actual, subset);
|
|
604
|
+
return this._report(!!passed, `to match object ${inspect(subset)}`, this._actual, subset, message);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Check that the value has a property at the given path.
|
|
608
|
+
* When `value` is provided, also checks property value equality.
|
|
609
|
+
*
|
|
610
|
+
* @param path Dot-separated property path (e.g. `"meta.created"`)
|
|
611
|
+
* @param value Expected value at the path (omit to check existence only)
|
|
612
|
+
* @param message Optional context prepended to the assertion message
|
|
613
|
+
*
|
|
614
|
+
* @example Existence check
|
|
615
|
+
* ```ts
|
|
616
|
+
* ctx.expect(body).toHaveProperty("id");
|
|
617
|
+
* ```
|
|
618
|
+
*
|
|
619
|
+
* @example Value check with message
|
|
620
|
+
* ```ts
|
|
621
|
+
* ctx.expect(body).toHaveProperty("status", "active", "user status");
|
|
622
|
+
* ```
|
|
623
|
+
*/
|
|
624
|
+
toHaveProperty(path, value, message) {
|
|
625
|
+
const resolved = resolvePath(this._actual, path);
|
|
626
|
+
let passed = resolved.found;
|
|
627
|
+
if (passed && arguments.length >= 2) {
|
|
628
|
+
passed = deepEqual(resolved.value, value);
|
|
629
|
+
}
|
|
630
|
+
const msg = arguments.length >= 2
|
|
631
|
+
? `to have property "${path}" with value ${inspect(value)}`
|
|
632
|
+
: `to have property "${path}"`;
|
|
633
|
+
return this._report(passed, msg, arguments.length >= 2 ? resolved.value : resolved.found, arguments.length >= 2 ? value : true, message);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Check that the value has all of the given property keys.
|
|
637
|
+
* Reports all missing keys in a single assertion message.
|
|
638
|
+
*
|
|
639
|
+
* @param keys Array of property paths to check
|
|
640
|
+
* @param message Optional context prepended to the assertion message
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* ctx.expect(body).toHaveProperties(["id", "name", "email"], "user fields");
|
|
644
|
+
*/
|
|
645
|
+
toHaveProperties(keys, message) {
|
|
646
|
+
const missing = [];
|
|
647
|
+
for (const key of keys) {
|
|
648
|
+
const resolved = resolvePath(this._actual, key);
|
|
649
|
+
if (!resolved.found) {
|
|
650
|
+
missing.push(key);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const passed = missing.length === 0;
|
|
654
|
+
const msg = passed
|
|
655
|
+
? `to have properties [${keys.join(", ")}]`
|
|
656
|
+
: `to have properties [${keys.join(", ")}] — missing: [${missing.join(", ")}]`;
|
|
657
|
+
return this._report(passed, msg, missing, [], message);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Custom predicate assertion.
|
|
661
|
+
*
|
|
662
|
+
* @param predicate Function that returns `true` if the assertion passes
|
|
663
|
+
* @param message Optional context prepended to the assertion message
|
|
664
|
+
*
|
|
665
|
+
* @example ctx.expect(body).toSatisfy((b) => b.items.length > 0, "response should have items");
|
|
666
|
+
*/
|
|
667
|
+
toSatisfy(predicate, message) {
|
|
668
|
+
let passed;
|
|
669
|
+
try {
|
|
670
|
+
passed = predicate(this._actual);
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
passed = false;
|
|
674
|
+
}
|
|
675
|
+
const desc = "to satisfy predicate";
|
|
676
|
+
return this._report(passed, desc, this._actual, undefined, message);
|
|
677
|
+
}
|
|
678
|
+
// -------------------------------------------------------------------------
|
|
679
|
+
// HTTP-specific helpers
|
|
680
|
+
// -------------------------------------------------------------------------
|
|
681
|
+
/**
|
|
682
|
+
* Assert on the `status` property (typically a `Response` object).
|
|
683
|
+
*
|
|
684
|
+
* @param code Expected HTTP status code
|
|
685
|
+
* @param message Optional context prepended to the assertion message
|
|
686
|
+
*
|
|
687
|
+
* @example ctx.expect(res).toHaveStatus(200, "GET /users");
|
|
688
|
+
*/
|
|
689
|
+
toHaveStatus(code, message) {
|
|
690
|
+
const actual = this._actual?.status;
|
|
691
|
+
return this._report(actual === code, `to have status ${code}`, actual, code, message);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Assert that a Response-like object has a JSON body matching the given subset
|
|
695
|
+
* (partial deep match, like `toMatchObject`).
|
|
696
|
+
*
|
|
697
|
+
* This method is **async** because it calls `.json()` on the response.
|
|
698
|
+
*
|
|
699
|
+
* @param subset Expected subset of properties in the JSON body
|
|
700
|
+
* @param message Optional context prepended to the assertion message
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* await ctx.expect(res).toHaveJsonBody({ success: true }, "create order response");
|
|
704
|
+
* (await ctx.expect(res).toHaveJsonBody({ ok: true }, "health check")).orFail();
|
|
705
|
+
*/
|
|
706
|
+
async toHaveJsonBody(subset, message) {
|
|
707
|
+
const actual = this._actual;
|
|
708
|
+
if (!actual || typeof actual.json !== "function") {
|
|
709
|
+
return this._report(false, `to have JSON body matching ${inspect(subset)} — actual is not a Response`, this._actual, subset, message);
|
|
710
|
+
}
|
|
711
|
+
let body;
|
|
712
|
+
try {
|
|
713
|
+
body = await actual.json();
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
return this._report(false, `to have JSON body matching ${inspect(subset)} — failed to parse JSON`, this._actual, subset, message);
|
|
717
|
+
}
|
|
718
|
+
const passed = typeof body === "object" &&
|
|
719
|
+
body !== null &&
|
|
720
|
+
matchesObject(body, subset);
|
|
721
|
+
return this._report(!!passed, `to have JSON body matching ${inspect(subset)}`, body, subset, message);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Assert that a `Response` or headers-like object has a specific header.
|
|
725
|
+
* Optionally check the header value against a string or regex.
|
|
726
|
+
*
|
|
727
|
+
* @param name Header name (case-insensitive for `Headers` objects)
|
|
728
|
+
* @param value Optional expected value or pattern
|
|
729
|
+
* @param message Optional context prepended to the assertion message
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ctx.expect(res).toHaveHeader("content-type", /json/, "response content type");
|
|
733
|
+
* ctx.expect(res).toHaveHeader("x-request-id");
|
|
734
|
+
*/
|
|
735
|
+
toHaveHeader(name, value, message) {
|
|
736
|
+
let headerValue;
|
|
737
|
+
const actual = this._actual;
|
|
738
|
+
if (actual?.headers) {
|
|
739
|
+
if (typeof actual.headers.get === "function") {
|
|
740
|
+
headerValue = actual.headers.get(name);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
headerValue = actual.headers[name] ??
|
|
744
|
+
actual.headers[name.toLowerCase()];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
let passed;
|
|
748
|
+
if (value === undefined) {
|
|
749
|
+
passed = headerValue !== null && headerValue !== undefined;
|
|
750
|
+
}
|
|
751
|
+
else if (value instanceof RegExp) {
|
|
752
|
+
passed = headerValue != null && value.test(headerValue);
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
passed = headerValue === value;
|
|
756
|
+
}
|
|
757
|
+
const expectedDesc = value === undefined
|
|
758
|
+
? `to have header "${name}"`
|
|
759
|
+
: `to have header "${name}" matching ${inspect(value)}`;
|
|
760
|
+
return this._report(passed, expectedDesc, headerValue, value ?? "(present)", message);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
//# sourceMappingURL=expect.js.map
|