@lingxia/test 0.18.0 → 0.20.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.
Files changed (126) hide show
  1. package/dist/clock.d.ts +30 -0
  2. package/dist/clock.d.ts.map +1 -0
  3. package/dist/clock.js +99 -0
  4. package/dist/clock.js.map +1 -0
  5. package/dist/coverage.d.ts +25 -0
  6. package/dist/coverage.d.ts.map +1 -0
  7. package/dist/coverage.js +92 -0
  8. package/dist/coverage.js.map +1 -0
  9. package/dist/deadline.d.ts +82 -0
  10. package/dist/deadline.d.ts.map +1 -0
  11. package/dist/deadline.js +184 -0
  12. package/dist/deadline.js.map +1 -0
  13. package/dist/dialogs.d.ts +28 -0
  14. package/dist/dialogs.d.ts.map +1 -0
  15. package/dist/dialogs.js +74 -0
  16. package/dist/dialogs.js.map +1 -0
  17. package/dist/equal.d.ts +28 -0
  18. package/dist/equal.d.ts.map +1 -0
  19. package/dist/equal.js +149 -0
  20. package/dist/equal.js.map +1 -0
  21. package/dist/errors.d.ts +8 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/errors.js +37 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/expect.d.ts +35 -7
  26. package/dist/expect.d.ts.map +1 -1
  27. package/dist/expect.js +216 -68
  28. package/dist/expect.js.map +1 -1
  29. package/dist/fixture.d.ts +201 -23
  30. package/dist/fixture.d.ts.map +1 -1
  31. package/dist/fixture.js +1127 -157
  32. package/dist/fixture.js.map +1 -1
  33. package/dist/format.d.ts +2 -0
  34. package/dist/format.d.ts.map +1 -1
  35. package/dist/format.js +78 -0
  36. package/dist/format.js.map +1 -1
  37. package/dist/host-types.d.ts +65 -0
  38. package/dist/host-types.d.ts.map +1 -0
  39. package/dist/host-types.js +2 -0
  40. package/dist/host-types.js.map +1 -0
  41. package/dist/host.d.ts +20 -0
  42. package/dist/host.d.ts.map +1 -1
  43. package/dist/host.js +33 -1
  44. package/dist/host.js.map +1 -1
  45. package/dist/ids.d.ts +14 -0
  46. package/dist/ids.d.ts.map +1 -1
  47. package/dist/ids.js +37 -2
  48. package/dist/ids.js.map +1 -1
  49. package/dist/index.d.ts +5 -7
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -5
  52. package/dist/index.js.map +1 -1
  53. package/dist/json-result.d.ts +3 -0
  54. package/dist/json-result.d.ts.map +1 -0
  55. package/dist/json-result.js +62 -0
  56. package/dist/json-result.js.map +1 -0
  57. package/dist/junit.d.ts +1 -1
  58. package/dist/junit.d.ts.map +1 -1
  59. package/dist/junit.js +32 -4
  60. package/dist/junit.js.map +1 -1
  61. package/dist/locator.d.ts +87 -9
  62. package/dist/locator.d.ts.map +1 -1
  63. package/dist/locator.js +413 -86
  64. package/dist/locator.js.map +1 -1
  65. package/dist/mock.d.ts +43 -0
  66. package/dist/mock.d.ts.map +1 -0
  67. package/dist/mock.js +162 -0
  68. package/dist/mock.js.map +1 -0
  69. package/dist/network.d.ts +68 -0
  70. package/dist/network.d.ts.map +1 -0
  71. package/dist/network.js +287 -0
  72. package/dist/network.js.map +1 -0
  73. package/dist/openapi.d.ts +127 -0
  74. package/dist/openapi.d.ts.map +1 -0
  75. package/dist/openapi.js +414 -0
  76. package/dist/openapi.js.map +1 -0
  77. package/dist/pending.d.ts +84 -0
  78. package/dist/pending.d.ts.map +1 -0
  79. package/dist/pending.js +311 -0
  80. package/dist/pending.js.map +1 -0
  81. package/dist/redact.d.ts +41 -0
  82. package/dist/redact.d.ts.map +1 -0
  83. package/dist/redact.js +168 -0
  84. package/dist/redact.js.map +1 -0
  85. package/dist/remote.d.ts +32 -0
  86. package/dist/remote.d.ts.map +1 -0
  87. package/dist/remote.js +117 -0
  88. package/dist/remote.js.map +1 -0
  89. package/dist/report-extras.d.ts +16 -0
  90. package/dist/report-extras.d.ts.map +1 -0
  91. package/dist/report-extras.js +125 -0
  92. package/dist/report-extras.js.map +1 -0
  93. package/dist/report-types.d.ts +428 -0
  94. package/dist/report-types.d.ts.map +1 -0
  95. package/dist/report-types.js +6 -0
  96. package/dist/report-types.js.map +1 -0
  97. package/dist/report.d.ts +15 -1
  98. package/dist/report.d.ts.map +1 -1
  99. package/dist/report.js +195 -19
  100. package/dist/report.js.map +1 -1
  101. package/dist/runner.d.ts +13 -0
  102. package/dist/runner.d.ts.map +1 -0
  103. package/dist/runner.js +11 -0
  104. package/dist/runner.js.map +1 -0
  105. package/dist/runtime.d.ts +54 -4
  106. package/dist/runtime.d.ts.map +1 -1
  107. package/dist/runtime.js +1375 -97
  108. package/dist/runtime.js.map +1 -1
  109. package/dist/schema.d.ts +29 -0
  110. package/dist/schema.d.ts.map +1 -0
  111. package/dist/schema.js +463 -0
  112. package/dist/schema.js.map +1 -0
  113. package/dist/spec-api.d.ts +7 -2
  114. package/dist/spec-api.d.ts.map +1 -1
  115. package/dist/tags.d.ts +26 -0
  116. package/dist/tags.d.ts.map +1 -0
  117. package/dist/tags.js +78 -0
  118. package/dist/tags.js.map +1 -0
  119. package/dist/types.d.ts +852 -165
  120. package/dist/types.d.ts.map +1 -1
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. package/package.json +18 -5
  124. package/schemas/lxdev.schema.json +50 -0
  125. package/schemas/mock-config.schema.json +21 -0
  126. package/schemas/scenario.schema.json +180 -0
