@jterrazz/test 17.0.0 → 18.0.1

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.
Files changed (53) hide show
  1. package/dist/browser/index.js +1 -2
  2. package/dist/browser/services.js +1 -2
  3. package/dist/browser/stubs.js +1 -2
  4. package/dist/{shared-index.browser.js → browser-shared-index.browser.js} +82 -40
  5. package/dist/checker.js +15 -824
  6. package/dist/in-place.global-setup.d.ts +1 -0
  7. package/dist/in-place.global-setup.js +1 -1
  8. package/dist/index.d.ts +2 -14
  9. package/dist/index.js +83 -204
  10. package/dist/mobile-session.global-setup.d.ts +1 -0
  11. package/dist/oxlint.cjs +2 -2
  12. package/dist/oxlint.js +67 -2321
  13. package/dist/route-coverage.global-setup.d.ts +1 -0
  14. package/dist/route-coverage.global-setup.js +1 -2
  15. package/dist/services.global-setup.d.ts +1 -1
  16. package/dist/services.global-setup.js +1 -2
  17. package/dist/services.js +1 -1
  18. package/dist/shared-appium.adapter.js +14 -14
  19. package/dist/shared-artifacts.js +7 -5
  20. package/dist/shared-derived-projects.js +23 -8
  21. package/dist/shared-in-place.js +1 -1
  22. package/dist/shared-ledger.js +1 -1
  23. package/dist/shared-listened-port.js +19 -0
  24. package/dist/shared-literate-plugin.js +2 -1
  25. package/dist/shared-pause.js +12 -1
  26. package/dist/shared-playwright.adapter.js +17 -19
  27. package/dist/shared-process-alive.js +13 -0
  28. package/dist/shared-record-key.js +13 -0
  29. package/dist/shared-serve.js +180 -69
  30. package/dist/shared-services.global-setup.js +4 -9
  31. package/dist/shared-session-ledger.js +10 -6
  32. package/dist/shared-spec-document.js +2 -64
  33. package/dist/shared-spec-grammar.js +66 -0
  34. package/dist/shared-specs-modules.js +1033 -0
  35. package/dist/{shared-stream-body.js → shared-stub-reply.js} +40 -3
  36. package/dist/shared-stub-server.js +30 -78
  37. package/dist/shared-temp-root.global-setup.js +3 -10
  38. package/dist/shared-testcontainers.adapter.js +111 -8
  39. package/dist/temp-root.global-setup.js +1 -2
  40. package/dist/vitest.d.ts +17 -22
  41. package/dist/vitest.js +37 -53
  42. package/docs/concepts/architecture.md +20 -20
  43. package/docs/concepts/conventions.md +2 -2
  44. package/docs/guides/cli-specs.md +2 -0
  45. package/docs/guides/develop.md +20 -17
  46. package/docs/guides/mobile-specs.md +3 -3
  47. package/docs/guides/release.md +4 -3
  48. package/docs/guides/website-specs.md +1 -1
  49. package/docs/reference/linting.md +2 -2
  50. package/docs/reference/services.md +13 -9
  51. package/docs/reference/stubs.md +4 -2
  52. package/docs/start/set-up-a-project.md +31 -17
  53. package/package.json +10 -10
