@jterrazz/test 14.1.0 → 14.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/appium.adapter.js +37 -12
- package/dist/artifacts.js +109 -27
- package/dist/index.d.ts +26 -1
- package/dist/index.js +9 -70
- package/dist/queue.js +1 -1
- package/package.json +1 -1
package/dist/appium.adapter.js
CHANGED
|
@@ -179,12 +179,12 @@ function projectScreen(xml) {
|
|
|
179
179
|
}
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region src/integrations/appium/appium.adapter.ts
|
|
182
|
-
const
|
|
182
|
+
const DEFAULT_ACTION_TIMEOUT_MS = 3e4;
|
|
183
183
|
const POLL_INTERVAL_MS = 500;
|
|
184
184
|
/** How many candidates the ambiguity error enumerates before truncating. */
|
|
185
185
|
const MAX_REPORTED_MATCHES = 10;
|
|
186
186
|
/** WebDriverAgent builds once per simulator — the first session is the slow one. */
|
|
187
|
-
const
|
|
187
|
+
const DEFAULT_WDA_LAUNCH_TIMEOUT_MS = 24e4;
|
|
188
188
|
const delay = (ms) => new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
189
189
|
/** Escape a name for an NSPredicate single-quoted string literal. */
|
|
190
190
|
function escapePredicate(value) {
|
|
@@ -213,6 +213,19 @@ function scopeChain(element) {
|
|
|
213
213
|
for (let current = element; current; current = current.scope) chain.unshift(current);
|
|
214
214
|
return chain;
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Load webdriverio's session factory, naming the install when the optional
|
|
218
|
+
* peer dependency is absent. Lazy: the dependency is only required by a spec
|
|
219
|
+
* that actually opens the app.
|
|
220
|
+
*/
|
|
221
|
+
async function loadRemote() {
|
|
222
|
+
try {
|
|
223
|
+
const { remote: remoteFn } = await import("webdriverio");
|
|
224
|
+
return remoteFn;
|
|
225
|
+
} catch {
|
|
226
|
+
throw new Error(".open() requires webdriverio (optional peer dependency): npm install -D appium webdriverio && npx appium driver install xcuitest");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
216
229
|
/** Capture one candidate's evidence for the ambiguity refusal. */
|
|
217
230
|
async function captureMatch(element) {
|
|
218
231
|
const [type, label, value, identifier] = await Promise.all([
|
|
@@ -241,10 +254,27 @@ async function captureMatch(element) {
|
|
|
241
254
|
* this module is only loaded when a spec calls `.open()`.
|
|
242
255
|
*/
|
|
243
256
|
var AppiumAdapter = class {
|
|
257
|
+
/** The verb poll budget — the runner's `timeouts.action`, or the default. */
|
|
258
|
+
actionTimeoutMs;
|
|
244
259
|
driver = null;
|
|
260
|
+
/** The WebDriverAgent launch budget — the runner's `timeouts.launch`, or the default. */
|
|
261
|
+
launchTimeoutMs;
|
|
245
262
|
options;
|
|
263
|
+
/**
|
|
264
|
+
* The session factory. Absent in production — webdriverio is then loaded
|
|
265
|
+
* lazily on the first `open()`. Injected by this module's own unit test,
|
|
266
|
+
* which drives the waits over a stub driver with no simulator in reach
|
|
267
|
+
* (mocks are CODE here, CONVENTIONS I4).
|
|
268
|
+
*/
|
|
269
|
+
remote;
|
|
246
270
|
constructor(options) {
|
|
247
|
-
this.options =
|
|
271
|
+
this.options = {
|
|
272
|
+
serverUrl: options.serverUrl,
|
|
273
|
+
udid: options.udid
|
|
274
|
+
};
|
|
275
|
+
this.remote = options.remote;
|
|
276
|
+
this.actionTimeoutMs = options.timeouts?.action ?? DEFAULT_ACTION_TIMEOUT_MS;
|
|
277
|
+
this.launchTimeoutMs = options.timeouts?.launch ?? DEFAULT_WDA_LAUNCH_TIMEOUT_MS;
|
|
248
278
|
}
|
|
249
279
|
async close() {
|
|
250
280
|
if (this.driver) {
|
|
@@ -305,7 +335,7 @@ var AppiumAdapter = class {
|
|
|
305
335
|
*/
|
|
306
336
|
async resolveOne(driver, element, verb, cardinality) {
|
|
307
337
|
const chain = scopeChain(element);
|
|
308
|
-
const deadline = Date.now() +
|
|
338
|
+
const deadline = Date.now() + this.actionTimeoutMs;
|
|
309
339
|
let scrollAttempted = false;
|
|
310
340
|
for (;;) {
|
|
311
341
|
const resolved = await this.resolveChain(driver, chain, cardinality, { tryScroll: !scrollAttempted });
|
|
@@ -367,17 +397,12 @@ var AppiumAdapter = class {
|
|
|
367
397
|
const labels = [...new Set([...source.matchAll(/label="(?<label>[^"]{2,60})"/g)].map((match) => match.groups?.["label"] ?? ""))].slice(0, 15);
|
|
368
398
|
if (labels.length > 0) excerpt = `\nCurrently on screen: ${labels.join(" | ")}`;
|
|
369
399
|
} catch {}
|
|
370
|
-
return /* @__PURE__ */ new Error(`${verb}(${formatElement(element)}) timed out after ${
|
|
400
|
+
return /* @__PURE__ */ new Error(`${verb}(${formatElement(element)}) timed out after ${this.actionTimeoutMs}ms — no visible match.${excerpt}`);
|
|
371
401
|
}
|
|
372
402
|
/** Create the shared driver session (once), with an actionable error when webdriverio is absent. */
|
|
373
403
|
async session(bundleId) {
|
|
374
404
|
if (this.driver) return this.driver;
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
({remote: remoteFn} = await import("webdriverio"));
|
|
378
|
-
} catch {
|
|
379
|
-
throw new Error(".open() requires webdriverio (optional peer dependency): npm install -D appium webdriverio && npx appium driver install xcuitest");
|
|
380
|
-
}
|
|
405
|
+
const remoteFn = this.remote ?? await loadRemote();
|
|
381
406
|
const url = new URL(this.options.serverUrl);
|
|
382
407
|
this.driver = await remoteFn({
|
|
383
408
|
capabilities: {
|
|
@@ -385,7 +410,7 @@ var AppiumAdapter = class {
|
|
|
385
410
|
"appium:bundleId": bundleId,
|
|
386
411
|
"appium:noReset": true,
|
|
387
412
|
"appium:udid": this.options.udid,
|
|
388
|
-
"appium:wdaLaunchTimeout":
|
|
413
|
+
"appium:wdaLaunchTimeout": this.launchTimeoutMs,
|
|
389
414
|
platformName: "iOS"
|
|
390
415
|
},
|
|
391
416
|
hostname: url.hostname,
|
package/dist/artifacts.js
CHANGED
|
@@ -932,6 +932,95 @@ function removeContainers(ids) {
|
|
|
932
932
|
} catch {}
|
|
933
933
|
}
|
|
934
934
|
//#endregion
|
|
935
|
+
//#region src/core/specification/shared/resolve.ts
|
|
936
|
+
/** What makes a directory a project root — a manifest, or a test stack. */
|
|
937
|
+
const ROOT_MARKERS = ["package.json", "docker/compose.test.yaml"];
|
|
938
|
+
/**
|
|
939
|
+
* The NEAREST ancestor of `startDir` (itself included) carrying a root marker,
|
|
940
|
+
* `undefined` when none does.
|
|
941
|
+
*
|
|
942
|
+
* Both markers are probed at each ancestor, in ONE walk. Two walks — the
|
|
943
|
+
* compose file all the way up, then `package.json` all the way up — made the
|
|
944
|
+
* further directory win: in a workspace, a `docker/compose.test.yaml` at the
|
|
945
|
+
* repository root outranked the `package.json` of the very package being
|
|
946
|
+
* tested, and every path the runner resolved was measured from the wrong unit.
|
|
947
|
+
* The unit is the nearest thing that declares itself a project.
|
|
948
|
+
*
|
|
949
|
+
* The existence probe is injected so the lint layer can pass its cached one
|
|
950
|
+
* (A9's `a9w-redundant-root` derives the same root, and must derive it the
|
|
951
|
+
* same way).
|
|
952
|
+
*/
|
|
953
|
+
function findRoot(startDir, exists) {
|
|
954
|
+
let dir = startDir;
|
|
955
|
+
for (;;) {
|
|
956
|
+
if (ROOT_MARKERS.some((marker) => exists(resolve(dir, marker)))) return dir;
|
|
957
|
+
const parent = dirname(dir);
|
|
958
|
+
if (parent === dir) return;
|
|
959
|
+
dir = parent;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Auto-discover the project root from a starting directory (CONVENTIONS A9):
|
|
964
|
+
* walk up to the first directory that carries `package.json` or
|
|
965
|
+
* `docker/compose.test.yaml`; if none does, the starting directory itself.
|
|
966
|
+
*/
|
|
967
|
+
function discoverRoot(startDir) {
|
|
968
|
+
return findRoot(startDir, existsSync) ?? startDir;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Resolve the project root for a specification. An explicit `root` option is
|
|
972
|
+
* an override (resolved from the caller's directory when relative); when
|
|
973
|
+
* absent, the root is auto-discovered by walking up from the calling
|
|
974
|
+
* specification file.
|
|
975
|
+
*/
|
|
976
|
+
function resolveRoot(root, callerDir) {
|
|
977
|
+
if (!root) return discoverRoot(callerDir);
|
|
978
|
+
if (isAbsolute(root)) return root;
|
|
979
|
+
return resolve(callerDir, root);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Resolve a command — checks node_modules/.bin, then treats as absolute/PATH.
|
|
983
|
+
*/
|
|
984
|
+
function resolveCommand(command, root) {
|
|
985
|
+
if (isAbsolute(command)) return command;
|
|
986
|
+
const binPath = resolve(root, "node_modules/.bin", command);
|
|
987
|
+
if (existsSync(binPath)) return binPath;
|
|
988
|
+
const cwdBinPath = resolve(process.cwd(), "node_modules/.bin", command);
|
|
989
|
+
if (existsSync(cwdBinPath)) return cwdBinPath;
|
|
990
|
+
return command;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* The realpath form of a working directory — what a child process prints as
|
|
994
|
+
* `$PWD` (macOS resolves the tmpdir symlink), and therefore what `{{workdir}}`
|
|
995
|
+
* compares against. Falls back to the path itself when it cannot be resolved.
|
|
996
|
+
*
|
|
997
|
+
* @internal Shared by the result scope, the literate runner and `$WORKDIR`.
|
|
998
|
+
*/
|
|
999
|
+
function safeRealpath(path) {
|
|
1000
|
+
try {
|
|
1001
|
+
return realpathSync(path);
|
|
1002
|
+
} catch {
|
|
1003
|
+
return path;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Expand the `$WORKDIR` token in every string value of a child environment —
|
|
1008
|
+
* the one place the token is spelled out, for both doors (`.env()` on the
|
|
1009
|
+
* chain, `env:` in a `<case>.spec.yaml`).
|
|
1010
|
+
*
|
|
1011
|
+
* It expands to the RESOLVED directory, never the raw temp path: a value the
|
|
1012
|
+
* child receives comes back in its output, and it must come back spelled the
|
|
1013
|
+
* way `{{workdir}}` holds it (realpath form, CONVENTIONS D4). Expanding the raw
|
|
1014
|
+
* path made `HOME=$WORKDIR` on macOS echo `/var/folders/…` against a
|
|
1015
|
+
* `{{workdir}}` holding `/private/var/folders/…` — a golden no run could match.
|
|
1016
|
+
*/
|
|
1017
|
+
function expandWorkdir(env, workDir) {
|
|
1018
|
+
const workdir = safeRealpath(workDir);
|
|
1019
|
+
const expanded = {};
|
|
1020
|
+
for (const [key, value] of Object.entries(env)) expanded[key] = typeof value === "string" ? value.replace(/\$WORKDIR/g, workdir) : value;
|
|
1021
|
+
return expanded;
|
|
1022
|
+
}
|
|
1023
|
+
//#endregion
|
|
935
1024
|
//#region src/core/specification/shared/result/result.ts
|
|
936
1025
|
/**
|
|
937
1026
|
* Base result - common accessors available after any action type.
|
|
@@ -986,20 +1075,6 @@ var BaseResult = class {
|
|
|
986
1075
|
}
|
|
987
1076
|
};
|
|
988
1077
|
/**
|
|
989
|
-
* The realpath form of a working directory — what a child process prints as
|
|
990
|
-
* `$PWD` (macOS resolves the tmpdir symlink), and therefore what `{{workdir}}`
|
|
991
|
-
* compares against. Falls back to the path itself when it cannot be resolved.
|
|
992
|
-
*
|
|
993
|
-
* @internal Shared with the literate runner, which builds its own file-wide scope.
|
|
994
|
-
*/
|
|
995
|
-
function safeRealpath(path) {
|
|
996
|
-
try {
|
|
997
|
-
return realpathSync(path);
|
|
998
|
-
} catch {
|
|
999
|
-
return path;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
1078
|
* Enforce CONVENTIONS A7: with two or more databases in the services record
|
|
1004
1079
|
* the `database` option is mandatory on `.seed()` / `.table()`; with exactly
|
|
1005
1080
|
* one it is forbidden (redundant).
|
|
@@ -1596,11 +1671,19 @@ function updates(outcomes, scope) {
|
|
|
1596
1671
|
}));
|
|
1597
1672
|
}
|
|
1598
1673
|
/**
|
|
1599
|
-
* Start every server the document names, in declaration order,
|
|
1600
|
-
* run. They all stay live for the whole
|
|
1601
|
-
* document is one scenario, and its servers
|
|
1674
|
+
* Start every server the document names, in declaration order, after `fixture:`
|
|
1675
|
+
* has been copied and before the first run. They all stay live for the whole
|
|
1676
|
+
* file and are stopped together — one document is one scenario, and its servers
|
|
1677
|
+
* are part of its ground.
|
|
1678
|
+
*
|
|
1679
|
+
* Every child is seeded with `TEST_WORKDIR`, the resolved working directory the
|
|
1680
|
+
* `{{workdir}}` token holds and `$WORKDIR` expands to. The fixtures are already
|
|
1681
|
+
* on disk when the child starts, so a served stub reads the world the document
|
|
1682
|
+
* laid down exactly as a stub binary reads its own fixture tree: a scenario is
|
|
1683
|
+
* a layer of files, never a flag threaded through the server. A document's
|
|
1684
|
+
* mapping-form env wins over it, key by key.
|
|
1602
1685
|
*/
|
|
1603
|
-
async function startServers(document, config, displayPath) {
|
|
1686
|
+
async function startServers(document, config, displayPath, workdir) {
|
|
1604
1687
|
const registry = config.serveRegistry ?? {};
|
|
1605
1688
|
const started = [];
|
|
1606
1689
|
const env = {};
|
|
@@ -1617,7 +1700,10 @@ async function startServers(document, config, displayPath) {
|
|
|
1617
1700
|
const adapter = new ServeAdapter({
|
|
1618
1701
|
command: registration.command,
|
|
1619
1702
|
ready: registration.ready
|
|
1620
|
-
}, config.root ?? process.cwd(), "cli",
|
|
1703
|
+
}, config.root ?? process.cwd(), "cli", {
|
|
1704
|
+
TEST_WORKDIR: workdir,
|
|
1705
|
+
...entry.env
|
|
1706
|
+
});
|
|
1621
1707
|
started.push(adapter);
|
|
1622
1708
|
try {
|
|
1623
1709
|
await adapter.start();
|
|
@@ -1650,11 +1736,6 @@ function documentEnv(document, config, displayPath) {
|
|
|
1650
1736
|
}
|
|
1651
1737
|
return env;
|
|
1652
1738
|
}
|
|
1653
|
-
function expandWorkdir(env, workDir) {
|
|
1654
|
-
const resolved = {};
|
|
1655
|
-
for (const [key, value] of Object.entries(env)) resolved[key] = typeof value === "string" ? value.replace(/\$WORKDIR/g, workDir) : value;
|
|
1656
|
-
return resolved;
|
|
1657
|
-
}
|
|
1658
1739
|
/**
|
|
1659
1740
|
* Run one spec document end to end and resolve with the LAST run's result —
|
|
1660
1741
|
* the handle a `.test.ts` adds its own assertion to (a directory golden, a
|
|
@@ -1679,8 +1760,9 @@ async function runSpecDocument(options) {
|
|
|
1679
1760
|
const { dest, src } = copyPlan(fixture.path, fileDir, workDir);
|
|
1680
1761
|
cpSync(src, dest, { recursive: true });
|
|
1681
1762
|
}
|
|
1682
|
-
const
|
|
1683
|
-
const
|
|
1763
|
+
const workdir = safeRealpath(workDir);
|
|
1764
|
+
const servers = await startServers(document, config, displayPath, workdir);
|
|
1765
|
+
const scope = new CaptureScope(workdir);
|
|
1684
1766
|
const env = expandWorkdir({
|
|
1685
1767
|
...options.baseEnv,
|
|
1686
1768
|
...servers.env,
|
|
@@ -1734,4 +1816,4 @@ const COVERAGE_DIR = `${VITEST_ARTIFACTS_DIR}/coverage`;
|
|
|
1734
1816
|
/** Where `sqlite()` caches the schema template it copies per worker. */
|
|
1735
1817
|
const SQLITE_TEMPLATE_DIR = `${VITEST_ARTIFACTS_DIR}/sqlite`;
|
|
1736
1818
|
//#endregion
|
|
1737
|
-
export {
|
|
1819
|
+
export { formatResponseDiff as A, structuralSubset as B, text as C, diffDirectories as D, DirectoryAccessor as E, mergePreservingPlaceholders as F, mergeTextPreservingPlaceholders as I, placeholderPatternSource as L, formatStdoutDiff as M, formatTableDiff as N, walkDirectory as O, hasPlaceholders as P, renderExpected as R, TextAccessor as S, FilesystemAccessor as T, textEquals as V, findContainersByLabel as _, specDescription as a, TableAccessor as b, shouldUpdateSnapshots as c, BaseResult as d, validateDatabaseOption as f, resolveRoot as g, resolveCommand as h, runSpecDocument as i, formatStartupReport as j, formatDirectoryDiff as k, ContainerAccessor as l, expandWorkdir as m, SQLITE_TEMPLATE_DIR as n, ServeAdapter as o, discoverRoot as p, VITEST_ARTIFACTS_DIR as r, UPDATE_HINT as s, COVERAGE_DIR as t, CliResult as u, inspectContainer as v, getCallerDir as w, JsonAccessor as x, removeContainers as y, structuralEquals as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -798,6 +798,24 @@ interface DeviceOpenOptions {
|
|
|
798
798
|
/** The interaction scenario to run after launch; the capture reflects the final state. */
|
|
799
799
|
scenario?: MobileScenario;
|
|
800
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* How long a device session waits, in milliseconds. Every field is optional
|
|
803
|
+
* and falls back to the framework's own default, so a runner states only the
|
|
804
|
+
* wait it needs to move — the defaults suit a release build, and a project
|
|
805
|
+
* that drives a DEV build pays for its bundler's cold boot.
|
|
806
|
+
*/
|
|
807
|
+
interface DeviceTimeouts {
|
|
808
|
+
/**
|
|
809
|
+
* How long every verb polls for a visible match before refusing.
|
|
810
|
+
* Default: `30_000` — enough for a cold app boot on a release bundle.
|
|
811
|
+
*/
|
|
812
|
+
action?: number;
|
|
813
|
+
/**
|
|
814
|
+
* How long the FIRST session waits for WebDriverAgent to build and
|
|
815
|
+
* launch on the simulator. Default: `240_000`.
|
|
816
|
+
*/
|
|
817
|
+
launch?: number;
|
|
818
|
+
}
|
|
801
819
|
/**
|
|
802
820
|
* Abstract device interface for the mobile specification runner.
|
|
803
821
|
* One implementation lives in `integrations/appium/` — a single driver
|
|
@@ -1776,6 +1794,13 @@ interface MobileSpecificationOptions {
|
|
|
1776
1794
|
* file when absent.
|
|
1777
1795
|
*/
|
|
1778
1796
|
root?: string;
|
|
1797
|
+
/**
|
|
1798
|
+
* How long this runner's session waits, in milliseconds — every field
|
|
1799
|
+
* optional, each falling back to the framework's default. A project
|
|
1800
|
+
* driving a DEV build states the wait its bundler's cold boot needs
|
|
1801
|
+
* (`{ action: 45_000 }`) instead of sleeping inside its scenarios.
|
|
1802
|
+
*/
|
|
1803
|
+
timeouts?: DeviceTimeouts;
|
|
1779
1804
|
}
|
|
1780
1805
|
/**
|
|
1781
1806
|
* The record returned by {@link startMobile | specification.mobile}.
|
|
@@ -2512,4 +2537,4 @@ declare module 'vitest' {
|
|
|
2512
2537
|
}
|
|
2513
2538
|
}
|
|
2514
2539
|
//#endregion
|
|
2515
|
-
export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, BaseResult, type BrowserConsoleMessage, type BrowserLinkElement, type BrowserMetaElement, type BrowserOpenOptions, type BrowserPage, type BrowserPort, type CaptureScope, type CliEnv, type CliHandle, type CliInput, type CliOutput, type CliPort, CliResult, type CliSpecification, type CliSpecificationOptions, ContainerAccessor, type ContainerPort, type Contract, type ContractInput, type ContractRequest, type ContractResponder, type ContractResponse, type ContractResponseValue, type Contracts, type DatabaseKeys, type DatabasePort, type DeviceOpenOptions, type DevicePort, type DeviceScreen, DirectoryAccessor, type DockerSpecConfig, type ElementKind, type ElementMatch, type ElementOptions, type ElementRef, type ExecOptions, FetchResult, type FileAccessor, FilesystemAccessor, type HonoApp, type HttpContractFilter, type HttpResponseInit, HttpResult, type IsolationStrategy, type JobHandle, type JobsHandle, type JobsSpecification, type JobsSpecificationOptions, JsonAccessor, type LandmarkKind, type LiterateRunFlags, type LiterateServeRegistration, type MatchFixtureOptions, type MatchableRequest, Matcher, type MatcherKind, type MobileBackendOptions, type MobileElementKind, type MobileElementMatch, type MobileElementRef, type MobileHandle, type MobileScenario, type MobileSpecification, type MobileSpecificationOptions, type MobileVisitor, type MockDatePort, type MockPort, Orchestrator, PageResult, type PostgresOptions, type RedisOptions, ResponseAccessor, type ScreenNode, ScreenResult, type ServeOptions, type ServerPort, type ServerResponse, type ServiceHandle, type ServiceRecord, type SpecDocument, type SpecEnvToken, type SpecFileAssertion, type SpecFixture, type SpecKind, type SpecRun, type SpecServeEntry, type SpecStream, type SpecificationConfig, type SpecificationMode, type SqliteOptions, TableAccessor, TextAccessor, type TextFilter, type VisitScenario, type Visitor, type WebsiteBackendOptions, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, anthropic, banner, button, complementary, content, contentinfo, defineContract, defineContracts, field, findContainersByLabel, form, heading, http, inspectContainer, link, main, match, mockOf, mockOfDate, navigation, openai, postgres, redis, region, removeContainers, search, specification, sqlite, testId, text, within };
|
|
2540
|
+
export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, BaseResult, type BrowserConsoleMessage, type BrowserLinkElement, type BrowserMetaElement, type BrowserOpenOptions, type BrowserPage, type BrowserPort, type CaptureScope, type CliEnv, type CliHandle, type CliInput, type CliOutput, type CliPort, CliResult, type CliSpecification, type CliSpecificationOptions, ContainerAccessor, type ContainerPort, type Contract, type ContractInput, type ContractRequest, type ContractResponder, type ContractResponse, type ContractResponseValue, type Contracts, type DatabaseKeys, type DatabasePort, type DeviceOpenOptions, type DevicePort, type DeviceScreen, type DeviceTimeouts, DirectoryAccessor, type DockerSpecConfig, type ElementKind, type ElementMatch, type ElementOptions, type ElementRef, type ExecOptions, FetchResult, type FileAccessor, FilesystemAccessor, type HonoApp, type HttpContractFilter, type HttpResponseInit, HttpResult, type IsolationStrategy, type JobHandle, type JobsHandle, type JobsSpecification, type JobsSpecificationOptions, JsonAccessor, type LandmarkKind, type LiterateRunFlags, type LiterateServeRegistration, type MatchFixtureOptions, type MatchableRequest, Matcher, type MatcherKind, type MobileBackendOptions, type MobileElementKind, type MobileElementMatch, type MobileElementRef, type MobileHandle, type MobileScenario, type MobileSpecification, type MobileSpecificationOptions, type MobileVisitor, type MockDatePort, type MockPort, Orchestrator, PageResult, type PostgresOptions, type RedisOptions, ResponseAccessor, type ScreenNode, ScreenResult, type ServeOptions, type ServerPort, type ServerResponse, type ServiceHandle, type ServiceRecord, type SpecDocument, type SpecEnvToken, type SpecFileAssertion, type SpecFixture, type SpecKind, type SpecRun, type SpecServeEntry, type SpecStream, type SpecificationConfig, type SpecificationMode, type SqliteOptions, TableAccessor, TextAccessor, type TextFilter, type VisitScenario, type Visitor, type WebsiteBackendOptions, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, anthropic, banner, button, complementary, content, contentinfo, defineContract, defineContracts, field, findContainersByLabel, form, heading, http, inspectContainer, link, main, match, mockOf, mockOfDate, navigation, openai, postgres, redis, region, removeContainers, search, specification, sqlite, testId, text, within };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as copyPlan, c as GROUND_EXPECTED, d as GROUND_SEEDS, i as match, n as Matcher, t as CaptureScope, u as GROUND_REQUESTS } from "./match.js";
|
|
2
|
-
import { C as
|
|
2
|
+
import { A as formatResponseDiff, B as structuralSubset, C as text, D as diffDirectories, E as DirectoryAccessor, F as mergePreservingPlaceholders, I as mergeTextPreservingPlaceholders, M as formatStdoutDiff, N as formatTableDiff, O as walkDirectory, R as renderExpected, S as TextAccessor, T as FilesystemAccessor, V as textEquals, _ as findContainersByLabel, b as TableAccessor, c as shouldUpdateSnapshots, d as BaseResult, f as validateDatabaseOption, g as resolveRoot, h as resolveCommand, i as runSpecDocument, j as formatStartupReport, k as formatDirectoryDiff, l as ContainerAccessor, m as expandWorkdir, n as SQLITE_TEMPLATE_DIR, o as ServeAdapter, p as discoverRoot, s as UPDATE_HINT, u as CliResult, v as inspectContainer, w as getCallerDir, x as JsonAccessor, y as removeContainers, z as structuralEquals } from "./artifacts.js";
|
|
3
3
|
import { a as isContract, i as defineContracts, n as contractsOf, o as isContracts, r as defineContract, t as ContractQueue } from "./queue.js";
|
|
4
4
|
import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
5
|
-
import { dirname,
|
|
5
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { parse } from "yaml";
|
|
7
7
|
import { execFile, execSync, spawn, spawnSync } from "node:child_process";
|
|
8
8
|
import { Client } from "pg";
|
|
@@ -1092,8 +1092,10 @@ var SpecificationBuilder = class {
|
|
|
1092
1092
|
* Set environment variables for the command process. Merged on top of process.env.
|
|
1093
1093
|
* Use `null` to unset a variable. Multiple calls merge.
|
|
1094
1094
|
*
|
|
1095
|
-
* The token `$WORKDIR` (in any value) is replaced with the
|
|
1096
|
-
*
|
|
1095
|
+
* The token `$WORKDIR` (in any value) is replaced at run-time with the
|
|
1096
|
+
* spec's working directory in its resolved form — the same string
|
|
1097
|
+
* `{{workdir}}` asserts against — so a path handed to the child comes back
|
|
1098
|
+
* matchable. Useful for tests that need a fully isolated `HOME`.
|
|
1097
1099
|
*
|
|
1098
1100
|
* @example
|
|
1099
1101
|
* cli.env({ HOME: "$WORKDIR", TZ: "UTC" }).exec("status");
|
|
@@ -1346,14 +1348,8 @@ var SpecificationBuilder = class {
|
|
|
1346
1348
|
}
|
|
1347
1349
|
}
|
|
1348
1350
|
resolveEnv(workDir) {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
const resolved = {};
|
|
1352
|
-
for (const key of keys) {
|
|
1353
|
-
const value = this.commandEnv[key];
|
|
1354
|
-
resolved[key] = typeof value === "string" ? value.replace(/\$WORKDIR/g, workDir) : value;
|
|
1355
|
-
}
|
|
1356
|
-
return resolved;
|
|
1351
|
+
if (Object.keys(this.commandEnv).length === 0) return;
|
|
1352
|
+
return expandWorkdir(this.commandEnv, workDir);
|
|
1357
1353
|
}
|
|
1358
1354
|
prepareWorkDir() {
|
|
1359
1355
|
return mkdtempSync(resolve(tmpdir(), "spec-command-"));
|
|
@@ -1890,64 +1886,6 @@ var Orchestrator = class {
|
|
|
1890
1886
|
}
|
|
1891
1887
|
};
|
|
1892
1888
|
//#endregion
|
|
1893
|
-
//#region src/core/specification/shared/resolve.ts
|
|
1894
|
-
/** What makes a directory a project root — a manifest, or a test stack. */
|
|
1895
|
-
const ROOT_MARKERS = ["package.json", "docker/compose.test.yaml"];
|
|
1896
|
-
/**
|
|
1897
|
-
* The NEAREST ancestor of `startDir` (itself included) carrying a root marker,
|
|
1898
|
-
* `undefined` when none does.
|
|
1899
|
-
*
|
|
1900
|
-
* Both markers are probed at each ancestor, in ONE walk. Two walks — the
|
|
1901
|
-
* compose file all the way up, then `package.json` all the way up — made the
|
|
1902
|
-
* further directory win: in a workspace, a `docker/compose.test.yaml` at the
|
|
1903
|
-
* repository root outranked the `package.json` of the very package being
|
|
1904
|
-
* tested, and every path the runner resolved was measured from the wrong unit.
|
|
1905
|
-
* The unit is the nearest thing that declares itself a project.
|
|
1906
|
-
*
|
|
1907
|
-
* The existence probe is injected so the lint layer can pass its cached one
|
|
1908
|
-
* (A9's `a9w-redundant-root` derives the same root, and must derive it the
|
|
1909
|
-
* same way).
|
|
1910
|
-
*/
|
|
1911
|
-
function findRoot(startDir, exists) {
|
|
1912
|
-
let dir = startDir;
|
|
1913
|
-
for (;;) {
|
|
1914
|
-
if (ROOT_MARKERS.some((marker) => exists(resolve(dir, marker)))) return dir;
|
|
1915
|
-
const parent = dirname(dir);
|
|
1916
|
-
if (parent === dir) return;
|
|
1917
|
-
dir = parent;
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
/**
|
|
1921
|
-
* Auto-discover the project root from a starting directory (CONVENTIONS A9):
|
|
1922
|
-
* walk up to the first directory that carries `package.json` or
|
|
1923
|
-
* `docker/compose.test.yaml`; if none does, the starting directory itself.
|
|
1924
|
-
*/
|
|
1925
|
-
function discoverRoot(startDir) {
|
|
1926
|
-
return findRoot(startDir, existsSync) ?? startDir;
|
|
1927
|
-
}
|
|
1928
|
-
/**
|
|
1929
|
-
* Resolve the project root for a specification. An explicit `root` option is
|
|
1930
|
-
* an override (resolved from the caller's directory when relative); when
|
|
1931
|
-
* absent, the root is auto-discovered by walking up from the calling
|
|
1932
|
-
* specification file.
|
|
1933
|
-
*/
|
|
1934
|
-
function resolveRoot(root, callerDir) {
|
|
1935
|
-
if (!root) return discoverRoot(callerDir);
|
|
1936
|
-
if (isAbsolute(root)) return root;
|
|
1937
|
-
return resolve(callerDir, root);
|
|
1938
|
-
}
|
|
1939
|
-
/**
|
|
1940
|
-
* Resolve a command — checks node_modules/.bin, then treats as absolute/PATH.
|
|
1941
|
-
*/
|
|
1942
|
-
function resolveCommand(command, root) {
|
|
1943
|
-
if (isAbsolute(command)) return command;
|
|
1944
|
-
const binPath = resolve(root, "node_modules/.bin", command);
|
|
1945
|
-
if (existsSync(binPath)) return binPath;
|
|
1946
|
-
const cwdBinPath = resolve(process.cwd(), "node_modules/.bin", command);
|
|
1947
|
-
if (existsSync(cwdBinPath)) return cwdBinPath;
|
|
1948
|
-
return command;
|
|
1949
|
-
}
|
|
1950
|
-
//#endregion
|
|
1951
1889
|
//#region src/core/specification/shared/services.ts
|
|
1952
1890
|
function getWorkerId() {
|
|
1953
1891
|
return process.env.VITEST_POOL_ID ?? "0";
|
|
@@ -2615,6 +2553,7 @@ async function startMobile(options) {
|
|
|
2615
2553
|
const { AppiumAdapter } = await import("./appium.adapter.js");
|
|
2616
2554
|
device = new AppiumAdapter({
|
|
2617
2555
|
serverUrl: appium.url,
|
|
2556
|
+
timeouts: options.timeouts,
|
|
2618
2557
|
udid
|
|
2619
2558
|
});
|
|
2620
2559
|
}
|
package/dist/queue.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as CaptureScope } from "./match.js";
|
|
2
|
-
import {
|
|
2
|
+
import { L as placeholderPatternSource, P as hasPlaceholders, V as textEquals } from "./artifacts.js";
|
|
3
3
|
//#region src/core/contracts/contract.ts
|
|
4
4
|
/**
|
|
5
5
|
* Declare one contract. Identity function — its value is the enforced shape
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/test",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.3.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",
|