@jterrazz/test 3.2.0 → 3.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/dist/build.cjs CHANGED
@@ -1,11 +1,11 @@
1
1
  const require_chunk = require("./chunk.cjs");
2
2
  const require_dist$5 = require("./dist.cjs");
3
+ let node_os = require("node:os");
4
+ node_os = require_chunk.__toESM(node_os);
3
5
  let node_crypto = require("node:crypto");
4
6
  node_crypto = require_chunk.__toESM(node_crypto);
5
7
  let node_net = require("node:net");
6
8
  node_net = require_chunk.__toESM(node_net);
7
- let node_os = require("node:os");
8
- node_os = require_chunk.__toESM(node_os);
9
9
  //#region node_modules/graceful-fs/polyfills.js
10
10
  var require_polyfills = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
11
11
  var constants = require("constants");
package/dist/build.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, r as __exportAll, t as __commonJSMin } from "./chunk.js";
2
2
  import { t as require_dist$4 } from "./dist.js";
3
+ import os from "node:os";
3
4
  import crypto from "node:crypto";
4
5
  import net from "node:net";
5
- import os from "node:os";
6
6
  //#region node_modules/graceful-fs/polyfills.js
7
7
  var require_polyfills = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8
8
  var constants = __require("constants");
package/dist/index.cjs CHANGED
@@ -4,9 +4,10 @@ const require_dist$2 = require("./dist.cjs");
4
4
  let mockdate = require("mockdate");
5
5
  mockdate = require_chunk.__toESM(mockdate);
6
6
  let vitest_mock_extended = require("vitest-mock-extended");
7
+ let node_fs = require("node:fs");
7
8
  let node_path = require("node:path");
8
9
  let node_child_process = require("node:child_process");
9
- let node_fs = require("node:fs");
10
+ let node_os = require("node:os");
10
11
  //#region src/mocking/mock-of-date.ts
11
12
  const mockOfDate = mockdate.default;
12
13
  //#endregion
@@ -281,6 +282,60 @@ function formatResponseDiff(file, expected, actual) {
281
282
  }
282
283
  return lines.join("\n");
283
284
  }
285
+ function formatExitCodeError(expected, received, stdout, stderr) {
286
+ const lines = [];
287
+ lines.push(`Expected exit code: ${GREEN}${expected}${RESET}`);
288
+ lines.push(`Received exit code: ${RED}${received}${RESET}`);
289
+ if (stdout.trim()) {
290
+ lines.push("");
291
+ lines.push(`${DIM}stdout:${RESET}`);
292
+ for (const line of stdout.trim().split("\n").slice(-15)) lines.push(` ${DIM}${line}${RESET}`);
293
+ }
294
+ if (stderr.trim()) {
295
+ lines.push("");
296
+ lines.push(`${DIM}stderr:${RESET}`);
297
+ for (const line of stderr.trim().split("\n").slice(-15)) lines.push(` ${RED}${line}${RESET}`);
298
+ }
299
+ return lines.join("\n");
300
+ }
301
+ function formatStdoutDiff(file, expected, actual) {
302
+ const lines = [];
303
+ lines.push(`Output mismatch (${file})`);
304
+ lines.push("");
305
+ lines.push(`${GREEN}- Expected${RESET}`);
306
+ lines.push(`${RED}+ Received${RESET}`);
307
+ lines.push("");
308
+ const expectedLines = expected.split("\n");
309
+ const actualLines = actual.split("\n");
310
+ const maxLines = Math.max(expectedLines.length, actualLines.length);
311
+ for (let i = 0; i < maxLines; i++) {
312
+ const exp = expectedLines[i];
313
+ const act = actualLines[i];
314
+ if (exp === act) lines.push(` ${exp}`);
315
+ else {
316
+ if (exp !== void 0) lines.push(`${GREEN}- ${exp}${RESET}`);
317
+ if (act !== void 0) lines.push(`${RED}+ ${act}${RESET}`);
318
+ }
319
+ }
320
+ return lines.join("\n");
321
+ }
322
+ function formatFileMissing(path) {
323
+ return `Expected file to exist: ${RED}${path}${RESET}`;
324
+ }
325
+ function formatFileUnexpected(path) {
326
+ return `Expected file NOT to exist: ${RED}${path}${RESET}`;
327
+ }
328
+ function formatFileContentMismatch(path, expected, actual) {
329
+ const lines = [];
330
+ lines.push(`File "${path}" does not contain expected content`);
331
+ lines.push("");
332
+ lines.push(`${GREEN}Expected to contain:${RESET}`);
333
+ lines.push(` ${GREEN}${expected}${RESET}`);
334
+ lines.push("");
335
+ lines.push(`${RED}Actual content (first 20 lines):${RESET}`);
336
+ for (const line of actual.split("\n").slice(0, 20)) lines.push(` ${DIM}${line}${RESET}`);
337
+ return lines.join("\n");
338
+ }
284
339
  function rowLabel(n) {
285
340
  return n === 1 ? "1 row" : `${n} rows`;
286
341
  }
