@ereo/testing 0.1.23 → 0.1.24

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/index.d.ts CHANGED
@@ -5,12 +5,12 @@
5
5
  * Makes testing loaders, actions, middleware, and components trivial.
6
6
  */
7
7
  export { createTestContext, createContextFactory, type TestContextOptions, type TestContext, } from './context';
8
- export { testLoader, createLoaderTester, type LoaderTestOptions, type LoaderTestResult, } from './loader';
9
- export { testAction, createActionTester, type ActionTestOptions, type ActionTestResult, } from './action';
10
- export { testMiddleware, createMiddlewareTester, type MiddlewareTestOptions, type MiddlewareTestResult, } from './middleware';
11
- export { createMockRequest, createFormRequest, createMockFormData, createMockHeaders, parseJsonResponse, parseTextResponse, type MockRequestOptions, } from './request';
12
- export { renderRoute, createRouteRenderer, type RenderRouteOptions, type RenderResult, } from './render';
13
- export { assertRedirect, assertJson, assertStatus, assertHeaders, assertCookies, type AssertionOptions, } from './assertions';
14
- export { createTestServer, type TestServer, type TestServerOptions, } from './server';
15
- export { snapshotLoader, snapshotAction, type SnapshotOptions, } from './snapshot';
8
+ export { testLoader, createLoaderTester, testLoadersParallel, testLoaderMatrix, testLoaderError, type LoaderTestOptions, type LoaderTestResult, } from './loader';
9
+ export { testAction, createActionTester, testActionMatrix, testActionError, testActionWithFile, type ActionTestOptions, type ActionTestResult, } from './action';
10
+ export { testMiddleware, createMiddlewareTester, testMiddlewareChain, testMiddlewareMatrix, testMiddlewareError, testMiddlewareContext, type MiddlewareTestOptions, type MiddlewareTestResult, } from './middleware';
11
+ export { createMockRequest, createFormRequest, createMockFormData, createMockHeaders, createMockFile, parseJsonResponse, parseTextResponse, extractCookies, type MockRequestOptions, } from './request';
12
+ export { renderRoute, createRouteRenderer, renderComponent, renderRouteMatrix, testRouteRenders, getRouteMeta, type RenderRouteOptions, type RenderResult, } from './render';
13
+ export { assertRedirect, assertJson, assertStatus, assertHeaders, assertCookies, assertThrows, assertSchema, type AssertionOptions, } from './assertions';
14
+ export { createTestServer, createMockServer, type TestServer, type TestServerOptions, } from './server';
15
+ export { snapshotLoader, snapshotAction, createSnapshotMatrix, commonReplacers, applyReplacements, deterministicSnapshot, type SnapshotOptions, } from './snapshot';
16
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,KAAK,kBAAkB,EACvB,KAAK,YAAY,GAClB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,cAAc,EACd,UAAU,EACV,YAAY,EACZ,aAAa,EACb,aAAa,EACb,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,iBAAiB,GACvB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,cAAc,EACd,cAAc,EACd,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,YAAY,GAClB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,cAAc,EACd,UAAU,EACV,YAAY,EACZ,aAAa,EACb,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,iBAAiB,GACvB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -179,6 +179,22 @@ async function parseJsonResponse(response) {
179
179
  async function parseTextResponse(response) {
180
180
  return response.text();
181
181
  }
182
+ function createMockFile(name, content, type = "application/octet-stream") {
183
+ const blob = typeof content === "string" ? new Blob([content], { type }) : content;
184
+ return new File([blob], name, { type });
185
+ }
186
+ function extractCookies(response) {
187
+ const cookies = {};
188
+ const setCookieHeaders = response.headers.getSetCookie?.() || [];
189
+ for (const header of setCookieHeaders) {
190
+ const [nameValue] = header.split(";");
191
+ const [name, value] = nameValue.split("=");
192
+ if (name && value !== undefined) {
193
+ cookies[name.trim()] = value.trim();
194
+ }
195
+ }
196
+ return cookies;
197
+ }
182
198
 
183
199
  // src/loader.ts
184
200
  async function testLoader(loader, options = {}) {
@@ -211,6 +227,31 @@ function createLoaderTester(loader, baseOptions = {}) {
211
227
  });
212
228
  };
