@schmock/core 1.13.0 → 2.0.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.
Files changed (40) hide show
  1. package/dist/builder.d.ts +2 -0
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +13 -0
  4. package/dist/constants.d.ts +8 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +12 -0
  7. package/dist/helpers.d.ts +9 -0
  8. package/dist/helpers.d.ts.map +1 -0
  9. package/dist/helpers.js +37 -0
  10. package/dist/index.d.ts +4 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -1
  13. package/dist/interceptor.d.ts +5 -0
  14. package/dist/interceptor.d.ts.map +1 -0
  15. package/dist/interceptor.js +213 -0
  16. package/dist/plugin-pipeline.js +1 -1
  17. package/dist/types.d.ts +5 -0
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +1 -1
  20. package/src/builder.ts +23 -0
  21. package/src/constants.test.ts +40 -0
  22. package/src/constants.ts +18 -0
  23. package/src/helpers.test.ts +147 -0
  24. package/src/helpers.ts +58 -0
  25. package/src/index.ts +21 -0
  26. package/src/interceptor.test.ts +291 -0
  27. package/src/interceptor.ts +272 -0
  28. package/src/parser.property.test.ts +101 -0
  29. package/src/plugin-pipeline.ts +1 -1
  30. package/src/response-parsing.test.ts +74 -0
  31. package/src/server.test.ts +49 -0
  32. package/src/steps/async-support.steps.ts +0 -35
  33. package/src/steps/basic-usage.steps.ts +0 -84
  34. package/src/steps/developer-experience.steps.ts +0 -269
  35. package/src/steps/error-handling.steps.ts +0 -66
  36. package/src/steps/http-methods.steps.ts +0 -66
  37. package/src/steps/interceptor.steps.ts +206 -0
  38. package/src/steps/request-history.steps.ts +0 -75
  39. package/src/steps/route-key-format.steps.ts +0 -19
  40. package/src/types.ts +5 -0