package/dist/types.d.ts CHANGED
@@ -1,257 +1,944 @@
1
- import type { Automation, LxAppDriver, PageDriver, PageQueryResult, PageTarget } from "@lingxia/types/automation";
2
- export type SpecStatus = "passed" | "failed" | "skipped" | "timeout" | "xfail" | "xpass";
3
- export type StepStatus = "passed" | "failed" | "timeout";
1
+ /// <reference types="@lingxia/types/testing" preserve="true" />
2
+ /// <reference types="@lingxia/types/logic-globals" preserve="true" />
3
+ import type { PageContract } from "@lingxia/types/page";
4
+ export type { PageContract } from "@lingxia/types/page";
5
+ import type { ActionSheetAnswer, ActionSheetRecord, Automation, AutomationErrorCode, BrowserDriver, ClockAdvance, ClockInstallOptions, ClockRunAllOptions, ClockState, ClockTime, DesktopDriver, HostRunAutomation, LxAppDriver, ModalAnswer, ModalRecord, NetworkRouteHandler, NetworkRoutePattern, PageInfo, PageKey, PagePointer, PageScrollOptions, PageTarget, ProfileRestoreResult, ScenarioCallFilter, ScenarioInput, ScenarioRuleInfo, Screenshot, TerminalDriver, ToastRecord } from "@lingxia/types/automation";
6
+ /**
7
+ * Every `code` a spec can meet on a rejection or a failed spec: the
8
+ * automation driver codes, a broken OpenAPI contract, a fixture call that
9
+ * ran out of time, and a runtime skip. A driver timeout reaches a spec as
10
+ * `E_TIMEOUT`, with the driver's own code in `error.cause.code`.
11
+ */
12
+ export type TestErrorCode = Exclude<AutomationErrorCode, "E_AUTOMATION_TIMEOUT" | "E_EVAL_TIMEOUT" | "E_DESKTOP_TIMEOUT"> | "E_OPENAPI_CONTRACT" | "E_TIMEOUT" | "E_SKIPPED";
13
+ /**
14
+ * The app's own error codes, empty until the app declares them by merging:
15
+ *
16
+ * ```ts
17
+ * declare module '@lingxia/test' {
18
+ * interface AppErrorCodes { E_QUOTA: true }
19
+ * }
20
+ * ```
21
+ *
22
+ * A declared code is then accepted wherever an expected `code` is.
23
+ */
24
+ export interface AppErrorCodes {
25
+ }
26
+ /** A `code` a spec may expect: LingXia's `TestErrorCode`s and the app's declared ones. */
27
+ export type ExpectedErrorCode = TestErrorCode | Extract<keyof AppErrorCodes, string>;
4
28
  export interface SpecOptions {
5
29
  /** Stable id. ASCII titles slug by default; non-ASCII titles need this or become `file-n`. */
6
30
  id?: string;
7
31
  /** Declared coverage tags. Journeys omit this. */
8
32
  covers?: readonly string[];
33
+ /**
34
+ * Selection tags (`routed`, `live`, `smoke`, …), merged after the
35
+ * file's `spec.configure({ tags })`. `lxdev test --tag` selects by them and
36
+ * the report summarizes each. Letters, digits and `_ . : / -`.
37
+ */
38
+ tags?: readonly string[];
9
39
  /** Spec budget in ms (default 30_000). */
10
40
  timeout?: number;
11
- /** Relaunch the home page before the body. */
12
- fresh?: boolean;
41
+ /**
42
+ * Relaunch the app on this page before the body (and its `beforeEach`
43
+ * hooks): a fresh instance, every other page unloaded.
44
+ */
45
+ start?: SpecStart;
46
+ /**
47
+ * Snapshot the app's isolated data before this spec and roll it back after,
48
+ * so the next spec never sees this one's writes. Relaunches the app on
49
+ * `start`, or on its home page. Needs an
50
+ * isolated run (`lxdev test --profile`); if the rollback fails, the rest of
51
+ * the run is not run. `{ keep: ['auth.*'] }` rolls back everything except
52
+ * the `lx.getStorage()` keys those globs match, which keep their state at
53
+ * the end of the spec (see `ProfileRestoreOptions`).
54
+ */
55
+ restoreProfile?: boolean | RestoreProfileOptions;
13
56
  /** Independent cleanup budget; a pending cleanup stops subsequent specs. */
14
57
  timeoutCleanup?: number;
15
58
  /** Pin `t.app` to this lxapp id instead of the current one. */
16
59
  app?: string;
17
60
  /** Skip auto-attached failure forensics (only when capture itself would wedge). */
18
61
  forensics?: boolean;
19
- /** Why a skip/fixme spec is registered. Shown in the HTML/JSON report. */
62
+ /** Why a skip/fixme spec is registered. Shown in the HTML/JSON report; `t.skip(reason)` overrides it. */
20
63
  reason?: string;
64
+ /**
65
+ * What the spec needs from the run. Unmet, it is reported `skipped` with a
66
+ * reason naming what to pass, and its body never runs. Merged with the
67
+ * file's `spec.configure({ requires })`.
68
+ */
69
+ requires?: SpecRequirements;
70
+ }
71
+ /** `start`: the page a spec begins on. */
72
+ export interface SpecStart {
73
+ /** Configured page name (from lxapp.json). */
74
+ page: string;
75
+ /** Query forwarded to the page. */
76
+ query?: Record<string, unknown>;
77
+ }
78
+ /** `requires`: run inputs a spec cannot mean anything without. */
79
+ export interface SpecRequirements {
80
+ /** `--arg` / `--secret-arg` keys that must be given. */
81
+ args?: readonly string[];
82
+ /** The run must check an OpenAPI contract (`lxdev test --openapi`). */
83
+ openapi?: boolean;
84
+ }
85
+ /** `restoreProfile: { keep }`. */
86
+ export interface RestoreProfileOptions {
87
+ /** `lx.getStorage()` key globs that survive the rollback (`*`, `?`). */
88
+ keep: string[];
89
+ }
90
+ /**
91
+ * `spec.configure()` options: defaults for every spec in the calling file.
92
+ * Every `SpecOptions` key but `id`; a spec's own option overrides the file's,
93
+ * except `tags`, `covers` and `requires`, which add to it.
94
+ */
95
+ export type FileOptions = Omit<SpecOptions, "id">;
96
+ /** `spec.fail` options. */
97
+ export interface FailOptions extends SpecOptions {
98
+ /**
99
+ * The failure this spec is known to produce. When set, only a body failure
100
+ * matching it grades `xfail`; any other failure grades `failed`. Omit it to
101
+ * accept any body failure.
102
+ */
103
+ expected?: RejectExpected;
21
104
  }
22
105
  export type SpecBody = (t: Fixture) => void | Promise<void>;
106
+ /** Retrying assertion and wait options. */
23
107
  export interface ExpectOptions {
108
+ /** Default 5000 ms, clamped to the spec's remaining budget. */
24
109
  timeout?: number;
110
+ /** Poll interval in ms (default 50). */
25
111
  interval?: number;
112
+ /** Business intent shown beside this assertion in the report. */
113
+ message?: string;
114
+ }
115
+ /** `type` / `press` options. */
116
+ export interface InputOptions {
117
+ /** Default 5000 ms, clamped to the spec's remaining budget. */
118
+ timeout?: number;
119
+ }
120
+ /** `click` / `fill` options. */
121
+ export interface ActionOptions extends InputOptions {
122
+ /**
123
+ * Skip the in-viewport, stability and hit-test waits and dispatch to the
124
+ * element itself; it must still be attached (one match) and enabled. Use it
125
+ * for an element no scroll can bring under the pointer, such as the lower
126
+ * part of an overflowing sheet, or for a desktop app window that another
127
+ * window covers: a forced action is dispatched as DOM events on the
128
+ * element, never as input at its position on screen. The action then
129
+ * proves less about what a user can reach, so prefer the default wherever
130
+ * it works.
131
+ */
132
+ force?: boolean;
133
+ }
134
+ /** `locator.filter()` options. */
135
+ export interface LocatorFilterOptions {
136
+ /**
137
+ * Keep matches whose text contains this string (case-insensitive,
138
+ * whitespace-normalized) or matches this RegExp.
139
+ */
140
+ hasText: string | RegExp;
26
141
  }
27
142
  export interface RejectExpected {
28
- code?: string;
143
+ /**
144
+ * The rejection's `code`: a `TestErrorCode` (driver codes such as
145
+ * `E_PAGE_NOT_ACTIVE`, plus `E_TIMEOUT`, `E_OPENAPI_CONTRACT`; see
146
+ * `TEST_ERROR_CODES`) or one the app declared in `AppErrorCodes`. Both are
147
+ * closed, so a code that no longer exists or was never declared does not
148
+ * compile.
149
+ */
150
+ code?: ExpectedErrorCode;
29
151
  message?: string | RegExp;
30
152
  }