213
229
  }
230
+ async function testLoadersParallel(loaders) {
231
+ return Promise.all(loaders.map(({ loader, params, request, context }) => testLoader(loader, { params, request, context })));
232
+ }
233
+ async function testLoaderMatrix(loader, options) {
234
+ return Promise.all(options.params.map((params) => testLoader(loader, {
235
+ params,
236
+ request: options.request,
237
+ context: options.context
238
+ })));
239
+ }
240
+ async function testLoaderError(loader, options = {}) {
241
+ const request = createMockRequest(options.request);
242
+ const context = createTestContext(options.context);
243
+ const params = options.params || {};
244
+ try {
245
+ await loader({ request, params, context });
246
+ return { error: null, context, request };
247
+ } catch (error) {
248
+ return {
249
+ error: error instanceof Error ? error : new Error(String(error)),
250
+ context,
251
+ request
252
+ };
253
+ }
254
+ }
214
255
  // src/action.ts
215
256
  async function testAction(action, options = {}) {
216
257
  const requestOptions = {
@@ -273,6 +314,50 @@ function createActionTester(action, baseOptions = {}) {
273
314
  });
274
315
  };
275
316
  }
317
+ async function testActionMatrix(action, options) {
318
+ return Promise.all(options.submissions.map((submission) => testAction(action, {
319
+ params: options.params,
320
+ context: options.context,
321
+ formData: submission.formData,
322
+ body: submission.body
323
+ })));
324
+ }
325
+ async function testActionError(action, options = {}) {
326
+ const requestOptions = {
327
+ method: "POST",
328
+ ...options.request
329
+ };
330
+ if (options.formData) {
331
+ requestOptions.formData = options.formData;
332
+ } else if (options.body) {
333
+ requestOptions.body = options.body;
334
+ }
335
+ const request = createMockRequest(requestOptions);
336
+ const context = createTestContext(options.context);
337
+ const params = options.params || {};
338
+ try {
339
+ await action({ request, params, context });
340
+ return { error: null, context, request };
341
+ } catch (error) {
342
+ return {
343
+ error: error instanceof Error ? error : new Error(String(error)),
344
+ context,
345
+ request
346
+ };
347
+ }
348
+ }
349
+ async function testActionWithFile(action, options) {
350
+ const { file, extraFields = {}, ...rest } = options;
351
+ const blob = typeof file.content === "string" ? new Blob([file.content], { type: file.type || "application/octet-stream" }) : file.content;
352
+ const formData = {
353
+ ...extraFields,
354
+ [file.field]: new File([blob], file.name, { type: file.type })
355
+ };
356
+ return testAction(action, {
357
+ ...rest,
358
+ formData
359
+ });
360
+ }
276
361
  // src/middleware.ts
277
362
  async function testMiddleware(middleware, options = {}) {
278
363
  const request = createMockRequest(options.request);
@@ -316,6 +401,78 @@ function createMiddlewareTester(middleware, baseOptions = {}) {
316
401
  });
317
402
  };
318
403
  }
