@schmock/core 2.2.3 → 2.3.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.
Files changed (45) hide show
  1. package/dist/binary.d.ts +8 -0
  2. package/dist/binary.d.ts.map +1 -0
  3. package/dist/binary.js +9 -0
  4. package/dist/builder.d.ts.map +1 -1
  5. package/dist/builder.js +60 -29
  6. package/dist/http-helpers.d.ts +15 -3
  7. package/dist/http-helpers.d.ts.map +1 -1
  8. package/dist/http-helpers.js +24 -6
  9. package/dist/index.d.ts +90 -27
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/interceptor.d.ts.map +1 -1
  13. package/dist/interceptor.js +71 -20
  14. package/dist/plugin-pipeline.d.ts +11 -4
  15. package/dist/plugin-pipeline.d.ts.map +1 -1
  16. package/dist/plugin-pipeline.js +98 -35
  17. package/dist/response-parser.d.ts.map +1 -1
  18. package/dist/response-parser.js +47 -17
  19. package/package.json +5 -3
  20. package/src/binary.test.ts +126 -0
  21. package/src/binary.ts +11 -0
  22. package/src/builder.ts +82 -35
  23. package/src/http-helpers.test.ts +39 -0
  24. package/src/http-helpers.ts +46 -10
  25. package/src/index.ts +1 -0
  26. package/src/interceptor.test.ts +197 -0
  27. package/src/interceptor.ts +215 -128
  28. package/src/parser.property.test.ts +91 -55
  29. package/src/plugin-pipeline.ts +150 -45
  30. package/src/plugin-system.test.ts +95 -0
  31. package/src/response-parser.ts +58 -19
  32. package/src/server.test.ts +81 -12
  33. package/src/steps/async-support.steps.ts +29 -8
  34. package/src/steps/audit-onerror-tuple.steps.ts +14 -30
  35. package/src/steps/error-handling.steps.ts +463 -253
  36. package/src/steps/fetch-interceptor.steps.ts +457 -0
  37. package/src/steps/http-methods.steps.ts +19 -8
  38. package/src/steps/lifecycle-events.steps.ts +50 -43
  39. package/src/steps/performance-reliability.steps.ts +78 -411
  40. package/src/steps/plugin-integration.steps.ts +54 -0
  41. package/src/steps/request-history.steps.ts +412 -230
  42. package/src/steps/standalone-server.steps.ts +7 -13
  43. package/src/steps/state-concurrency.steps.ts +142 -714
  44. package/src/steps/interceptor.steps.ts +0 -260
  45. package/src/steps/stateful-workflows.steps.ts +0 -353
@@ -1,13 +1,115 @@
1
1
  import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
2
  import { expect } from "vitest";
3
+ import { schmock, toHttpMethod } from "../index";
3
4
  import type { CallableMockInstance } from "../types";
4
- import { schmock } from "../index";
5
5
 
6
6
  const feature = await loadFeature("../../features/request-history.feature");
7
7
 