@@ -4844,6 +4899,41 @@ var Orchestrator = class {
4844
4899
  }
4845
4900
  };
4846
4901
  //#endregion
4902
+ //#region src/specification/adapters/exec.adapter.ts
4903
+ /**
4904
+ * Executes CLI commands via execSync.
4905
+ * Used by cli() for local command execution.
4906
+ */
4907
+ var ExecAdapter = class {
4908
+ command;
4909
+ constructor(command) {
4910
+ this.command = command;
4911
+ }
4912
+ async exec(args, cwd) {
4913
+ try {
4914
+ return {
4915
+ exitCode: 0,
4916
+ stdout: (0, node_child_process.execSync)(`${this.command} ${args}`, {
4917
+ cwd,
4918
+ encoding: "utf8",
4919
+ stdio: [
4920
+ "pipe",
4921
+ "pipe",
4922
+ "pipe"
4923
+ ]
4924
+ }),
4925
+ stderr: ""
4926
+ };
4927
+ } catch (error) {
4928
+ return {
4929
+ exitCode: error.status ?? 1,
4930
+ stdout: error.stdout?.toString() ?? "",
4931
+ stderr: error.stderr?.toString() ?? ""
4932
+ };
4933
+ }
4934
+ }
4935
+ };
4936
+ //#endregion
4847
4937
  //#region src/specification/adapters/fetch.adapter.ts
