@jterrazz/test 6.1.0 → 6.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.
package/README.md CHANGED
@@ -35,13 +35,13 @@ afterAll(() => run.cleanup());
35
35
  import { run } from '../../setup/integration.specification.js';
36
36
 
37
37
  test('creates a user', async () => {
38
- // Given one existing user
38
+ // Given - one existing user
39
39
  const result = await run('creates user')
40
40
  .seed('initial-users.sql')
41
41
  .post('/users', 'new-user.json')
42
42
  .run();
43
43
 
44
- // Then user created
44
+ // Then - user created
45
45
  expect(result.status).toBe(201);
46
46
  await result.table('users').toMatch({
47
47
  columns: ['name'],
@@ -67,10 +67,10 @@ export const run = await spec(command(resolve(import.meta.dirname, '../../bin/my
67
67
  import { run } from '../../setup/cli.specification.js';
68
68
 
69
69
  test('builds the project', async () => {
70
- // Given sample app project
70
+ // Given - sample app project
71
71
  const result = await run('build').project('sample-app').exec('build').run();
72
72
 
73
- // Then ESM output with source maps
73
+ // Then - ESM output with source maps
74
74
  expect(result.exitCode).toBe(0);
75
75
  expect(result.stdout).toContain('Build completed');
76
76
  expect(result.file('dist/index.js').exists).toBe(true);
@@ -83,7 +83,7 @@ test('builds the project', async () => {
83
83
 
84
84
  Three modes, same builder API. Each handles infrastructure and cleanup automatically.
85
85
 
86
- ### `spec(app(...))` testcontainers + in-process app
86
+ ### `spec(app(...))` - testcontainers + in-process app
87
87
 
88
88
  Starts real containers via testcontainers. App runs in-process (Hono). Fastest feedback loop.
89
89
 
@@ -103,7 +103,7 @@ export const run = await spec(
103
103
  );
104
104
  ```
105
105
 
106
- ### `spec(stack(...))` docker compose up + real HTTP
106
+ ### `spec(stack(...))` - docker compose up + real HTTP
107
107
 
108
108
  Starts the full `docker/compose.test.yaml` stack. App URL and databases auto-detected.
109
109
 
@@ -113,7 +113,7 @@ import { spec, stack } from '@jterrazz/test';
113
113
  export const run = await spec(stack('../../'));
114
114
  ```
115
115
 
116
- ### `spec(command(...))` local command execution
116
+ ### `spec(command(...))` - local command execution
117
117
 
118
118
  Runs CLI commands against fixture projects in temp directories. Optionally starts infrastructure.
119
119
 
@@ -331,13 +331,13 @@ Every test uses `// Given` and `// Then` comments. Always both, never one withou
331
331
 
332
332
  ```typescript
333
333
  test('creates a user and returns 201', async () => {
334
- // Given two existing users
334
+ // Given - two existing users
335
335
  const result = await run('creates user')
336
336
  .seed('initial-users.sql')
337
337
  .post('/users', 'new-user.json')
338
338
  .run();
339
339
 
340
- // Then user created with all three in table
340
+ // Then - user created with all three in table
341
341
  expect(result.status).toBe(201);
342
342
  await result.table('users').toMatch({
343
343
  columns: ['name'],
package/dist/index.cjs CHANGED
@@ -115,22 +115,25 @@ var FetchAdapter = class {
115
115
  constructor(url) {
116
116
  this.baseUrl = url.replace(/\/$/, "");
117
117
  }
118
- async request(method, path, body) {
118
+ async request(method, path, body, headers) {
119
119
  const init = {
120
120
  method,
121
- headers: { "Content-Type": "application/json" }
121
+ headers: {
122
+ "Content-Type": "application/json",
123
+ ...headers
124
+ }
122
125
  };
123
126
  if (body !== void 0) init.body = JSON.stringify(body);
124
127
  const response = await fetch(`${this.baseUrl}${path}`, init);
125
128
  const responseBody = await response.json().catch(() => null);
126
- const headers = {};
129
+ const responseHeaders = {};
127
130
  response.headers.forEach((value, key) => {
128
- headers[key] = value;
131
+ responseHeaders[key] = value;
129
132
  });
130
133
  return {
131
134
  status: response.status,
132
135
  body: responseBody,
133
- headers
136
+ headers: responseHeaders
134
137
  };
135
138
  }
136
139
  };
@@ -145,22 +148,25 @@ var HonoAdapter = class {
145
148
  constructor(app) {
146
149
  this.app = app;
147
150
  }
148
- async request(method, path, body) {
151
+ async request(method, path, body, headers) {
149
152
  const init = {
150
153
  method,
151
- headers: { "Content-Type": "application/json" }
154
+ headers: {
155
+ "Content-Type": "application/json",
156
+ ...headers
157
+ }
152
158
  };
153
159
  if (body !== void 0) init.body = JSON.stringify(body);
154
160
  const response = await this.app.request(path, init);
155
161
  const responseBody = await response.json().catch(() => null);
156
- const headers = {};
162
+ const responseHeaders = {};
157
163
  response.headers.forEach((value, key) => {
158
- headers[key] = value;
164
+ responseHeaders[key] = value;
159
165
  });
160
166
  return {
161
167
  status: response.status,
162
168
  body: responseBody,
163
- headers
169
+ headers: responseHeaders
164
170
  };
165
171
  }
166
172
  };
@@ -586,10 +592,12 @@ var SpecificationBuilder = class {
586
592
  commandEnv = {};
587
593
  config;
588
594
  fixtures = [];
595
+ intercepts = [];
589
596
  label;
590
597
  mocks = [];
591
598
  projectName = null;
592
599
  request = null;
600
+ requestHeaders = {};
593
601
  seeds = [];
594
602
  spawnConfig = null;
595
603
  testDir;
@@ -644,6 +652,41 @@ var SpecificationBuilder = class {
644
652
  return this;
645
653
  }
646
654
  /**
655
+ * Set HTTP headers for the request. Multiple calls merge.
656
+ *
657
+ * @example
658
+ * spec("french").headers({ 'Accept-Language': 'fr' }).get("/articles").run();
659
+ */
660
+ headers(headers) {
661
+ this.requestHeaders = {
662
+ ...this.requestHeaders,
663
+ ...headers
664
+ };
665
+ return this;
666
+ }
667
+ /**
668
+ * Intercept an outgoing HTTP request and return a controlled response.
669
+ * Uses MSW under the hood. Intercepts are queued — multiple calls with the
670
+ * same trigger fire sequentially (first match consumed first).
671
+ *
672
+ * @param trigger - What to match (use openai.chat(), anthropic.messages(), http.get(), etc.)
673
+ * @param response - What to return (use openai.response(), http.json(), etc.)
674
+ *
675
+ * @example
676
+ * spec('pipeline')
677
+ * .intercept(openai.chat(), openai.response({ categories: ['TECH'] }))
678
+ * .intercept(openai.chat(), openai.response({ headline: 'AI News' }))
679
+ * .exec('process')
680
+ * .run();
681
+ */
682
+ intercept(trigger, response) {
683
+ this.intercepts.push({
684
+ trigger,
685
+ response
686
+ });
687
+ return this;
688
+ }
689
+ /**
647
690
  * Send a GET request to the server adapter.
648
691
  *
649
692
  * @example
@@ -737,9 +780,17 @@ var SpecificationBuilder = class {
737
780
  await db.seed(sql);
738
781
  }
739
782
  if (this.fixtures.length > 0 && workDir) for (const entry of this.fixtures) (0, node_fs.cpSync)((0, node_path.resolve)(this.testDir, "fixtures", entry.file), (0, node_path.resolve)(workDir, entry.file), { recursive: true });
740
- for (const entry of this.mocks) JSON.parse((0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "mock", entry.file), "utf8"));
741
- if (hasHttpAction) return this.runHttpAction();
742
- return this.runCliAction(workDir);
783
+ let cleanupIntercepts = null;
784
+ if (this.intercepts.length > 0) {
785
+ const { registerIntercepts } = await Promise.resolve().then(() => require("./server.cjs"));
786
+ cleanupIntercepts = await registerIntercepts(this.intercepts);
787
+ }
788
+ try {
789
+ if (hasHttpAction) return await this.runHttpAction();
790
+ return await this.runCliAction(workDir);
791
+ } finally {
792
+ if (cleanupIntercepts) cleanupIntercepts();
793
+ }
743
794
  }
744
795
  resolveEnv(workDir) {
745
796
  const keys = Object.keys(this.commandEnv);
@@ -764,7 +815,8 @@ var SpecificationBuilder = class {
764
815
  if (!this.config.server) throw new Error("HTTP actions require a server adapter (use integration() or e2e())");
765
816
  let body;
766
817
  if (this.request.bodyFile) body = JSON.parse((0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "requests", this.request.bodyFile), "utf8"));
767
- const response = await this.config.server.request(this.request.method, this.request.path, body);
818
+ const headers = Object.keys(this.requestHeaders).length > 0 ? this.requestHeaders : void 0;
819
+ const response = await this.config.server.request(this.request.method, this.request.path, body, headers);
768
820
  return new SpecificationResult({
769
821
  config: this.config,
770
822
  requestInfo: {