31
- export interface LocatorOptions extends PageTarget {
32
- /** Zero-based match index; omit to require a unique match. */
33
- index?: number;
153
+ /**
154
+ * Locator states are strict about ambiguity (narrow with `.nth()`,
155
+ * `.first()`, `.last()` or `.filter()`):
156
+ * - `attached`: exactly one match in the DOM, rendered or not.
157
+ * - `visible`: exactly one match, and it is rendered — a non-empty box, not
158
+ * `display:none`, `visibility:hidden` or `opacity:0` — whether or not it is
159
+ * scrolled into the viewport (content below the fold or in an overflowing
160
+ * sheet is visible).
161
+ * - `inViewport`: exactly one visible match that intersects the viewport.
162
+ * - `hidden`: no visible match, including no match at all.
163
+ * - `detached`: no match.
164
+ * Several matches satisfy only `hidden` (when none is visible). The raw
165
+ * `page.waitFor` driver checks the first match instead; see `PageWaitState`.
166
+ */
167
+ export type LocatorState = "attached" | "detached" | "visible" | "hidden" | "inViewport";
168
+ export interface LocatorWaitOptions extends ExpectOptions {
169
+ /** Defaults to `visible`. */
170
+ state?: LocatorState;
34
171
  }
35
172
  export interface Locator {
36
173
  readonly selector: string;
37
- click(options?: ExpectOptions): Promise<void>;
38
- fill(text: string, options?: ExpectOptions): Promise<void>;
39
- type(text: string, options?: ExpectOptions): Promise<void>;
40
- press(key: string, options?: ExpectOptions): Promise<void>;
174
+ /** Wait until the locator reaches `state`; rejects at the timeout. */
175
+ waitFor(options?: LocatorWaitOptions): Promise<void>;
176
+ click(options?: ActionOptions): Promise<void>;
177
+ fill(text: string, options?: ActionOptions): Promise<void>;
178
+ type(text: string, options?: InputOptions): Promise<void>;
179
+ press(key: string, options?: InputOptions): Promise<void>;
180
+ /** Pick the match at `index` (of the filtered matches, after `.filter()`). */
41
181
  nth(index: number): Locator;
42
- /** Read once; use t.expect for retrying assertions. */
43
- query(): Promise<PageQueryResult>;
182
+ first(): Locator;
183
+ last(): Locator;
184
+ /** Narrow the matches; `nth`/`first`/`last` then pick among what is left. */
185
+ filter(options: LocatorFilterOptions): Locator;
186
+ /** How many elements match now. */
187
+ count(): Promise<number>;
188
+ /** Whether exactly one match is rendered now; several matches reject. */
189
+ isVisible(): Promise<boolean>;
190
+ /** The single match's text as the user sees it, whitespace-collapsed; rejects unless exactly one matches. */
191
+ textContent(): Promise<string>;
192
+ /** The single match's value (input, textarea, select); rejects unless exactly one matches, or it has no value. */
193
+ inputValue(): Promise<string>;
194
+ /** The single match's attribute, `null` when absent; rejects unless exactly one matches. */
195
+ getAttribute(name: string): Promise<string | null>;
196
+ }
197
+ /** A value that crosses the eval boundary unchanged. */
198
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
199
+ [key: string]: JsonValue;
200
+ };
201
+ /** JSON members preserve their shape; unsupported values reject at runtime. */
202
+ type JsonMember<R> = 0 extends 1 & R ? any : unknown extends R ? unknown : R extends JsonValue ? R : R extends (...args: never[]) => unknown ? never : R extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | WeakMap<object, unknown> | WeakSet<object> | PromiseLike<unknown> | symbol | bigint ? never : R extends URL | URLSearchParams | Response | Request | Headers | Blob | ArrayBuffer | ArrayBufferView | Error ? never : R extends ViewElement | ViewDocument | ViewWindow ? never : R extends readonly unknown[] ? {
203
+ [K in keyof R]: JsonMember<R[K]>;
204
+ } : R extends object ? {
205
+ [K in keyof R]: JsonMember<R[K]>;
206
+ } : never;
207
+ /** A remote result is JSON, or top-level void; nested undefined is rejected. */
208
+ export type Jsonable<R> = R extends undefined | void ? R : JsonMember<R>;
209
+ type CheckedResult<R> = [Awaited<R>] extends [Jsonable<Awaited<R>>] ? unknown : {
210
+ readonly __resultMustBeJson: "Return JSON values or top-level void";
211
+ };
212
+ type JsonArgs<A extends unknown[]> = A & {
213
+ [K in keyof A]: JsonMember<A[K]>;
214
+ };
215
+ /** A page instance as app Logic sees it (`getCurrentPages()` entries). */
216
+ export interface LogicPage<TData = Record<string, unknown>> {
217
+ readonly route: string;
218
+ readonly data: TData;
219
+ setData(patch: Record<string, unknown>): void;
220
+ /** Resolves once pending `setData` writes reached the View. */
221
+ flush(): Promise<void>;
222
+ }
223
+ /** A page whose declared methods are not typed: any member may be read. */
224
+ export type AnyLogicPage<TData = Record<string, unknown>> = LogicPage<TData> & {
225
+ /** Page methods declared in `Page({...})`. */
226
+ readonly [member: string]: unknown;
227
+ };
228
+ /** The app instance as app Logic sees it (`getApp()`). */
229
+ export interface LogicApp {
230
+ readonly [member: string]: unknown;
231
+ }
232
+ /**
233
+ * What a function passed to `t.app.logic.eval(fn)` receives. It runs inside
234
+ * the app's Logic runtime, so these are the app's own `lx`, `getApp` and
235
+ * `getCurrentPages` — not globals of the test context.
236
+ */
237
+ export interface LogicScope {
238
+ readonly lx: Lx & {
239
+ automation(): Automation;
240
+ };
241
+ getApp<T extends LogicApp = LogicApp>(): T | null;
242
+ getCurrentPages<T extends LogicPage<any> = AnyLogicPage>(): T[];
243
+ /** A live page by instance id, surface pages included; `undefined` once it is gone. */
244
+ getPage<T extends LogicPage<any> = AnyLogicPage>(instanceId: string): T | undefined;
245
+ }
246
+ type PageGlobal<K extends string, Fallback> = typeof globalThis extends {
247
+ [P in K]: infer V;
248
+ } ? V : Fallback;
249
+ /**
250
+ * An element as `t.app.view.eval(fn)` reads it, when the test tsconfig has no
251
+ * DOM library: enough to read text, values and attributes without a cast.
252
+ */
253
+ export interface ViewElement {
254
+ readonly tagName: string;
255
+ readonly id: string;
256
+ readonly className: string;
257
+ readonly textContent: string | null;
258
+ readonly innerText?: string;
259
+ /** Inputs, textareas and selects. */
260
+ readonly value?: string;
261
+ /** Checkboxes and radios. */
262
+ readonly checked?: boolean;
263
+ readonly disabled?: boolean;
264
+ readonly children: ArrayLike<ViewElement>;
265
+ getAttribute(name: string): string | null;
266
+ hasAttribute(name: string): boolean;
267
+ querySelector(selector: string): ViewElement | null;
268
+ querySelectorAll(selector: string): ArrayLike<ViewElement>;
269
+ closest(selector: string): ViewElement | null;
270
+ getBoundingClientRect(): {
271
+ left: number;
272
+ top: number;
273
+ width: number;
274
+ height: number;
275
+ };
276
+ }
277
+ /** The page's `document` in `t.app.view.eval(fn)` without a DOM library. */
278
+ export interface ViewDocument {
279
+ readonly title: string;
280
+ readonly body: ViewElement;
281
+ readonly documentElement: ViewElement;
282
+ readonly activeElement: ViewElement | null;
283
+ querySelector(selector: string): ViewElement | null;
284
+ querySelectorAll(selector: string): ArrayLike<ViewElement>;
285
+ getElementById(id: string): ViewElement | null;
286
+ }
287
+ /** The page's `window` in `t.app.view.eval(fn)` without a DOM library. */
288
+ export interface ViewWindow {
289
+ readonly innerWidth: number;
290
+ readonly innerHeight: number;
291
+ readonly scrollX: number;
292
+ readonly scrollY: number;
293
+ readonly location: {
294
+ readonly href: string;
295
+ readonly pathname: string;
296
+ readonly search: string;
297
+ };
298
+ getComputedStyle(element: ViewElement): {
299
+ getPropertyValue(name: string): string;
300
+ readonly [property: string]: unknown;
301
+ };
302
+ readonly [member: string]: unknown;
303
+ }
304
+ /**
305
+ * What a function passed to `t.app.view.eval(fn)` receives: the page
306
+ * WebView's `document` and `window`. With the DOM library in the test
307
+ * tsconfig they are the DOM's own types; without it (the recommended test
308
+ * tsconfig), `ViewDocument` and `ViewWindow`.
309
+ */
310
+ export interface ViewScope {
311
+ readonly document: PageGlobal<"document", ViewDocument>;
312
+ readonly window: PageGlobal<"window", ViewWindow>;
313
+ }
314
+ /**
315
+ * A function evaluated in the app's Logic runtime. It is sent as source text
316
+ * (`fn.toString()`), so it must be self-contained: no variables, imports or
317
+ * helpers from the spec. Pass values as JSON arguments instead.
318
+ */
319
+ export type LogicFunction<R, A extends unknown[]> = ((scope: LogicScope, ...args: A) => R | Promise<R>) & CheckedResult<R>;
320
+ /** A function evaluated in the page WebView; self-contained like `LogicFunction`. */
321
+ export type ViewFunction<R, A extends unknown[]> = ((scope: ViewScope, ...args: A) => R | Promise<R>) & CheckedResult<R>;
322
+ /**
323
+ * A page's View — locators, function eval, and DOM scrolling. `t.app.view`
324
+ * is whichever page is current at each call; `(await t.app.page()).view` is
325
+ * one fixed page instance. Read elements and act on them through locators;
326
+ * they wait for the element and retry, where a raw driver call would not.
327
+ */
328
+ export interface TestView {
329
+ testId(id: string): Locator;
330
+ css(selector: string): Locator;
331
+ /**
332
+ * Run `fn` in the page's WebView with JSON `args` and resolve to its JSON
333
+ * result. `fn` must be self-contained (see `ViewFunction`).
334
+ */
335
+ eval<R, A extends unknown[]>(fn: ViewFunction<R, A>, ...args: JsonArgs<A>): Promise<Awaited<R>>;
336
+ /** The same with a `timeout` (see `EvalOptions`). */
337
+ eval<R, A extends unknown[]>(options: EvalOptions, fn: ViewFunction<R, A>, ...args: JsonArgs<A>): Promise<Awaited<R>>;
338
+ screenshot(): Promise<Screenshot>;
339
+ /** Scroll the page DOM by a pixel delta (nearest scrollable container). */
340
+ scroll(options?: Omit<PageScrollOptions, "page">): Promise<void>;
341
+ }
342
+ /** Native app-window input, independent of a page selector or DOM focus. */
343
+ export interface TestWindow {
344
+ /** Pointer input; coordinates and window selection follow the native driver. */
345
+ readonly pointer: PagePointer;
346
+ readonly key: PageKey;
44
347
  }