@@ -1,2 +1 @@
1
- import { A as region, B as option, C as component, D as form, E as contentinfo, F as dialog, G as directory, H as status, I as field, J as defineStubs, K as file, L as heading, M as within, N as button, O as main, P as content, R as link, S as http, T as complementary, U as table, V as row, W as testId, X as match, Y as Matcher, _ as clock, a as ruleTester, b as openai, c as waitUntil, d as disabled, f as enabled, g as mockOf, h as valued, i as redis, j as search, k as navigation, l as required, m as selected, n as postgres, o as specification, p as focused, q as defineStub, r as process, s as sqlite, t as ProcessHandle, u as text, v as packed, w as banner, x as anthropic, y as intercept, z as listitem } from "../shared-index.browser.js";
2
- export { Matcher, ProcessHandle, anthropic, banner, button, clock, complementary, component, content, contentinfo, defineStub, defineStubs, dialog, directory, disabled, enabled, field, file, focused, form, heading, http, intercept, link, listitem, main, match, mockOf, navigation, openai, option, packed, postgres, process, redis, region, required, row, ruleTester, search, selected, specification, sqlite, status, table, testId, text, valued, waitUntil, within };
1
+ export { Y as Matcher, t as ProcessHandle, x as anthropic, w as banner, N as button, _ as clock, T as complementary, C as component, P as content, E as contentinfo, q as defineStub, J as defineStubs, F as dialog, G as directory, d as disabled, f as enabled, I as field, K as file, p as focused, D as form, L as heading, S as http, y as intercept, R as link, z as listitem, O as main, X as match, g as mockOf, k as navigation, b as openai, B as option, v as packed, n as postgres, r as process, i as redis, A as region, l as required, V as row, a as ruleTester, j as search, m as selected, o as specification, s as sqlite, H as status, U as table, W as testId, u as text, h as valued, c as waitUntil, M as within } from "../browser-shared-index.browser.js";
@@ -1,2 +1 @@
1
- import { i as redis, n as postgres, r as process, s as sqlite, t as ProcessHandle } from "../shared-index.browser.js";
2
- export { ProcessHandle, postgres, process, redis, sqlite };
1
+ export { t as ProcessHandle, n as postgres, r as process, i as redis, s as sqlite } from "../browser-shared-index.browser.js";
@@ -1,2 +1 @@
1
- import { J as defineStubs, S as http, b as openai, q as defineStub, x as anthropic, y as intercept } from "../shared-index.browser.js";
2
- export { anthropic, defineStub, defineStubs, http, intercept, openai };
1
+ export { x as anthropic, q as defineStub, J as defineStubs, S as http, y as intercept, b as openai } from "../browser-shared-index.browser.js";
@@ -191,14 +191,6 @@ const clock$1 = defineCapability({
191
191
  }
192
192
  });
193
193
  //#endregion
194
- //#region src/chain/pause.ts
195
- async function pause(ms) {
196
- if (ms <= 0) return;
197
- await new Promise((resolve) => {
198
- setTimeout(resolve, ms);
199
- });
200
- }
201
- //#endregion
202
194
  //#region src/stubs/exchange.ts
203
195
  /** Each adapter serialises an object as JSON, so a stream declared as a plain value would arrive as `{}`: the tag tells them apart. */
204
196
  const STREAM_BODY = "@jterrazz/test:stream";
@@ -718,7 +710,36 @@ var StubQueue = class {
718
710
  }
719
711
  };
720
712
  //#endregion
713
+ //#region src/chain/pause.ts
714
+ async function pause(ms) {
715
+ if (ms <= 0) return;
716
+ await new Promise((resolve) => {
717
+ setTimeout(resolve, ms);
718
+ });
719
+ }
720
+ /** Asks `probe` until it answers something other than `undefined`, which it hands back, or answers `undefined` once `timeout` has passed. Measured on `performance.now()`, so `clock.at()`, which fakes `Date`, cannot freeze it. */
721
+ async function untilDeadline(probe, { interval = 0, timeout, wait }) {
722
+ const deadline = performance.now() + timeout;
723
+ for (;;) {
724
+ const answer = await probe();
725
+ if (answer !== void 0) return answer;
726
+ const remaining = deadline - performance.now();
727
+ if (remaining <= 0) return;
728
+ await (wait === void 0 ? pause(interval) : wait(remaining));
729
+ }
730
+ }
731
+ //#endregion
721
732
  //#region src/stubs/delivery/stream-body.ts
733
+ async function nextTask() {
734
+ const { port1, port2 } = new MessageChannel();
735
+ await new Promise((resolve) => {
736
+ port1.onmessage = () => {
737
+ port1.close();
738
+ resolve();
739
+ };
740
+ port2.postMessage(null);
741
+ });
742
+ }
722
743
  /** One serialisation for all three deliveries (msw server, msw worker, `node:http` stub). */
723
744
  function toReadableStream(body) {
724
745
  const encoder = new TextEncoder();
@@ -729,12 +750,39 @@ function toReadableStream(body) {
729
750
  controller.close();
730
751
  return;
731
752
  }
732
- if (body.delayBetweenChunks > 0 && index > 0) await pause(body.delayBetweenChunks);
753
+ if (index > 0) {
754
+ await pause(body.delayBetweenChunks);
755
+ await nextTask();
756
+ }
733
757
  index += 1;
734
758
  controller.enqueue(encoder.encode(chunk));
735
759
  } });