4848
4938
  /**
4849
4939
  * Server adapter for real HTTP — sends actual fetch requests.
@@ -4906,25 +4996,60 @@ var HonoAdapter = class {
4906
4996
  //#endregion
4907
4997
  //#region src/specification/specification.ts
4908
4998
  var SpecificationResult = class {
4909
- response;
4999
+ commandResult;
4910
5000
  config;
4911
- testDir;
4912
5001
  requestInfo;
4913
- constructor(response, config, testDir, requestInfo) {
4914
- this.response = response;
4915
- this.config = config;
4916
- this.testDir = testDir;
4917
- this.requestInfo = requestInfo;
5002
+ response;
5003
+ testDir;
5004
+ workDir;
5005
+ constructor(options) {
5006
+ this.response = options.response;
5007
+ this.commandResult = options.commandResult;
5008
+ this.config = options.config;
5009
+ this.testDir = options.testDir;
5010
+ this.requestInfo = options.requestInfo;
5011
+ this.workDir = options.workDir;
4918
5012
  }
4919
5013
  expectStatus(code) {
5014
+ if (!this.response || !this.requestInfo) throw new Error("expectStatus requires an HTTP action (.get(), .post(), etc.)");
4920
5015
  if (this.response.status !== code) throw new Error(formatStatusError(code, this.response.status, this.requestInfo, this.response.body));
4921
5016
  return this;
4922
5017
  }
4923
5018
  expectResponse(file) {
5019
+ if (!this.response) throw new Error("expectResponse requires an HTTP action (.get(), .post(), etc.)");
4924
5020
  const expected = JSON.parse((0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "responses", file), "utf8"));
4925
5021
  if (JSON.stringify(this.response.body) !== JSON.stringify(expected)) throw new Error(formatResponseDiff(file, expected, this.response.body));
4926
5022
  return this;
4927
5023
  }
5024
+ expectExitCode(code) {
5025
+ if (!this.commandResult) throw new Error("expectExitCode requires a CLI action (.exec())");
5026
+ if (this.commandResult.exitCode !== code) throw new Error(formatExitCodeError(code, this.commandResult.exitCode, this.commandResult.stdout, this.commandResult.stderr));
5027
+ return this;
5028
+ }
5029
+ expectStdout(file) {
5030
+ if (!this.commandResult) throw new Error("expectStdout requires a CLI action (.exec())");
5031
+ const expected = (0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "expected", file), "utf8").trim();
5032
+ const actual = this.commandResult.stdout.trim();
5033
+ if (actual !== expected) throw new Error(formatStdoutDiff(file, expected, actual));
5034
+ return this;
5035
+ }
5036
+ expectStdoutContains(str) {
5037
+ if (!this.commandResult) throw new Error("expectStdoutContains requires a CLI action (.exec())");
5038
+ if (!this.commandResult.stdout.includes(str)) throw new Error(`Expected stdout to contain: "${str}"\n\nActual stdout:\n${this.commandResult.stdout}`);
5039
+ return this;
5040
+ }
5041
+ expectStderr(file) {
5042
+ if (!this.commandResult) throw new Error("expectStderr requires a CLI action (.exec())");
5043
+ const expected = (0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "expected", file), "utf8").trim();
5044
+ const actual = this.commandResult.stderr.trim();
5045
+ if (actual !== expected) throw new Error(formatStdoutDiff(file, expected, actual));
5046
+ return this;
5047
+ }
5048
+ expectStderrContains(str) {
5049
+ if (!this.commandResult) throw new Error("expectStderrContains requires a CLI action (.exec())");
5050
+ if (!this.commandResult.stderr.includes(str)) throw new Error(`Expected stderr to contain: "${str}"\n\nActual stderr:\n${this.commandResult.stderr}`);
5051
+ return this;
5052
+ }
4928
5053
  async expectTable(table, options) {
4929
5054
  const db = this.resolveDatabase(options.service);
4930
5055
  if (!db) throw new Error(options.service ? `expectTable requires database "${options.service}" but it was not found` : "expectTable requires a database adapter");
@@ -4932,18 +5057,40 @@ var SpecificationResult = class {
4932
5057
  if (JSON.stringify(actual) !== JSON.stringify(options.rows)) throw new Error(formatTableDiff(table, options.columns, options.rows, actual));
4933
5058
  return this;
4934
5059
  }
5060
+ expectFile(path) {
5061
+ if (!(0, node_fs.existsSync)(this.resolveWorkPath(path))) throw new Error(formatFileMissing(path));
5062
+ return this;
5063
+ }
5064
+ expectNoFile(path) {
5065
+ if ((0, node_fs.existsSync)(this.resolveWorkPath(path))) throw new Error(formatFileUnexpected(path));
5066
+ return this;
5067
+ }
5068
+ expectFileContains(path, content) {
5069
+ const resolved = this.resolveWorkPath(path);
5070
+ if (!(0, node_fs.existsSync)(resolved)) throw new Error(formatFileMissing(path));
5071
+ const actual = (0, node_fs.readFileSync)(resolved, "utf8");
5072
+ if (!actual.includes(content)) throw new Error(formatFileContentMismatch(path, content, actual));
5073
+ return this;
5074
+ }
4935
5075
  resolveDatabase(serviceName) {
4936
5076
  if (serviceName && this.config.databases) return this.config.databases.get(serviceName);
4937
5077
  return this.config.database;
4938
5078
  }
5079
+ resolveWorkPath(path) {
5080
+ if (this.workDir) return (0, node_path.resolve)(this.workDir, path);
5081
+ return (0, node_path.resolve)(this.testDir, path);
5082
+ }
4939
5083
  };
4940
5084
  var SpecificationBuilder = class {
5085
+ commandArgs = null;
4941
5086
  config;
4942
- testDir;
5087
+ fixtures = [];
4943
5088
  label;
4944
- seeds = [];
4945
5089
  mocks = [];
5090
+ projectName = null;
4946
5091
  request = null;
5092
+ seeds = [];
5093
+ testDir;
4947
5094
  constructor(config, testDir, label) {
4948
5095
  this.config = config;
4949
5096
  this.testDir = testDir;
@@ -4956,6 +5103,14 @@ var SpecificationBuilder = class {
4956
5103
  });
4957
5104
  return this;
4958
5105
  }
5106
+ fixture(file) {
5107
+ this.fixtures.push({ file });
5108
+ return this;
5109
+ }
5110
+ project(name) {
5111
+ this.projectName = name;
5112
+ return this;
5113
+ }
4959
5114
  mock(file) {
4960
5115
  this.mocks.push({ file });
4961
5116
  return this;
@@ -4969,17 +5124,17 @@ var SpecificationBuilder = class {
4969
5124
  }
4970
5125
  post(path, bodyFile) {
4971
5126
  this.request = {
5127
+ bodyFile,
4972
5128
  method: "POST",
4973
- path,
4974
- bodyFile
5129
+ path
4975
5130
  };
4976
5131
  return this;
4977
5132
  }
4978
5133
  put(path, bodyFile) {
4979
5134
  this.request = {
5135
+ bodyFile,
4980
5136
  method: "PUT",
4981
- path,
4982
- bodyFile
5137
+ path
4983
5138
  };
4984
5139
  return this;
4985
5140
  }
@@ -4990,8 +5145,17 @@ var SpecificationBuilder = class {
4990
5145
  };
4991
5146
  return this;
4992
5147
  }
5148
+ exec(args) {
5149
+ this.commandArgs = args;
5150
+ return this;
5151
+ }
4993
5152
  async run() {
4994
- if (!this.request) throw new Error(`Specification "${this.label}": no request defined. Call .get(), .post(), etc. before .run()`);
5153
+ const hasHttpAction = this.request !== null;
5154
+ const hasCliAction = this.commandArgs !== null;
5155
+ if (!hasHttpAction && !hasCliAction) throw new Error(`Specification "${this.label}": no action defined. Call .get(), .post(), .exec(), etc. before .run()`);
5156
+ if (hasHttpAction && hasCliAction) throw new Error(`Specification "${this.label}": cannot mix HTTP (.get/.post) and CLI (.exec) actions`);
5157
+ let workDir = null;
5158
+ if (hasCliAction) workDir = this.prepareWorkDir();
4995
5159
  if (this.config.databases) for (const db of this.config.databases.values()) await db.reset();
4996
5160
  else if (this.config.database) await this.config.database.reset();
4997
5161
  for (const entry of this.seeds) {
@@ -5004,13 +5168,43 @@ var SpecificationBuilder = class {
5004
5168
  const sql = (0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "seeds", entry.file), "utf8");
5005
5169
  await db.seed(sql);
5006
5170
  }
5171
+ 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 });
5007
5172
  for (const entry of this.mocks) JSON.parse((0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "mock", entry.file), "utf8"));
5173
+ if (hasHttpAction) return this.runHttpAction();
5174
+ return this.runCliAction(workDir);
5175
+ }
5176
+ prepareWorkDir() {
5177
+ const tempDir = (0, node_fs.mkdtempSync)((0, node_path.resolve)((0, node_os.tmpdir)(), "spec-cli-"));
5178
+ if (this.projectName && this.config.fixturesRoot) {
5179
+ const projectDir = (0, node_path.resolve)(this.config.fixturesRoot, this.projectName);
5180
+ if (!(0, node_fs.existsSync)(projectDir)) throw new Error(`project("${this.projectName}"): fixture project not found at ${projectDir}`);
5181
+ (0, node_fs.cpSync)(projectDir, tempDir, { recursive: true });
5182
+ }
5183
+ return tempDir;
5184
+ }
5185
+ async runHttpAction() {
5186
+ if (!this.config.server) throw new Error("HTTP actions require a server adapter (use integration() or e2e())");
5008
5187
  let body;
5009
5188
  if (this.request.bodyFile) body = JSON.parse((0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "requests", this.request.bodyFile), "utf8"));
5010
- return new SpecificationResult(await this.config.server.request(this.request.method, this.request.path, body), this.config, this.testDir, {
5011
- method: this.request.method,
5012
- path: this.request.path,
5013
- body
5189
+ const response = await this.config.server.request(this.request.method, this.request.path, body);
5190
+ return new SpecificationResult({
5191
+ config: this.config,
5192
+ requestInfo: {
5193
+ body,
5194
+ method: this.request.method,
5195
+ path: this.request.path
5196
+ },
5197
+ response,
5198
+ testDir: this.testDir
5199
+ });
5200
+ }
5201
+ async runCliAction(workDir) {
5202
+ if (!this.config.command) throw new Error("CLI actions require a command adapter (use cli())");
5203
+ return new SpecificationResult({
5204
+ commandResult: await this.config.command.exec(this.commandArgs, workDir),
5205
+ config: this.config,
5206
+ testDir: this.testDir,
5207
+ workDir
5014
5208
  });
5015
5209
  }
5016
5210
  };
@@ -5059,21 +5253,25 @@ function resolveProjectRoot(root) {
5059
5253
  return (0, node_path.resolve)(process.cwd(), root);
5060
5254
  }
5061
5255
  /**
5256
+ * Resolve a CLI command — checks node_modules/.bin, then treats as absolute/PATH.
5257
+ */
5258
+ function resolveCommand(command, root) {
5259
+ if ((0, node_path.isAbsolute)(command)) return command;
5260
+ const binPath = (0, node_path.resolve)(root, "node_modules/.bin", command);
5261
+ if ((0, node_fs.existsSync)(binPath)) return binPath;
5262
+ const cwdBinPath = (0, node_path.resolve)(process.cwd(), "node_modules/.bin", command);
5263
+ if ((0, node_fs.existsSync)(cwdBinPath)) return cwdBinPath;
5264
+ return command;
5265
+ }
5266
+ /**
5062
5267
  * Create an integration specification runner.
5063
5268
  * Starts infra containers via testcontainers, app runs in-process.
5064
- *
5065
- * @example
5066
- * const db = postgres({ compose: "db" });
5067
- * export const spec = await integration({
5068
- * services: [db],
5069
- * app: () => createApp({ databaseUrl: db.connectionString }),
5070
- * });
5071
5269
  */