45
- export interface TestPage extends PageDriver {
46
- testId(id: string, options?: LocatorOptions): Locator;
47
- css(selector: string, options?: LocatorOptions): Locator;
348
+ /**
349
+ * Leading options of `eval(options, fn, ...args)`. Without them an eval may
350
+ * take 10 s, clamped to the spec's remaining budget.
351
+ */
352
+ export interface EvalOptions {
353
+ /**
354
+ * How long `fn` may run, in ms, for work that legitimately takes longer
355
+ * (a download, a transcode). Clamped to the spec's remaining budget.
356
+ */
357
+ timeout?: number;
48
358
  }
49
- export interface TestApp extends LxAppDriver {
50
- readonly page: TestPage;
359
+ /**
360
+ * `t.app.page()` selector: a configured page name, which must match exactly
361
+ * one live instance, or a live instance id.
362
+ */
363
+ export type PageSelector = {
364
+ name: string;
365
+ instanceId?: never;
366
+ } | {
367
+ instanceId: string;
368
+ name?: never;
369
+ };
370
+ /** `t.app.page()` options. */
371
+ export interface PageBindOptions {
372
+ /**
373
+ * How long to wait for a matching live instance, in ms (default 5000),
374
+ * clamped to the spec's remaining budget.
375
+ */
376
+ timeout?: number;
377
+ }
378
+ /** What a page action that takes more than one parameter becomes. */
379
+ export interface UnaryActionsOnly {
380
+ readonly __pageActionsTakeOnePayload: "Page actions take at most one JSON payload; change the action to take an object";
381
+ }
382
+ /**
383
+ * A contract's actions as a test calls them: each takes at most one JSON
384
+ * payload and resolves to its JSON result once Logic settles it.
385
+ * Generators (streamed actions) are not callable from a test.
386
+ */
387
+ export type PageActionCalls<A> = {
388
+ readonly [K in keyof A as A[K] extends (...args: any[]) => any ? K : never]: A[K] extends (...args: any[]) => AsyncGenerator<any, any, any> | Generator<any, any, any> ? never : A[K] extends (...args: infer P) => infer R ? P extends [] | [unknown?] ? (...args: JsonArgs<P> & CheckedResult<R>) => Promise<Jsonable<Awaited<R>>> : UnaryActionsOnly : never;
389
+ };
390
+ /** Call controls kept separate from an action's JSON payload. */
391
+ export interface PageInvokeOptions {
392
+ /** Per-call budget in ms; defaults to the spec's remaining budget. */
393
+ timeout?: number;
394
+ }
395
+ export type PageInvokeCalls<A> = {
396
+ readonly [K in keyof PageActionCalls<A>]: PageActionCalls<A>[K] extends (...args: infer P) => infer R ? (name: K, options: PageInvokeOptions & (P extends [] ? {
397
+ payload?: never;
398
+ } : [] extends P ? {
399
+ payload?: P[0];
400
+ } : {
401
+ payload: P[0];
402
+ })) => R : never;
403
+ };
404
+ /**
405
+ * One live page instance, fixed when `t.app.page()` resolved: it never
406
+ * follows navigation or a replacement, and every call on it rejects once
407
+ * the instance is gone. Type it with the page's `PageContract`.
408
+ */
409
+ export interface TestPage<C extends PageContract> {
410
+ readonly instanceId: string;
411
+ /** Configured page name. */
412
+ readonly name: string;
413
+ /** This instance's View. */
414
+ readonly view: TestView;
415
+ /** This instance's Logic `data`, as `setData` delivers it to the View. */
416
+ data(): Promise<C["data"]>;
417
+ /** The contract's public actions, invoked through the page's own bridge. */
418
+ readonly actions: PageActionCalls<C["actions"]>;
419
+ /** Invoke one action with a local timeout, separate from its payload. */
420
+ invoke<K extends keyof PageInvokeCalls<C["actions"]>>(name: K, options: Parameters<Extract<PageInvokeCalls<C["actions"]>[K], (...args: any[]) => any>>[1]): ReturnType<Extract<PageInvokeCalls<C["actions"]>[K], (...args: any[]) => any>>;
421
+ }
422
+ /**
423
+ * `t.app.logic`: the app's Logic runtime, for what the UI cannot show. Read
424
+ * page state with `(await t.app.page()).data()`; call actions through
425
+ * `page.actions`.
426
+ */
427
+ export interface TestLogic {
428
+ /**
429
+ * Run `fn` in the app's Logic runtime with JSON `args` and resolve to its
430
+ * JSON result. `fn` must be self-contained (see `LogicFunction`).
431
+ */
432
+ eval<R, A extends unknown[]>(fn: LogicFunction<R, A>, ...args: JsonArgs<A>): Promise<Awaited<R>>;
433
+ /** The same with a `timeout` (see `EvalOptions`). */
434
+ eval<R, A extends unknown[]>(options: EvalOptions, fn: LogicFunction<R, A>, ...args: JsonArgs<A>): Promise<Awaited<R>>;
435
+ }
436
+ /** Nav actions' wait. */
437
+ export interface NavWaitOptions {
438
+ /**
439
+ * `'ready'` (default) resolves after the landed page's `onReady` and
440
+ * rejects if the app replaces it first; `'commit'` resolves once the page
441
+ * stack changed.
442
+ */
443
+ waitUntil?: "commit" | "ready";
444
+ /** Bound for `'ready'` in ms (default 15000), clamped to the spec's remaining budget. */
445
+ timeout?: number;
446
+ }
447
+ /** `t.app.nav.to` / `redirect` / `switchTab` / `relaunch` options. */
448
+ export interface NavOptions extends NavWaitOptions {
449
+ /** Configured page name (from lxapp.json). */
450
+ page: string;
451
+ /** Query forwarded to the destination page. */
452
+ query?: Record<string, unknown>;
453
+ }
454
+ /** `t.app.nav.back` options. */
455
+ export interface NavBackOptions extends NavWaitOptions {
456
+ /** Number of pages to pop (default 1). */
457
+ delta?: number;
458
+ }
459
+ /** `t.app.nav`: the page stack. Actions resolve to the landed page. */
460
+ export interface TestNav {
461
+ /** Push a page onto the stack. */
462
+ to(options: NavOptions): Promise<PageInfo>;
463
+ /** Replace the current page (rejects tab-bar targets). */
464
+ redirect(options: NavOptions): Promise<PageInfo>;
465
+ /** Switch to a configured tab page. */
466
+ switchTab(options: NavOptions): Promise<PageInfo>;
467
+ /** Unload every page (cached tab pages included) and open a fresh instance of a page. */
468
+ relaunch(options: NavOptions): Promise<PageInfo>;
469
+ back(options?: NavBackOptions): Promise<PageInfo>;
470
+ current(): Promise<PageInfo>;
471
+ /** Status of a configured page by name; omit `page` for the current page. */
472
+ info(options?: PageTarget): Promise<PageInfo>;
473
+ stack(): Promise<PageInfo[]>;
474
+ }
475
+ /**
476
+ * One call the app made, as `route.calls()`, a scenario's `calls()` and
477
+ * `t.app.network.calls()` list it.
478
+ */
479
+ export interface NetworkCall {
480
+ /**
481
+ * Increasing within the log the call was read from, never reused: calls
482
+ * in one millisecond differ by it. A route's calls share the host's
483
+ * request log; a scenario counts its HTTP calls and the companion its
484
+ * Function calls separately.
485
+ */
486
+ seq: number;
487
+ /** Epoch milliseconds. */
488
+ time: number;
489
+ /** `http`: Logic `fetch` or `Rong.SSE`; `function`: a Worker Function call. */
490
+ kind: "http" | "sse" | "function";
491
+ /** `http`: upper-case method and URL. */
492
+ method?: string;
493
+ url?: string;
494
+ /** `function`: the Function's name. */
495
+ function?: string;
496
+ /** `http`: the answered status; `null` when none was (abort, hang, still pending). */
497
+ status?: number | null;
498
+ /**
499
+ * The request body (parsed when it is JSON; text otherwise), or a
500
+ * Function call's arguments. `null` when there is none or it could not be
501
+ * read without consuming it.
502
+ */
503
+ body?: unknown;
504
+ /** Request headers with lower-case names; calls a route handled only. */
505
+ headers?: Record<string, string>;
506
+ /**
507
+ * What answered: a scenario `rule`, a test `route`, a `mock` handler
508
+ * (`mocks/`), the `real` backend, or the dev session's `companion` for a
509
+ * Function no rule matched.
510
+ */
511
+ answeredBy: "rule" | "route" | "mock" | "real" | "companion";
512
+ /** The scenario rule that answered (1-based). */
513
+ rule?: number;
514
+ /** `function`: `result`, `error`, `fault` or `default`. */
515
+ outcome?: string;
516
+ /** Why no scenario rule matched, when rules targeted the call. */
517
+ noMatch?: string;
518
+ }
519
+ /** `waitForCall()` options. */
520
+ export interface WaitForCallOptions {
521
+ /** Default: the action timeout (5000 ms), clamped to the spec's remaining budget. */
522
+ timeout?: number;
523
+ interval?: number;
524
+ }
525
+ /** A test route, spec-scoped: removed when the spec ends. */
526
+ export interface TestRoute {
527
+ readonly id: number;
528
+ readonly pattern: string;
529
+ /** Remove the route. Removing one that already expired is not an error. */
530
+ remove(): Promise<void>;
531
+ /** Calls this route handled, oldest first. */
532
+ calls(): Promise<NetworkCall[]>;
533
+ /**
534
+ * Resolve with the oldest call this route handled that an earlier
535
+ * `waitForCall()` did not already return, waiting for it if there is none
536
+ * yet. On timeout the error lists the route's recent calls. The host's
537
+ * request log is bounded (1000 requests, 16 MiB of bodies, across apps);
538
+ * when it dropped this route's calls before a wait read them, the wait
539
+ * fails saying so rather than skip them, and the next one resumes after
540
+ * the gap.
541
+ */
542
+ waitForCall(options?: WaitForCallOptions): Promise<NetworkCall>;
51
543
  }
52
- export interface TestAutomation extends Omit<Automation, "lxapp"> {
544
+ /**
545
+ * `t.app.network`: test routing of the app's Logic `fetch` and `Rong.SSE`,
546
+ * spec-scoped: routes are removed when the spec ends.
547
+ */
548
+ export interface TestNetwork {
549
+ /** The newest matching route handles a request; unmatched requests are untouched. */
550
+ route(pattern: NetworkRoutePattern, handler: NetworkRouteHandler): Promise<TestRoute>;
551
+ /** Calls this spec's routes handled, oldest first. */
552
+ calls(): Promise<NetworkCall[]>;
553
+ }
554
+ /** `scenario.waitForCall()` target: a rule's target as the file writes it, or its number. */
555
+ export type ScenarioCallTarget = ScenarioCallFilter;
556
+ /** `t.scenario`. */
557
+ export interface TestScenarios {
558
+ /**
559
+ * Put an app into a product state from a scenario file (and one of its
560
+ * variants), on top of the mock selection: `http` rules answer Logic
561
+ * `fetch`, `function` rules go to the dev session's companion.
562
+ * `app` installs `http` rules on that lxapp instead of `t.app`; a
563
+ * scenario with `function` rules rejects, since those are not app-scoped.
564
+ * Spec-scoped: a second call replaces the first, and the spec's end
565
+ * removes it. A failed spec reports its per-rule hits and the calls that
566
+ * reached it.
567
+ */
568
+ use(definition: ScenarioInput, options?: {
569
+ app?: string;
570
+ variant?: string;
571
+ }): Promise<TestScenario>;
572
+ }
573
+ /** The scenario `t.scenario.use()` installed, spec-scoped. */
574
+ export interface TestScenario {
575
+ readonly name: string | null;
576
+ readonly variant: string | null;
577
+ /** Its rules with their hit counts, read when accessed. */
578
+ readonly rules: ScenarioRuleInfo[];
579
+ /**
580
+ * Calls that reached the scenario since it was installed, oldest first:
581
+ * all of them, or those of one rule target (`{ http: 'GET **\/x' }`,
582
+ * `{ function: 'orders.submit' }`) or rule number (`{ rule: 2 }`).
583
+ */
584
+ calls(filter?: ScenarioCallFilter): Promise<NetworkCall[]>;
585
+ /**
586
+ * Resolve with the oldest call to `target` that an earlier `waitForCall`
587
+ * for the same target did not already return, waiting for one if needed.
588
+ * On timeout the error lists the scenario's recent calls. A scenario
589
+ * keeps its last 200 HTTP calls and the companion its last Function
590
+ * calls; when calls were dropped before a wait read them, the wait fails
591
+ * saying so, and the next one resumes after the gap.
592
+ */
593
+ waitForCall(target: ScenarioCallTarget, options?: WaitForCallOptions): Promise<NetworkCall>;
594
+ /** Remove the scenario before the spec ends. */
595
+ remove(): Promise<void>;
596
+ }
597
+ /**
598
+ * `t.app.clock`: test clock for the app's Logic (`Date`, timers,
599
+ * `performance.now`). Spec-scoped: a clock still installed when the spec
600
+ * ends is uninstalled, and if that drops pending test timers the next spec
601
+ * starts from a relaunched home page.
602
+ */
603
+ export interface TestClock {
604
+ /** Put Logic on test time. Rejects with `E_CLOCK_INSTALLED` when a clock already is. */
605
+ install(options?: ClockInstallOptions): Promise<ClockState>;
606
+ /** Advance by `ms`, firing each timer due on the way at its own time. */
607
+ tick(ms: number): Promise<ClockAdvance>;
608
+ /** Fire timers, including those they schedule, until none is left. */
609
+ runAll(options?: ClockRunAllOptions): Promise<ClockAdvance>;
610
+ /** Change what `Date` reads without firing timers. */
611
+ setSystemTime(time: ClockTime): Promise<ClockState>;
612
+ /**
613
+ * Return Logic to real time. Pending test timers are dropped, never fired;
614
+ * the report's trace says how many.
615
+ */
616
+ uninstall(): Promise<void>;
617
+ }
618
+ /**
619
+ * `t.app.dialogs`: the dialogs the app's Logic opens during this spec, all
620
+ * recorded. Toasts are always drawn. Modals (`lx.showModal`, `alert`,
621
+ * `confirm`) and action sheets are drawn unless an answer is queued for the
622
+ * next one or strict answer mode is explicitly enabled.
623
+ * An answer no dialog used fails the spec when it ends. Each spec starts
624
+ * with nothing recorded or queued; outside a test run dialogs draw as usual.
625
+ */
626
+ export interface TestDialogs {
627
+ /** Toasts presented so far, oldest first. Poll it: `expect.poll(() => t.app.dialogs.toasts())`. */
628
+ toasts(): Promise<ToastRecord[]>;
629
+ /** Modals opened so far, with the answer each got (the user's, when drawn). */
630
+ modals(): Promise<ModalRecord[]>;
631
+ /** `lx.showActionSheet` calls so far, with the answer each got. */
632
+ actionSheets(): Promise<ActionSheetRecord[]>;
633
+ /**
634
+ * Answer the next modal: `{ confirm: true }` confirms, `{ confirm: false }`
635
+ * cancels. Later modals draw normally unless strict mode is enabled.
636
+ */
637
+ answerNextModal(answer: ModalAnswer): Promise<void>;
638
+ /**
639
+ * Answer the next action sheet: `{ index }` picks that item, `{ cancel: true }`
640
+ * dismisses it. Later sheets draw normally unless strict mode is enabled.
641
+ */
642
+ answerNextActionSheet(answer: ActionSheetAnswer): Promise<void>;
643
+ /** Require queued answers for every dialog of a kind until changed again. Returns the prior modes. */
644
+ setAnswerMode(mode: {
645
+ modals?: "draw" | "strict";
646
+ actionSheets?: "draw" | "strict";
647
+ }): Promise<{
648
+ modals: "draw" | "strict";
649
+ actionSheets: "draw" | "strict";
650
+ }>;
651
+ /** Apply answer modes while `body` runs, then restore the previous modes, including when it throws. Await scopes sequentially. */
652
+ withAnswerMode<T>(mode: {
653
+ modals?: "draw" | "strict";
654
+ actionSheets?: "draw" | "strict";
655
+ }, body: () => T | Promise<T>): Promise<T>;
656
+ }
657
+ /**
658
+ * `t.app`: the app under test. A saved `t.app` (or any part of it) keeps
659
+ * reaching the app after a profile checkpoint or restore reopens it.
660
+ */
661
+ export interface TestApp {
662
+ /**
663
+ * Bind one live page instance: the current page, or the one `selector`
664
+ * names (a page kept below the current one, or one a surface shows),
665
+ * waiting for it to open. The handle never follows navigation.
666
+ */
667
+ page<C extends PageContract = PageContract>(selector?: PageSelector, options?: PageBindOptions): Promise<TestPage<C>>;
668
+ /** The View of whichever page is current at each call. */
669
+ readonly view: TestView;
670
+ /** Native app-window input, independent of pages and DOM focus. */
671
+ readonly window: TestWindow;
672
+ /** The app's Logic runtime: `eval(fn)`. */
673
+ readonly logic: TestLogic;
674
+ /** The page stack; actions wait for the landed page's `onReady` unless you pass `waitUntil`. */
675
+ readonly nav: TestNav;
676
+ /** Spec-scoped test routing of Logic `fetch`. */
677
+ readonly network: TestNetwork;
678
+ /** Spec-scoped test clock for the app's Logic. */
679
+ readonly clock: TestClock;
680
+ /** Toasts, modals and action sheets the app's Logic opens during the spec. */
681
+ readonly dialogs: TestDialogs;
682
+ /** Checkpoint and roll back the app's isolated data. */
683
+ readonly profile: ProfileFixture;
684
+ info: LxAppDriver["info"];
685
+ pages: LxAppDriver["pages"];
686
+ surfaceLayout: LxAppDriver["surfaceLayout"];
687
+ }
688
+ /** A checkpoint of the app's isolated data. */
689
+ export interface ProfileCheckpoint {
690
+ readonly id: string;
691
+ }
692
+ /**
693
+ * `t.app.profile`: checkpoint and roll back the app's isolated data by hand.
694
+ * Needs an isolated run (`lxdev test --profile`); otherwise every call
695
+ * rejects with `E_PROFILE_NOT_ISOLATED`. `checkpoint` and `restore` close the
696
+ * app and reopen it at its initial page; every fixture app of it, saved or
697
+ * not, follows the reopened app.
698
+ */
699
+ export interface ProfileFixture {
700
+ /** Snapshot the app's data. */
701
+ checkpoint(): Promise<ProfileCheckpoint>;
702
+ /**
703
+ * Roll the app's data back to `checkpoint` (or its id). With `keep`, the
704
+ * `lx.getStorage()` keys those globs match keep their current state;
705
+ * `kept` lists the ones that existed.
706
+ */
707
+ restore(checkpoint: ProfileCheckpoint | string, options?: ProfileRestoreOptions): Promise<ProfileRestoreResult>;
708
+ /** Discard `checkpoint`. */
709
+ drop(checkpoint: ProfileCheckpoint | string): Promise<void>;
710
+ }
711
+ /** `t.app.profile.restore` options. */
712
+ export interface ProfileRestoreOptions {
713
+ /**
714
+ * `lx.getStorage()` keys whose current state survives the rollback: globs
715
+ * over the whole key (`*` any run of characters, `?` one). A matching key
716
+ * keeps its current value, one added since the checkpoint stays, and one
717
+ * deleted since stays deleted. Files always roll back.
718
+ */
719
+ keep?: readonly string[];
720
+ }
721
+ /**
722
+ * `t.automation`: the host-run automation root, traced and stopped with the
723
+ * spec like `t.app`.
724
+ */
725
+ export interface TestAutomation extends Omit<HostRunAutomation, "lxapp" | "browser" | "desktop" | "terminal"> {
726
+ /** The app `t.app` is pinned to (the same object), or another running one by id. */
53
727
  lxapp(): TestApp;
54
728
  lxapp(appId: string): TestApp;
729
+ /**
730
+ * The host app's browser tabs.
731
+ *
732
+ * @privileged host — reading it never throws; on a host without a browser
733
+ * shell each call rejects.
734
+ */
735
+ readonly browser: BrowserDriver;
736
+ /**
737
+ * Local-OS desktop automation (Windows/macOS).
738
+ *
739
+ * @privileged host — reading it never throws; on a host built without
740
+ * desktop automation each call rejects.
741
+ */
742
+ readonly desktop: DesktopDriver;
743
+ /**
744
+ * Native terminal workspace state and pane actions.
745
+ *
746
+ * @privileged host — reading it never throws; on a host without a native
747
+ * terminal each call rejects.
748
+ */
749
+ readonly terminal: TerminalDriver;
55
750
  }
56
- export interface Apps {
57
- lxapp(appId: string): TestApp;
751
+ /**
752
+ * Awaiting `expect(…)` or `expect.poll(…)` itself checks nothing: this member
753
+ * makes `await expect(x)` without a matcher a type error, and it rejects at
754
+ * run time naming the line.
755
+ */
756
+ export interface NeedsMatcher<Hint extends string> {
757
+ then(needsAMatcher: Hint): never;
58
758
  }
59
- export interface RetryMatchers<T> {
759
+ /** `expect.poll(read)` matchers: they call `read` until the matcher passes. */
760
+ export interface RetryMatchers<T> extends NeedsMatcher<"expect.poll(read) checks nothing until a matcher is called: await expect.poll(read).toBe(expected)"> {
60
761
  readonly not: RetryMatchers<T>;
61
762
  toBe(expected: unknown): Promise<void>;
62
763
  toEqual(expected: unknown): Promise<void>;
63
764
  toContain(expected: unknown): Promise<void>;
765
+ /** An array has an element equal to `expected` (as `toEqual` compares). */
766
+ toContainEqual(expected: unknown): Promise<void>;
64
767
  toMatch(expected: string | RegExp): Promise<void>;
65
768
  toBeTruthy(): Promise<void>;
66
769
  toBeFalsy(): Promise<void>;
67
770
  toBeDefined(): Promise<void>;
68
771
  toBeUndefined(): Promise<void>;
69
772
  toBeInstanceOf(expected: Function): Promise<void>;
773
+ toHaveLength(expected: number): Promise<void>;
70
774
  toBeGreaterThan(expected: number): Promise<void>;
71
775
  toBeGreaterThanOrEqual(expected: number): Promise<void>;
72
776
  toBeLessThan(expected: number): Promise<void>;
73
777
  toBeLessThanOrEqual(expected: number): Promise<void>;
74
778
  }
75
- export interface LocatorMatchers {
779
+ /** `expect(locator)` matchers: they retry until the element passes. */
780
+ export interface LocatorMatchers extends NeedsMatcher<"expect(locator) checks nothing until a matcher is called: await expect(locator).toBeVisible()"> {
76
781
  readonly not: LocatorMatchers;
782
+ /** One rendered match, in the viewport or scrolled out of it. */
77
783
  toBeVisible(options?: ExpectOptions): Promise<void>;
784
+ /** One rendered match that intersects the viewport. */
785
+ toBeInViewport(options?: ExpectOptions): Promise<void>;
78
786
  toBeHidden(options?: ExpectOptions): Promise<void>;
79
787
  toBeAttached(options?: ExpectOptions): Promise<void>;
80
788
  toBeEnabled(options?: ExpectOptions): Promise<void>;
81
789
  toBeDisabled(options?: ExpectOptions): Promise<void>;
82
790
  toBeEditable(options?: ExpectOptions): Promise<void>;
83
791
  toHaveText(expected: string | RegExp, options?: ExpectOptions): Promise<void>;
792
+ /** The text contains `expected` (exact substring) or matches the RegExp. */
793
+ toContainText(expected: string | RegExp, options?: ExpectOptions): Promise<void>;
794
+ /**
795
+ * The single match has attribute `name`; with `value`, equal to it (or
796
+ * matching the RegExp). `not.toHaveAttribute(name)` passes when it is absent.
797
+ */
798
+ toHaveAttribute(name: string, value?: string | RegExp, options?: ExpectOptions): Promise<void>;
84
799
  toHaveCount(expected: number, options?: ExpectOptions): Promise<void>;
85
800
  toHaveValue(expected: string | RegExp, options?: ExpectOptions): Promise<void>;
86
801
  }
87
- export interface FixtureExpect {
88
- (locator: Locator): LocatorMatchers;
89
- poll<T>(read: () => T | Promise<T>, options?: ExpectOptions): RetryMatchers<T>;
802
+ /** What `expect(promise)` gives: nothing to call. Await the promise, or poll a read. */
803
+ export interface PromiseNotAllowed {
804
+ readonly "expect(promise)": "await the value first, or retry a read with expect.poll(() => promise)";
805
+ }
806
+ /**
807
+ * What `expect(fn)` gives: `toThrow`, which calls it once. A function is not
808
+ * a read to retry; that is `expect.poll(read)`.
809
+ */
810
+ export interface FunctionMatchers extends NeedsMatcher<"expect(fn) checks nothing until a matcher is called: expect(fn).toThrow()"> {
811
+ readonly not: FunctionMatchers;
812
+ toThrow(expected?: unknown): void;
813
+ readonly "expect(fn)": "a function is only called by toThrow; to retry a read until it passes, use expect.poll(read)";
814
+ }
815
+ /** The matchers `expect(subject)` gives for a subject of type `T`. */
816
+ export type ExpectResult<T> = 0 extends 1 & T ? Matchers<any> : [T] extends [Locator] ? LocatorMatchers : [T] extends [PromiseLike<unknown>] ? PromiseNotAllowed : [T] extends [(...args: never[]) => unknown] ? FunctionMatchers : Matchers<T>;
817
+ /**
818
+ * The one assertion entry point:
819
+ * - `expect(locator)` retries the matcher until the element passes (await it);
820
+ * - `expect(value)` checks once, synchronously;
821
+ * - `expect.poll(() => read())` calls `read` until the matcher passes (await it).
822
+ * Retries end at `timeout` (default 5000 ms), clamped to the spec's
823
+ * remaining budget; locators and `poll` need a running spec.
824
+ */
825
+ export interface Expect {
826
+ <T>(subject: T): ExpectResult<T>;
827
+ <T>(subject: T, message: string): ExpectResult<T>;
828
+ poll<T>(read: () => T | Promise<T>, options?: ExpectOptions): RetryMatchers<Awaited<T>>;
829
+ /**
830
+ * Equal (in `toEqual`, `toContainEqual` and friends) to any object with
831
+ * every key of `sample` equal; other keys are ignored.
832
+ */
833
+ objectContaining(sample: Record<string, unknown>): unknown;
834
+ }
835
+ export interface WaitForOptions<T = unknown> {
836
+ /** When the value is the one to wait for. Default: it is truthy. */
837
+ until?: (value: T) => boolean;
838
+ /** Default: the action timeout (5000 ms), clamped to the spec's remaining budget. */
839
+ timeout?: number;
840
+ interval?: number;
841
+ /**
842
+ * Whether an error thrown by `read` means "not yet". Default: retry any
843
+ * error except `TypeError`, `ReferenceError` and `SyntaxError`, which are
844
+ * programming mistakes and fail at once.
845
+ */
846
+ retryIf?: (error: unknown) => boolean;
847
+ }
848
+ export interface ArgOptions {
849
+ /** Throw when the arg is missing. Default: true unless `default` is given. */
850
+ required?: boolean;
851
+ /** Value used when the arg is missing. */
852
+ default?: string;
853
+ }
854
+ /** `t.openapi`: the contract a run loaded with `--openapi`. */
855
+ export interface OpenApiRun {
856
+ documents: Array<{
857
+ name: string;
858
+ version: string;
859
+ title?: string;
860
+ }>;
861
+ }
862
+ /** Explicit parent for nested steps; scoped calls preserve the report tree. */
863
+ export interface StepScope {
864
+ step<T>(name: string, body: (scope: StepScope) => T | Promise<T>): Promise<T>;
90
865
  }
91
866
  export interface Fixture {
92
867
  /** Guarded host drivers; use these in tests so actions are traced and stop with the fixture. */
93
868
  readonly automation: TestAutomation;
94
869
  readonly app: TestApp;
95
- readonly apps: Apps;
96
- readonly args: Record<string, string>;
97
- step<T>(name: string, body: () => T | Promise<T>): Promise<T>;
98
- expect: FixtureExpect;
870
+ /** One product scenario per spec; replacing it replaces HTTP and Function rules together. */
871
+ readonly scenario: TestScenarios;
872
+ /**
873
+ * The OpenAPI documents this run checks against (`lxdev test --openapi`),
874
+ * or `undefined` without them. A spec that only means something against a
875
+ * contract declares it: `requires: { openapi: true }`.
876
+ */
877
+ readonly openapi: OpenApiRun | undefined;
878
+ /**
879
+ * Read one `--arg` / `--secret-arg` value. Missing, it throws naming the
880
+ * `--arg` to pass, unless `default` is given or `required` is false.
881
+ */
882
+ arg(name: string, options: {
883
+ required: false;
884
+ default?: undefined;
885
+ }): string | undefined;
886
+ arg(name: string, options?: ArgOptions): string;
887
+ /** Top-level steps must not overlap. Nest through the callback's scope. */
888
+ step<T>(name: string, body: (scope: StepScope) => T | Promise<T>): Promise<T>;
99
889
  reject(operation: () => unknown | Promise<unknown>, expected?: RejectExpected): Promise<unknown>;
890
+ /**
891
+ * Call `read` until `until` (default: truthy) accepts its value and
892
+ * resolve to that value. A thrown error retries only when `retryIf` allows
893
+ * it (see `WaitForOptions`); on timeout it rejects with a `TimeoutError`
894
+ * (`E_TIMEOUT`) naming the last value or error.
895
+ */
896
+ waitFor<T>(read: () => T | Promise<T>, options?: WaitForOptions<Awaited<T>>): Promise<Awaited<T>>;
100
897
  defer(cleanup: () => void | Promise<void>): void;
101
898
  attach(name: string, data: unknown): Promise<void>;
899
+ /**
900
+ * Stop this spec and report it `skipped` with `reason`, for a precondition
901
+ * only knowable at run time. Throws; deferred cleanup still runs. Call it
902
+ * from the body or `beforeEach`; it rejects during cleanup.
903
+ */
904
+ skip(reason: string): never;
102
905
  }
103
- export interface Matchers<T> {
906
+ /** Matchers that check once, synchronously. */
907
+ export interface Matchers<T> extends NeedsMatcher<"expect(value) checks nothing until a matcher is called: expect(value).toBe(expected)"> {
104
908
  readonly not: Matchers<T>;
105
909
  toBe(expected: unknown): void;
106
910
  toEqual(expected: unknown): void;
107
911
  toContain(expected: unknown): void;
912
+ /** An array has an element equal to `expected` (as `toEqual` compares). */
913
+ toContainEqual(expected: unknown): void;
108
914
  toMatch(expected: string | RegExp): void;
109
915
  toBeTruthy(): void;
110
916
  toBeFalsy(): void;
111
917
  toBeDefined(): void;
112
918
  toBeUndefined(): void;
113
919
  toBeInstanceOf(expected: Function): void;
920
+ /** A string's or array's `length`. */
921
+ toHaveLength(expected: number): void;
114
922
  toThrow(expected?: unknown): void;
115
923
  /** Numeric ordering. Comparing anything but numbers fails the assertion. */
116
924
  toBeGreaterThan(expected: number): void;
117
925
  toBeGreaterThanOrEqual(expected: number): void;
118
926
  toBeLessThan(expected: number): void;
119
927
  toBeLessThanOrEqual(expected: number): void;
928
+ /**
929
+ * The value matches a schema of the run's OpenAPI documents (`lxdev test
930
+ * --openapi`): a component schema name (`'Device'`), a `'#/…'` ref, or
931
+ * `{ ref, document }` when several documents define it. Without
932
+ * `--openapi` it fails saying so.
933
+ */
934
+ toMatchSchema(schema: string | {
935
+ ref: string;
936
+ document?: string;
937
+ }): void;
120
938
  }
121
939
  export interface SourceLocation {
122
940
  source: string;
123
941
  line: number;
124
942
  column: number;
125
943
  }
126
- export interface AssertionRecord {
127
- matcher: string;
128
- expected: string;
129
- actual: string;
130
- passed: boolean;
131
- step?: string;
132
- }
133
- export interface StepRecord {
134
- name: string;
135
- path: string;
136
- /** `step` is authored with `t.step`; `action` is a recorded driver call. */
137
- kind?: "step" | "action";
138
- /** Short argument summary for an action — a selector, a page, a script head. */
139
- detail?: string;
140
- /** Identical consecutive actions collapse into one row with a count. */
141
- repeat?: number;
142
- status: StepStatus;
143
- duration_ms: number;
144
- error?: ReportError;
145
- steps: StepRecord[];
146
- attachments: AttachmentRef[];
147
- assertions: AssertionRecord[];
148
- }
149
- export interface AttachmentRef {
150
- name: string;
151
- path: string;
152
- mimeType: string;
153
- }
154
- export interface ReportError {
155
- code?: string;
156
- data?: unknown;
157
- phase?: string;
158
- name: string;
159
- message: string;
160
- stack?: string;
161
- matcher?: string;
162
- expected?: string;
163
- actual?: string;
164
- location?: string;
165
- step?: string;
166
- }
167
- export interface CaseRecord {
168
- attempt?: number;
169
- attempts?: CaseRecord[];
170
- flaky?: boolean;
171
- id: string;
172
- title: string;
173
- name: string;
174
- full_name: string;
175
- /** Source file the spec was registered from, remapped through the bundle map. */
176
- file?: string;
177
- line?: number;
178
- /** Display group in the report — the spec file's path inside the project. */
179
- suite?: string;
180
- status: SpecStatus;
181
- duration_ms: number;
182
- covers: string[];
183
- /**
184
- * `lx.*` members the spec's evals actually reached, observed by the runtime
185
- * rather than declared. A `covers` tag absent from here was claimed but never
186
- * exercised.
187
- */
188
- observed?: string[];
189
- steps: StepRecord[];
190
- assertions: AssertionRecord[];
191
- attachments: AttachmentRef[];
192
- error?: ReportError;
193
- timeout_ms: number;
194
- reason?: string;
195
- }
196
- /** The app under test, so a report identifies its own subject. */
197
- export interface RunSubject {
198
- appid?: string;
199
- app_name?: string;
200
- version?: string;
201
- release_type?: string;
202
- pages?: number;
203
- }
204
- export interface RunMeta {
205
- started_at: string;
206
- duration_ms: number;
207
- args: Record<string, string>;
208
- platform?: string;
209
- framework?: string;
210
- subject?: RunSubject;
211
- /** The suite opted into measuring the whole published `lx` surface. */
212
- surface_coverage?: boolean;
213
- }
214
- export interface JsonReport {
215
- schema_version?: number;
216
- framework: {
217
- name: string;
218
- version: string;
219
- };
220
- meta: RunMeta;
221
- partial: boolean;
222
- filtered: boolean;
223
- total: number;
224
- passed: number;
225
- failed: number;
226
- skipped: number;
227
- xfail: number;
228
- xpass: number;
229
- timeout: number;
230
- duration_ms: number;
231
- cases: CaseRecord[];
232
- }
233
- export type ProtocolReport = JsonReport;
234
- export interface LingxiaTestController {
235
- run(): Promise<ProtocolReport>;
236
- readonly version: string;
237
- /** Clears the registry. Used by this package's own Node tests. */
238
- reset(): void;
239
- }
240
- export interface AutomationHost {
241
- args?: Record<string, string>;
242
- attach?: (name: string, artifact: {
243
- mimeType: string;
244
- base64: string;
245
- }) => void | Promise<void>;
246
- emit?: (event: Record<string, unknown>) => void | Promise<void>;
247
- report?: (event: Record<string, unknown>) => void | Promise<void>;
248
- logs?: () => string | string[] | Promise<string | string[]>;
249
- }
250
- declare global {
251
- var __LINGXIA_TEST__: LingxiaTestController | undefined;
252
- var __LINGXIA_AUTOMATION_HOST__: AutomationHost | undefined;
253
- var __RONG_TEST_HOST__: AutomationHost | undefined;
254
- var __LINGXIA_TEST_SOURCE_MAP__: unknown;
255
- var __LINGXIA_CLI_VERSION__: string | undefined;
256
- }
257
944
  //# sourceMappingURL=types.d.ts.map