8
+ interface HistoryTableRow {
9
+ field: string;
10
+ value: string;
11
+ }
12
+
13
+ function parseRequest(request: string): {
14
+ method: Schmock.HttpMethod;
15
+ path: string;
16
+ } {
17
+ const separator = request.indexOf(" ");
18
+ const methodText = request.slice(0, separator);
19
+ const path = request.slice(separator + 1);
20
+ if (separator <= 0 || !path.startsWith("/")) {
21
+ throw new Error(`Expected request in METHOD /path format, got: ${request}`);
22
+ }
23
+ return { method: toHttpMethod(methodText), path };
24
+ }
25
+
26
+ function requireRecord(
27
+ value: unknown,
28
+ description: string,
29
+ ): Record<string, unknown> {
30
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
31
+ throw new Error(`Expected ${description} to be an object`);
32
+ }
33
+ return Object.fromEntries(Object.entries(value));
34
+ }
35
+
36
+ function requireStringRecord(
37
+ value: unknown,
38
+ description: string,
39
+ ): Record<string, string> {
40
+ const record = requireRecord(value, description);
41
+ const strings: Record<string, string> = {};
42
+ for (const [key, entry] of Object.entries(record)) {
43
+ if (typeof entry !== "string") {
44
+ throw new Error(`Expected ${description}.${key} to be a string`);
45
+ }
46
+ strings[key] = entry;
47
+ }
48
+ return strings;
49
+ }
50
+
51
+ function parseJson(docString: string): unknown {
52
+ const parsed: unknown = JSON.parse(docString);
53
+ return parsed;
54
+ }
55
+
56
+ function parseRequestOptions(docString: string): Schmock.RequestOptions {
57
+ const parsed = requireRecord(parseJson(docString), "request options");
58
+ const options: Schmock.RequestOptions = {};
59
+ if (parsed.headers !== undefined) {
60
+ options.headers = requireStringRecord(parsed.headers, "request headers");
61
+ }
62
+ if (parsed.query !== undefined) {
63
+ options.query = requireStringRecord(parsed.query, "request query");
64
+ }
65
+ if ("body" in parsed) {
66
+ options.body = parsed.body;
67
+ }
68
+ return options;
69
+ }
70
+
71
+ function requireHistoryTable(table: unknown): HistoryTableRow[] {
72
+ if (!Array.isArray(table)) {
73
+ throw new Error("Expected request history table rows");
74
+ }
75
+ return table.map((candidate) => {
76
+ const row = requireRecord(candidate, "request history table row");
77
+ if (typeof row.field !== "string" || typeof row.value !== "string") {
78
+ throw new Error("Expected request history table field and value strings");
79
+ }
80
+ return { field: row.field, value: row.value };
81
+ });
82
+ }
83
+
84
+ function requestRecordField(
85
+ record: Schmock.RequestRecord,
86
+ field: string,
87
+ ): unknown {
88
+ switch (field) {
89
+ case "method":
90
+ return record.method;
91
+ case "path":
92
+ return record.path;
93
+ case "params":
94
+ return record.params;
95
+ case "query":
96
+ return record.query;
97
+ case "headers":
98
+ return record.headers;
99
+ case "body":
100
+ return record.body;
101
+ case "timestamp":
102
+ return record.timestamp;
103
+ case "response":
104
+ return record.response;
105
+ default:
106
+ throw new Error(`Unknown request record field: ${field}`);
107
+ }
108
+ }
109
+
8
110
  describeFeature(feature, ({ Scenario }) => {
9
111
  let mock: CallableMockInstance;
10
- let response: any;
112
+ let response: Schmock.Response;
11
113
 
12
114
  Scenario("Record multiple requests", ({ Given, When, And, Then }) => {
13
115
  Given("I create a mock with GET and POST user routes", () => {
@@ -17,22 +119,22 @@ describeFeature(feature, ({ Scenario }) => {
17
119
  });
18
120
 
19
121
  When("I request {string}", async (_, request: string) => {
20
- const [method, path] = request.split(" ");
21
- response = await mock.handle(method as any, path);
122
+ const { method, path } = parseRequest(request);
123
+ response = await mock.handle(method, path);
22
124
  });
23
125
 
24
126
  And(
25
127
  "I request {string} with body:",
26
128
  async (_, request: string, docString: string) => {
27
- const [method, path] = request.split(" ");
28
- const body = JSON.parse(docString);
29
- response = await mock.handle(method as any, path, { body });
129
+ const { method, path } = parseRequest(request);
130
+ const body = parseJson(docString);
131
+ response = await mock.handle(method, path, { body });
30
132
  },
31
133
  );
32
134
 
33
135
  And("I request {string}", async (_, request: string) => {
34
- const [method, path] = request.split(" ");
35
- response = await mock.handle(method as any, path);
136
+ const { method, path } = parseRequest(request);
137
+ response = await mock.handle(method, path);
36
138
  });
37
139
 
38
140
  Then("the call count should be 3", () => {
@@ -42,235 +144,315 @@ describeFeature(feature, ({ Scenario }) => {
42
144
  And(
43
145
  "the call count for {string} should be {int}",
44
146
  (_, route: string, count: number) => {
45
- const [method, path] = route.split(" ");
46
- expect(mock.callCount(method as any, path)).toBe(count);
147
+ const { method, path } = parseRequest(route);
148
+ expect(mock.callCount(method, path)).toBe(count);
47
149
  },
48
150
  );
49
151
 
50
152
  And(
51
153
  "the call count for {string} should be {int} request",
52
154
  (_, route: string, count: number) => {
53
- const [method, path] = route.split(" ");
54
- expect(mock.callCount(method as any, path)).toBe(count);
55
- },
56
- );
57
- });
58
-
59
- Scenario("Filter history by method and path", ({ Given, When, And, Then }) => {
60
- Given("I create a mock with users and posts routes", () => {
61
- mock = schmock();
62
- mock("GET /users", []);
63
- mock("POST /users", ({ body }) => [201, body]);
64
- mock("GET /posts", []);
65
- });
66
-
67
- When("I request {string}", async (_, request: string) => {
68
- const [method, path] = request.split(" ");
69
- response = await mock.handle(method as any, path);
70
- });
71
-
72
- And(
73
- "I request {string} with body:",
74
- async (_, request: string, docString: string) => {
75
- const [method, path] = request.split(" ");
76
- const body = JSON.parse(docString);
77
- response = await mock.handle(method as any, path, { body });
78
- },
79
- );
80
-
81
- And("I request {string}", async (_, request: string) => {
82
- const [method, path] = request.split(" ");
83
- response = await mock.handle(method as any, path);
84
- });
85
-
86
- Then(
87
- "the history for {string} should have {int} record",
88
- (_, route: string, count: number) => {
89
- const [method, path] = route.split(" ");
90
- expect(mock.history(method as any, path)).toHaveLength(count);
91
- },
92
- );
93
-
94
- And(
95
- "the history for {string} should have {int} record",
96
- (_, route: string, count: number) => {
97
- const [method, path] = route.split(" ");
98
- expect(mock.history(method as any, path)).toHaveLength(count);
99
- },
100
- );
101
-
102
- And(
103
- "the history for {string} should have {int} entry",
104
- (_, route: string, count: number) => {
105
- const [method, path] = route.split(" ");
106
- expect(mock.history(method as any, path)).toHaveLength(count);
107
- },
108
- );
109
-
110
- And(
111
- "the history for {string} should have {int} entries",
112
- (_, route: string, count: number) => {
113
- const [method, path] = route.split(" ");
114
- expect(mock.history(method as any, path)).toHaveLength(count);
115
- },
116
- );
117
- });
118
-
119
- Scenario("Check if specific route was called", ({ Given, When, Then, And }) => {
120
- Given("I create a mock with users and posts list routes", () => {
121
- mock = schmock();
122
- mock("GET /users", []);
123
- mock("GET /posts", []);
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("{string} should have been called", (_, route: string) => {
132
- const [method, path] = route.split(" ");
133
- expect(mock.called(method as any, path)).toBe(true);
134
- });
135
-
136
- And("{string} should not have been called", (_, route: string) => {
137
- const [method, path] = route.split(" ");
138
- expect(mock.called(method as any, path)).toBe(false);
139
- });
140
- });
141
-
142
- Scenario("Request record captures full details", ({ Given, When, Then, And }) => {
143
- Given("I create a mock with a parameterized POST route", () => {
144
- mock = schmock();
145
- mock("POST /users/:id", ({ params, body }) => [
146
- 200,
147
- { ...(body as Record<string, unknown>), id: params.id },
148
- ]);
149
- });
150
-
151
- When(
152
- "I request {string} with headers and body:",
153
- async (_, request: string, docString: string) => {
154
- const [method, path] = request.split(" ");
155
- const options = JSON.parse(docString);
156
- response = await mock.handle(method as any, path, options);
157
- },
158
- );
159
-
160
- Then("the last request should have:", (_, table: any) => {
161
- const record = mock.lastRequest();
162
- expect(record).toBeDefined();
163
- for (const row of table) {
164
- const field = row.field as keyof typeof record;
165
- expect(record![field]).toBe(row.value);
166
- }
167
- });
168
-
169
- And(
170
- "the last request params should include {string} = {string}",
171
- (_, key: string, value: string) => {
172
- expect(mock.lastRequest()?.params[key]).toBe(value);
173
- },
174
- );
175
-
176
- And(
177
- "the last request headers should include {string} = {string}",
178
- (_, key: string, value: string) => {
179
- expect(mock.lastRequest()?.headers[key]).toBe(value);
180
- },
181
- );
182
-
183
- And(
184
- "the last request body should have property {string} with value {string}",
185
- (_, prop: string, value: string) => {
186
- expect((mock.lastRequest()?.body as any)?.[prop]).toBe(value);
187
- },
188
- );
189
-
190
- And("the last request should have a timestamp", () => {
191
- const record = mock.lastRequest();
192
- expect(record?.timestamp).toBeDefined();
193
- expect(typeof record?.timestamp).toBe("number");
194
- expect(record!.timestamp).toBeGreaterThan(0);
195
- });
196
-
197
- And(
198
- "the last request response status should be {int}",
199
- (_, status: number) => {
200
- expect(mock.lastRequest()?.response.status).toBe(status);
201
- },
202
- );
203
- });
204
-
205
- Scenario("Get last request for a specific route", ({ Given, When, And, Then }) => {
206
- Given("I create a mock echoing POST body at {string}", (_, path: string) => {
207
- mock = schmock();
208
- mock(`POST ${path}`, ({ body }) => [201, body]);
209
- });
210
-
211
- When(
212
- "I request {string} with body:",
213
- async (_, request: string, docString: string) => {
214
- const [method, path] = request.split(" ");
215
- const body = JSON.parse(docString);
216
- response = await mock.handle(method as any, path, { body });
217
- },
218
- );
219
-
220
- And(
221
- "I request {string} with body:",
222
- async (_, request: string, docString: string) => {
223
- const [method, path] = request.split(" ");
224
- const body = JSON.parse(docString);
225
- response = await mock.handle(method as any, path, { body });
226
- },
227
- );
228
-
229
- Then(
230
- "the last request for {string} body should have property {string} with value {string}",
231
- (_, route: string, prop: string, value: string) => {
232
- const [method, path] = route.split(" ");
233
- const record = mock.lastRequest(method as any, path);
234
- expect((record?.body as any)?.[prop]).toBe(value);
155
+ const { method, path } = parseRequest(route);
156
+ expect(mock.callCount(method, path)).toBe(count);
235
157
  },
236
158
  );
237
159
  });
238
160
 
239
- Scenario("404 requests are not recorded in history", ({ Given, When, Then, And }) => {
240
- Given("I create a mock with only a users route", () => {
241
- mock = schmock();
242
- mock("GET /users", []);
243
- });
244
-
245
- When("I request {string}", async (_, request: string) => {
246
- const [method, path] = request.split(" ");
247
- response = await mock.handle(method as any, path);
248
- });
249
-
250
- Then("the mock should not have been called", () => {
251
- expect(mock.called()).toBe(false);
252
- });
253
-
254
- And("the call count should be 0", () => {
255
- expect(mock.callCount()).toBe(0);
256
- });
257
- });
258
-
259
- Scenario("maxHistorySize bounds the history with FIFO eviction", ({ Given, When, Then }) => {
260
- Given("I create a mock with maxHistorySize {int} and a users route", (_, maxHistorySize: number) => {
261
- mock = schmock({ maxHistorySize });
262
- mock("GET /users", []);
263
- });
264
-
265
- When("I issue {int} requests to {string}", async (_, count: number, request: string) => {
266
- const [method, path] = request.split(" ");
267
- for (let i = 0; i < count; i++) {
268
- await mock.handle(method as any, path);
269
- }
270
- });
271
-
272
- Then("the call count should be {int}", (_, expected: number) => {
273
- expect(mock.callCount()).toBe(expected);
274
- });
275
- });
161
+ Scenario(
162
+ "Filter history by method and path",
163
+ ({ Given, When, And, Then }) => {
164
+ Given("I create a mock with users and posts routes", () => {
165
+ mock = schmock();
166
+ mock("GET /users", []);
167
+ mock("POST /users", ({ body }) => [201, body]);
168
+ mock("GET /posts", []);
169
+ });
170
+
171
+ When("I request {string}", async (_, request: string) => {
172
+ const { method, path } = parseRequest(request);
173
+ response = await mock.handle(method, path);
174
+ });
175
+
176
+ And(
177
+ "I request {string} with body:",
178
+ async (_, request: string, docString: string) => {
179
+ const { method, path } = parseRequest(request);
180
+ const body = parseJson(docString);
181
+ response = await mock.handle(method, path, { body });
182
+ },
183
+ );
184
+
185
+ And("I request {string}", async (_, request: string) => {
186
+ const { method, path } = parseRequest(request);
187
+ response = await mock.handle(method, path);
188
+ });
189
+
190
+ Then(
191
+ "the history for {string} should have {int} record",
192
+ (_, route: string, count: number) => {
193
+ const { method, path } = parseRequest(route);
194
+ expect(mock.history(method, path)).toHaveLength(count);
195
+ },
196
+ );
197
+
198
+ And(
199
+ "the history for {string} should have {int} record",
200
+ (_, route: string, count: number) => {
201
+ const { method, path } = parseRequest(route);
202
+ expect(mock.history(method, path)).toHaveLength(count);
203
+ },
204
+ );
205
+
206
+ And(
207
+ "the history for {string} should have {int} entry",
208
+ (_, route: string, count: number) => {
209
+ const { method, path } = parseRequest(route);
210
+ expect(mock.history(method, path)).toHaveLength(count);
211
+ },
212
+ );
213
+
214
+ And(
215
+ "the history for {string} should have {int} entries",
216
+ (_, route: string, count: number) => {
217
+ const { method, path } = parseRequest(route);
218
+ expect(mock.history(method, path)).toHaveLength(count);
219
+ },
220
+ );
221
+ },
222
+ );
223
+
224
+ Scenario(
225
+ "Check if specific route was called",
226
+ ({ Given, When, Then, And }) => {
227
+ Given("I create a mock with users and posts list routes", () => {
228
+ mock = schmock();
229
+ mock("GET /users", []);
230
+ mock("GET /posts", []);
231
+ });
232
+
233
+ When("I request {string}", async (_, request: string) => {
234
+ const { method, path } = parseRequest(request);
235
+ response = await mock.handle(method, path);
236
+ });
237
+
238
+ Then("{string} should have been called", (_, route: string) => {
239
+ const { method, path } = parseRequest(route);
240
+ expect(mock.called(method, path)).toBe(true);
241
+ });
242
+
243
+ And("{string} should not have been called", (_, route: string) => {
244
+ const { method, path } = parseRequest(route);
245
+ expect(mock.called(method, path)).toBe(false);
246
+ });
247
+ },
248
+ );
249
+
250
+ Scenario(
251
+ "Request record captures full details",
252
+ ({ Given, When, Then, And }) => {
253
+ Given("I create a mock with a parameterized POST route", () => {
254
+ mock = schmock();
255
+ mock("POST /users/:id", ({ params, body }) => {
256
+ const requestBody = requireRecord(body, "request body");
257
+ return [200, { ...requestBody, id: params.id }];
258
+ });
259
+ });
260
+
261
+ When(
262
+ "I request {string} with headers and body:",
263
+ async (_, request: string, docString: string) => {
264
+ const { method, path } = parseRequest(request);
265
+ const options = parseRequestOptions(docString);
266
+ response = await mock.handle(method, path, options);
267
+ },
268
+ );
269
+
270
+ Then("the last request should have:", (_, table: unknown) => {
271
+ const record = mock.lastRequest();
272
+ if (!record) {
273
+ throw new Error("Expected a recorded request");
274
+ }
275
+ for (const row of requireHistoryTable(table)) {
276
+ expect(requestRecordField(record, row.field)).toBe(row.value);
277
+ }
278
+ });
279
+
280
+ And(
281
+ "the last request params should include {string} = {string}",
282
+ (_, key: string, value: string) => {
283
+ expect(mock.lastRequest()?.params[key]).toBe(value);
284
+ },
285
+ );
286
+
287
+ And(
288
+ "the last request headers should include {string} = {string}",
289
+ (_, key: string, value: string) => {
290
+ expect(mock.lastRequest()?.headers[key]).toBe(value);
291
+ },
292
+ );
293
+
294
+ And(
295
+ "the last request body should have property {string} with value {string}",
296
+ (_, prop: string, value: string) => {
297
+ const record = mock.lastRequest();
298
+ if (!record) {
299
+ throw new Error("Expected a recorded request");
300
+ }
301
+ expect(requireRecord(record.body, "request body")[prop]).toBe(value);
302
+ },
303
+ );
304
+
305
+ And("the last request should have a timestamp", () => {
306
+ const record = mock.lastRequest();
307
+ if (!record) {
308
+ throw new Error("Expected a recorded request");
309
+ }
310
+ expect(typeof record.timestamp).toBe("number");
311
+ expect(record.timestamp).toBeGreaterThan(0);
312
+ });
313
+
314
+ And(
315
+ "the last request response status should be {int}",
316
+ (_, status: number) => {
317
+ expect(mock.lastRequest()?.response.status).toBe(status);
318
+ },
319
+ );
320
+ },
321
+ );
322
+
323
+ Scenario(
324
+ "Get last request for a specific route",
325
+ ({ Given, When, And, Then }) => {
326
+ Given(
327
+ "I create a mock echoing POST body at {string}",
328
+ (_, path: string) => {
329
+ mock = schmock();
330
+ mock(`POST ${path}`, ({ body }) => [201, body]);
331
+ },
332
+ );
333
+
334
+ When(
335
+ "I request {string} with body:",
336
+ async (_, request: string, docString: string) => {
337
+ const { method, path } = parseRequest(request);
338
+ const body = parseJson(docString);
339
+ response = await mock.handle(method, path, { body });
340
+ },
341
+ );
342
+
343
+ And(
344
+ "I request {string} with body:",
345
+ async (_, request: string, docString: string) => {
346
+ const { method, path } = parseRequest(request);
347
+ const body = parseJson(docString);
348
+ response = await mock.handle(method, path, { body });
349
+ },
350
+ );
351
+
352
+ Then(
353
+ "the last request for {string} body should have property {string} with value {string}",
354
+ (_, route: string, prop: string, value: string) => {
355
+ const { method, path } = parseRequest(route);
356
+ const record = mock.lastRequest(method, path);
357
+ if (!record) {
358
+ throw new Error(`Expected a recorded request for ${route}`);
359
+ }
360
+ expect(requireRecord(record.body, "request body")[prop]).toBe(value);
361
+ },
362
+ );
363
+ },
364
+ );
365
+
366
+ Scenario(
367
+ "404 requests are not recorded in history",
368
+ ({ Given, When, Then, And }) => {
369
+ Given("I create a mock with only a users route", () => {
370
+ mock = schmock();
371
+ mock("GET /users", []);
372
+ });
373
+
374
+ When("I request {string}", async (_, request: string) => {
375
+ const { method, path } = parseRequest(request);
376
+ response = await mock.handle(method, path);
377
+ });
378
+
379
+ Then("the mock should not have been called", () => {
380
+ expect(mock.called()).toBe(false);
381
+ });
382
+
383
+ And("the call count should be 0", () => {
384
+ expect(mock.callCount()).toBe(0);
385
+ });
386
+ },
387
+ );
388
+
389
+ Scenario(
390
+ "maxHistorySize bounds the history with FIFO eviction",
391
+ ({ Given, When, Then, And }) => {
392
+ Given(
393
+ "I create a mock with maxHistorySize {int} and a users route",
394
+ (_, maxHistorySize: number) => {
395
+ mock = schmock({ maxHistorySize });
396
+ mock("GET /users", []);
397
+ },
398
+ );
399
+
400
+ When(
401
+ "I issue {int} sequenced requests to {string}",
402
+ async (_, count: number, request: string) => {
403
+ const { method, path } = parseRequest(request);
404
+ for (let i = 0; i < count; i++) {
405
+ await mock.handle(method, path, {
406
+ query: { sequence: String(i + 1) },
407
+ });
408
+ }
409
+ },
410
+ );
411
+
412
+ Then("the call count should be {int}", (_, expected: number) => {
413
+ expect(mock.callCount()).toBe(expected);
414
+ });
415
+
416
+ And(
417
+ "the retained request sequence should be {string}",
418
+ (_, sequence: string) => {
419
+ expect(mock.history().map((record) => record.query.sequence)).toEqual(
420
+ sequence.split(","),
421
+ );
422
+ },
423
+ );
424
+ },
425
+ );
426
+
427
+ Scenario(
428
+ "Resetting history preserves routes and shared state",
429
+ ({ Given, When, Then, And }) => {
430
+ Given("I create a stateful mock and record a request", async () => {
431
+ mock = schmock({ state: { marker: "preserved" } });
432
+ mock("GET /stateful", ({ state }) => ({ marker: state.marker }));
433
+ await mock.handle("GET", "/stateful");
434
+ expect(mock.callCount()).toBe(1);
435
+ });
436
+
437
+ When("I reset only the request history", () => {
438
+ mock.resetHistory();
439
+ });
440
+
441
+ Then("the call count should be {int}", (_, expected: number) => {
442
+ expect(mock.callCount()).toBe(expected);
443
+ });
444
+
445
+ And("the registered route should still respond", async () => {
446
+ response = await mock.handle("GET", "/stateful");
447
+ expect(response.status).toBe(200);
448
+ });
449
+
450
+ And(
451
+ "the shared state marker should still be {string}",
452
+ (_, marker: string) => {
453
+ expect(mock.getState().marker).toBe(marker);
454
+ },
455
+ );
456
+ },
457
+ );
276
458
  });