404
+ async function testMiddlewareChain(middlewares, options = {}) {
405
+ const request = createMockRequest(options.request);
406
+ const context = createTestContext(options.context);
407
+ const middlewareResults = [];
408
+ let index = 0;
409
+ const buildNext = (currentIndex) => {
410
+ return async () => {
411
+ middlewareResults[currentIndex].nextCalled = true;
412
+ if (currentIndex + 1 >= middlewares.length) {
413
+ return options.nextResponse || new Response("OK", { status: 200 });
414
+ }
415
+ const startTime2 = performance.now();
416
+ middlewareResults.push({
417
+ index: currentIndex + 1,
418
+ nextCalled: false,
419
+ duration: 0
420
+ });
421
+ const response2 = await middlewares[currentIndex + 1](request, context, buildNext(currentIndex + 1));
422
+ middlewareResults[currentIndex + 1].duration = performance.now() - startTime2;
423
+ return response2;
424
+ };
425
+ };
426
+ middlewareResults.push({ index: 0, nextCalled: false, duration: 0 });
427
+ const startTime = performance.now();
428
+ const response = await middlewares[0](request, context, buildNext(0));
429
+ middlewareResults[0].duration = performance.now() - startTime;
430
+ return {
431
+ response,
432
+ context,
433
+ request,
434
+ middlewareResults
435
+ };
436
+ }
437
+ async function testMiddlewareMatrix(middleware, options) {
438
+ return Promise.all(options.requests.map((request) => testMiddleware(middleware, {
439
+ request,
440
+ context: options.context
441
+ })));
442
+ }
443
+ async function testMiddlewareError(middleware, options) {
444
+ const request = createMockRequest(options.request);
445
+ const context = createTestContext(options.context);
446
+ try {
447
+ const response = await middleware(request, context, options.next);
448
+ return { response, error: null, context };
449
+ } catch (error) {
450
+ return {
451
+ response: null,
452
+ error: error instanceof Error ? error : new Error(String(error)),
453
+ context
454
+ };
455
+ }
456
+ }
457
+ async function testMiddlewareContext(middleware, options) {
458
+ const result = await testMiddleware(middleware, options);
459
+ const contextDiff = {};
460
+ let contextMatches = true;
461
+ for (const [key, expected] of Object.entries(options.expectContextValues)) {
462
+ const actual = result.context.get(key);
463
+ const matches = JSON.stringify(actual) === JSON.stringify(expected);
464
+ if (!matches) {
465
+ contextMatches = false;
466
+ contextDiff[key] = { expected, actual };
467
+ }
468
+ }
469
+ return {
470
+ response: result.response,
471
+ context: result.context,
472
+ contextMatches,
473
+ contextDiff
474
+ };
475
+ }
319
476
  // src/render.ts
320
477
  async function renderRoute(module, options = {}) {
321
478
  const request = createMockRequest(options.request);
@@ -367,6 +524,56 @@ function createRouteRenderer(module, baseOptions = {}) {
367
524
  });
368
525
  };
369
526
  }
527
+ function renderComponent(Component, props) {
528
+ return {
529
+ type: Component,
530
+ props,
531
+ key: null
532
+ };
533
+ }
534
+ async function renderRouteMatrix(module, options) {
535
+ return Promise.all(options.params.map((params) => renderRoute(module, {
536
+ params,
537
+ request: options.request,
538
+ context: options.context
539
+ })));
540
+ }
541
+ async function testRouteRenders(module, options = {}) {
542
+ try {
543
+ const result = await renderRoute(module, options);
544
+ return { renders: true, error: null, result };
545
+ } catch (error) {
546
+ return {
547
+ renders: false,
548
+ error: error instanceof Error ? error : new Error(String(error)),
549
+ result: null
550
+ };
551
+ }
552
+ }
553
+ async function getRouteMeta(module, options = {}) {
554
+ if (!module.meta) {
555
+ return [];
556
+ }
557
+ const request = createMockRequest(options.request);
558
+ const context = createTestContext(options.context);
559
+ const params = options.params || {};
560
+ const url = new URL(request.url);
561
+ let data;
562
+ if (options.loaderData !== undefined) {
563
+ data = options.loaderData;
564
+ } else if (module.loader) {
565
+ data = await module.loader({ request, params, context });
566
+ }
567
+ return module.meta({
568
+ data,
569
+ params,
570
+ location: {
571
+ pathname: url.pathname,
572
+ search: url.search,
573
+ hash: url.hash
574
+ }
575
+ });
576
+ }
370
577
  // src/assertions.ts
