@jterrazz/test 15.1.0 → 15.2.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/ambiguity.js CHANGED
@@ -19,14 +19,20 @@ const CONSTRUCTORS = {
19
19
  button: "button",
20
20
  complementary: "complementary",
21
21
  contentinfo: "contentinfo",
22
+ dialog: "dialog",
22
23
  field: "field",
23
24
  form: "form",
24
25
  heading: "heading",
25
26
  link: "link",
27
+ listitem: "listitem",
26
28
  main: "main",
27
29
  navigation: "navigation",
30
+ option: "option",
28
31
  region: "region",
32
+ row: "row",
29
33
  search: "search",
34
+ status: "status",
35
+ table: "table",
30
36
  testId: "testId",
31
37
  text: "content"
32
38
  };
@@ -62,12 +68,22 @@ function formatBare(element) {
62
68
  /** `1. <a href="/articles">Articles</a> in <nav>` — one evidence line per candidate. */
63
69
  function formatMatch(match, index) {
64
70
  const attribute = match.detail ? ` ${quoteDetail(match)}` : "";
71
+ const named = match.accessibleName ? ` named ${JSON.stringify(match.accessibleName)}` : "";
65
72
  const context = match.context ? ` in <${match.context}>` : "";
66
- return ` ${index + 1}. <${match.tag}${attribute}>${match.text}</${match.tag}>${context}`;
73
+ return ` ${index + 1}. <${match.tag}${attribute}>${match.text}</${match.tag}>${named}${context}`;
67
74
  }
68
75
  function quoteDetail(match) {
69
76
  return `${match.tag === "a" ? "href" : "name"}=${JSON.stringify(match.detail)}`;
70
77
  }
78
+ /** Each landmark the candidates sit in, and how many of them it holds. */
79
+ function landmarkCounts(matches) {
80
+ const counts = /* @__PURE__ */ new Map();
81
+ for (const match of matches) {
82
+ const landmark = match.context === void 0 ? void 0 : CONTEXT_LANDMARKS[match.context];
83
+ if (landmark !== void 0) counts.set(landmark, (counts.get(landmark) ?? 0) + 1);
84
+ }
85
+ return [...counts].toSorted(([, a], [, b]) => a - b);
86
+ }
71
87
  /**
72
88
  * The rewrites worth offering, in the order they should be tried. Scoping is
73
89
  * always available; `exact` only when it would actually narrow the set, and
@@ -76,9 +92,12 @@ function quoteDetail(match) {
76
92
  */
77
93
  function formatFixes(element, matches) {
78
94
  const fixes = [];
79
- const landmarks = [...new Set(matches.map((match) => match.context && CONTEXT_LANDMARKS[match.context]).filter((landmark) => Boolean(landmark)))];
80
- if (landmarks.length > 0 && !element.scope) fixes.push(`scope it within(${landmarks[0]}, ${formatBare(element)})${landmarks.length > 1 ? ` [also here: ${landmarks.slice(1).join(", ")}]` : ""}`);
81
- else if (!element.scope) fixes.push(`scope it within(main(), ${formatBare(element)})`);
95
+ const narrowing = landmarkCounts(matches).filter(([, count]) => count < matches.length);
96
+ if (!element.scope) {
97
+ const [first, ...rest] = narrowing;
98
+ const alsoHere = rest.length > 0 ? ` [also here: ${rest.map(([name]) => name).join(", ")}]` : "";
99
+ fixes.push(first === void 0 ? `scope it within(<a container holding only this one>, ${formatBare(element)})` : `scope it within(${first[0]}, ${formatBare(element)}) [leaves ${first[1]} of ${matches.length}]${alsoHere}`);
100
+ }
82
101
  if (!element.exact && element.name !== void 0) {
83
102
  const remaining = matches.filter((match) => match.text === element.name).length;
84
103
  if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${formatBare({
package/dist/artifacts.js CHANGED
@@ -1,82 +1,10 @@
1
1
  import { i as SpecSyntaxError, l as updateSpecFile, s as readSpecFile } from "./spec-document.js";
2
2
  import { a as copyPlan, n as Matcher, r as TOKEN_KINDS, t as CaptureScope } from "./match.js";
3
3
  import { cpSync, existsSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
4
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
5
  import { execSync, spawn } from "node:child_process";
6
6
  import { readdir } from "node:fs/promises";
7
7
  import { createServer } from "node:net";
8
- //#region src/specification/facets/_common/caller.ts
9
- /** Sibling module tests are callers, not internals (CONVENTIONS I2). */
10
- const TEST_FILE = /\.test\.[cm]?[jt]s$/u;
11
- /** Where this very module sits inside the framework's SOURCE tree. */
12
- const SOURCE_LOCATION = join("specification", "facets", "_common");
13
- /** The real path of `path`, or `path` itself when it cannot be resolved. */
14
- function realPath(path) {
15
- try {
16
- return realpathSync(path);
17
- } catch {
18
- return path;
19
- }
20
- }
21
- /**
22
- * Directory of the running framework module itself. From the built package
23
- * this is `<package root>/dist`, whatever the resolution path — including a
24
- * `file:` link, where the real path carries neither `node_modules` nor
25
- * `/src/…` and would otherwise be mistaken for a caller frame.
26
- */
27
- const FRAMEWORK_DIR = realPath(import.meta.dirname);
28
- /**
29
- * The directory holding the framework's OWN modules: `<package>/dist` for a
30
- * built install (everything is bundled there), `<repo>/src` when the framework
31
- * runs from source — this module's known location inside that tree is what
32
- * tells the two apart.
33
- */
34
- const FRAMEWORK_TREE = FRAMEWORK_DIR.endsWith(`${sep}${SOURCE_LOCATION}`) ? resolve(FRAMEWORK_DIR, "../../..") : FRAMEWORK_DIR;
35
- /** Is `path` inside `directory` (strictly — a directory is not inside itself)? */
36
- function isInside(directory, path) {
37
- const rest = relative(directory, path);
38
- return rest !== "" && !rest.startsWith("..") && !isAbsolute(rest);
39
- }
40
- /**
41
- * Is this stack frame a module of the framework itself, rather than of the code
42
- * calling it?
43
- *
44
- * Answered by IDENTITY: the frame is inside the framework's own directory.
45
- * Answering it by substring — `/src/core/`, `/src/integrations/`, `/src/vitest/`
46
- * — was answering a different question, "does this path look like the framework's
47
- * layout", and a consumer that happens to have its own `src/core/` matched it.
48
- * Its files were then skipped as framework internals, and fixture resolution
49
- * anchored on the wrong directory (or fell through to the cwd) with nothing
50
- * reported.
51
- *
52
- * @internal
53
- */
54
- function isFrameworkFrame(filePath, frameworkTree = FRAMEWORK_TREE) {
55
- if (TEST_FILE.test(filePath)) return false;
56
- return isInside(realPath(frameworkTree), realPath(filePath));
57
- }
58
- /**
59
- * Detect the directory of the first stack frame that lives outside this
60
- * package. Used to anchor fixture resolution (`_seeds/`, `_requests/`,
61
- * `_expected/`, …) on the calling test / specification file.
62
- *
63
- * @internal
64
- */
65
- function getCallerDir() {
66
- const { stack } = /* @__PURE__ */ new Error("caller detection");
67
- if (!stack) throw new Error("Cannot detect caller directory: no stack trace");
68
- const lines = stack.split("\n");
69
- for (const line of lines) {
70
- const match = /at\s+(?:.*?\()?(?:file:\/\/)?(?<filePath>[^:)]+):\d+:\d+/u.exec(line);
71
- if (!match?.groups?.filePath) continue;
72
- const { filePath } = match.groups;
73
- if (filePath.includes("node_modules")) continue;
74
- if (isFrameworkFrame(filePath)) continue;
75
- return resolve(filePath, "..");
76
- }
77
- return process.cwd();
78
- }
79
- //#endregion
80
8
  //#region src/specification/facets/_common/result/grep.ts
81
9
  /**
82
10
  * Extract text blocks from output that contain a pattern.
@@ -163,26 +91,6 @@ var TextAccessor = class TextAccessor {
163
91
  return this.text;
164
92
  }
165
93
  };
166
- /**
167
- * Wrap an arbitrary string into a {@link TextAccessor} anchored on the calling
168
- * test's directory — the same caller-detection the builders use.
169
- *
170
- * The product surface of a test framework is its own error messages, checker
171
- * output, and reports; those deserve the same goldening as any other output.
172
- * `text()` makes an ad-hoc string a first-class snapshot subject:
173
- *
174
- * ```typescript
175
- * const message = await catchMessage(() => expect(result.response).toMatch('wrong-body.http'));
176
- * expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to _expected/
177
- * ```
178
- *
179
- * ANSI is stripped before every comparison (the raw form stays on `.text`),
180
- * the `{{token}}` grammar applies to the fixture, and `.grep()` composition
181
- * works exactly as on any stream accessor.
182
- */
183
- function text(value) {
184
- return new TextAccessor(value, "text", getCallerDir());
185
- }
186
94
  //#endregion
187
95
  //#region src/specification/facets/_common/result/json.ts
188
96
  /**
@@ -228,45 +136,35 @@ var JsonAccessor = class {
228
136
  }
229
137
  };
230
138
  //#endregion
231
- //#region src/integrations/docker/docker-lookup.ts
139
+ //#region src/specification/facets/_common/result/table.ts
232
140
  /**
233
- * Thin synchronous wrappers around the `docker` CLI used by the docker() spec
234
- * mode. Sync is deliberate — the calls are fast, run only on the host, and
235
- * keep the dispose path straightforward (`Symbol.asyncDispose` can still be
236
- * async without needing these to be).
141
+ * Read-only accessor for a database table after a specification run.
142
+ *
143
+ * Assertions go through `expect()` (async — they query the database):
144
+ *
145
+ * await expect(result.table('users', { database: 'db' })).toMatchRows({
146
+ * columns: ['name'],
147
+ * rows: [['Alice']],
148
+ * });
149
+ * await expect(result.table('users', { database: 'db' })).toBeEmpty();
237
150
  */
238
- const DEFAULT_TIMEOUT = 1e4;
239
- /** Return all container IDs (running or stopped) that carry `key=value`. */
240
- function findContainersByLabel(key, value) {
241
- try {
242
- return execSync(`docker ps -aq -f label=${key}=${value}`, {
243
- encoding: "utf8",
244
- timeout: DEFAULT_TIMEOUT
245
- }).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
246
- } catch {
247
- return [];
151
+ var TableAccessor = class {
152
+ /** @internal Ref-capture scope shared by the current spec execution. */
153
+ captures;
154
+ /** @internal */
155
+ db;
156
+ /** The table name this accessor reads. */
157
+ name;
158
+ constructor(name, db, captures) {
159
+ this.name = name;
160
+ this.db = db;
161
+ this.captures = captures ?? new CaptureScope();
248
162
  }
249
- }
250
- /** Return the raw `docker inspect` payload (object, not array) for a container. */
251
- function inspectContainer(id) {
252
- const raw = execSync(`docker inspect ${id}`, {
253
- encoding: "utf8",
254
- timeout: DEFAULT_TIMEOUT
255
- });
256
- const parsed = JSON.parse(raw);
257
- return Array.isArray(parsed) ? parsed[0] : parsed;
258
- }
259
- /** Force-remove the given container IDs in a single call. Errors are swallowed. */
260
- function removeContainers(ids) {
261
- if (ids.length === 0) return;
262
- try {
263
- execSync(`docker rm -f ${ids.join(" ")}`, {
264
- encoding: "utf8",
265
- stdio: "pipe",
266
- timeout: DEFAULT_TIMEOUT
267
- });
268
- } catch {}
269
- }
163
+ /** @internal Query the table — used by the toMatchRows / toBeEmpty matchers. */
164
+ async query(columns) {
165
+ return await this.db.query(this.name, columns);
166
+ }
167
+ };
270
168
  //#endregion
271
169
  //#region src/specification/matching/structural.ts
272
170
  /**
@@ -605,6 +503,46 @@ function mergeTextPreservingPlaceholders(previous, actual, scope) {
605
503
  return merged.join("\n");
606
504
  }
607
505
  //#endregion
506
+ //#region src/integrations/docker/docker-lookup.ts
507
+ /**
508
+ * Thin synchronous wrappers around the `docker` CLI used by the docker() spec
509
+ * mode. Sync is deliberate — the calls are fast, run only on the host, and
510
+ * keep the dispose path straightforward (`Symbol.asyncDispose` can still be
511
+ * async without needing these to be).
512
+ */
513
+ const DEFAULT_TIMEOUT = 1e4;
514
+ /** Return all container IDs (running or stopped) that carry `key=value`. */
515
+ function findContainersByLabel(key, value) {
516
+ try {
517
+ return execSync(`docker ps -aq -f label=${key}=${value}`, {
518
+ encoding: "utf8",
519
+ timeout: DEFAULT_TIMEOUT
520
+ }).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
521
+ } catch {
522
+ return [];
523
+ }
524
+ }
525
+ /** Return the raw `docker inspect` payload (object, not array) for a container. */
526
+ function inspectContainer(id) {
527
+ const raw = execSync(`docker inspect ${id}`, {
528
+ encoding: "utf8",
529
+ timeout: DEFAULT_TIMEOUT
530
+ });
531
+ const parsed = JSON.parse(raw);
532
+ return Array.isArray(parsed) ? parsed[0] : parsed;
533
+ }
534
+ /** Force-remove the given container IDs in a single call. Errors are swallowed. */
535
+ function removeContainers(ids) {
536
+ if (ids.length === 0) return;
537
+ try {
538
+ execSync(`docker rm -f ${ids.join(" ")}`, {
539
+ encoding: "utf8",
540
+ stdio: "pipe",
541
+ timeout: DEFAULT_TIMEOUT
542
+ });
543
+ } catch {}
544
+ }
545
+ //#endregion
608
546
  //#region src/specification/facets/_common/result/directory.ts
609
547
  const DEFAULT_IGNORES = [
610
548
  ".git",
@@ -811,36 +749,6 @@ function expandWorkdir(env, workDir) {
811
749
  return expanded;
812
750
  }
813
751
  //#endregion
814
- //#region src/specification/facets/_common/result/table.ts
815
- /**
816
- * Read-only accessor for a database table after a specification run.
817
- *
818
- * Assertions go through `expect()` (async — they query the database):
819
- *
820
- * await expect(result.table('users', { database: 'db' })).toMatchRows({
821
- * columns: ['name'],
822
- * rows: [['Alice']],
823
- * });
824
- * await expect(result.table('users', { database: 'db' })).toBeEmpty();
825
- */
826
- var TableAccessor = class {
827
- /** @internal Ref-capture scope shared by the current spec execution. */
828
- captures;
829
- /** @internal */
830
- db;
831
- /** The table name this accessor reads. */
832
- name;
833
- constructor(name, db, captures) {
834
- this.name = name;
835
- this.db = db;
836
- this.captures = captures ?? new CaptureScope();
837
- }
838
- /** @internal Query the table — used by the toMatchRows / toBeEmpty matchers. */
839
- async query(columns) {
840
- return await this.db.query(this.name, columns);
841
- }
842
- };
843
- //#endregion
844
752
  //#region src/specification/facets/_common/result/result.ts
845
753
  /**
846
754
  * Base result - common accessors available after any action type.
@@ -1794,4 +1702,4 @@ const COVERAGE_DIR = `${VITEST_ARTIFACTS_DIR}/coverage`;
1794
1702
  /** Where `sqlite()` caches the schema template it copies per worker. */
1795
1703
  const SQLITE_TEMPLATE_DIR = `${VITEST_ARTIFACTS_DIR}/sqlite`;
1796
1704
  //#endregion
1797
- export { placeholderPatternSource as A, text as B, FilesystemAccessor as C, hasPlaceholders as D, walkDirectory as E, findContainersByLabel as F, inspectContainer as I, removeContainers as L, structuralEquals as M, structuralSubset as N, mergePreservingPlaceholders as O, textEquals as P, JsonAccessor as R, resolveRoot as S, diffDirectories as T, getCallerDir as V, validateDatabaseOption as _, specDescription as a, expandWorkdir as b, shouldUpdateSnapshots as c, formatStartupReport as d, formatStdoutDiff as f, BaseResult as g, CliResult as h, runSpecDocument as i, renderExpected as j, mergeTextPreservingPlaceholders as k, formatDirectoryDiff as l, ContainerAccessor as m, SQLITE_TEMPLATE_DIR as n, ServeAdapter as o, formatTableDiff as p, VITEST_ARTIFACTS_DIR as r, UPDATE_HINT as s, COVERAGE_DIR as t, formatResponseDiff as u, TableAccessor as v, DirectoryAccessor as w, resolveCommand as x, discoverRoot as y, TextAccessor as z };
1705
+ export { mergePreservingPlaceholders as A, DirectoryAccessor as C, inspectContainer as D, findContainersByLabel as E, structuralSubset as F, textEquals as I, TableAccessor as L, placeholderPatternSource as M, renderExpected as N, removeContainers as O, structuralEquals as P, JsonAccessor as R, FilesystemAccessor as S, walkDirectory as T, validateDatabaseOption as _, specDescription as a, resolveCommand as b, shouldUpdateSnapshots as c, formatStartupReport as d, formatStdoutDiff as f, BaseResult as g, CliResult as h, runSpecDocument as i, mergeTextPreservingPlaceholders as j, hasPlaceholders as k, formatDirectoryDiff as l, ContainerAccessor as m, SQLITE_TEMPLATE_DIR as n, ServeAdapter as o, formatTableDiff as p, VITEST_ARTIFACTS_DIR as r, UPDATE_HINT as s, COVERAGE_DIR as t, formatResponseDiff as u, discoverRoot as v, diffDirectories as w, resolveRoot as x, expandWorkdir as y, TextAccessor as z };