@nestia/e2e 7.4.0 → 8.0.0-dev.20250829-2

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.
@@ -10,21 +10,21 @@ import { json_equal_to } from "./internal/json_equal_to";
10
10
  * HTTP error validation, pagination testing, search functionality validation,
11
11
  * and sorting validation.
12
12
  *
13
- * All functions follow a currying pattern to enable reusable test
14
- * configurations and provide detailed error messages for debugging failed
15
- * assertions.
13
+ * Most functions use direct parameter passing for simplicity, while some
14
+ * maintain currying patterns for advanced composition. All provide detailed
15
+ * error messages for debugging failed assertions.
16
16
  *
17
17
  * @author Jeongho Nam - https://github.com/samchon
18
18
  * @example
19
19
  * ```typescript
20
20
  * // Basic condition testing
21
- * TestValidator.predicate("user should be authenticated")(user.isAuthenticated);
21
+ * TestValidator.predicate("user should be authenticated", user.isAuthenticated);
22
22
  *
23
23
  * // Equality validation
24
- * TestValidator.equals("API response should match expected")(expected)(actual);
24
+ * TestValidator.equals("API response should match expected", x, y);
25
25
  *
26
26
  * // Error validation
27
- * TestValidator.error("should throw on invalid input")(() => validateInput(""));
27
+ * TestValidator.error("should throw on invalid input", () => assertInput(""));
28
28
  * ```;
29
29
  */