371
578
  function assertRedirect(response, expectedLocation, options = {}) {
372
579
  if (!response) {
@@ -504,6 +711,56 @@ function assertCookies(response, expected, options = {}) {
504
711
  }
505
712
  }
506
713
  }
714
+ async function assertThrows(fn, expected = {}, options = {}) {
715
+ let error = null;
716
+ try {
717
+ await fn();
718
+ } catch (e) {
719
+ error = e instanceof Error ? e : new Error(String(e));
720
+ }
721
+ if (!error) {
722
+ throw new Error(options.message || "Expected function to throw but it did not");
723
+ }
724
+ if (expected.message) {
725
+ if (expected.message instanceof RegExp) {
726
+ if (!expected.message.test(error.message)) {
727
+ throw new Error(options.message || `Expected error message to match ${expected.message} but got "${error.message}"`);
728
+ }
729
+ } else {
730
+ if (error.message !== expected.message) {
731
+ throw new Error(options.message || `Expected error message to be "${expected.message}" but got "${error.message}"`);
732
+ }
733
+ }
734
+ }
735
+ if (expected.name && error.name !== expected.name) {
736
+ throw new Error(options.message || `Expected error name to be "${expected.name}" but got "${error.name}"`);
737
+ }
738
+ if (expected.status && "status" in error && error.status !== expected.status) {
739
+ throw new Error(options.message || `Expected error status to be ${expected.status} but got ${error.status}`);
740
+ }
741
+ }
742
+ function assertSchema(data, schema, options = {}) {
743
+ if (typeof data !== "object" || data === null) {
744
+ throw new Error(options.message || "Expected data to be an object");
745
+ }
746
+ const obj = data;
747
+ for (const [key, expectedType] of Object.entries(schema)) {
748
+ const value = obj[key];
749
+ let actualType;
750
+ if (value === null) {
751
+ actualType = "null";
752
+ } else if (value === undefined) {
753
+ actualType = "undefined";
754
+ } else if (Array.isArray(value)) {
755
+ actualType = "array";
756
+ } else {
757
+ actualType = typeof value;
758
+ }
759
+ if (actualType !== expectedType) {
760
+ throw new Error(options.message || `Expected "${key}" to be ${expectedType} but got ${actualType}`);
761
+ }
762
+ }
763
+ }
507
764
  // src/server.ts