@@ -10,134 +10,6 @@ describeFeature(feature, ({ Scenario }) => {
10
10
  let response: any;
11
11
  let responses: any[] = [];
12
12
 
13
- Scenario("Forgetting to provide response data", ({ Given, When, Then, And }) => {
14
- Given("I create a mock with no response data on {string}", (_, route: string) => {
15
- mock = schmock();
16
- mock(route as Schmock.RouteKey, undefined);
17
- });
18
-
19
- When("I request {string}", async (_, request: string) => {
20
- const [method, path] = request.split(" ");
21
- response = await mock.handle(method as any, path);
22
- });
23
-
24
- Then("I should receive status {int}", (_, status: number) => {
25
- expect(response.status).toBe(status);
26
- });
27
-
28
- And("the response body should be empty", () => {
29
- expect(response.body).toBeUndefined();
30
- });
31
- });
32
-
33
- Scenario("Using wrong parameter name in route", ({ Given, When, Then }) => {
34
- Given("I create a mock with a mismatched parameter name on {string}", (_, route: string) => {
35
- mock = schmock();
36
- mock(route as Schmock.RouteKey, ({ params }) => ({ id: params.id }));
37
- });
38
-
39
- When("I request {string}", async (_, request: string) => {
40
- const [method, path] = request.split(" ");
41
- response = await mock.handle(method as any, path);
42
- });
43
-
44
- Then("the wrong parameter should be undefined", () => {
45
- expect(response.body).toEqual({ id: undefined });
46
- });
47
- });
48
-
49
- Scenario("Correct parameter usage", ({ Given, When, Then }) => {
50
- Given("I create a mock with a matching parameter name on {string}", (_, route: string) => {
51
- mock = schmock();
52
- mock(route as Schmock.RouteKey, ({ params }) => ({ id: params.userId }));
53
- });
54
-
55
- When("I request {string}", async (_, request: string) => {
56
- const [method, path] = request.split(" ");
57
- response = await mock.handle(method as any, path);
58
- });
59
-
60
- Then("I should receive:", (_, docString: string) => {
61
- const expected = JSON.parse(docString);
62
- expect(response.body).toEqual(expected);
63
- });
64
- });
65
-
66
- Scenario("Mixing content types without explicit configuration", ({ Given, When, Then, And }) => {
67
- Given("I create a mock with JSON, text, number, and boolean routes", () => {
68
- mock = schmock();
69
- mock("GET /json", { data: "json" });
70
- mock("GET /text", "plain text");
71
- mock("GET /number", 42);
72
- mock("GET /boolean", true);
73
- });
74
-
75
- When("I test all mixed content type routes", async () => {
76
- responses = [];
77
- responses.push(await mock.handle("GET", "/json"));
78
- responses.push(await mock.handle("GET", "/text"));
79
- responses.push(await mock.handle("GET", "/number"));
80
- responses.push(await mock.handle("GET", "/boolean"));
81
- });
82
-
83
- Then("JSON route should have content-type {string}", (_, contentType: string) => {
84
- expect(responses[0].headers?.["content-type"]).toBe(contentType);
85
- });
86
-
87
- And("text route should have content-type {string}", (_, contentType: string) => {
88
- expect(responses[1].headers?.["content-type"]).toBe(contentType);
89
- });
90
-
91
- And("number route should have content-type {string}", (_, contentType: string) => {
92
- expect(responses[2].headers?.["content-type"]).toBe(contentType);
93
- });
94
-
95
- And("boolean route should have content-type {string}", (_, contentType: string) => {
96
- expect(responses[3].headers?.["content-type"]).toBe(contentType);
97
- });
98
- });
99
-
100
- Scenario("Expecting JSON but getting string conversion", ({ Given, When, Then, And }) => {
101
- Given("I create a mock returning a decimal number on {string}", (_, route: string) => {
102
- mock = schmock();
103
- mock(route as Schmock.RouteKey, 19.99);
104
- });
105
-
106
- When("I request {string}", async (_, request: string) => {
107
- const [method, path] = request.split(" ");
108
- response = await mock.handle(method as any, path);
109
- });
110
-
111
- Then("I should receive text {string}", (_, expectedText: string) => {
112
- expect(response.body).toBe(expectedText);
113
- });
114
-
115
- And("the content-type should be {string}", (_, contentType: string) => {
116
- expect(response.headers?.["content-type"]).toBe(contentType);
117
- });
118
- });
119
-
120
- Scenario("Forgetting await with async generators", ({ Given, When, Then, And }) => {
121
- Given("I create a mock with an async handler on {string}", (_, route: string) => {
122
- mock = schmock();
123
- mock(route as Schmock.RouteKey, async () => ({ async: true }));
124
- });
125
-
126
- When("I request {string}", async (_, request: string) => {
127
- const [method, path] = request.split(" ");
128
- response = await mock.handle(method as any, path);
129
- });
130
-
131
- Then("I should receive:", (_, docString: string) => {
132
- const expected = JSON.parse(docString);
133
- expect(response.body).toEqual(expected);
134
- });
135
-
136
- And("the content-type should be {string}", (_, contentType: string) => {
137
- expect(response.headers?.["content-type"]).toBe(contentType);
138
- });
139
- });
140
-
141
13
  Scenario("State confusion between global and local state", ({ Given, When, Then, And }) => {
142
14
  Given("I create a mock with global state and a local state counter", () => {
143
15
  mock = schmock({ state: { global: 1 } });
@@ -166,62 +38,6 @@ describeFeature(feature, ({ Scenario }) => {
166
38
  });
167
39
  });
168
40
 
169
- Scenario("Query parameter edge cases", ({ Given, When, Then }) => {
170
- Given("I create a mock that echoes query parameters on {string}", (_, route: string) => {
171
- mock = schmock();
172
- mock(route as Schmock.RouteKey, ({ query }) => ({
173
- term: query.q,
174
- page: query.page,
175
- empty: query.empty,
176
- }));
177
- });
178
-
179
- When("I request {string}", async (_, request: string) => {
180
- const [method, fullPath] = request.split(" ");
181
- const [path, queryString] = fullPath.split("?");
182
-
183
- const query: Record<string, string> = {};
184
- if (queryString) {
185
- queryString.split("&").forEach((param) => {
186
- const [key, value] = param.split("=");
187
- query[key] = value || "";
188
- });
189
- }
190
-
191
- response = await mock.handle(method as any, path, { query });
192
- });
193
-
194
- Then("I should receive:", (_, docString: string) => {
195
- const expected = JSON.parse(docString);
196
- expect(response.body).toEqual(expected);
197
- });
198
- });
199
-
200
- Scenario("Headers case sensitivity", ({ Given, When, Then }) => {
201
- Given("I create a mock that echoes headers on {string}", (_, route: string) => {
202
- mock = schmock();
203
- mock(route as Schmock.RouteKey, ({ headers }) => ({
204
- auth: headers.authorization,
205
- authUpper: headers.Authorization,
206
- contentType: headers["content-type"],
207
- }));
208
- });
209
-
210
- When("I request {string} with headers:", async (_, request: string, docString: string) => {
211
- const [method, path] = request.split(" ");
212
- const headers = JSON.parse(docString);
213
- response = await mock.handle(method as any, path, { headers });
214
- });
215
-
216
- Then("the header case sensitivity should show expected values", () => {
217
- expect(response.body).toEqual({
218
- auth: undefined,
219
- authUpper: "Bearer token",
220
- contentType: undefined,
221
- });
222
- });
223
- });
224
-
225
41
  Scenario("Route precedence with similar paths", ({ Given, When, Then, And }) => {
226
42
  Given("I create a mock with an exact route and a parameterized route on {string}", (_, basePath: string) => {
227
43
  mock = schmock();
@@ -277,28 +93,6 @@ describeFeature(feature, ({ Scenario }) => {
277
93
  });
278
94
  });
279
95
 
280
- Scenario("Namespace confusion with absolute paths", ({ Given, When, Then, And }) => {
281
- Given("I create a mock with namespace {string} and a users route", (_, namespace: string) => {
282
- mock = schmock({ namespace });
283
- mock("GET /users", []);
284
- });
285
-
286
- When("I test both namespace scenarios", async () => {
287
- responses = [];
288
- responses.push(await mock.handle("GET", "/users"));
289
- responses.push(await mock.handle("GET", "/api/v1/users"));
290
- });
291
-
292
- Then("the wrong namespace should receive status {int}", (_, status: number) => {
293
- expect(responses[0].status).toBe(status);
294
- });
295
-
296
- And("the correct namespace should receive:", (_, docString: string) => {
297
- const expected = JSON.parse(docString);
298
- expect(responses[1].body).toEqual(expected);
299
- });
300
- });
301
-
302
96
  Scenario("Plugin expecting different context structure", ({ Given, When, Then }) => {
303
97
  Given("I create a mock with a plugin that reads context properties", () => {
304
98
  mock = schmock();
@@ -371,46 +165,6 @@ describeFeature(feature, ({ Scenario }) => {
371
165
  });
372
166
  });
373
167
 
374
- Scenario("Common typos in method names", ({ Given, When, Then, And }) => {
375
- let errors: Error[] = [];
376
-
377
- Given("I create an empty mock for testing method typos", () => {
378
- mock = schmock();
379
- });
380
-
381
- When("I test all common method typos", () => {
382
- errors = [];
383
- const typos = ["GETS /users", "post /users", "GET/users"];
384
-
385
- for (const typo of typos) {
386
- try {
387
- mock(typo as Schmock.RouteKey, "test");
388
- errors.push(new Error("No error thrown"));
389
- } catch (e) {
390
- errors.push(e as Error);
391
- }
392
- }
393
- });
394
-
395
- Then("the wrong method typo should throw RouteParseError", () => {
396
- expect(errors[0]).not.toBeNull();
397
- expect(errors[0].constructor.name).toBe("RouteParseError");
398
- expect(errors[0].message).toContain("Invalid route key format");
399
- });
400
-
401
- And("the lowercase method typo should throw RouteParseError", () => {
402
- expect(errors[1]).not.toBeNull();
403
- expect(errors[1].constructor.name).toBe("RouteParseError");
404
- expect(errors[1].message).toContain("Invalid route key format");
405
- });
406
-
407
- And("the missing space typo should throw RouteParseError", () => {
408
- expect(errors[2]).not.toBeNull();
409
- expect(errors[2].constructor.name).toBe("RouteParseError");
410
- expect(errors[2].message).toContain("Invalid route key format");
411
- });
412
- });
413
-
414
168
  Scenario("Registering duplicate routes first route wins", ({ Given, When, Then }) => {
415
169
  Given("I create a mock with two routes on {string} with different data", (_, route: string) => {
416
170
  mock = schmock();
@@ -429,27 +183,4 @@ describeFeature(feature, ({ Scenario }) => {
429
183
  });
430
184
  });
431
185
 
432
- Scenario("Plugin returning unexpected structure", ({ Given, When, Then, And }) => {
433
- Given("I create a mock with a plugin that returns an invalid structure", () => {
434
- mock = schmock();
435
- const badPlugin = {
436
- name: "bad-structure",
437
- process: () => ({ wrong: "structure" }),
438
- };
439
- mock("GET /bad", "original").pipe(badPlugin as any);
440
- });
441
-
442
- When("I request {string}", async (_, request: string) => {
443
- const [method, path] = request.split(" ");
444
- response = await mock.handle(method as any, path);
445
- });
446
-
447
- Then("I should receive status {int}", (_, status: number) => {
448
- expect(response.status).toBe(status);
449
- });
450
-
451
- And("the response should contain error {string}", (_, errorMessage: string) => {
452
- expect(response.body.error).toContain(errorMessage);
453
- });
454
- });
455
186
  });
@@ -8,7 +8,6 @@ const feature = await loadFeature("../../features/error-handling.feature");
8
8
  describeFeature(feature, ({ Scenario }) => {
9
9
  let mock: CallableMockInstance;
10
10
  let response: any;
11
- let error: Error | null = null;
12
11
 
13
12
  Scenario("Route not found returns 404", ({ Given, When, Then, And }) => {
14
13
  Given("I create a mock with a GET /users route returning a user list", () => {
@@ -54,48 +53,6 @@ describeFeature(feature, ({ Scenario }) => {
54
53
  });
55
54
  });
56
55
 
57
- Scenario("Invalid route key throws RouteDefinitionError", ({ Given, Then, And }) => {
58
- Given("I attempt to register a route with an invalid HTTP method", () => {
59
- error = null;
60
- try {
61
- mock = schmock();
62
- mock("INVALID_METHOD /path" as any, "response");
63
- } catch (e) {
64
- error = e as Error;
65
- }
66
- });
67
-
68
- Then("it should throw a RouteDefinitionError", () => {
69
- expect(error).not.toBeNull();
70
- expect(error!.constructor.name).toBe("RouteParseError");
71
- });
72
-
73
- And("the error message should contain {string}", (_, message: string) => {
74
- expect(error!.message).toContain("Invalid route key format");
75
- });
76
- });
77
-
78
- Scenario("Empty route path throws RouteDefinitionError", ({ Given, Then, And }) => {
79
- Given("I attempt to register a route with an empty path", () => {
80
- error = null;
81
- try {
82
- mock = schmock();
83
- mock("GET " as any, "response");
84
- } catch (e) {
85
- error = e as Error;
86
- }
87
- });
88
-
89
- Then("it should throw a RouteDefinitionError", () => {
90
- expect(error).not.toBeNull();
91
- expect(error!.constructor.name).toBe("RouteParseError");
92
- });
93
-
94
- And("the error message should contain {string}", (_, message: string) => {
95
- expect(error!.message).toContain("Invalid route key format");
96
- });
97
- });
98
-
99
56
  Scenario("Plugin throws error returns 500 with PluginError", ({ Given, When, Then, And }) => {
100
57
  Given("I create a mock with a plugin that throws {string}", (_, errorMsg: string) => {
101
58
  mock = schmock();
@@ -197,29 +154,6 @@ describeFeature(feature, ({ Scenario }) => {
197
154
  });
198
155
  });
199
156
 
200
- Scenario("Invalid JSON generator with JSON content-type throws RouteDefinitionError", ({ Given, Then, And }) => {
201
- Given("I attempt to register a route with a circular reference as JSON", () => {
202
- error = null;
203
- try {
204
- mock = schmock();
205
- const circularRef: Record<string, unknown> = {};
206
- circularRef.self = circularRef;
207
- mock("GET /invalid", circularRef, { contentType: "application/json" });
208
- } catch (e) {
209
- error = e as Error;
210
- }
211
- });
212
-
213
- Then("it should throw a RouteDefinitionError", () => {
214
- expect(error).not.toBeNull();
215
- expect(error!.constructor.name).toBe("RouteDefinitionError");
216
- });
217
-
218
- And("the error message should contain {string}", (_, message: string) => {
219
- expect(error!.message).toContain(message);
220
- });
221
- });
222
-
223
157
  Scenario("Namespace mismatch returns 404", ({ Given, When, Then, And }) => {
224
158
  Given("I create a mock with namespace {string} and a GET /users route", (_, namespace: string) => {
225
159
  mock = schmock({ namespace });
@@ -9,7 +9,6 @@ describeFeature(feature, ({ Scenario }) => {
9
9
  let mock: CallableMockInstance;
10
10
  let response: any;
11
11
  let responses: any[] = [];
12
- let error: Error | null = null;
13
12
 
14
13
  Scenario("GET method with query parameters", ({ Given, When, Then }) => {
15
14
  Given("I create a mock with a GET search endpoint", () => {
@@ -255,71 +254,6 @@ describeFeature(feature, ({ Scenario }) => {
255
254
  });
256
255
  });
257
256
 
258
- Scenario("Method case sensitivity", ({ Given, When, Then }) => {
259
- error = null;
260
-
261
- Given("I create an empty mock for case sensitivity testing", () => {
262
- mock = schmock();
263
- });
264
-
265
- When("I attempt to create a mock with lowercase method", () => {
266
- error = null;
267
- try {
268
- mock('get /test' as Schmock.RouteKey, { method: 'get' });
269
- } catch (e) {
270
- error = e as Error;
271
- }
272
- });
273
-
274
- Then("it should throw RouteParseError for invalid method case", () => {
275
- expect(error).not.toBeNull();
276
- expect(error!.constructor.name).toBe('RouteParseError');
277
- expect(error!.message).toContain('Invalid route key format');
278
- });
279
- });
280
-
281
- Scenario("Unsupported HTTP methods", ({ Given, When, Then }) => {
282
- error = null;
283
-
284
- Given("I create an empty mock for unsupported method testing", () => {
285
- mock = schmock();
286
- });
287
-
288
- When("I attempt to create a mock with unsupported method", () => {
289
- error = null;
290
- try {
291
- mock('CUSTOM /endpoint' as Schmock.RouteKey, { custom: true });
292
- } catch (e) {
293
- error = e as Error;
294
- }
295
- });
296
-
297
- Then("it should throw RouteParseError for unsupported method", () => {
298
- expect(error).not.toBeNull();
299
- expect(error!.constructor.name).toBe('RouteParseError');
300
- expect(error!.message).toContain('Invalid route key format');
301
- });
302
- });
303
-
304
- Scenario("Method with special characters in path", ({ Given, When, Then }) => {
305
- Given("I create a mock with nested parameterized path segments", () => {
306
- mock = schmock();
307
- mock('GET /api/v1/users/:id/posts/:post-id', ({ params }) => ({
308
- userId: params.id,
309
- postId: params['post-id'],
310
- }));
311
- });
312
-
313
- When("I make a GET request to {string}", async (_, path: string) => {
314
- response = await mock.handle('GET', path);
315
- });
316
-
317
- Then("I should receive special characters response:", (_, docString: string) => {
318
- const expected = JSON.parse(docString);
319
- expect(response.body).toEqual(expected);
320
- });
321
- });
322
-
323
257
  Scenario("Method with request headers validation", ({ Given, When, Then, And }) => {
324
258
  Given("I create a mock with authorization header checking", () => {
325
259
  mock = schmock();
@@ -0,0 +1,206 @@
1
+ /// <reference path="../../schmock.d.ts" />
2
+
3
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
4
+ import { expect, vi } from "vitest";
5
+ import { schmock } from "../index.js";
6
+
7
+ const feature = await loadFeature("../../features/fetch-interceptor.feature");
8
+
9
+ describeFeature(feature, ({ Scenario, AfterEachScenario }) => {
10
+ let mock: Schmock.CallableMockInstance;
11
+ let handle: Schmock.InterceptHandle | undefined;
12
+ let originalFetch: typeof globalThis.fetch;
13
+ let savedFetch: ReturnType<typeof vi.fn>;
14
+ let fetchResponse: Response | undefined;
15
+
16
+ function setup() {
17
+ originalFetch = globalThis.fetch;
18
+ savedFetch = vi.fn().mockResolvedValue(new Response("real backend"));
19
+ globalThis.fetch = savedFetch;
20
+ mock = schmock();
21
+ fetchResponse = undefined;
22
+ }
23
+
24
+ AfterEachScenario(() => {
25
+ handle?.restore();
26
+ handle = undefined;
27
+ globalThis.fetch = originalFetch;
28
+ });
29
+
30
+ Scenario("Intercept a matched fetch request", ({ Given, When, Then, And }) => {
31
+ Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
32
+ setup();
33
+ mock("GET /api/users", [{ id: 1, name: "Alice" }]);
34
+ });
35
+
36
+ And("fetch is intercepted", () => {
37
+ handle = mock.intercept();
38
+ });
39
+
40
+ When("I fetch \"/api/users\"", async () => {
41
+ fetchResponse = await fetch("http://localhost/api/users");
42
+ });
43
+
44
+ Then("the fetch response status should be 200", () => {
45
+ expect(fetchResponse?.status).toBe(200);
46
+ });
47
+
48
+ And("the fetch response body should be the mocked users", async () => {
49
+ const body = await fetchResponse?.json();
50
+ expect(body).toEqual([{ id: 1, name: "Alice" }]);
51
+ });
52
+ });
53
+
54
+ Scenario("Passthrough for unmatched routes", ({ Given, When, Then, And }) => {
55
+ Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
56
+ setup();
57
+ mock("GET /api/users", [{ id: 1 }]);
58
+ });
59
+
60
+ And("fetch is intercepted with passthrough enabled", () => {
61
+ handle = mock.intercept({ passthrough: true });
62
+ });
63
+
64
+ When("I fetch \"/api/other\"", async () => {
65
+ fetchResponse = await fetch("http://localhost/api/other");
66
+ });
67
+
68
+ Then("the original fetch should have been called", () => {
69
+ expect(savedFetch).toHaveBeenCalled();
70
+ });
71
+ });
72
+
73
+ Scenario("Passthrough disabled returns 404", ({ Given, When, Then, And }) => {
74
+ Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
75
+ setup();
76
+ mock("GET /api/users", [{ id: 1 }]);
77
+ });
78
+
79
+ And("fetch is intercepted with passthrough disabled", () => {
80
+ handle = mock.intercept({ passthrough: false });
81
+ });
82
+
83
+ When("I fetch \"/api/other\"", async () => {
84
+ fetchResponse = await fetch("http://localhost/api/other");
85
+ });
86
+
87
+ Then("the fetch response status should be 404", () => {
88
+ expect(fetchResponse?.status).toBe(404);
89
+ });
90
+ });
91
+
92
+ Scenario("Restore puts original fetch back", ({ Given, When, Then, And }) => {
93
+ let savedRef: typeof globalThis.fetch;
94
+
95
+ Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
96
+ setup();
97
+ mock("GET /api/users", [{ id: 1 }]);
98
+ });
99
+
100
+ And("fetch is intercepted", () => {
101
+ savedRef = globalThis.fetch;
102
+ handle = mock.intercept();
103
+ });
104
+
105
+ When("I restore the interceptor", () => {
106
+ handle?.restore();
107
+ });
108
+
109
+ Then("globalThis.fetch should be the original function", () => {
110
+ expect(globalThis.fetch).toBe(savedRef);
111
+ });
112
+ });
113
+
114
+ Scenario("BaseUrl filters which requests are intercepted", ({ Given, When, Then, And }) => {
115
+ Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
116
+ setup();
117
+ mock("GET /api/users", [{ id: 1 }]);
118
+ });
119
+
120
+ And("fetch is intercepted with baseUrl \"/api\"", () => {
121
+ handle = mock.intercept({ baseUrl: "/api" });
122
+ });
123
+
124
+ When("I fetch \"/other/endpoint\"", async () => {
125
+ await fetch("http://localhost/other/endpoint");
126
+ });
127
+
128
+ Then("the original fetch should have been called", () => {
129
+ expect(savedFetch).toHaveBeenCalled();
130
+ });
131
+ });
132
+
133
+ Scenario("beforeRequest hook modifies the request", ({ Given, When, Then, And }) => {
134
+ Given("a Schmock instance with route \"GET /api/users\" that reads headers", () => {
135
+ setup();
136
+ mock("GET /api/users", ({ headers }) => [200, { token: headers["x-token"] }]);
137
+ });
138
+
139
+ And("fetch is intercepted with a beforeRequest hook that adds a header", () => {
140
+ handle = mock.intercept({
141
+ beforeRequest: (req) => ({
142
+ ...req,
143
+ headers: { ...req.headers, "x-token": "injected" },
144
+ }),
145
+ });
146
+ });
147
+
148
+ When("I fetch \"/api/users\"", async () => {
149
+ fetchResponse = await fetch("http://localhost/api/users");
150
+ });
151
+
152
+ Then("the response should contain the injected header value", async () => {
153
+ const body = await fetchResponse?.json();
154
+ expect(body).toEqual({ token: "injected" });
155
+ });
156
+ });
157
+
158
+ Scenario("beforeResponse hook modifies the response", ({ Given, When, Then, And }) => {
159
+ Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
160
+ setup();
161
+ mock("GET /api/users", [{ id: 1 }]);
162
+ });
163
+
164
+ And("fetch is intercepted with a beforeResponse hook that adds a header", () => {
165
+ handle = mock.intercept({
166
+ beforeResponse: (res) => ({
167
+ ...res,
168
+ headers: { ...res.headers, "x-mock": "true" },
169
+ }),
170
+ });
171
+ });
172
+
173
+ When("I fetch \"/api/users\"", async () => {
174
+ fetchResponse = await fetch("http://localhost/api/users");
175
+ });
176
+
177
+ Then("the fetch response should have the injected header", () => {
178
+ expect(fetchResponse?.headers.get("x-mock")).toBe("true");
179
+ });
180
+ });
181
+
182
+ Scenario("Double intercept throws an error", ({ Given, When, Then, And }) => {
183
+ let error: Error | undefined;
184
+
185
+ Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
186
+ setup();
187
+ mock("GET /api/users", [{ id: 1 }]);
188
+ });
189
+
190
+ And("fetch is intercepted", () => {
191
+ handle = mock.intercept();
192
+ });
193
+
194
+ When("I try to intercept again", () => {
195
+ try {
196
+ mock.intercept();
197
+ } catch (e) {
198
+ error = e as Error;
199
+ }
200
+ });
201
+
202
+ Then("it should throw an error about already intercepting", () => {
203
+ expect(error?.message).toMatch(/already intercepting/i);
204
+ });
205
+ });
206
+ });