30
30
  export namespace TestValidator {
@@ -38,53 +38,56 @@ export namespace TestValidator {
38
38
  * @example
39
39
  * ```typescript
40
40
  * // Synchronous boolean
41
- * TestValidator.predicate("user should exist")(user !== null);
41
+ * TestValidator.predicate("user should exist", user !== null);
42
42
  *
43
43
  * // Synchronous function
44
- * TestValidator.predicate("array should be empty")(() => arr.length === 0);
44
+ * TestValidator.predicate("array should be empty", () => arr.length === 0);
45
45
  *
46
46
  * // Asynchronous function
47
- * await TestValidator.predicate("database should be connected")(
47
+ * await TestValidator.predicate("database should be connected",
48
48
  * async () => await db.ping()
49
49
  * );
50
50
  * ```;
51
51
  *
52
52
  * @param title - Descriptive title used in error messages when validation
53
53
  * fails
54
- * @returns A currying function that accepts the condition to validate
54
+ * @param condition - The condition to validate (boolean, function, or async
55
+ * function)
56
+ * @returns Void or Promise<void> based on the input type
55
57
  * @throws Error with descriptive message when condition is not satisfied
56
58
  */
57
- export const predicate =
58
- (title: string) =>
59
- <T extends boolean | (() => boolean) | (() => Promise<boolean>)>(
60
- condition: T,
61
- ): T extends () => Promise<boolean> ? Promise<void> : void => {
62
- const message = () =>
63
- `Bug on ${title}: expected condition is not satisfied.`;
59
+ export function predicate<
60
+ T extends boolean | (() => boolean) | (() => Promise<boolean>),
61
+ >(
62
+ title: string,
63
+ condition: T,
64
+ ): T extends () => Promise<boolean> ? Promise<void> : void {
65
+ const message = () =>
66
+ `Bug on ${title}: expected condition is not satisfied.`;
64
67
 
65
- // SCALAR
66
- if (typeof condition === "boolean") {
67
- if (condition !== true) throw new Error(message());
68
- return undefined as any;
69
- }
68
+ // SCALAR
69
+ if (typeof condition === "boolean") {
70
+ if (condition !== true) throw new Error(message());
71
+ return undefined as any;
72
+ }
70
73
 
71
- // CLOSURE
72
- const output: boolean | Promise<boolean> = condition();
73
- if (typeof output === "boolean") {
74
- if (output !== true) throw new Error(message());
75
- return undefined as any;
76
- }
74
+ // CLOSURE
75
+ const output: boolean | Promise<boolean> = condition();
76
+ if (typeof output === "boolean") {
77
+ if (output !== true) throw new Error(message());
78
+ return undefined as any;
79
+ }
77
80
 
78
- // ASYNCHRONOUS
79
- return new Promise<void>((resolve, reject) => {
80
- output
81
- .then((flag) => {
82
- if (flag === true) resolve();
83
- else reject(message());
84
- })
85
- .catch(reject);
86
- }) as any;
87
- };
81
+ // ASYNCHRONOUS
82
+ return new Promise<void>((resolve, reject) => {
83
+ output
84
+ .then((flag) => {
85
+ if (flag === true) resolve();
86
+ else reject(message());
87
+ })
88
+ .catch(reject);
89
+ }) as any;
90
+ }
88
91
 
89
92
  /**
90
93
  * Validates deep equality between two values using JSON comparison.
@@ -93,61 +96,50 @@ export namespace TestValidator {
93
96
  * exception filter to ignore specific keys during comparison. Useful for
94
97
  * validating API responses, data transformations, and object state changes.
95
98
  *
96
- * **Type Safety Notes:**
97
- *
98
- * - The generic type T is inferred from the `actual` parameter (first in the
99
- * currying chain)
100
- * - The `expected` parameter must be assignable to `T | null | undefined`
101
- * - For objects, `expected` must have the same or subset of properties as
102
- * `actual`
103
- * - For union types like `string | null`, ensure proper type compatibility:
104
- *
105
- * ```typescript
106
- * const x: string | null;
107
- * TestValidator.equals("works")(x)(null); // ✅ Works: null is assignable to string | null
108
- * TestValidator.equals("error")(null)(x); // ❌ Error: x might be string, but expected is null
109
- * ```
110
- *
111
99
  * @example
112
100
  * ```typescript
113
101
  * // Basic equality
114
- * TestValidator.equals("response should match expected")(expectedUser)(actualUser);
102
+ * TestValidator.equals("response should match expected", expectedUser, actualUser);
115
103
  *
116
104
  * // Ignore timestamps in comparison
117
- * TestValidator.equals("user data should match", (key) => key === "updatedAt")(
118
- * expectedUser
119
- * )(actualUser);
105
+ * TestValidator.equals("user data should match", expectedUser, actualUser,
106
+ * (key) => key === "updatedAt"
107
+ * );
120
108
  *
121
109
  * // Validate API response structure
122
- * const validateResponse = TestValidator.equals("API response structure");
123
- * validateResponse({ id: 1, name: "John" })({ id: 1, name: "John" });
110
+ * TestValidator.equals("API response structure",
111
+ * { id: 1, name: "John" },
112
+ * { id: 1, name: "John" }
113
+ * );
124
114
  *
125
115
  * // Type-safe nullable comparisons
126
116
  * const nullableData: { name: string } | null = getData();
127
- * TestValidator.equals("nullable check")(nullableData)(null); // ✅ Safe
117
+ * TestValidator.equals("nullable check", nullableData, null);
128
118
  * ```;
129
119
  *
130
120
  * @param title - Descriptive title used in error messages when values differ
121
+ * @param x - The first value to compare
122
+ * @param y - The second value to compare (can be null or undefined)
131
123
  * @param exception - Optional filter function to exclude specific keys from
132
124
  * comparison
133
- * @returns A currying function chain: first accepts expected value, then
134
- * actual value
135
125
  * @throws Error with detailed diff information when values are not equal
136
126
  */
137
- export const equals =
138
- (title: string, exception: (key: string) => boolean = () => false) =>
139
- <T>(actual: T) =>
140
- (expected: T | null | undefined) => {
141
- const diff: string[] = json_equal_to(exception)(actual)(expected);
142
- if (diff.length)
143
- throw new Error(
144
- [
145
- `Bug on ${title}: found different values - [${diff.join(", ")}]:`,
146
- "\n",
147
- JSON.stringify({ actual, expected }, null, 2),
148
- ].join("\n"),
149
- );
150
- };
127
+ export function equals<T>(
128
+ title: string,
129
+ x: T,
130
+ y: T | null | undefined,
131
+ exception?: (key: string) => boolean,
132
+ ): void {
133
+ const diff: string[] = json_equal_to(exception ?? (() => false))(x)(y);
134
+ if (diff.length)
135
+ throw new Error(
136
+ [
137
+ `Bug on ${title}: found different values - [${diff.join(", ")}]:`,
138
+ "\n",
139
+ JSON.stringify({ x, y }, null, 2),
140
+ ].join("\n"),
141
+ );
142
+ }
151
143
 
152
144
  /**
153
145
  * Validates deep inequality between two values using JSON comparison.
@@ -157,62 +149,48 @@ export namespace TestValidator {
157
149
  * comparison. Useful for validating that data has changed, objects are
158
150
  * different, or mutations have occurred.
159
151
  *
160
- * **Type Safety Notes:**
161
- *
162
- * - The generic type T is inferred from the `actual` parameter (first in the
163
- * currying chain)
164
- * - The `expected` parameter must be assignable to `T | null | undefined`
165
- * - For objects, `expected` must have the same or subset of properties as
166
- * `actual`
167
- * - For union types like `string | null`, ensure proper type compatibility:
168
- *
169
- * ```typescript
170
- * const x: string | null;
171
- * TestValidator.notEquals("works")(x)(null); // ✅ Works: null is assignable to string | null
172
- * TestValidator.notEquals("error")(null)(x); // ❌ Error: x might be string, but expected is null
173
- * ```
174
- *
175
152
  * @example
176
153
  * ```typescript
177
154
  * // Basic inequality
178
- * TestValidator.notEquals("user should be different after update")(originalUser)(updatedUser);
155
+ * TestValidator.notEquals("user should be different after update", originalUser, updatedUser);
179
156
  *
180
157
  * // Ignore timestamps in comparison
181
- * TestValidator.notEquals("user data should differ", (key) => key === "updatedAt")(
182
- * originalUser
183
- * )(modifiedUser);
158
+ * TestValidator.notEquals("user data should differ", originalUser, modifiedUser,
159
+ * (key) => key === "updatedAt"
160
+ * );
184
161
  *
185
162
  * // Validate state changes
186
- * const validateStateChange = TestValidator.notEquals("state should have changed");
187
- * validateStateChange(initialState)(currentState);
163
+ * TestValidator.notEquals("state should have changed", initialState, currentState);
188
164
  *
189
165
  * // Type-safe nullable comparisons
190
166
  * const mutableData: { count: number } | null = getMutableData();
191
- * TestValidator.notEquals("should have changed")(mutableData)(null); // ✅ Safe
167
+ * TestValidator.notEquals("should have changed", mutableData, null);
192
168
  * ```;
193
169
  *
194
170
  * @param title - Descriptive title used in error messages when values are
195
171
  * equal
172
+ * @param x - The first value to compare
173
+ * @param y - The second value to compare (can be null or undefined)
196
174
  * @param exception - Optional filter function to exclude specific keys from
197
175
  * comparison
198
- * @returns A currying function chain: first accepts expected value, then
199
- * actual value
200
176
  * @throws Error when values are equal (indicating validation failure)
201
177
  */
202
- export const notEquals =
203
- (title: string, exception: (key: string) => boolean = () => false) =>
204
- <T>(actual: T) =>
205
- (expected: T | null | undefined) => {
206
- const diff: string[] = json_equal_to(exception)(actual)(expected);
207
- if (diff.length === 0)
208
- throw new Error(
209
- [
210
- `Bug on ${title}: values should be different but are equal:`,
211
- "\n",
212
- JSON.stringify({ actual, expected }, null, 2),
213
- ].join("\n"),
214
- );
215
- };
178
+ export function notEquals<T>(
179
+ title: string,
180
+ x: T,
181
+ y: T | null | undefined,
182
+ exception?: (key: string) => boolean,
183
+ ): void {
184
+ const diff: string[] = json_equal_to(exception ?? (() => false))(x)(y);
185
+ if (diff.length === 0)
186
+ throw new Error(
187
+ [
188
+ `Bug on ${title}: values should be different but are equal:`,
189
+ "\n",
190
+ JSON.stringify({ x, y }, null, 2),
191
+ ].join("\n"),
192
+ );
193
+ }
216
194
 
217
195
  /**
218
196
  * Validates that a function throws an error or rejects when executed.
@@ -224,41 +202,43 @@ export namespace TestValidator {
224
202
  * @example
225
203
  * ```typescript
226
204
  * // Synchronous error validation
227
- * TestValidator.error("should reject invalid email")(
205
+ * TestValidator.error("should reject invalid email",
228
206
  * () => validateEmail("invalid-email")
229
207
  * );
230
208
  *
231
209
  * // Asynchronous error validation
232
- * await TestValidator.error("should reject unauthorized access")(
210
+ * await TestValidator.error("should reject unauthorized access",
233
211
  * async () => await api.functional.getSecretData()
234
212
  * );
235
213
  *
236
214
  * // Validate input validation
237
- * TestValidator.error("should throw on empty string")(
215
+ * TestValidator.error("should throw on empty string",
238
216
  * () => processRequiredInput("")
239
217
  * );
240
218
  * ```;
241
219
  *
242
220
  * @param title - Descriptive title used in error messages when no error
243
221
  * occurs
244
- * @returns A currying function that accepts the task function to validate
222
+ * @param task - The function that should throw an error or reject
223
+ * @returns Void or Promise<void> based on the input type
245
224
  * @throws Error when the task function does not throw an error or reject
246
225
  */
247
- export const error =
248
- (title: string) =>
249
- <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
250
- const message = () => `Bug on ${title}: exception must be thrown.`;
251
- try {
252
- const output: T = task();
253
- if (is_promise(output))
254
- return new Promise<void>((resolve, reject) =>
255
- output.catch(() => resolve()).then(() => reject(message())),
256
- ) as any;
257
- else throw new Error(message());
258
- } catch {
259
- return undefined as any;
260
- }
261
- };
226
+ export function error<T>(
227
+ title: string,
228
+ task: () => T,
229
+ ): T extends Promise<any> ? Promise<void> : void {
230
+ const message = () => `Bug on ${title}: exception must be thrown.`;
231
+ try {
232
+ const output: T = task();
233
+ if (is_promise(output))
234
+ return new Promise<void>((resolve, reject) =>
235
+ output.catch(() => resolve()).then(() => reject(message())),
236
+ ) as any;
237
+ else throw new Error(message());
238
+ } catch {
239
+ return undefined as any;
240
+ }
241
+ }
262
242
 
263
243
  /**
264
244
  * Validates that a function throws an HTTP error with specific status codes.
@@ -270,119 +250,72 @@ export namespace TestValidator {
270
250
  * @example
271
251
  * ```typescript
272
252
  * // Validate 401 Unauthorized
273
- * await TestValidator.httpError("should return 401 for invalid token")(401)(
253
+ * await TestValidator.httpError("should return 401 for invalid token", 401,
274
254
  * async () => await api.functional.getProtectedResource("invalid-token")
275
255
  * );
276
256
  *
277
257
  * // Validate multiple possible error codes
278
- * await TestValidator.httpError("should return client error")(400, 404, 422)(
258
+ * await TestValidator.httpError("should return client error", [400, 404, 422],
279
259
  * async () => await api.functional.updateNonexistentResource(data)
280
260
  * );
281
261
  *
282
262
  * // Validate server errors
283
- * TestValidator.httpError("should handle server errors")(500, 502, 503)(
263
+ * TestValidator.httpError("should handle server errors", [500, 502, 503],
284
264
  * () => callFaultyEndpoint()
285
265
  * );
286
266
  * ```;
287
267
  *
288
268
  * @param title - Descriptive title used in error messages
289
- * @returns A currying function that accepts status codes, then the task
290
- * function
269
+ * @param status - Expected status code(s), can be a single number or array
270
+ * @param task - The function that should throw an HttpError
271
+ * @returns Void or Promise<void> based on the input type
291
272
  * @throws Error when function doesn't throw HttpError or status code doesn't
292
273
  * match
293
274
  */
294
- export const httpError =
295
- (title: string) =>
296
- (...statuses: number[]) =>
297
- <T>(task: () => T): T extends Promise<any> ? Promise<void> : void => {
298
- const message = (actual?: number) =>
299
- typeof actual === "number"
300
- ? `Bug on ${title}: status code must be ${statuses.join(
301
- " or ",
302
- )}, but ${actual}.`
303
- : `Bug on ${title}: status code must be ${statuses.join(
304
- " or ",
305
- )}, but succeeded.`;
306
- const predicate = (exp: any): Error | null =>
307
- typeof exp === "object" &&
308
- exp.constructor.name === "HttpError" &&
309
- statuses.some((val) => val === exp.status)
310
- ? null
311
- : new Error(
312
- message(
313
- typeof exp === "object" && exp.constructor.name === "HttpError"
314
- ? exp.status
315
- : undefined,
316
- ),
317
- );
318
- try {
319
- const output: T = task();
320
- if (is_promise(output))
321
- return new Promise<void>((resolve, reject) =>
322
- output
323
- .catch((exp) => {
324
- const res: Error | null = predicate(exp);
325
- if (res) reject(res);
326
- else resolve();
327
- })
328
- .then(() => reject(new Error(message()))),
329
- ) as any;
330
- else throw new Error(message());
331
- } catch (exp) {
332
- const res: Error | null = predicate(exp);
333
- if (res) throw res;
334
- return undefined!;
335
- }
336
- };
337
-
338
- /**
339
- * Safely executes a function and captures any errors without throwing.
340
- *
341
- * Utility function for error handling in tests. Executes the provided
342
- * function and returns any error that occurs, or null if successful. Supports
343
- * both synchronous and asynchronous functions. Useful for testing error
344
- * conditions without stopping test execution.
345
- *
346
- * @example
347
- * ```typescript
348
- * // Synchronous error capture
349
- * const error = TestValidator.proceed(() => {
350
- * throw new Error("Something went wrong");
351
- * });
352
- * console.log(error?.message); // "Something went wrong"
353
- *
354
- * // Asynchronous error capture
355
- * const asyncError = await TestValidator.proceed(async () => {
356
- * await failingAsyncOperation();
357
- * });
358
- *
359
- * // Success case
360
- * const noError = TestValidator.proceed(() => {
361
- * return "success";
362
- * });
363
- * console.log(noError); // null
364
- * ```;
365
- *
366
- * @param task - Function to execute safely
367
- * @returns Error object if function throws/rejects, null if successful
368
- */
369
- export function proceed(task: () => Promise<any>): Promise<Error | null>;
370
- export function proceed(task: () => any): Error | null;
371
- export function proceed(
372
- task: () => any,
373
- ): Promise<Error | null> | (Error | null) {
275
+ export function httpError<T>(
276
+ title: string,
277
+ status: number | number[],
278
+ task: () => T,
279
+ ): T extends Promise<any> ? Promise<void> : void {
280
+ if (typeof status === "number") status = [status];
281
+ const message = (actual?: number) =>
282
+ typeof actual === "number"
283
+ ? `Bug on ${title}: status code must be ${status.join(
284
+ " or ",
285
+ )}, but ${actual}.`
286
+ : `Bug on ${title}: status code must be ${status.join(
287
+ " or ",
288
+ )}, but succeeded.`;
289
+ const predicate = (exp: any): Error | null =>
290
+ typeof exp === "object" &&
291
+ exp.constructor.name === "HttpError" &&
292
+ status.some((val) => val === exp.status)
293
+ ? null
294
+ : new Error(
295
+ message(
296
+ typeof exp === "object" && exp.constructor.name === "HttpError"
297
+ ? exp.status
298
+ : undefined,
299
+ ),
300
+ );
374
301
  try {
375
- const output: any = task();
302
+ const output: T = task();
376
303
  if (is_promise(output))
377
- return new Promise<Error | null>((resolve) =>
304
+ return new Promise<void>((resolve, reject) =>
378
305
  output
379
- .catch((exp) => resolve(exp as Error))
380
- .then(() => resolve(null)),
381
- );
306
+ .catch((exp) => {
307
+ const res: Error | null = predicate(exp);
308
+ if (res) reject(res);
309
+ else resolve();
310
+ })
311
+ .then(() => reject(new Error(message()))),
312
+ ) as any;
313
+ else throw new Error(message());
382
314
  } catch (exp) {
383
- return exp as Error;
315
+ const res: Error | null = predicate(exp);
316
+ if (res) throw res;
317
+ return undefined!;
384
318
  }
385
- return null;
386
319
  }
387
320
 
388
321
  /**
@@ -399,8 +332,7 @@ export namespace TestValidator {
399
332
  * const expectedArticles = await db.articles.findAll({ order: 'created_at DESC' });
400
333
  * const actualArticles = await api.functional.getArticles({ page: 1, limit: 10 });
401
334
  *
402
- * TestValidator.index("article pagination order")(expectedArticles)(
403
- * actualArticles,
335
+ * TestValidator.index("article pagination order", expectedArticles, actualArticles,
404
336
  * true // enable trace logging
405
337
  * );
406
338
  *
@@ -408,42 +340,41 @@ export namespace TestValidator {
408
340
  * const manuallyFilteredUsers = allUsers.filter(u => u.name.includes("John"));
409
341
  * const apiSearchResults = await api.functional.searchUsers({ query: "John" });
410
342
  *
411
- * TestValidator.index("user search results")(manuallyFilteredUsers)(
412
- * apiSearchResults
413
- * );
343
+ * TestValidator.index("user search results", manuallyFilteredUsers, apiSearchResults);
414
344
  * ```;
415
345
  *
416
346
  * @param title - Descriptive title used in error messages when order differs
417
- * @returns A currying function chain: expected entities, then actual entities
347
+ * @param expected - The expected entities in correct order
348
+ * @param gotten - The actual entities returned by the API
349
+ * @param trace - Optional flag to enable debug logging (default: false)
418
350
  * @throws Error when entity order differs between expected and actual results
419
351
  */
420
- export const index =
421
- (title: string) =>
422
- <Solution extends IEntity<any>>(expected: Solution[]) =>
423
- <Summary extends IEntity<any>>(
424
- gotten: Summary[],
425
- trace: boolean = false,
426
- ): void => {
427
- const length: number = Math.min(expected.length, gotten.length);
428
- expected = expected.slice(0, length);
429
- gotten = gotten.slice(0, length);
352
+ export const index = <Summary extends IEntity<any>>(
353
+ title: string,
354
+ expected: Summary[],
355
+ gotten: Summary[],
356
+ trace: boolean = false,
357
+ ): void => {
358
+ const length: number = Math.min(expected.length, gotten.length);
359
+ expected = expected.slice(0, length);
360
+ gotten = gotten.slice(0, length);
430
361
 
431
- const xIds: string[] = get_ids(expected).slice(0, length);
432
- const yIds: string[] = get_ids(gotten)
433
- .filter((id) => id >= xIds[0])
434
- .slice(0, length);
362
+ const xIds: string[] = get_ids(expected).slice(0, length);
363
+ const yIds: string[] = get_ids(gotten)
364
+ .filter((id) => id >= xIds[0])
365
+ .slice(0, length);
435
366
 
436
- const equals: boolean = xIds.every((x, i) => x === yIds[i]);
437
- if (equals === true) return;
438
- else if (trace === true)
439
- console.log({
440
- expected: xIds,
441
- gotten: yIds,
442
- });
443
- throw new Error(
444
- `Bug on ${title}: result of the index is different with manual aggregation.`,
445
- );
446
- };
367
+ const equals: boolean = xIds.every((x, i) => x === yIds[i]);
368
+ if (equals === true) return;
369
+ else if (trace === true)
370
+ console.log({
371
+ expected: xIds,
372
+ gotten: yIds,
373
+ });
374
+ throw new Error(
375
+ `Bug on ${title}: result of the index is different with manual aggregation.`,
376
+ );
377
+ };
447
378
 
448
379
  /**
449
380
  * Validates search functionality by testing API results against manual
@@ -456,55 +387,68 @@ export namespace TestValidator {
456
387
  *
457
388
  * @example
458
389
  * ```typescript
459
- * // Test article search functionality
390
+ * // Test article search functionality with exact matching
460
391
  * const allArticles = await db.articles.findAll();
461
- * const searchValidator = TestValidator.search("article search API")(
462
- * (req) => api.searchArticles(req)
463
- * )(allArticles, 5); // test with 5 random samples
392
+ * const searchValidator = TestValidator.search(
393
+ * "article search API",
394
+ * (req) => api.searchArticles(req),
395
+ * allArticles,
396
+ * 5 // test with 5 random samples
397
+ * );
398
+ *
399
+ * // Test exact match search
400
+ * await searchValidator({
401
+ * fields: ["title"],
402
+ * values: (article) => [article.title], // full title for exact match
403
+ * filter: (article, [title]) => article.title === title, // exact match
404
+ * request: ([title]) => ({ search: { title } })
405
+ * });
464
406
  *
407
+ * // Test partial match search with includes
465
408
  * await searchValidator({
466
- * fields: ["title", "content"],
467
- * values: (article) => [article.title.split(" ")[0]], // first word
468
- * filter: (article, [keyword]) =>
469
- * article.title.includes(keyword) || article.content.includes(keyword),
409
+ * fields: ["content"],
410
+ * values: (article) => [article.content.substring(0, 20)], // partial content
411
+ * filter: (article, [keyword]) => article.content.includes(keyword),
470
412
  * request: ([keyword]) => ({ q: keyword })
471
413
  * });
472
414
  *
473
- * // Test user search with multiple criteria
474
- * await TestValidator.search("user search with filters")(
475
- * (req) => api.getUsers(req)
476
- * )(allUsers, 3)({
477
- * fields: ["status", "role"],
478
- * values: (user) => [user.status, user.role],
479
- * filter: (user, [status, role]) =>
480
- * user.status === status && user.role === role,
481
- * request: ([status, role]) => ({ status, role })
415
+ * // Test multi-field search with exact matching
416
+ * await searchValidator({
417
+ * fields: ["writer", "title"],
418
+ * values: (article) => [article.writer, article.title],
419
+ * filter: (article, [writer, title]) =>
420
+ * article.writer === writer && article.title === title,
421
+ * request: ([writer, title]) => ({ search: { writer, title } })
482
422
  * });
483
423
  * ```;
484
424
  *
485
425
  * @param title - Descriptive title used in error messages when search fails
486
- * @returns A currying function chain: API getter function, then dataset and
487
- * sample count
426
+ * @param getter - API function that performs the search
427
+ * @param total - Complete dataset to sample from for testing
428
+ * @param sampleCount - Number of random samples to test (default: 1)
429
+ * @returns A function that accepts search configuration properties
488
430
  * @throws Error when API search results don't match manual filtering results
489
431
  */
490
432
  export const search =
491
- (title: string) =>
492
433
  <Entity extends IEntity<any>, Request>(
434
+ title: string,
493
435
  getter: (input: Request) => Promise<Entity[]>,
436
+ total: Entity[],
437
+ sampleCount: number = 1,
494
438
  ) =>
495
- (total: Entity[], sampleCount: number = 1) =>
496
439
  async <Values extends any[]>(
497
440
  props: ISearchProps<Entity, Values, Request>,
498
- ): Promise<void> => {
499
- const samples: Entity[] = RandomGenerator.sample(total)(sampleCount);
441
+ ) => {
442
+ const samples: Entity[] = RandomGenerator.sample(total, sampleCount);
500
443
  for (const s of samples) {
501
444
  const values: Values = props.values(s);
502
445
  const filtered: Entity[] = total.filter((entity) =>
503
446
  props.filter(entity, values),
504
447
  );
505
448
  const gotten: Entity[] = await getter(props.request(values));
506
-
507
- TestValidator.index(`${title} (${props.fields.join(", ")})`)(filtered)(
449
+ TestValidator.index(
450
+ `${title} (${props.fields.join(", ")})`,
451
+ filtered,
508
452
  gotten,
509
453
  );
510
454
  }
@@ -566,38 +510,47 @@ export namespace TestValidator {
566
510
  *
567
511
  * @example
568
512
  * ```typescript
569
- * // Test single field sorting
570
- * const sortValidator = TestValidator.sort("article sorting")(
513
+ * // Test single field sorting with GaffComparator
514
+ * const sortValidator = TestValidator.sort(
515
+ * "article sorting",
571
516
  * (sortable) => api.getArticles({ sort: sortable })
572
517
  * )("created_at")(
573
- * (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
518
+ * GaffComparator.dates((a) => a.created_at)
574
519
  * );
575
520
  *
576
521
  * await sortValidator("+"); // ascending
577
522
  * await sortValidator("-"); // descending
578
523
  *
579
- * // Test multi-field sorting with filtering
580
- * const userSortValidator = TestValidator.sort("user sorting")(
524
+ * // Test multi-field sorting with GaffComparator
525
+ * const userSortValidator = TestValidator.sort(
526
+ * "user sorting",
581
527
  * (sortable) => api.getUsers({ sort: sortable })
582
- * )("status", "created_at")(
583
- * (a, b) => {
584
- * if (a.status !== b.status) return a.status.localeCompare(b.status);
585
- * return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
586
- * },
528
+ * )("lastName", "firstName")(
529
+ * GaffComparator.strings((user) => [user.lastName, user.firstName]),
587
530
  * (user) => user.isActive // only test active users
588
531
  * );
589
532
  *
590
533
  * await userSortValidator("+", true); // ascending with trace logging
534
+ *
535
+ * // Custom comparator for complex logic
536
+ * const customSortValidator = TestValidator.sort(
537
+ * "custom sorting",
538
+ * (sortable) => api.getProducts({ sort: sortable })
539
+ * )("price", "rating")(
540
+ * (a, b) => {
541
+ * const priceDiff = a.price - b.price;
542
+ * return priceDiff !== 0 ? priceDiff : b.rating - a.rating; // price asc, rating desc
543
+ * }
544
+ * );
591
545
  * ```;
592
546
  *
593
547
  * @param title - Descriptive title used in error messages when sorting fails
594
- * @returns A currying function chain: API getter, field names, comparator,
595
- * then direction
548
+ * @param getter - API function that fetches sorted data
549
+ * @returns A currying function chain: field names, comparator, then direction
596
550
  * @throws Error when API results are not properly sorted according to
597
551
  * specification
598
552
  */
599
553
  export const sort =
600
- (title: string) =>
601
554
  <
602
555
  T extends object,
603
556
  Fields extends string,
@@ -605,6 +558,7 @@ export namespace TestValidator {
605
558
  `-${Fields}` | `+${Fields}`
606
559
  >,
607
560
  >(
561
+ title: string,
608
562
  getter: (sortable: Sortable) => Promise<T[]>,
609
563
  ) =>
610
564
  (...fields: Fields[]) =>
@@ -658,10 +612,12 @@ interface IEntity<Type extends string | number | bigint> {
658
612
  id: Type;
659
613
  }
660
614
 
615
+ /** @internal */
661
616
  function get_ids<Entity extends IEntity<any>>(entities: Entity[]): string[] {
662
617
  return entities.map((entity) => entity.id).sort((x, y) => (x < y ? -1 : 1));
663
618
  }
664
619
 
620
+ /** @internal */
665
621
  function is_promise(input: any): input is Promise<any> {
666
622
  return (
667
623
  typeof input === "object" &&
@@ -671,6 +627,7 @@ function is_promise(input: any): input is Promise<any> {
671
627
  );
672
628
  }
673
629
 
630
+ /** @internal */
674
631
  function is_sorted<T>(data: T[], comp: (x: T, y: T) => number): boolean {
675
632
  for (let i: number = 1; i < data.length; ++i)
676
633
  if (comp(data[i - 1], data[i]) > 0) return false;