508
765
  async function createTestServer(options = {}) {
509
766
  const port = options.port || await getAvailablePort();
@@ -597,6 +854,45 @@ async function getAvailablePort() {
597
854
  server.stop();
598
855
  return port;
599
856
  }
857
+ async function createMockServer(options) {
858
+ const port = options.port || await getAvailablePort();
859
+ const server = Bun.serve({
860
+ port,
861
+ async fetch(request) {
862
+ const url = new URL(request.url);
863
+ const method = request.method;
864
+ const path = url.pathname;
865
+ const routeKey = `${method} ${path}`;
866
+ const handler = options.routes[routeKey];
867
+ if (!handler) {
868
+ return new Response("Not Found", { status: 404 });
869
+ }
870
+ let responseData;
871
+ if (typeof handler === "function") {
872
+ let body;
873
+ try {
874
+ body = await request.json();
875
+ } catch {
876
+ body = undefined;
877
+ }
878
+ responseData = handler({ body, params: {} });
879
+ } else {
880
+ responseData = handler;
881
+ }
882
+ return new Response(JSON.stringify(responseData), {
883
+ status: 200,
884
+ headers: { "Content-Type": "application/json" }
885
+ });
886
+ }
887
+ });
888
+ return {
889
+ url: `http://localhost:${port}`,
890
+ port,
891
+ stop: async () => {
892
+ server.stop();
893
+ }
894
+ };
895
+ }
600
896
  // src/snapshot.ts
601
897
  function prepareForSnapshot(data, options = {}) {
602
898
  if (data === null || data === undefined) {
@@ -633,29 +929,83 @@ async function snapshotAction(action, testOptions = {}, snapshotOptions = {}) {
633
929
  const result = await testAction(action, testOptions);
634
930
  return prepareForSnapshot(result.data, snapshotOptions);
635
931
  }
932
+ async function createSnapshotMatrix(loader, options) {
933
+ const snapshots = {};
934
+ for (const [name, testOptions] of Object.entries(options.scenarios)) {
935
+ snapshots[name] = await snapshotLoader(loader, testOptions, options.snapshotOptions);
936
+ }
937
+ return snapshots;
938
+ }
939
+ var commonReplacers = {
940
+ date: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g,
941
+ uuid: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
942
+ numericId: /\d+/g
943
+ };
944
+ function applyReplacements(data, replacements) {
945
+ const json = JSON.stringify(data);
946
+ let result = json;
947
+ for (const [pattern, replacement] of Object.entries(replacements)) {
948
+ result = result.split(pattern).join(replacement);
949
+ }
950
+ return JSON.parse(result);
951
+ }
952
+ function deterministicSnapshot(data) {
953
+ return JSON.stringify(data, (_, value) => {
954
+ if (value && typeof value === "object" && !Array.isArray(value)) {
955
+ return Object.keys(value).sort().reduce((sorted, key) => {
956
+ sorted[key] = value[key];
957
+ return sorted;
958
+ }, {});
959
+ }
960
+ return value;
961
+ }, 2);
962
+ }
636
963
  export {
964
+ testRouteRenders,
965
+ testMiddlewareMatrix,
966
+ testMiddlewareError,
967
+ testMiddlewareContext,
968
+ testMiddlewareChain,
637
969
  testMiddleware,
970
+ testLoadersParallel,
971
+ testLoaderMatrix,
972
+ testLoaderError,
638
973
  testLoader,
974
+ testActionWithFile,
975
+ testActionMatrix,
976
+ testActionError,
639
977
  testAction,
640
978
  snapshotLoader,
641
979
  snapshotAction,
980
+ renderRouteMatrix,
642
981
  renderRoute,
982
+ renderComponent,
643
983
  parseTextResponse,
644
984
  parseJsonResponse,
985
+ getRouteMeta,
986
+ extractCookies,
987
+ deterministicSnapshot,
645
988
  createTestServer,
646
989
  createTestContext,
990
+ createSnapshotMatrix,
647
991
  createRouteRenderer,
992
+ createMockServer,
648
993
  createMockRequest,
649
994
  createMockHeaders,
650
995
  createMockFormData,
996
+ createMockFile,
651
997
  createMiddlewareTester,
652
998
  createLoaderTester,
653
999
  createFormRequest,
654
1000
  createContextFactory,
655
1001
  createActionTester,
1002
+ commonReplacers,
1003
+ assertThrows,
656
1004
  assertStatus,
1005
+ assertSchema,
657
1006
  assertRedirect,
658
1007
  assertJson,
659
1008
  assertHeaders,
660
- assertCookies
1009
+ assertCookies,
1010
+ applyReplacements
661
1011
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ereo/testing",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "license": "MIT",
5
5
  "author": "Ereo Team",
6
6
  "homepage": "https://ereo.dev",
@@ -32,9 +32,9 @@
32
32
  "typecheck": "tsc --noEmit --skipLibCheck"
33
33
  },
34
34
  "dependencies": {
35
- "@ereo/core": "^0.1.23",
36
- "@ereo/router": "^0.1.23",
37
- "@ereo/data": "^0.1.23"
35
+ "@ereo/core": "^0.1.24",
36
+ "@ereo/router": "^0.1.24",
37
+ "@ereo/data": "^0.1.24"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/bun": "^1.1.0",