5072
5270
  async function integration(options) {
5073
5271
  const orchestrator = new Orchestrator({
5074
- services: options.services,
5075
5272
  mode: "integration",
5076
- root: resolveProjectRoot(options.root)
5273
+ root: resolveProjectRoot(options.root),
5274
+ services: options.services
5077
5275
  });
5078
5276
  await orchestrator.start();
5079
5277
  const app = options.app();
@@ -5091,17 +5289,12 @@ async function integration(options) {
5091
5289
  /**
5092
5290
  * Create an E2E specification runner.
5093
5291
  * Starts full docker compose stack. App URL and database auto-detected.
5094
- *
5095
- * @example
5096
- * export const spec = await e2e({
5097
- * root: "../fixtures/app",
5098
- * });
5099
5292
  */
5100
5293
  async function e2e(options = {}) {
5101
5294
  const orchestrator = new Orchestrator({
5102
- services: [],
5103
5295
  mode: "e2e",
5104
- root: resolveProjectRoot(options.root)
5296
+ root: resolveProjectRoot(options.root),
5297
+ services: []
5105
5298
  });
5106
5299
  await orchestrator.startCompose();
5107
5300
  const appUrl = orchestrator.getAppUrl();
@@ -5117,10 +5310,51 @@ async function e2e(options = {}) {
5117
5310
  runner.orchestrator = orchestrator;
5118
5311
  return runner;
5119
5312
  }
5313
+ /**
5314
+ * Create a CLI specification runner.
5315
+ * Runs CLI commands against fixture projects. Optionally starts infrastructure.
5316
+ *
5317
+ * @example
5318
+ * export const spec = await cli({
5319
+ * command: resolve(import.meta.dirname, "../../bin/my-cli.sh"),
5320
+ * root: "../fixtures",
5321
+ * });
5322
+ */
5323
+ async function cli(options) {
5324
+ const root = resolveProjectRoot(options.root);
5325
+ const command = resolveCommand(options.command, root);
5326
+ let orchestrator = null;
5327
+ let database;
5328
+ let databases;
5329
+ if (options.services?.length) {
5330
+ orchestrator = new Orchestrator({
5331
+ mode: "integration",
5332
+ root,
5333
+ services: options.services
5334
+ });
5335
+ await orchestrator.start();
5336
+ database = orchestrator.getDatabase() ?? void 0;
5337
+ const dbMap = orchestrator.getDatabases();
5338
+ databases = dbMap.size > 0 ? dbMap : void 0;
5339
+ }
5340
+ const runner = createSpecificationRunner({
5341
+ command: new ExecAdapter(command),
5342
+ database,
5343
+ databases,
5344
+ fixturesRoot: root
5345
+ });
5346
+ runner.cleanup = async () => {
5347
+ if (orchestrator) await orchestrator.stop();
5348
+ };
5349
+ runner.orchestrator = orchestrator;
5350
+ return runner;
5351
+ }
5120
5352
  //#endregion
5353
+ exports.ExecAdapter = ExecAdapter;
5121
5354
  exports.FetchAdapter = FetchAdapter;
5122
5355
  exports.HonoAdapter = HonoAdapter;
5123
5356
  exports.Orchestrator = Orchestrator;
5357
+ exports.cli = cli;
5124
5358
  exports.e2e = e2e;
5125
5359
  exports.integration = integration;
5126
5360
  exports.mockOf = mockOf;