736
760
  }
737
761
  //#endregion
762
+ //#region src/stubs/delivery/stub-reply.ts
763
+ /** A request's body as a stub's filter reads it: the parsed JSON, the raw text when it is none, `null` when there is none. */
764
+ function requestBody(raw) {
765
+ if (raw.length === 0) return null;
766
+ try {
767
+ return JSON.parse(raw);
768
+ } catch {
769
+ return raw;
770
+ }
771
+ }
772
+ /** The reply a matched stub states for this request, once its delay has passed. */
773
+ async function replyOf(response, observed) {
774
+ const reply = typeof response === "function" ? response(observed) : response;
775
+ await pause(reply.delay ?? 0);
776
+ return reply;
777
+ }
778
+ /** The `error` of the 501 every delivery answers a request it refuses, whichever process serves it. */
779
+ function refusedReply(summary) {
780
+ return `@jterrazz/test strict stubs: ${summary}`;
781
+ }
782
+ function unmatchedSummary(method, url) {
783
+ return `unmatched request ${method} ${url}`;
784
+ }
785
+ //#endregion
738
786
  //#region src/stubs/delivery/msw-handlers.ts
739
787
  const NEVER_CACHED = { "cache-control": "no-store" };
740
788
  function toMswResponse(msw, response) {
@@ -786,13 +834,14 @@ function handlerOf(msw, method) {
786
834
  function buildStubHandlers(msw, stubs, bypass) {
787
835
  const queue = new StubQueue(stubs);
788
836
  let violation = null;
789
- const recordViolation = (method, url) => {
790
- violation ??= queue.unmatchedError(method, url);
791
- return msw.HttpResponse.json({ error: `@jterrazz/test strict stubs: unmatched request ${method} ${url}` }, {
837
+ const refuse = (error, summary) => {
838
+ violation ??= error;
839
+ return msw.HttpResponse.json({ error: refusedReply(summary) }, {
792
840
  headers: NEVER_CACHED,
793
841
  status: 501
794
842
  });
795
843
  };
844
+ const recordViolation = (method, url) => refuse(queue.unmatchedError(method, url), unmatchedSummary(method, url));
796
845
  const handlers = [];
797
846
  for (const route of queue.routes) for (const method of route.methods) {
798
847
  const handlerFn = handlerOf(msw, method);
@@ -801,27 +850,19 @@ function buildStubHandlers(msw, stubs, bypass) {
801
850
  const observed = await observe(request);
802
851
  const stub = queue.take(observed);
803
852
  if (!stub) return recordViolation(request.method, request.url);
804
- const response = typeof stub.response === "function" ? stub.response(observed) : stub.response;
805
- await pause(response.delay ?? 0);
806
- return toMswResponse(msw, response);
853
+ return toMswResponse(msw, await replyOf(stub.response, observed));
807
854
  }));
808
855
  }
809
856
  handlers.push(msw.http.all("*", ({ request }) => bypass?.(request.url) === true ? msw.passthrough() : recordViolation(request.method, request.url)));
810
857
  return {
811
858
  handlers,
859
+ refuse,
812
860
  violation: () => queue.verdict(violation)
813
861
  };
814
862
  }
815
863
  async function observe(request) {
816
- let body = null;
817
- const rawText = await request.clone().text();
818
- if (rawText) try {
819
- body = JSON.parse(rawText);
820
- } catch {
821
- body = rawText;
822
- }
823
864
  return {
824
- body,
865
+ body: requestBody(await request.clone().text()),
825
866
  headers: Object.fromEntries(request.headers),
826
867
  method: request.method.toUpperCase(),
827
868
  url: request.url
@@ -842,15 +883,19 @@ function isRunnerRequest(url) {
842
883
  const { pathname } = new URL(url);
843
884
  return RUNNER_PREFIXES.some((prefix) => pathname.startsWith(prefix));
844
885
  }
886
+ /** msw's hook for a frame no handler took: the subject's HTTP request prints msw's error; the runner's own request and a WebSocket frame, which carries none, pass. */
887
+ function onUnhandledFrame({ defaults, frame }) {
888
+ const { data } = frame;
889
+ if (typeof data !== "object" || data === null || !("request" in data)) return;
890
+ if (data.request instanceof Request && !isRunnerRequest(data.request.url)) defaults.error();
891
+ }
845
892
  async function ensureStubWorker() {
846
893
  if (workerInstance !== null) return;
847
894
  const { setupWorker } = await import("msw/browser");
848
895
  const worker = setupWorker();
849
896
  workerInstance = worker;
850
897
  await worker.start({
851
- onUnhandledRequest: (request, print) => {
852
- if (!isRunnerRequest(request.url)) print.error();
853
- },
898
+ onUnhandledFrame,
854
899
  quiet: true,
855
900
  serviceWorker: { url: "/mockServiceWorker.js" }
856
901
  });
@@ -1260,7 +1305,7 @@ function recordConsole() {
1260
1305
  const ANSI_RE = /\u001B\[[0-9;]*m/gu;
1261
1306
  /** rule `no-transform-duplicating-tokens`: a golden compares the text, never its colour. */
1262
1307
  function stripAnsiCodes(value) {
1263
- return value.replace(ANSI_RE, "");
1308
+ return value.replaceAll(ANSI_RE, "");
1264
1309
  }
1265
1310
  //#endregion
1266
1311
  //#region src/expectations/text/grep.ts
@@ -3154,18 +3199,15 @@ const DEFAULT_INTERVAL_MS = 50;
3154
3199
  /** Wait until a condition holds; measured on `performance.now()`, so `clock.at()` cannot freeze it. Under `clock.run()` it never wakes: use `clock.advance()`. */
3155
3200
  async function waitUntil(predicate, options = {}) {
3156
3201
  const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
3157
- const interval = options.interval ?? DEFAULT_INTERVAL_MS;
3158
- const deadline = performance.now() + timeout;
3159
- for (;;) {
3160
- if (await predicate()) return;
3161
- if (performance.now() >= deadline) throw new Error(refusal({
3162
- fix: "raise the budget if the wait is legitimate, or wait on what actually changes",
3163
- found: `waitUntil(): ${options.why ?? "the condition"} did not hold within ${timeout}ms`,
3164
- read: "docs/reference/assertions.md#two-helpers-that-are-not-matchers",
3165
- why: "`waitUntil()` waits on a condition, within a budget."
3166
- }));
3167
- await pause(interval);
3168
- }
3202
+ if (await untilDeadline(async () => await predicate() || void 0, {
3203
+ interval: options.interval ?? DEFAULT_INTERVAL_MS,
3204
+ timeout
3205
+ }) === void 0) throw new Error(refusal({
3206
+ fix: "raise the budget if the wait is legitimate, or wait on what actually changes",
3207
+ found: `waitUntil(): ${options.why ?? "the condition"} did not hold within ${timeout}ms`,
3208
+ read: "docs/reference/assertions.md#two-helpers-that-are-not-matchers",
3209
+ why: "`waitUntil()` waits on a condition, within a budget."
3210
+ }));
3169
3211
  }
3170
3212
  //#endregion
3171
3213
  //#region src/index.browser.ts
@@ -3188,4 +3230,4 @@ const sqlite = () => refuse("sqlite()");
3188
3230
  //#endregion
3189
3231
  export { region as A, option as B, component as C, form as D, contentinfo as E, dialog as F, directory as G, status as H, field as I, defineStubs as J, file as K, heading as L, within as M, button as N, main as O, content as P, link as R, http as S, complementary as T, table as U, row as V, testId as W, match as X, Matcher as Y, clock as _, ruleTester as a, openai as b, waitUntil as c, disabled as d, enabled as f, mockOf as g, valued as h, redis as i, search as j, navigation as k, required as l, selected as m, postgres as n, specification as o, focused as p, defineStub as q, process$1 as r, sqlite as s, ProcessHandle as t, text as u, packed as v, banner as w, anthropic as x, intercept as y, listitem as z };
3190
3232
 
3191
- //# sourceMappingURL=shared-index.browser.js.map
3233
+ //# sourceMappingURL=browser-shared-index.browser.js.map