@jterrazz/test 15.0.0 → 15.1.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.
@@ -14,8 +14,8 @@ const MAX_REPORTED_MATCHES = 10;
14
14
  */
15
15
  function locate(root, element) {
16
16
  const scope = element.scope ? locate(root, element.scope) : root;
17
- const options = {
18
- exact: element.exact,
17
+ const options = element.name === void 0 ? { exact: element.exact ?? false } : {
18
+ exact: element.exact ?? false,
19
19
  name: element.name
20
20
  };
21
21
  switch (element.kind) {
@@ -30,9 +30,9 @@ function locate(root, element) {
30
30
  case "button":
31
31
  case "heading":
32
32
  case "link": return scope.getByRole(element.kind, options);
33
- case "field": return scope.getByLabel(element.name ?? "", { exact: element.exact });
33
+ case "field": return scope.getByLabel(element.name ?? "", { exact: element.exact ?? false });
34
34
  case "testId": return scope.getByTestId(element.name ?? "");
35
- case "text": return scope.getByText(element.name ?? "", { exact: element.exact });
35
+ case "text": return scope.getByText(element.name ?? "", { exact: element.exact ?? false });
36
36
  }
37
37
  }
38
38
  /** Playwright signals "more than one match" through this error text. */
@@ -41,7 +41,7 @@ function isStrictViolation(error) {
41
41
  }
42
42
  /** Capture the candidates in-page — the evidence the refusal enumerates. */
43
43
  async function captureMatches(locator) {
44
- return locator.evaluateAll((nodes, limit) => {
44
+ return await locator.evaluateAll((nodes, limit) => {
45
45
  const LANDMARKS = "nav, header, footer, main, aside, section, form, [role]";
46
46
  return nodes.slice(0, limit).map((node) => {
47
47
  const element = node;
@@ -52,7 +52,7 @@ async function captureMatches(locator) {
52
52
  context: context ?? void 0,
53
53
  detail: detail ?? void 0,
54
54
  tag: element.tagName.toLowerCase(),
55
- text: (element.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 80)
55
+ text: (element.textContent ?? "").replaceAll(/\s+/gu, " ").trim().slice(0, 80)
56
56
  };
57
57
  });
58
58
  }, MAX_REPORTED_MATCHES);
@@ -86,17 +86,39 @@ async function findAmbiguousLevel(page, element) {
86
86
  /** The visitor implementation — every action auto-waits via playwright actionability. */
87
87
  function createVisitor(page, baseUrl) {
88
88
  return {
89
- check: (element) => act(page, element, (locator) => locator.check()),
90
- click: (element) => act(page, element, (locator) => locator.click()),
91
- fill: (element, value) => act(page, element, (locator) => locator.fill(value)),
89
+ check: async (element) => {
90
+ await act(page, element, async (locator) => {
91
+ await locator.check();
92
+ });
93
+ },
94
+ click: async (element) => {
95
+ await act(page, element, async (locator) => {
96
+ await locator.click();
97
+ });
98
+ },
99
+ fill: async (element, value) => {
100
+ await act(page, element, async (locator) => {
101
+ await locator.fill(value);
102
+ });
103
+ },
92
104
  goto: async (path) => {
93
105
  await page.goto(`${baseUrl}${path}`, { waitUntil: "load" });
94
106
  },
95
- hover: (element) => act(page, element, (locator) => locator.hover()),
96
- press: (key) => page.keyboard.press(key),
97
- see: (element) => act(page, element, (locator) => locator.waitFor({ state: "visible" })),
107
+ hover: async (element) => {
108
+ await act(page, element, async (locator) => {
109
+ await locator.hover();
110
+ });
111
+ },
112
+ press: async (key) => {
113
+ await page.keyboard.press(key);
114
+ },
115
+ see: async (element) => {
116
+ await act(page, element, async (locator) => {
117
+ await locator.waitFor({ state: "visible" });
118
+ });
119
+ },
98
120
  select: async (element, option) => {
99
- await act(page, element, (locator) => locator.selectOption(option));
121
+ await act(page, element, async (locator) => await locator.selectOption(option));
100
122
  }
101
123
  };
102
124
  }
@@ -119,7 +141,7 @@ var PlaywrightAdapter = class {
119
141
  }
120
142
  }
121
143
  async open(url, options) {
122
- const context = await (await this.launch()).newContext({ extraHTTPHeaders: options.headers });
144
+ const context = await (await this.launch()).newContext({ extraHTTPHeaders: options.headers ?? {} });
123
145
  if (options.external === "block") {
124
146
  const allowed = /* @__PURE__ */ new Set([new URL(options.baseUrl).origin, ...options.allowedOrigins ?? []]);
125
147
  await context.route("**/*", (route) => {
package/dist/queue.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as CaptureScope } from "./match.js";
2
- import { L as placeholderPatternSource, P as hasPlaceholders, V as textEquals } from "./artifacts.js";
3
- //#region src/core/contracts/contract.ts
2
+ import { A as placeholderPatternSource, D as hasPlaceholders, P as textEquals } from "./artifacts.js";
3
+ //#region src/specification/contracts/contract.ts
4
4
  /**
5
5
  * Declare one contract. Identity function — its value is the enforced shape
6
6
  * and the naming convention:
@@ -86,7 +86,7 @@ function contractsOf(input) {
86
86
  return flatten([input]);
87
87
  }
88
88
  //#endregion
89
- //#region src/core/contracts/queue.ts
89
+ //#region src/specification/contracts/queue.ts
90
90
  /**
91
91
  * The ONE contract queue — pure, engine-agnostic (no MSW, no node:http).
92
92
  * Both engines consume it: the MSW integration on api/jobs chains, and the
@@ -172,20 +172,27 @@ function contractMatches(contract, request) {
172
172
  function routePatternOf(declared) {
173
173
  if (declared instanceof RegExp) return declared;
174
174
  const { base } = splitDeclared(declared);
175
- if (base.startsWith("/")) return new RegExp(String.raw`^https?:\/\/[^/?#]+${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`);
176
- if (hasPlaceholders(base)) return new RegExp(String.raw`^${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`);
175
+ if (base.startsWith("/")) return new RegExp(String.raw`^https?:\/\/[^/?#]+${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`, "u");
176
+ if (hasPlaceholders(base)) return new RegExp(String.raw`^${placeholderPatternSource(base)}(?:\?[^#]*)?(?:#.*)?$`, "u");
177
177
  return base;
178
178
  }
179
+ /** How a declared contract reads in a failure message: spent, served, or silent. */
180
+ function stateOf({ contract, served }) {
181
+ const { times } = contract;
182
+ if (times !== void 0 && served >= times) return ` (exhausted after ${times})`;
183
+ return served === 0 ? "" : ` (served ${served} time(s))`;
184
+ }
179
185
  var ContractQueue = class {
180
- contracts;
181
- served;
186
+ entries;
182
187
  constructor(contracts) {
183
- this.contracts = contracts;
184
- this.served = contracts.map(() => 0);
188
+ this.entries = contracts.map((contract) => ({
189
+ contract,
190
+ served: 0
191
+ }));
185
192
  }
186
193
  /** How many contracts the chain declared. */
187
194
  get size() {
188
- return this.contracts.length;
195
+ return this.entries.length;
189
196
  }
190
197
  /**
191
198
  * Routes to register with an out-of-process router, deduped by pattern —
@@ -193,7 +200,7 @@ var ContractQueue = class {
193
200
  */
194
201
  get routes() {
195
202
  const routes = /* @__PURE__ */ new Map();
196
- for (const contract of this.contracts) {
203
+ for (const { contract } of this.entries) {
197
204
  const url = routePatternOf(contract.request.url);
198
205
  const key = String(url);
199
206
  const route = routes.get(key) ?? {
@@ -207,7 +214,7 @@ var ContractQueue = class {
207
214
  }
208
215
  /** The declared routes, in order — the enumeration a 501 body carries. */
209
216
  declaredRoutes() {
210
- return this.contracts.map((contract) => describeRoute(contract));
217
+ return this.entries.map(({ contract }) => describeRoute(contract));
211
218
  }
212
219
  /**
213
220
  * Serve the first contract in declaration order that matches and is not
@@ -215,13 +222,12 @@ var ContractQueue = class {
215
222
  * matches is spent") — the strict-violation signal.
216
223
  */
217
224
  take(request) {
218
- for (let i = 0; i < this.contracts.length; i++) {
219
- const contract = this.contracts[i];
220
- const { times } = contract;
221
- if (times !== void 0 && this.served[i] >= times) continue;
222
- if (!contractMatches(contract, request)) continue;
223
- this.served[i] += 1;
224
- return contract;
225
+ for (const entry of this.entries) {
226
+ const { times } = entry.contract;
227
+ if (times !== void 0 && entry.served >= times) continue;
228
+ if (!contractMatches(entry.contract, request)) continue;
229
+ entry.served += 1;
230
+ return entry.contract;
225
231
  }
226
232
  return null;
227
233
  }
@@ -232,9 +238,8 @@ var ContractQueue = class {
232
238
  */
233
239
  requiredError() {
234
240
  const unmet = [];
235
- for (const [i, contract] of this.contracts.entries()) {
241
+ for (const { contract, served } of this.entries) {
236
242
  if (!contract.required) continue;
237
- const served = this.served[i];
238
243
  const { times } = contract;
239
244
  if (times === void 0) {
240
245
  if (served === 0) unmet.push(` - ${describeRoute(contract)} — declared and required but never requested`);
@@ -249,15 +254,9 @@ var ContractQueue = class {
249
254
  * and how often it was served.
250
255
  */
251
256
  unmatchedError(method, url) {
252
- const declared = this.contracts.length === 0 ? " (no contracts declared)" : this.contracts.map((contract, i) => ` - ${describeRoute(contract)}${this.stateOf(i)}`).join("\n");
257
+ const declared = this.entries.length === 0 ? " (no contracts declared)" : this.entries.map((entry) => ` - ${describeRoute(entry.contract)}${stateOf(entry)}`).join("\n");
253
258
  return /* @__PURE__ */ new Error(`Unmatched outgoing HTTP request during spec: ${method} ${url}\nDeclared contracts:\n${declared}\nEvery outgoing request of a chain that declares contracts must match one — add a contract for it (or raise its \`times\` when it is exhausted).`);
254
259
  }
255
- stateOf(index) {
256
- const served = this.served[index];
257
- const { times } = this.contracts[index];
258
- if (times !== void 0 && served >= times) return ` (exhausted after ${times})`;
259
- return served === 0 ? "" : ` (served ${served} time(s))`;
260
- }
261
260
  };
262
261
  //#endregion
263
262
  export { isContract as a, defineContracts as i, contractsOf as n, isContracts as o, defineContract as r, ContractQueue as t };
@@ -1,7 +1,7 @@
1
1
  import { LineCounter, Scalar, Scalar as Scalar$1, isMap, isScalar, isSeq, parseDocument, visit } from "yaml";
2
2
  //#region src/integrations/yaml/document.ts
3
3
  /** A line that opens a mapping key — ` key:` or ` - key:`, never block-scalar text. */
4
- const KEY_LINE = /^(?<indent> +)(?:- )?[A-Za-z_$'"][^\n]*:/;
4
+ const KEY_LINE = /^(?<indent> +)(?:- )?[A-Za-z_$'"][^\n]*:/u;
5
5
  /**
6
6
  * The indentation step the file was written with, read from the shallowest
7
7
  * mapping key it carries. A rewrite must come back in the author's style — a
@@ -11,8 +11,8 @@ const KEY_LINE = /^(?<indent> +)(?:- )?[A-Za-z_$'"][^\n]*:/;
11
11
  function detectIndent(text) {
12
12
  let smallest = 0;
13
13
  for (const line of text.split("\n")) {
14
- const found = KEY_LINE.exec(line);
15
- if (found?.groups && (smallest === 0 || found.groups.indent.length < smallest)) smallest = found.groups.indent.length;
14
+ const indent = KEY_LINE.exec(line)?.groups?.indent;
15
+ if (indent !== void 0 && (smallest === 0 || indent.length < smallest)) smallest = indent.length;
16
16
  }
17
17
  return smallest === 0 ? 2 : smallest;
18
18
  }
@@ -37,7 +37,7 @@ const DEEPEST_STATABLE_STEP = 9;
37
37
  * at the root, which YAML counts from -1.
38
38
  */
39
39
  function stateIndentation(written, ctx) {
40
- if (!/^[|>][1-9]/.test(written)) return written;
40
+ if (!/^[|>][1-9]/u.test(written)) return written;
41
41
  const step = ctx.indent === "" ? 1 : ctx.indentStep.length;
42
42
  if (step > DEEPEST_STATABLE_STEP) throw new Error(`A block scalar indented by ${step} spaces cannot state its indentation: YAML writes that indicator as a single digit, so ${DEEPEST_STATABLE_STEP} spaces is the deepest step a document may be written with.`);
43
43
  return `${written[0]}${step}${written.slice(2)}`;
@@ -96,13 +96,13 @@ function lineStart(text, offset) {
96
96
  /** Back over the whitespace before `offset`, to just past the `:` or `-` that owns it. */
97
97
  function afterOwner(text, offset) {
98
98
  let at = offset;
99
- while (at > 0 && /\s/.test(text[at - 1])) at -= 1;
99
+ while (at > 0 && /\s/u.test(text[at - 1] ?? "")) at -= 1;
100
100
  return at;
101
101
  }
102
102
  /** How many spaces a line opens with. */
103
103
  function indentOf(text, offset) {
104
104
  const start = lineStart(text, offset);
105
- return /^ */.exec(text.slice(start))[0].length;
105
+ return /^ */u.exec(text.slice(start))[0].length;
106
106
  }
107
107
  /**
108
108
  * Every OUTERMOST flow collection of a document, in source order. A nested one
@@ -113,11 +113,13 @@ function flowCollections(text) {
113
113
  const found = [];
114
114
  const take = (node) => {
115
115
  if (node.flow !== true || !node.range) return;
116
- const from = afterOwner(text, node.range[0]);
116
+ const [start, end] = node.range;
117
+ if (start === void 0 || end === void 0) return;
118
+ const from = afterOwner(text, start);
117
119
  found.push({
118
120
  ownerIndent: indentOf(text, from === 0 ? 0 : from - 1),
119
- range: [from, node.range[1]],
120
- text: text.slice(from, node.range[1]),
121
+ range: [from, end],
122
+ text: text.slice(from, end),
121
123
  value: JSON.stringify(node.toJSON())
122
124
  });
123
125
  return visit.SKIP;
@@ -154,10 +156,12 @@ function restoreUntouchedFlow(rendered, text) {
154
156
  for (const target of flowCollections(rendered)) {
155
157
  const at = sources.findIndex((candidate, index) => !taken.has(index) && candidate.value === target.value && candidate.ownerIndent === target.ownerIndent);
156
158
  if (at === -1) continue;
159
+ const source = sources[at];
160
+ if (source === void 0) continue;
157
161
  taken.add(at);
158
162
  splices.push({
159
163
  range: target.range,
160
- text: sources[at].text
164
+ text: source.text
161
165
  });
162
166
  }
163
167
  let out = rendered;
@@ -179,7 +183,7 @@ function renderYamlSource(source) {
179
183
  }), source.text);
180
184
  }
181
185
  //#endregion
182
- //#region src/core/literate/spec-document.ts
186
+ //#region src/specification/literate/spec-document.ts
183
187
  /**
184
188
  * The `<case>.spec.yaml` document — one scenario, written as data.
185
189
  *
@@ -256,7 +260,7 @@ function lineOf(context, node) {
256
260
  }
257
261
  function nodeRange(node) {
258
262
  if ("range" in node && Array.isArray(node.range)) return node.range;
259
- const key = node.key;
263
+ const { key } = node;
260
264
  return key !== null && typeof key === "object" && "range" in key ? key.range ?? void 0 : void 0;
261
265
  }
262
266
  /** The line a scalar's CONTENT starts on — one past the `|` for a block scalar. */
@@ -423,8 +427,8 @@ function readRun(context, node, isLast) {
423
427
  */
424
428
  function readSpecFile(content, fileName) {
425
429
  const source = parseYamlSource(content);
426
- const syntax = yamlSyntaxErrors(source);
427
- if (syntax.length > 0) throw new SpecSyntaxError(fileName, syntax[0].line, syntax[0].message);
430
+ const [syntax] = yamlSyntaxErrors(source);
431
+ if (syntax !== void 0) throw new SpecSyntaxError(fileName, syntax.line, syntax.message);
428
432
  const context = {
429
433
  fileName,
430
434
  source
@@ -533,20 +537,10 @@ const STRING_OR_LIST = { oneOf: [{ type: "string" }, {
533
537
  minItems: 1,
534
538
  type: "array"
535
539
  }] };
536
- /**
537
- * The JSON Schema editors validate a `<case>.spec.yaml` against, built from the
538
- * constants above so it cannot describe a grammar the parser does not read. It
539
- * is committed as `schema/spec.schema.json` by `npm run docs` and published as
540
- * the `@jterrazz/test/schema` export.
541
- *
542
- * A schema states which keys exist, not which ORDER they come in — JSON Schema
543
- * has no vocabulary for the sequence of an object's members. The canonical
544
- * order is the `d4b-spec-key-order` lint pass's, which also rewrites it.
545
- */
546
540
  const SPEC_SCHEMA = {
547
541
  $defs: {
548
542
  fileAssertion: { oneOf: [{
549
- enum: [...FILE_STATES],
543
+ enum: FILE_STATES,
550
544
  type: "string"
551
545
  }, {
552
546
  additionalProperties: false,
package/dist/vitest.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ViteUserConfig } from "vitest/config";
2
2
  //#region src/vitest/literate-plugin.d.ts
3
- interface LiterateOptions {
3
+ type LiterateOptions = {
4
4
  /**
5
5
  * Globs added to the project's test include. Default: every `.spec.yaml`
6
6
  * file. Narrow it when a tree holds documents that are INPUTS to other
@@ -17,13 +17,13 @@ interface LiterateOptions {
17
17
  * scenario to the wrong command.
18
18
  */
19
19
  specification: string;
20
- }
20
+ };
21
21
  /**
22
22
  * The shape vite consumes. Declared structurally rather than imported from
23
23
  * `vite`, so the package's runner coupling stays the one dependency it already
24
24
  * has (CONVENTIONS I1) — the object is assignable to vite's `Plugin`.
25
25
  */
26
- interface LiteratePlugin {
26
+ type LiteratePlugin = {
27
27
  config: () => {
28
28
  test: {
29
29
  include: string[];
@@ -36,7 +36,7 @@ interface LiteratePlugin {
36
36
  enforce: 'pre';
37
37
  load: (id: string) => null | string;
38
38
  name: string;
39
- }
39
+ };
40
40
  declare function literate(options: LiterateOptions): LiteratePlugin;
41
41
  //#endregion
42
42
  //#region src/vitest/preset.d.ts
@@ -44,7 +44,7 @@ declare function literate(options: LiterateOptions): LiteratePlugin;
44
44
  * Options are a vite/vitest config plus one key of the framework's own.
45
45
  * Everything stated here wins over the preset's defaults.
46
46
  */
47
- interface SpecConfig extends ViteUserConfig {
47
+ type SpecConfig = {
48
48
  /**
49
49
  * Turns every matching `<case>.spec.yaml` into a test file bound to the
50
50
  * named specification — the {@link literate} plugin, added to `plugins`.
@@ -54,7 +54,7 @@ interface SpecConfig extends ViteUserConfig {
54
54
  * so the plugin goes in that project's own `plugins` instead.
55
55
  */
56
56
  literate?: LiterateOptions;
57
- }
57
+ } & ViteUserConfig;
58
58
  declare function defineSpecConfig(config?: SpecConfig): ViteUserConfig;
59
59
  //#endregion
60
60
  export { type LiterateOptions, type LiteratePlugin, type SpecConfig, defineSpecConfig, literate };
package/dist/vitest.js CHANGED
@@ -24,7 +24,7 @@ import { configDefaults, mergeConfig } from "vitest/config";
24
24
  * ```
25
25
  */
26
26
  /** The `.spec.yaml` extension, matched on the id vite hands the plugin. */
27
- const SPEC_FILE = /\.spec\.yaml(?:\?.*)?$/;
27
+ const SPEC_FILE = /\.spec\.yaml(?:\?.*)?$/u;
28
28
  /**
29
29
  * The module a `<case>.spec.yaml` becomes: one `test()` whose title is the
30
30
  * document's `description:` and whose body runs the whole file — its ground,
@@ -46,7 +46,7 @@ function literateModule(content, filePath, specificationPath) {
46
46
  }
47
47
  /** Strip vite's query suffix (`?v=…`) from a module id. */
48
48
  function cleanId(id) {
49
- return id.split("?")[0];
49
+ return id.split("?")[0] ?? id;
50
50
  }
51
51
  function literate(options) {
52
52
  const include = options.include ?? ["**/*.spec.yaml"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/test",
3
- "version": "15.0.0",
3
+ "version": "15.1.0",
4
4
  "description": "Declarative testing framework for HTTP APIs, CLIs, and background jobs — one entry point, real infrastructure, golden-file assertions, plus an oxlint convention plugin.",
5
5
  "keywords": [
6
6
  "api-testing",
@@ -41,9 +41,14 @@
41
41
  "import": "./dist/vitest.js"
42
42
  },
43
43
  "./oxlint": {
44
- "types": "./dist/oxlint.d.ts",
45
- "import": "./dist/oxlint.js",
46
- "require": "./dist/oxlint.cjs"
44
+ "import": {
45
+ "types": "./dist/oxlint.d.ts",
46
+ "default": "./dist/oxlint.js"
47
+ },
48
+ "require": {
49
+ "types": "./dist/oxlint.d.cts",
50
+ "default": "./dist/oxlint.cjs"
51
+ }
47
52
  },
48
53
  "./schema": "./schema/spec.schema.json"
49
54
  },
@@ -69,13 +74,11 @@
69
74
  },
70
75
  "devDependencies": {
71
76
  "@hono/node-server": "^2.0.8",
72
- "@jterrazz/typescript": "^9.2.0",
77
+ "@jterrazz/typescript": "^10.0.0",
73
78
  "@types/better-sqlite3": "^7.6.13",
74
79
  "@types/node": "^26.1.1",
75
80
  "@types/pg": "^8.20.0",
76
81
  "hono": "^4.12.30",
77
- "oxfmt": "^0.59.0",
78
- "oxlint": "^1.74.0",
79
82
  "playwright": "^1.49.0",
80
83
  "tsdown": "^0.22.7",
81
84
  "vitest": "^4.1.10",