@lingxia/types 0.17.0 → 0.19.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 (64) hide show
  1. package/dist/automation/index.d.ts +1039 -128
  2. package/dist/automation/index.d.ts.map +1 -1
  3. package/dist/automation/index.js +61 -3
  4. package/dist/automation/index.js.map +1 -1
  5. package/dist/error.d.ts +19 -0
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/error.js +23 -1
  8. package/dist/error.js.map +1 -1
  9. package/dist/esm/automation/index.js +60 -4
  10. package/dist/esm/automation/index.js.map +1 -1
  11. package/dist/esm/error.js +21 -0
  12. package/dist/esm/error.js.map +1 -1
  13. package/dist/esm/generated/error.js +1 -0
  14. package/dist/esm/generated/error.js.map +1 -1
  15. package/dist/esm/generated/i18n.js +10 -1
  16. package/dist/esm/generated/i18n.js.map +1 -1
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/mocks.js +2 -0
  19. package/dist/esm/mocks.js.map +1 -0
  20. package/dist/esm/page.js +2 -0
  21. package/dist/esm/page.js.map +1 -0
  22. package/dist/esm/testing/public-api.js +133 -48
  23. package/dist/esm/testing/public-api.js.map +1 -1
  24. package/dist/generated/error.d.ts +4 -0
  25. package/dist/generated/error.d.ts.map +1 -1
  26. package/dist/generated/error.js +1 -0
  27. package/dist/generated/error.js.map +1 -1
  28. package/dist/generated/i18n.d.ts +1 -1
  29. package/dist/generated/i18n.d.ts.map +1 -1
  30. package/dist/generated/i18n.js +10 -1
  31. package/dist/generated/i18n.js.map +1 -1
  32. package/dist/generated/logic-web.d.ts +24 -3
  33. package/dist/generated/logic.d.ts +772 -381
  34. package/dist/generated/logic.d.ts.map +1 -1
  35. package/dist/index.d.ts +17 -9
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/mocks.d.ts +58 -0
  39. package/dist/mocks.d.ts.map +1 -0
  40. package/dist/mocks.js +3 -0
  41. package/dist/mocks.js.map +1 -0
  42. package/dist/page.d.ts +26 -0
  43. package/dist/page.d.ts.map +1 -0
  44. package/dist/page.js +3 -0
  45. package/dist/page.js.map +1 -0
  46. package/dist/process.d.ts +15 -4
  47. package/dist/process.d.ts.map +1 -1
  48. package/dist/testing/public-api.d.ts +127 -51
  49. package/dist/testing/public-api.d.ts.map +1 -1
  50. package/dist/testing/public-api.js +133 -48
  51. package/dist/testing/public-api.js.map +1 -1
  52. package/package.json +8 -5
  53. package/src/automation/index.ts +1066 -128
  54. package/src/error.ts +23 -0
  55. package/src/generated/error.ts +1 -0
  56. package/src/generated/i18n.ts +10 -1
  57. package/src/generated/logic-web.d.ts +24 -3
  58. package/src/generated/logic.ts +830 -405
  59. package/src/index.ts +19 -10
  60. package/src/mocks.ts +62 -0
  61. package/src/page.ts +26 -0
  62. package/src/process.ts +14 -4
  63. package/src/testing/public-api.ts +155 -49
  64. package/dist/automation-test-globals.d.ts +0 -14
@@ -1,9 +1,20 @@
1
1
  /**
2
2
  * In-process UI/runtime automation — `lx.automation()`.
3
3
  *
4
- * Returns a stable selector root. Selecting the calling lxapp requires the
5
- * `automation` security privilege; cross-lxapp and host surfaces require
6
- * `host`. `lingxia dev` sessions and the Runner grant both implicitly.
4
+ * Two roots share one runtime object:
5
+ *
6
+ * - {@link Automation} is what app Logic may call. Selecting the calling lxapp
7
+ * requires the `automation` security privilege; cross-lxapp and host
8
+ * surfaces require `host`. A session holds one only when the privilege
9
+ * class is allowed for the app (the app registry, or its unrestricted
10
+ * default) and the native host seals a matching session grant — from a
11
+ * product `HostAddon`, or a devtools host such as the Runner, which grants
12
+ * the allowed automation privileges and nothing beyond them. Neither
13
+ * `lingxia dev` nor the Runner widens what app Logic is allowed.
14
+ * - {@link HostRunAutomation} is the root of a host automation run
15
+ * (`lxdev test`). That context carries the host's own authority, so every
16
+ * selector is available without a grant, and it adds the test-run-only
17
+ * members: `network`, nav `waitUntil: 'ready'`, and eval call tracing.
7
18
  *
8
19
  * This mirrors the devtool (`lxdev`) automation surface as a privilege-scoped,
9
20
  * product-side API.
@@ -13,30 +24,113 @@ import type { TerminalSettingsValue } from '../generated/logic.js';
13
24
 
14
25
  // ============================ factory ============================
15
26
 
16
- /** Stable automation root; it grants no capability until one is selected. */
27
+ // ============================ errors ============================
28
+
29
+ /**
30
+ * Stable `code`s of automation driver rejections. Anything without a more
31
+ * specific code rejects with `E_AUTOMATION`. Desktop codes describe OS
32
+ * automation failures. Match on the code, not the message.
33
+ */
34
+ export const AUTOMATION_ERROR_CODES = [
35
+ /** Fallback for a failure with no more specific code. */
36
+ 'E_AUTOMATION',
37
+ /** A scenario changed only partially or its outcome is unknown; the run is revoked. */
38
+ 'E_SCENARIO_STATE_UNKNOWN',
39
+ /** The caller lacks the `automation`/`host` privilege the call needs. */
40
+ 'E_AUTOMATION_PRIVILEGE',
41
+ /** The target page is not the active instance: not open, or replaced. `data: { page, instanceId?, current? }`. */
42
+ 'E_PAGE_NOT_ACTIVE',
43
+ /** The page has no WebView or current page to act on yet. */
44
+ 'E_PAGE_NOT_READY',
45
+ /** A page action (`page.action`) rejected without a code of its own. `data: { action, cause }`. */
46
+ 'E_PAGE_ACTION',
47
+ /** No element matched the selector at dispatch. */
48
+ 'E_ELEMENT_NOT_FOUND',
49
+ /** The element matched but cannot take the input (disabled, not editable, …). */
50
+ 'E_ELEMENT_NOT_INTERACTABLE',
51
+ /** A driver wait (`waitFor`, `waitUntil: 'ready'`, …) ran out of time. */
52
+ 'E_AUTOMATION_TIMEOUT',
53
+ /** The evaluated script threw. */
54
+ 'E_EVAL_SCRIPT',
55
+ /** The evaluation did not settle within its `timeoutMs`. */
56
+ 'E_EVAL_TIMEOUT',
57
+ /** Profile rollback outside an isolated run (`lxdev test --profile`), or for an lxapp the run does not isolate. */
58
+ 'E_PROFILE_NOT_ISOLATED',
59
+ /** A test clock call needs an installed clock and none is; an app that reopened is back on real time. */
60
+ 'E_CLOCK_NOT_INSTALLED',
61
+ /** `clock.install()` while this lxapp's Logic already runs on a test clock. */
62
+ 'E_CLOCK_INSTALLED',
63
+ 'E_DESKTOP_USAGE',
64
+ 'E_DESKTOP_NOT_FOUND',
65
+ 'E_DESKTOP_AMBIGUOUS',
66
+ 'E_DESKTOP_TIMEOUT',
67
+ 'E_DESKTOP_PERMISSION',
68
+ 'E_DESKTOP_UNSUPPORTED',
69
+ 'E_DESKTOP_UNAVAILABLE',
70
+ 'E_DESKTOP_STALE',
71
+ 'E_DESKTOP_FAILED',
72
+ ] as const;
73
+
74
+ export type AutomationErrorCode = (typeof AUTOMATION_ERROR_CODES)[number];
75
+
76
+ /** A page as automation error `data` names it. */
77
+ export interface AutomationPageRef {
78
+ /** Configured page name, else its path. */
79
+ page?: string;
80
+ /** The page instance the call resolved to, when it still resolves. */
81
+ instanceId?: string;
82
+ /** The page that was current when the call failed. */
83
+ current?: { name: string | null; path: string; instanceId: string };
84
+ }
85
+
86
+ /**
87
+ * Automation root as app Logic sees it (`lx.automation()` in Logic). It grants
88
+ * no capability until one is selected. Reading a driver property never throws:
89
+ * each driver method checks the caller and rejects with
90
+ * `E_AUTOMATION_PRIVILEGE` when it is not allowed.
91
+ */
17
92
  export interface Automation {
18
- /** Drive the calling/current lxapp. Requires `automation` outside dev. */
19
- lxapp(): LxAppDriver;
20
- /** Drive a specific running lxapp. Requires `host` outside dev. */
21
- lxapp(appid: string): LxAppDriver;
22
- /** Cross-lxapp lifecycle and host-window capture. */
93
+ /**
94
+ * Drive the calling lxapp. Requires the `automation` privilege. Evaluating
95
+ * the calling Logic runtime itself (`eval`) rejects.
96
+ */
97
+ lxapp(): LogicLxAppDriver;
98
+ /** Drive a specific running lxapp. Requires the `host` privilege. */
99
+ lxapp(appid: string): LogicLxAppDriver;
100
+ /** Cross-lxapp lifecycle and host-window capture. Requires `host`. */
23
101
  readonly lxapps: LxAppManager;
24
- /** The host app's browser tabs. */
102
+ /** The host app's browser tabs. Requires `host`. */
25
103
  readonly browser: BrowserDriver;
26
- /** Persisted host-shell shortcuts for deterministic test setup/assertion. */
104
+ /** Persisted host-shell shortcuts for deterministic test setup/assertion. Requires `host`. */
27
105
  readonly shell: ShellDriver;
28
- /** Simulated-device selection in a host runner. */
106
+ /** Simulated-device selection in a host runner. Requires `host`. */
29
107
  readonly device: DeviceDriver;
30
108
  /**
31
- * Session-less local-OS desktop automation (`lxdev desktop`). Beyond the
32
- * app sandbox, so restricted to dev/test hosts (`lingxia dev` or the
33
- * Runner) on top of the `host` privilege. Windows/macOS only.
109
+ * Session-less local-OS desktop automation (`lxdev desktop`). Windows/macOS
110
+ * only. Gated by the `host` privilege alone, and present only in hosts
111
+ * built with desktop automation (the Runner, or a product that enables the
112
+ * `desktop-automation` feature); elsewhere reading it throws. It drives the
113
+ * whole OS, beyond the app sandbox — grant `host` only to lxapps you trust
114
+ * with that.
34
115
  */
35
116
  readonly desktop: DesktopDriver;
36
- /** Native terminal workspace state and pane actions in trusted dev/test hosts. */
117
+ /** Native terminal workspace state and pane actions. Requires `host`. */
37
118
  readonly terminal: TerminalDriver;
38
119
  }
39
120
 
121
+ /**
122
+ * Automation root of a host automation run — `rawAutomation()` (and, traced,
123
+ * `t.automation`) from `@lingxia/test` in an `lxdev test` program. The run
124
+ * carries host authority, so no selector needs a privilege grant, and the
125
+ * selected lxapp driver adds the test-run-only members.
126
+ */
127
+ export interface HostRunAutomation extends Automation {
128
+ /** Drive the host's current lxapp. */
129
+ lxapp(): LxAppDriver;
130
+ /** Drive a specific running lxapp. */
131
+ lxapp(appid: string): LxAppDriver;
132
+ }
133
+
40
134
  // ========================== terminal tier ==========================
41
135
 
42
136
  export type TerminalSplitDirection = 'left' | 'right' | 'up' | 'down';
@@ -114,7 +208,10 @@ export interface TerminalSplitOptions extends TerminalSurfaceRef {
114
208
  direction: TerminalSplitDirection;
115
209
  }
116
210
 
117
- /** Native terminal automation; available only to trusted dev/test hosts. */
211
+ /**
212
+ * Native terminal automation. Requires the `host` privilege from app Logic
213
+ * (a host automation run needs none) and a host with a native terminal.
214
+ */
118
215
  export interface TerminalDriver {
119
216
  snapshot(options: TerminalSurfaceRef): Promise<TerminalWorkspaceSnapshot>;
120
217
  /** Send text to the focused pane, as if typed into its PTY. */
@@ -162,6 +259,8 @@ export type SetAutomationShellPinOptions = AutomationShellPin & {
162
259
  export interface ShellDriver {
163
260
  /** Ordered shortcuts exactly as projected into the host sidebar. */
164
261
  pins(): Promise<AutomationShellPin[]>;
262
+ /** Submit every current Pin exactly once, in the desired mixed order. */
263
+ reorderPins(items: AutomationShellPin[]): Promise<AutomationShellPin[]>;
165
264
  /**
166
265
  * Idempotently persist or remove one shortcut. New Pins append to the
167
266
  * existing order; adding beyond the host limit rejects without mutation.
@@ -174,7 +273,7 @@ export interface ShellDriver {
174
273
 
175
274
  /** Fields common to every page action; `page` defaults to the current page. */
176
275
  export interface PageTarget {
177
- /** Configured page name (from lxapp.json); defaults to the current page. */
276
+ /** Configured page name or live instance_id; defaults to the current page. */
178
277
  page?: string;
179
278
  }
180
279
 
@@ -184,6 +283,15 @@ export interface PageEvalOptions extends PageTarget {
184
283
  timeoutMs?: number;
185
284
  }
186
285
 
286
+ export interface PageActionOptions extends PageTarget {
287
+ /** Name of a unary action of the page's `Page({...})`; stream (generator) actions are refused. */
288
+ name: string;
289
+ /** The action's one JSON argument, passed as is; omit for none. */
290
+ payload?: unknown;
291
+ /** Budget for the page to become ready and the action to settle (default 5000). */
292
+ timeoutMs?: number;
293
+ }
294
+
187
295
  export interface PageQueryOptions extends PageTarget {
188
296
  /** CSS selector. */
189
297
  css: string;
@@ -207,6 +315,20 @@ export interface PageTypeOptions extends PageSelectorOptions {
207
315
  text: string;
208
316
  }
209
317
 
318
+ export interface PageClickOptions extends PageSelectorOptions {
319
+ /**
320
+ * Dispatch to the element itself, without the in-viewport and hit-test
321
+ * checks; it must still exist and be enabled. For content no scroll can
322
+ * bring under the pointer, such as the lower part of an overflowing sheet.
323
+ */
324
+ force?: boolean;
325
+ }
326
+
327
+ export interface PageFillOptions extends PageTypeOptions {
328
+ /** Write without the in-viewport check; see `PageClickOptions.force`. */
329
+ force?: boolean;
330
+ }
331
+
210
332
  export interface PagePressOptions extends PageTarget {
211
333
  /** Key name, e.g. `Enter`, `Escape`, `Tab`. */
212
334
  key: string;
@@ -228,23 +350,31 @@ export interface PageScrollToOptions extends PageTarget {
228
350
  css: string;
229
351
  }
230
352
 
353
+ /**
354
+ * Raw `page.waitFor` states check the first match of `css`:
355
+ * - `attached`: at least one match.
356
+ * - `detached`: no match (also while the page is not yet active).
357
+ * - `visible`: the first match is rendered (a non-empty box, not
358
+ * `display:none`, `visibility:hidden` or `opacity:0`), in the viewport or
359
+ * scrolled out of it.
360
+ * - `hidden`: the first match exists and is not rendered; no match does not
361
+ * satisfy it (wait for `detached`).
362
+ * - `enabled` / `editable`: the first match exists and is enabled / editable.
363
+ * `@lingxia/test` locators apply stricter, uniqueness-aware states.
364
+ */
231
365
  export type PageWaitState =
232
366
  | 'attached'
233
367
  | 'detached'
234
368
  | 'visible'
235
369
  | 'hidden'
236
370
  | 'enabled'
237
- | 'editable'
238
- /** @deprecated Use `attached`. */
239
- | 'exists'
240
- /** @deprecated Use `detached`. */
241
- | 'gone';
371
+ | 'editable';
242
372
 
243
373
  export interface PageWaitForOptions extends PageTarget {
244
374
  css: string;
245
375
  /** Condition to await (default `visible`). */
246
376
  state?: PageWaitState;
247
- /** Timeout in ms (default 10000, capped at 60000). */
377
+ /** Timeout in ms (default 30000, capped at 60000). */
248
378
  timeoutMs?: number;
249
379
  }
250
380
 
@@ -261,13 +391,13 @@ export interface ElementRect {
261
391
  height: number;
262
392
  right: number;
263
393
  bottom: number;
264
- center_x: number;
265
- center_y: number;
266
- viewport_width: number;
267
- viewport_height: number;
394
+ centerX: number;
395
+ centerY: number;
396
+ viewportWidth: number;
397
+ viewportHeight: number;
268
398
  }
269
399
 
270
- /** A matched element. Keys are the raw automation payload (snake_case). */
400
+ /** A matched element. Uses JavaScript field names. */
271
401
  export interface PageElement {
272
402
  exists: true;
273
403
  index: number;
@@ -279,16 +409,21 @@ export interface PageElement {
279
409
  id: string | null;
280
410
  name: string | null;
281
411
  role: string | null;
282
- aria_label: string | null;
412
+ ariaLabel: string | null;
283
413
  placeholder: string | null;
284
- /** Viewport-aware visibility (size, style, and in-viewport). */
414
+ /**
415
+ * Rendered: a non-empty box that is not `display:none`,
416
+ * `visibility:hidden` or `opacity:0`, wherever it is scrolled.
417
+ */
285
418
  visible: boolean;
419
+ /** Rendered and intersecting the viewport. */
420
+ inViewport: boolean;
286
421
  enabled: boolean;
287
422
  editable: boolean;
288
423
  text: string;
289
- text_truncated: boolean;
424
+ textTruncated: boolean;
290
425
  value: string | null;
291
- value_truncated: boolean;
426
+ valueTruncated: boolean;
292
427
  rect: ElementRect;
293
428
  }
294
429
 
@@ -298,6 +433,7 @@ export interface PageElementMiss {
298
433
  index: number;
299
434
  count: number;
300
435
  visible: false;
436
+ inViewport: false;
301
437
  enabled: false;
302
438
  editable: false;
303
439
  }
@@ -320,17 +456,27 @@ export interface Screenshot {
320
456
 
321
457
  /** Element-level automation of the selected lxapp's page WebViews. */
322
458
  export interface PageDriver {
323
- /** Evaluate JavaScript in the page WebView; resolves to the returned value. */
324
- eval(options: PageEvalOptions): Promise<unknown>;
459
+ /** Evaluate in the page WebView. T describes the expected JSON result; it is not runtime validation. */
460
+ eval<T = unknown>(options: PageEvalOptions): Promise<T>;
461
+ /**
462
+ * Invoke a page action as the View would and resolve to its JSON result.
463
+ * Waits for the page's runtime within `timeoutMs`. A rejection keeps the
464
+ * action's own `code` and message (`data.cause` has what it rejected with);
465
+ * `E_PAGE_NOT_READY` if the page never loaded its actions,
466
+ * `E_AUTOMATION_TIMEOUT` if the action did not settle in time.
467
+ */
468
+ action<T = unknown>(options: PageActionOptions): Promise<T>;
325
469
  /** Query one element's info. */
326
470
  query(options: PageQueryOptions & { all?: false }): Promise<PageQueryResult>;
327
471
  /** Query every matching element. */
328
472
  query(options: PageQueryOptions & { all: true }): Promise<PageQueryAll>;
329
- click(options: PageSelectorOptions): Promise<void>;
473
+ query(options: PageQueryOptions): Promise<PageQueryResult | PageQueryAll>;
474
+ /** Single dispatch; test locators provide actionability waiting. */
475
+ click(options: PageClickOptions): Promise<void>;
330
476
  /** Type text into an element without clearing existing content. */
331
477
  type(options: PageTypeOptions): Promise<void>;
332
478
  /** Replace an element's current value. */
333
- fill(options: PageTypeOptions): Promise<void>;
479
+ fill(options: PageFillOptions): Promise<void>;
334
480
  press(options: PagePressOptions): Promise<void>;
335
481
  /** Scroll the first matching element into view. */
336
482
  scrollTo(options: PageScrollToOptions): Promise<void>;
@@ -347,56 +493,104 @@ export interface PageDriver {
347
493
 
348
494
  // ============================ nav tier ============================
349
495
 
350
- export interface NavOptions {
496
+ /**
497
+ * When a nav action resolves. `'commit'` (default) resolves once the page
498
+ * stack changed, before the landed page is ready. `'ready'` also waits for its
499
+ * `onReady` and rejects if the page is disposed or replaced first (e.g. by the
500
+ * app's own `lx.reLaunch`); it is available only in a host automation run
501
+ * ({@link NavDriver}), since app Logic awaiting its own `onReady` would
502
+ * deadlock. The `@lingxia/test` fixture's `t.app.nav` defaults to `'ready'`.
503
+ */
504
+ export type NavWaitUntil = 'commit' | 'ready';
505
+
506
+ /** Nav wait options available to app Logic: only `'commit'`. */
507
+ export interface LogicNavWaitOptions {
508
+ waitUntil?: 'commit';
509
+ }
510
+
511
+ /** Nav wait options of a host automation run. */
512
+ export interface NavWaitOptions {
513
+ waitUntil?: NavWaitUntil;
514
+ /** Bound for `waitUntil: 'ready'` in ms (default 15000, capped at 60000). */
515
+ timeoutMs?: number;
516
+ }
517
+
518
+ interface NavTargetFields {
351
519
  /** Configured page name (from lxapp.json). */
352
520
  page: string;
353
521
  /** Query forwarded to the destination page. */
354
522
  query?: Record<string, unknown>;
355
523
  }
356
524
 
357
- export interface NavBackOptions {
525
+ interface NavBackFields {
358
526
  /** Number of pages to pop (default 1). */
359
527
  delta?: number;
360
528
  }
361
529
 
530
+ export interface LogicNavOptions extends NavTargetFields, LogicNavWaitOptions {}
531
+ export interface LogicNavBackOptions extends NavBackFields, LogicNavWaitOptions {}
532
+ export interface NavOptions extends NavTargetFields, NavWaitOptions {}
533
+ export interface NavBackOptions extends NavBackFields, NavWaitOptions {}
534
+
362
535
  /** A page's runtime position. */
363
536
  export interface PageInfo {
364
537
  path: string;
365
538
  /** Configured page name, if the path maps to one. */
366
539
  name: string | null;
540
+ /**
541
+ * The page instance. Navigation that replaces a page (even with the same
542
+ * path) gives it a new id. `null` when no live instance backs a stack
543
+ * entry.
544
+ */
545
+ instanceId: string | null;
367
546
  current: boolean;
368
547
  inStack: boolean;
369
- /** Whether the page has an attached WebView. */
548
+ /** Whether the page has dispatched `onReady` (what `waitUntil: 'ready'` awaits). */
370
549
  ready: boolean;
550
+ /** Whether the page currently has an attached WebView; precedes `ready`. */
551
+ webviewAttached: boolean;
371
552
  }
372
553
 
373
554
  /**
374
- * Page-stack navigation for the selected lxapp. Action verbs take a configured
375
- * page name (`redirect` rejects a tab-bar page); `back` pops; `current`/`stack`
376
- * read. Unlike the JS `lx.navigateTo` family this returns the landed page, but
377
- * like it does not wait for the destination WebView (awaiting in-process would
378
- * deadlock the logic thread).
555
+ * Page-stack navigation for the selected lxapp, as app Logic sees it. Action
556
+ * verbs take a configured page name (`redirect` rejects a tab-bar page);
557
+ * `back` pops; `current`/`stack` read. Unlike the JS `lx.navigateTo` family
558
+ * this returns the landed page. It resolves once the stack changed, before the
559
+ * destination is ready.
379
560
  */
380
- export interface NavDriver {
561
+ export interface LogicNavDriver {
381
562
  /** Push a page onto the stack. */
382
- to(options: NavOptions): Promise<PageInfo>;
563
+ to(options: LogicNavOptions): Promise<PageInfo>;
383
564
  /** Replace the current page (rejects tab-bar targets). */
384
- redirect(options: NavOptions): Promise<PageInfo>;
565
+ redirect(options: LogicNavOptions): Promise<PageInfo>;
385
566
  /** Switch to a configured tab page. */
386
- switchTab(options: NavOptions): Promise<PageInfo>;
387
- /** Clear the stack and relaunch at a page. */
388
- relaunch(options: NavOptions): Promise<PageInfo>;
389
- back(options?: NavBackOptions): Promise<PageInfo>;
567
+ switchTab(options: LogicNavOptions): Promise<PageInfo>;
568
+ /** Unload every page (cached tab pages included) and open a fresh instance of a page. */
569
+ relaunch(options: LogicNavOptions): Promise<PageInfo>;
570
+ back(options?: LogicNavBackOptions): Promise<PageInfo>;
390
571
  current(): Promise<PageInfo>;
391
572
  /** Status of a configured page by name; omit `page` for the current page. */
392
573
  info(options?: PageTarget): Promise<PageInfo>;
393
574
  stack(): Promise<PageInfo[]>;
394
575
  }
395
576
 
577
+ /**
578
+ * Page-stack navigation in a host automation run. Same verbs as
579
+ * {@link LogicNavDriver}; pass `waitUntil: 'ready'` to also wait for the
580
+ * landed page's `onReady`.
581
+ */
582
+ export interface NavDriver extends LogicNavDriver {
583
+ to(options: NavOptions): Promise<PageInfo>;
584
+ redirect(options: NavOptions): Promise<PageInfo>;
585
+ switchTab(options: NavOptions): Promise<PageInfo>;
586
+ relaunch(options: NavOptions): Promise<PageInfo>;
587
+ back(options?: NavBackOptions): Promise<PageInfo>;
588
+ }
589
+
396
590
  // ============================ lxapp driver ============================
397
591
 
398
592
  export interface LxAppSummary {
399
- appid: string;
593
+ appId: string;
400
594
  currentPage: string | null;
401
595
  }
402
596
 
@@ -405,21 +599,39 @@ export interface LxAppPageConfig {
405
599
  path: string;
406
600
  }
407
601
 
408
- export interface LxAppEvalOptions {
602
+ /**
603
+ * Envelope `eval` resolves to with `captureCalls: true`.
604
+ *
605
+ * @internal Report plumbing for the test runner; the `@lingxia/test` fixture
606
+ * unwraps it, and specs never see it.
607
+ */
608
+ export interface LxAppEvalTrace<T = unknown> {
609
+ __lxEval: 1;
610
+ value: T;
611
+ calls: string[];
612
+ }
613
+
614
+ /** Logic-runtime eval options available to app Logic. */
615
+ export interface LogicLxAppEvalOptions {
409
616
  /** JavaScript expression or function body run in the selected Logic runtime. */
410
617
  script: string;
411
618
  timeoutMs?: number;
619
+ }
620
+
621
+ /** Logic-runtime eval options of a host automation run. */
622
+ export interface LxAppEvalOptions extends LogicLxAppEvalOptions {
412
623
  /**
413
624
  * Resolve to `{ value, calls }`, where `calls` lists the `lx.*` members the
414
625
  * script reached, instead of the bare value.
415
626
  *
416
- * The test runner sets this so a report can tell a capability a spec
417
- * exercised from one it merely declared. Only the evaluated script is
627
+ * @internal The test runner sets this so a report can tell a capability a
628
+ * spec exercised from one it merely declared. Only the evaluated script is
418
629
  * observed — the lxapp's own concurrent work is not.
419
630
  */
420
631
  captureCalls?: boolean;
421
632
  }
422
633
 
634
+ /** Shell admission class. Content `lx.surface.watchContext` uses `compact` | `regular`. */
423
635
  export type SurfaceLayoutSizeClass = 'compact' | 'medium' | 'expanded';
424
636
  export type SurfaceLayoutSwitcherForm = 'none' | 'sidebar' | 'rail';
425
637
  export type SurfaceLayoutSplitForm = 'none' | 'split' | 'collapsible' | 'fullScreen';
@@ -517,18 +729,711 @@ export interface SurfaceLayoutSnapshot {
517
729
  tree?: SurfaceLayoutTree;
518
730
  }
519
731
 
520
- /** Capability for one selected running lxapp. */
521
- export interface LxAppDriver {
732
+ // ======================= test network routing =======================
733
+
734
+ /**
735
+ * Which Logic `fetch` requests a route handles: a URL glob over the whole URL
736
+ * (`**` any characters, `*` any except `/`, `{a,b}` alternatives; `?` is
737
+ * literal), a `RegExp` searched like `RegExp.test` (Rust regex syntax: no
738
+ * lookaround or backreferences), or either plus a method and a match budget.
739
+ */
740
+ export type NetworkRoutePattern =
741
+ | string
742
+ | RegExp
743
+ | {
744
+ url: string | RegExp;
745
+ /** Case-insensitive HTTP method; omit or `'*'` for any. */
746
+ method?: string;
747
+ /** Remove the route after this many matched requests. */
748
+ times?: number;
749
+ };
750
+
751
+ type NetworkRouteFulfillKey =
752
+ | 'status'
753
+ | 'statusText'
754
+ | 'headers'
755
+ | 'contentType'
756
+ | 'body'
757
+ | 'json'
758
+ | 'delay';
759
+ type NetworkRouteForbid<K extends string> = Partial<Record<K, never>>;
760
+
761
+ interface NetworkRouteFulfillFields {
762
+ /** 200..=599. Default 200. */
763
+ status?: number;
764
+ statusText?: string;
765
+ headers?: Record<string, string>;
766
+ /** Sets `content-type` unless `headers` already has one. */
767
+ contentType?: string;
768
+ /**
769
+ * Milliseconds to wait before the response resolves, 0..=30000; an
770
+ * `AbortSignal` passed to `fetch` still rejects it early.
771
+ */
772
+ delay?: number;
773
+ }
774
+
775
+ /**
776
+ * Answer the request without touching the network. The body is either `body`
777
+ * (text or bytes, sent verbatim) or `json` (serialized, implies
778
+ * `content-type: application/json`), never both.
779
+ */
780
+ export type NetworkRouteFulfill = NetworkRouteFulfillFields &
781
+ NetworkRouteForbid<'abort' | 'continue' | 'patchJson' | 'hang' | 'sse' | 'sequence'> &
782
+ (
783
+ | { body?: string | ArrayBuffer | Uint8Array; json?: never }
784
+ | { json: unknown; body?: never }
785
+ );
786
+
787
+ /**
788
+ * Reject the `fetch` like a transport failure (`TypeError: fetch failed`).
789
+ * `'failed'` is the only failure the wrapper emulates.
790
+ */
791
+ export type NetworkRouteAbort = { abort: 'failed' } &
792
+ NetworkRouteForbid<NetworkRouteFulfillKey | 'continue' | 'patchJson' | 'hang' | 'sse' | 'sequence'>;
793
+
794
+ /**
795
+ * Let the request reach the network, shadowing older matching routes. With
796
+ * `patchJson`, the real response's JSON body is rewritten host-side by that
797
+ * RFC 7396 merge patch: object keys merge, `null` deletes a key, and any
798
+ * other value (arrays included) replaces what it patches. Status and headers
799
+ * stay the real ones. A body that is not JSON rejects the `fetch` with a
800
+ * `TypeError` naming the URL; an empty body passes through. The patched
801
+ * body is re-serialized, so object keys come back sorted.
802
+ */
803
+ export type NetworkRouteContinue = { continue: true; patchJson?: unknown } &
804
+ NetworkRouteForbid<NetworkRouteFulfillKey | 'abort' | 'hang' | 'sse' | 'sequence'>;
805
+
806
+ /**
807
+ * Never answer: the `fetch` stays pending until the route is removed (the
808
+ * spec ends, `unroute()`, or the run ends), then rejects like a transport
809
+ * failure. An `AbortSignal` passed to `fetch` still rejects it early. For
810
+ * loading states and client-side timeouts. `times` limits which requests are
811
+ * held, not how long.
812
+ */
813
+ export type NetworkRouteHang = { hang: true } &
814
+ NetworkRouteForbid<NetworkRouteFulfillKey | 'abort' | 'continue' | 'patchJson' | 'sse' | 'sequence'>;
815
+
816
+ /**
817
+ * One item of an SSE answer, played in order: an event (`data` that is not a
818
+ * string is sent as JSON; multi-line data becomes several `data:` lines), a
819
+ * `comment` line, a pause of `delayMs` (0..=30000), or `drop`, which closes
820
+ * the stream as a server dropping the connection would and must come last.
821
+ */
822
+ export type NetworkSseItem =
823
+ | { event?: string; data: unknown; id?: string; retry?: number }
824
+ | { comment: string }
825
+ | { delayMs: number }
826
+ | { drop: true };
827
+
828
+ /**
829
+ * Answer with a `text/event-stream` (status 200) that plays `sse`. Without a
830
+ * final `{ drop: true }` the stream stays open after its last item, like a
831
+ * live server, until the route is removed. `Rong.SSE` in Logic receives the
832
+ * events and, after a drop, reconnects with `Last-Event-ID`, which the next
833
+ * answer of a `sequence` can serve. `delay` (0..=30000 ms) holds the response
834
+ * before it opens.
835
+ */
836
+ export type NetworkRouteSse = {
837
+ sse: NetworkSseItem[];
838
+ headers?: Record<string, string>;
839
+ delay?: number;
840
+ } & NetworkRouteForbid<
841
+ 'status' | 'statusText' | 'contentType' | 'body' | 'json' | 'abort' | 'continue' | 'patchJson' | 'hang' | 'sequence'
842
+ >;
843
+
844
+ /** Exactly one of fulfill, abort, continue, hang, or sse. */
845
+ export type NetworkRouteAnswer =
846
+ | NetworkRouteFulfill
847
+ | NetworkRouteAbort
848
+ | NetworkRouteContinue
849
+ | NetworkRouteHang
850
+ | NetworkRouteSse;
851
+
852
+ /**
853
+ * Answers served in call order: the first matched request gets the first
854
+ * answer, and the last answer repeats. Answers take text or `json` bodies,
855
+ * not bytes. Combine with a pattern's `times` to stop matching.
856
+ */
857
+ export type NetworkRouteSequence = { sequence: NetworkRouteAnswer[] } &
858
+ NetworkRouteForbid<NetworkRouteFulfillKey | 'abort' | 'continue' | 'patchJson' | 'hang' | 'sse'>;
859
+
860
+ /**
861
+ * One answer, or a `sequence` of them. String values may contain
862
+ * relative-time templates rendered each time the answer is served:
863
+ * `{{now}}`, `{{now-2h}}`, `{{now+30m}}` (ISO-8601 UTC, like
864
+ * `Date.prototype.toISOString`; units `ms`, `s`, `m`, `h`, `d`) and
865
+ * `{{nowMs}}` (epoch milliseconds, as text).
866
+ */
867
+ export type NetworkRouteHandler = NetworkRouteAnswer | NetworkRouteSequence;
868
+
869
+ /** `bodyBase64` answers: bytes in a file, instead of `body` or `json`. */
870
+ export type ScenarioHttpBinary = NetworkRouteFulfillFields & { bodyBase64: string } &
871
+ NetworkRouteForbid<'body' | 'json'>;
872
+
873
+ /**
874
+ * A `match` value: objects match when every listed key is present and
875
+ * matches (other keys are ignored), arrays match element by element with the
876
+ * same length, a `"/regex/flags"` string (flags among `imsu`) matches a
877
+ * scalar whose text it finds, and any other value matches an equal value.
878
+ */
879
+ export type ScenarioMatchValue = unknown;
880
+
881
+ /** Fields every rule may have. */
882
+ type ScenarioRuleCommon = {
883
+ /** Answer this many matching calls, then stand aside. */
884
+ times?: number;
885
+ /** Free text, ignored. */
886
+ note?: string;
887
+ };
888
+
889
+ /**
890
+ * A rule for the app's Logic `fetch` / `Rong.SSE`: `http` is
891
+ * `"METHOD url-glob"` (`*` for any method; the URL may be `/regex/flags`),
892
+ * and the answer is a route handler (or `bodyBase64`).
893
+ */
894
+ export type ScenarioHttpRule = ScenarioRuleCommon & {
895
+ http: string;
896
+ function?: never;
897
+ /** Match the request's JSON body. */
898
+ match?: { json: ScenarioMatchValue };
899
+ } & (NetworkRouteHandler | ScenarioHttpBinary);
900
+
901
+ /** How a `function` rule answers one call. */
902
+ export type ScenarioFunctionAnswer = { delay?: number } & (
903
+ | { result: unknown; error?: never; fault?: never }
904
+ | { error: { code: string; [key: string]: unknown }; result?: never; fault?: never }
905
+ | { fault: 'notRun' | 'unknown'; result?: never; error?: never }
906
+ );
907
+
908
+ /**
909
+ * A rule for a Worker Function call, answered by the dev session's
910
+ * companion: `result`, a declared `error`, or a transport `fault`.
911
+ */
912
+ export type ScenarioFunctionRule = ScenarioRuleCommon & {
913
+ function: string;
914
+ http?: never;
915
+ /** Match the call's arguments. */
916
+ match?: { args: ScenarioMatchValue };
917
+ } & (
918
+ | (ScenarioFunctionAnswer & { sequence?: never })
919
+ | { sequence: ScenarioFunctionAnswer[]; result?: never; error?: never; fault?: never; delay?: never }
920
+ );
921
+
922
+ export type ScenarioRule = ScenarioHttpRule | ScenarioFunctionRule;
923
+
924
+ /**
925
+ * A scenario file: rules tried in file order (the first match answers;
926
+ * nothing matched goes to the real backend), and named variants whose rules
927
+ * go before the shared ones. Unknown fields are rejected.
928
+ */
929
+ export type ScenarioDefinition = {
930
+ $schema?: string;
931
+ name?: string;
932
+ description?: string;
933
+ rules?: ScenarioRule[];
934
+ variants?: Record<string, { description?: string; rules: ScenarioRule[] }>;
935
+ };
936
+
937
+ /**
938
+ * What `scenario()` accepts: a typed definition, or an imported JSON file,
939
+ * whose literal types TypeScript widens. Either is validated by the host.
940
+ */
941
+ export type ScenarioInput =
942
+ | ScenarioDefinition
943
+ | { readonly rules: readonly object[]; readonly [key: string]: unknown }
944
+ | { readonly variants: { readonly [variant: string]: { readonly rules: readonly object[] } }; readonly [key: string]: unknown };
945
+
946
+ /** One rule of an installed scenario. */
947
+ export interface ScenarioRuleInfo {
948
+ /** 1-based, in precedence order (the variant's rules first). */
949
+ index: number;
950
+ /** `GET **\/wifi/main`, `function orders.submit`. */
951
+ target: string;
952
+ kind: 'http' | 'function';
953
+ /** Calls it answered (`http` rules); `null` for `function` rules. */
954
+ hits: number | null;
955
+ }
956
+
957
+ /** One call that reached a scenario. */
958
+ export interface ScenarioCall {
959
+ /**
960
+ * Increasing and never reused within its log: the scenario's for `http`
961
+ * calls, the dev companion's for `function` calls. Calls in one
962
+ * millisecond differ by it.
963
+ */
964
+ seq: number;
965
+ /** Epoch milliseconds. */
966
+ time: number;
967
+ kind: 'http' | 'function';
968
+ /** `http`: upper-case method and URL. */
969
+ method?: string;
970
+ url?: string;
971
+ /** `http`: the request body, parsed when it is JSON. */
972
+ body?: unknown;
973
+ /** `http`: the answered status, `null` for abort/continue/hang or no answer. */
974
+ status?: number | null;
975
+ /** `function`: its name, arguments, and `result`/`error`/`fault`/`default`. */
976
+ function?: string;
977
+ args?: unknown;
978
+ outcome?: string;
979
+ /** The rule that answered, or `null` when none did. */
980
+ rule: number | null;
981
+ /** `rule 2 (name:variant)`, `real`, or `companion default`. */
982
+ answeredBy: string;
983
+ /** Why no rule matched, when rules targeted the call. */
984
+ noMatch?: string;
985
+ }
986
+
987
+ /** `calls()` filter: one rule's target, or its number. */
988
+ export type ScenarioCallFilter = { http: string } | { function: string } | { rule: number };
989
+
990
+ /**
991
+ * Calls of one bounded log with a `seq` above the one asked for, oldest
992
+ * first. The logs drop their oldest entries first: `droppedThrough` is the
993
+ * highest `seq` a trim took (0 when none), so a reader whose last `seq` is
994
+ * below it may have missed calls.
995
+ */
996
+ export interface CallWindow<T> {
997
+ calls: T[];
998
+ droppedThrough: number;
999
+ }
1000
+
1001
+ /** `mock.reset()` result. */
1002
+ export interface MockResetResult {
1003
+ /** The app's new handler generation; `null` when it has no mocks loaded. */
1004
+ generation: number | null;
1005
+ /**
1006
+ * The companion's answer for the run's Functions, `null` when it does not
1007
+ * switch mocks.
1008
+ */
1009
+ function: { reset: boolean; reason?: string } | null;
1010
+ }
1011
+
1012
+ /** `lx.automation().lxapp().mock` in a host run. */
1013
+ export interface MockDriver {
1014
+ /**
1015
+ * Install a scenario file (with one of its variants) for the host run:
1016
+ * `http` rules answer Logic `fetch` before the mock selection, `function`
1017
+ * rules go to the dev session's companion. Validated as a whole; it
1018
+ * replaces the scenario the run installed before (including another app), and the
1019
+ * run's end removes it. Routes added with `network.route()` take
1020
+ * precedence over it.
1021
+ */
1022
+ use(definition: ScenarioInput, variant?: string): Promise<Scenario>;
1023
+ /**
1024
+ * Start the app's mock handler state over: the next intercepted call
1025
+ * evaluates `mocks/index.ts` again. A companion that switches mocks is
1026
+ * asked to do the same for the run.
1027
+ */
1028
+ reset(): Promise<MockResetResult>;
1029
+ }
1030
+
1031
+ /** Handle returned by `mock.use()`. */
1032
+ export interface Scenario {
1033
+ readonly name: string | null;
1034
+ readonly variant: string | null;
1035
+ /** Its rules with their hit counts, read when accessed. */
1036
+ readonly rules: ScenarioRuleInfo[];
1037
+ /** Calls that reached the scenario since it was installed, oldest first. */
1038
+ calls(filter?: ScenarioCallFilter): Promise<ScenarioCall[]>;
1039
+ /**
1040
+ * Calls to one target with a `seq` above `after`. `droppedThrough` counts
1041
+ * the log the target's kind reads (HTTP: the scenario's 200 calls;
1042
+ * Function: the companion's log, across every Function).
1043
+ */
1044
+ callsAfter(target: ScenarioCallFilter, after: number): Promise<CallWindow<ScenarioCall>>;
1045
+ /** Remove the scenario; resolves how many rules were still installed. */
1046
+ unroute(): Promise<number>;
1047
+ }
1048
+
1049
+ /** One request a route handled, as the app sent it. */
1050
+ export interface NetworkRouteRequest {
1051
+ /**
1052
+ * Increasing across the host's request log and never reused: requests in
1053
+ * one millisecond differ by it, and a trim leaves a gap.
1054
+ */
1055
+ seq: number;
1056
+ routeId: number;
1057
+ /** The route's glob or `/source/flags`. */
1058
+ pattern: string;
1059
+ /** Upper-case method. */
1060
+ method: string;
1061
+ url: string;
1062
+ /** Request headers with lower-case names. */
1063
+ headers: Record<string, string>;
1064
+ /**
1065
+ * Request body as UTF-8 text, cut to 64 KiB. `null` when there is none or
1066
+ * it cannot be read without consuming it: a stream, `Blob`, `FormData`, or
1067
+ * the body of a `Request` object passed as `fetch`'s first argument.
1068
+ */
1069
+ body: string | null;
1070
+ /** `body` was cut to the 64 KiB (65536-byte) limit. */
1071
+ bodyTruncated: boolean;
1072
+ /**
1073
+ * `continue` also covers a `patchJson` pass-through; `fulfill` also covers
1074
+ * an `sse` answer.
1075
+ */
1076
+ action: 'fulfill' | 'abort' | 'continue' | 'hang';
1077
+ /** Fulfilled status (200 for `sse`); `null` for abort/continue/hang. */
1078
+ status: number | null;
1079
+ /** Epoch milliseconds. */
1080
+ timestamp: number;
1081
+ }
1082
+
1083
+ export interface NetworkRoute {
1084
+ readonly id: number;
1085
+ readonly pattern: string;
1086
+ /** Resolves `false` when the route already expired or was removed. */
1087
+ unroute(): Promise<boolean>;
1088
+ /** Requests this route handled, oldest first. */
1089
+ requests(): Promise<NetworkRouteRequest[]>;
1090
+ /**
1091
+ * Requests this route handled with a `seq` above `after`; `droppedThrough`
1092
+ * is the highest `seq` of this route's requests the host log (1000
1093
+ * entries, 16 MiB of bodies, shared by every app and run) dropped.
1094
+ */
1095
+ requestsAfter(after: number): Promise<{ requests: NetworkRouteRequest[]; droppedThrough: number }>;
1096
+ }
1097
+
1098
+ /**
1099
+ * One Logic `fetch` response recorded by `NetworkDriver.captureResponses()`.
1100
+ * Headers are never recorded, and `url` keeps only scheme, host and path.
1101
+ *
1102
+ * @internal Test-runner plumbing for `lxdev test --openapi`.
1103
+ */
1104
+ export interface NetworkResponseRecord {
1105
+ /** Increasing across the run; pass it as `since` to read only newer ones. */
1106
+ seq: number;
1107
+ /** Upper-case method. */
1108
+ method: string;
1109
+ /** `scheme://host[:port]/path`: userinfo, query and fragment removed. */
1110
+ url: string;
1111
+ /**
1112
+ * `route`: a route fulfilled it. `patch`: the real server answered and a
1113
+ * route merge-patched the body. `network`: the real server answered.
1114
+ */
1115
+ source: 'route' | 'patch' | 'network';
1116
+ /** The route's pattern for `route` and `patch`. */
1117
+ pattern: string | null;
1118
+ status: number;
1119
+ contentType: string | null;
1120
+ /** JSON body text, cut to `maxBodyBytes`; `null` for other content types. */
1121
+ body: string | null;
1122
+ bodyTruncated: boolean;
1123
+ /** Epoch milliseconds. */
1124
+ timestamp: number;
1125
+ }
1126
+
1127
+ /** @internal `NetworkDriver.captureResponses()` options. */
1128
+ export interface NetworkCaptureOptions {
1129
+ /** Body bytes kept per response, 1..=1048576. Default 262144. */
1130
+ maxBodyBytes?: number;
1131
+ }
1132
+
1133
+ /**
1134
+ * Test-only routing of the selected lxapp's Logic `fetch` and `Rong.SSE`.
1135
+ * Available only in a host automation run (`lxdev test`); every route is
1136
+ * removed when that run ends. The newest matching route handles a request; unmatched requests are
1137
+ * untouched. A fulfillment never answers a host the app's network policy
1138
+ * refuses — the request goes to the real `fetch`, which rejects it.
1139
+ * WebView page requests are not routed.
1140
+ */
1141
+ export interface NetworkDriver {
1142
+ route(pattern: NetworkRoutePattern, handler: NetworkRouteHandler): Promise<NetworkRoute>;
1143
+ /** Remove every route this run installed for the app; resolves the count. */
1144
+ unrouteAll(): Promise<number>;
1145
+ /**
1146
+ * Run-scoped: requests any route of this automation run handled for the
1147
+ * app, oldest first, across every spec. `t.app.network.calls()` narrows
1148
+ * this to the current spec.
1149
+ */
1150
+ requests(): Promise<NetworkRouteRequest[]>;
1151
+ /**
1152
+ * Record the app's Logic `fetch` responses — status, content type and
1153
+ * JSON body, never headers — until the run ends. A buffered body is read
1154
+ * from a clone without delaying the app's fetch; a streamed one (no or a
1155
+ * large Content-Length) is read once and the app gets an equivalent
1156
+ * buffered `Response`.
1157
+ *
1158
+ * @internal Test-runner plumbing for `lxdev test --openapi`.
1159
+ */
1160
+ captureResponses(options?: NetworkCaptureOptions): Promise<void>;
1161
+ /**
1162
+ * Responses captured for the app in this run, oldest first; `since` skips
1163
+ * records up to that `seq`. `waitMs` (0..3000) first waits for captures
1164
+ * already in flight; one still unfinished is returned by a later read.
1165
+ * Rejects once when unread responses were lost. At most 500 records and
1166
+ * 8 MiB of bodies are kept; the oldest go first.
1167
+ *
1168
+ * @internal Test-runner plumbing for `lxdev test --openapi`.
1169
+ */
1170
+ responses(options?: { since?: number; waitMs?: number }): Promise<NetworkResponseRecord[]>;
1171
+ }
1172
+
1173
+ /**
1174
+ * Checkpoint and roll back the isolated data profile of a host automation run
1175
+ * (`lxdev test --profile`). Each call rejects with `E_PROFILE_NOT_ISOLATED`
1176
+ * unless the selected lxapp runs on this run's profile, so it can never touch
1177
+ * the app's real data.
1178
+ *
1179
+ * `checkpoint` and `restore` close the app, copy or swap its closed data and
1180
+ * reopen it at its initial page: a driver selected before the call is bound to
1181
+ * the closed instance, so select the lxapp again afterwards.
1182
+ */
1183
+ export interface ProfileDriver {
1184
+ /** Snapshot the profile; resolves the checkpoint id. */
1185
+ checkpoint(): Promise<string>;
1186
+ /**
1187
+ * Replace the profile with checkpoint `id`. With `keep`, the storage keys
1188
+ * those globs match keep their current values across the rollback.
1189
+ */
1190
+ restore(id: string, options?: ProfileRestoreOptions): Promise<ProfileRestoreResult>;
1191
+ /** Discard checkpoint `id`; the app keeps running. */
1192
+ drop(id: string): Promise<void>;
1193
+ }
1194
+
1195
+ /** `ProfileDriver.restore` options. */
1196
+ export interface ProfileRestoreOptions {
1197
+ /**
1198
+ * `lx.getStorage()` keys whose current state survives the rollback: globs
1199
+ * over the whole key, `*` any run of characters (dots included), `?` one
1200
+ * character, anything else literal (at most 64 patterns). A matching key
1201
+ * keeps its current value, one added since the checkpoint stays, and one
1202
+ * deleted since stays deleted. Files (`lx://userdata`, …) always roll back.
1203
+ */
1204
+ keep?: string[];
1205
+ }
1206
+
1207
+ export interface ProfileRestoreResult {
1208
+ /** Kept keys that currently exist and were carried into the restored data. */
1209
+ kept: string[];
1210
+ }
1211
+
1212
+ // ============================ test clock ============================
1213
+
1214
+ /** A time for the test clock: epoch milliseconds, a `Date.parse` string, or a `Date`. */
1215
+ export type ClockTime = number | string | Date;
1216
+
1217
+ export interface ClockInstallOptions {
1218
+ /** What Logic's `Date` reads at install. Default: the real current time. */
1219
+ now?: ClockTime;
1220
+ }
1221
+
1222
+ export interface ClockRunAllOptions {
1223
+ /** Reject once this many timers fired and more are pending. Default 1000. */
1224
+ maxTimers?: number;
1225
+ }
1226
+
1227
+ /** Logic's test time after a clock call. */
1228
+ export interface ClockState {
1229
+ /** Logic's `Date.now()` afterwards. */
1230
+ now: number;
1231
+ /** Timers still scheduled on the test clock. */
1232
+ pending: number;
1233
+ }
1234
+
1235
+ /** What `tick` / `runAll` did. */
1236
+ export interface ClockAdvance extends ClockState {
1237
+ /** Timers fired by this call. */
1238
+ fired: number;
1239
+ }
1240
+
1241
+ export interface ClockUninstallResult {
1242
+ /** `false` when no clock was installed (never, or the app reopened since). */
1243
+ uninstalled: boolean;
1244
+ /** Timers still pending on the test clock; they are discarded, never fired. */
1245
+ dropped: number;
1246
+ }
1247
+
1248
+ /**
1249
+ * Test clock for the selected lxapp's Logic context, scoped to the host
1250
+ * automation run (`lxdev test`); outside one every call rejects with
1251
+ * `E_AUTOMATION`. While installed, Logic's `Date` (no-argument
1252
+ * construction, `Date()`, `Date.now()`), `setTimeout` / `setInterval` and
1253
+ * their `clear*`, and `performance.now()` read test time, and those timers
1254
+ * fire only from `tick` / `runAll`, in time order. The page WebView, native
1255
+ * work, real network requests, test route `delay` / `hang` and timers started
1256
+ * before `install` keep real time. The run's end, or the app reopening,
1257
+ * returns the app to real time.
1258
+ */
1259
+ export interface ClockDriver {
1260
+ /**
1261
+ * Put Logic on test time; resolves its state (`pending` is 0: timers
1262
+ * started before `install` keep real time). Rejects with
1263
+ * `E_CLOCK_INSTALLED` when a clock is already installed.
1264
+ */
1265
+ install(options?: ClockInstallOptions): Promise<ClockState>;
1266
+ /**
1267
+ * Advance by `ms`, firing each timer due on the way at its own time. After
1268
+ * every firing, promise chains the callback started settle before the next
1269
+ * timer fires. Rejects with `E_CLOCK_NOT_INSTALLED` without a clock.
1270
+ */
1271
+ tick(ms: number): Promise<ClockAdvance>;
1272
+ /**
1273
+ * Fire timers, including those they schedule, until none is left; rejects
1274
+ * once `maxTimers` fired (an interval never lets the queue empty).
1275
+ */
1276
+ runAll(options?: ClockRunAllOptions): Promise<ClockAdvance>;
1277
+ /**
1278
+ * Change what `Date` reads without firing timers; timer due times and
1279
+ * `performance.now()` are unaffected. Resolves the new state.
1280
+ */
1281
+ setSystemTime(time: ClockTime): Promise<ClockState>;
1282
+ /** Return Logic to real time; pending test timers are dropped. */
1283
+ uninstall(): Promise<ClockUninstallResult>;
1284
+ }
1285
+
1286
+ // ============================== dialogs ==============================
1287
+
1288
+ /** A toast Logic presented while watched; the host drew it as usual. */
1289
+ export interface ToastRecord {
1290
+ title: string;
1291
+ /** `success` / `error` / `loading` / `none`. */
1292
+ icon: string;
1293
+ /** Milliseconds the toast asked to stay. */
1294
+ duration: number;
1295
+ /** Epoch milliseconds it was presented. */
1296
+ at: number;
1297
+ }
1298
+
1299
+ export interface ModalAnswer {
1300
+ confirm: boolean;
1301
+ }
1302
+
1303
+ /**
1304
+ * A modal Logic opened while watched: drawn (`drawn: true`) until the spec
1305
+ * queued a modal answer, answered from the queue after.
1306
+ */
1307
+ export interface ModalRecord {
1308
+ title: string;
1309
+ content: string;
1310
+ /** Only when the app set it (the default is localized). */
1311
+ confirmText?: string;
1312
+ /** Only when the modal has a cancel button and the app set its text. */
1313
+ cancelText?: string;
1314
+ /**
1315
+ * The queued answer it got, or the user's choice when drawn. `null`: a
1316
+ * drawn one still open or that failed to present, or (answering) no answer
1317
+ * was queued, so the call rejected and the spec failed.
1318
+ */
1319
+ answer: ModalAnswer | null;
1320
+ /** Drawn for the user, not answered from the queue. */
1321
+ drawn?: true;
1322
+ }
1323
+
1324
+ /** `{ index }` picks that item; `{ cancel: true }` dismisses the sheet. */
1325
+ export type ActionSheetAnswer = { index: number } | { cancel: true };
1326
+
1327
+ /** An `lx.showActionSheet` while watched: drawn, or answered like modals. */
1328
+ export interface ActionSheetRecord {
1329
+ /** Item labels, in order. */
1330
+ items: string[];
1331
+ answer: ActionSheetAnswer | null;
1332
+ drawn?: true;
1333
+ }
1334
+
1335
+ export interface DialogUnwatchResult {
1336
+ /** Modal answers queued that no modal used. */
1337
+ modalAnswers: number;
1338
+ /** Action sheet answers queued that no sheet used. */
1339
+ actionSheetAnswers: number;
1340
+ }
1341
+
1342
+ /**
1343
+ * The dialogs the selected lxapp's Logic opens while a spec of a host
1344
+ * automation run (`lxdev test`) watches it; outside one every call rejects
1345
+ * with `E_AUTOMATION`. Toasts, modals (`showModal`, `alert`, `confirm`) and
1346
+ * `showActionSheet` are recorded and drawn. A queued answer applies once;
1347
+ * later dialogs are drawn unless strict mode explicitly requires an answer.
1348
+ * An unwatched app presents every dialog as usual.
1349
+ */
1350
+ export interface DialogDriver {
1351
+ /** Start watching for the open spec attempt, with nothing recorded or queued. */
1352
+ watch(): void;
1353
+ /** Stop watching; returns the queued answers no dialog used. */
1354
+ unwatch(): DialogUnwatchResult;
1355
+ /** The first dialog that found no answer, or `null` once the watch ends. */
1356
+ unanswered(): Promise<string | null>;
1357
+ toasts(): ToastRecord[];
1358
+ modals(): ModalRecord[];
1359
+ actionSheets(): ActionSheetRecord[];
1360
+ answerNextModal(answer: ModalAnswer): void;
1361
+ answerNextActionSheet(answer: ActionSheetAnswer): void;
1362
+ /** Strict mode refuses an unqueued dialog; returns the previous modes. */
1363
+ setAnswerMode(mode: { modals?: "draw" | "strict"; actionSheets?: "draw" | "strict" }):
1364
+ { modals: "draw" | "strict"; actionSheets: "draw" | "strict" };
1365
+ }
1366
+
1367
+ /** Capability for one selected running lxapp, as app Logic sees it. */
1368
+ export interface LogicLxAppDriver {
522
1369
  readonly page: PageDriver;
523
- readonly nav: NavDriver;
1370
+ readonly nav: LogicNavDriver;
524
1371
  /** Complete runtime snapshot of the selected lxapp. */
525
1372
  info(): Promise<LxAppRuntimeInfo>;
526
1373
  /** Configured pages of the selected lxapp. */
527
1374
  pages(): Promise<LxAppPageConfig[]>;
528
1375
  /** Authoritative host surface render plan, for end-to-end assertions. */
529
1376
  surfaceLayout(): Promise<SurfaceLayoutSnapshot>;
530
- /** Logic-runtime eval; self-eval from that Logic runtime is rejected. */
531
- eval(options: LxAppEvalOptions): Promise<unknown>;
1377
+ /**
1378
+ * Logic-runtime eval of another lxapp; evaluating the calling Logic runtime
1379
+ * itself rejects. `T` describes the expected JSON result; it is not
1380
+ * runtime validation.
1381
+ */
1382
+ eval<T = unknown>(options: LogicLxAppEvalOptions): Promise<T>;
1383
+ }
1384
+
1385
+ /**
1386
+ * Capability for one selected running lxapp in a host automation run
1387
+ * (`HostRunAutomation.lxapp()`): the Logic driver plus test-run-only members.
1388
+ */
1389
+ export interface LxAppDriver extends LogicLxAppDriver {
1390
+ readonly nav: NavDriver;
1391
+ /**
1392
+ * Test-only Logic `fetch` routing, scoped to the host automation run.
1393
+ *
1394
+ * @remarks Reading the property always works, but every call rejects with
1395
+ * `E_AUTOMATION` outside a host test run (`lxdev test`) — from app Logic,
1396
+ * or in a host built without the automation runtime.
1397
+ */
1398
+ readonly network: NetworkDriver;
1399
+ /**
1400
+ * The app's mocks in the host run: scenario states on top of the mock
1401
+ * selection, and a fresh handler state.
1402
+ *
1403
+ * @remarks Reading the property always works, but every call rejects with
1404
+ * `E_AUTOMATION` outside a host test run (`lxdev test`).
1405
+ */
1406
+ readonly mock: MockDriver;
1407
+ /**
1408
+ * Isolated data profile rollback, scoped to the host automation run.
1409
+ *
1410
+ * @remarks Reading the property always works; every call rejects with
1411
+ * `E_PROFILE_NOT_ISOLATED` outside an isolated run.
1412
+ */
1413
+ readonly profile: ProfileDriver;
1414
+ /**
1415
+ * Test clock for the selected lxapp's Logic, scoped to the host automation
1416
+ * run.
1417
+ *
1418
+ * @remarks Reading the property always works, but every call rejects with
1419
+ * `E_AUTOMATION` outside a host test run (`lxdev test`).
1420
+ */
1421
+ readonly clock: ClockDriver;
1422
+ /**
1423
+ * The dialogs this lxapp's Logic opens while a spec watches it.
1424
+ *
1425
+ * @remarks Reading the property always works, but every call rejects with
1426
+ * `E_AUTOMATION` outside a host test run (`lxdev test`).
1427
+ */
1428
+ readonly dialogs: DialogDriver;
1429
+ /** @internal Test-runner plumbing: resolves to the call-trace envelope. */
1430
+ eval<T = unknown>(options: LxAppEvalOptions & { captureCalls: true }): Promise<LxAppEvalTrace<T>>;
1431
+ /**
1432
+ * Logic-runtime eval; evaluating the calling Logic runtime itself rejects.
1433
+ * `T` describes the expected JSON result; it is not runtime validation.
1434
+ */
1435
+ eval<T = unknown>(options: LxAppEvalOptions & { captureCalls?: false }): Promise<T>;
1436
+ eval<T = unknown>(options: LxAppEvalOptions): Promise<T | LxAppEvalTrace<T>>;
532
1437
  }
533
1438
 
534
1439
  // ======================= lxapp manager (host) =======================
@@ -539,38 +1444,40 @@ export interface LxAppPageEntry {
539
1444
  path: string;
540
1445
  }
541
1446
 
542
- /** Runtime snapshot of a running lxapp (raw payload, snake_case keys). */
1447
+ /** Runtime snapshot of a running lxapp. */
543
1448
  export interface LxAppRuntimeInfo {
544
- appid: string;
545
- app_name: string;
1449
+ appId: string;
1450
+ appName: string;
546
1451
  version: string;
547
- release_type: string;
548
- session_id: number;
1452
+ releaseType: string;
1453
+ sessionId: number;
549
1454
  status: string;
550
1455
  /** True while the lxapp holds a place in the host's page stack. */
551
- in_stack: boolean;
552
- is_home: boolean;
553
- current_page: string | null;
554
- initial_route: string;
555
- pages_count: number;
556
- page_entries: LxAppPageEntry[];
557
- page_stack: string[];
558
- tab_bar: LxAppRuntimeTabBarInfo | null;
559
- navigation_bar: LxAppRuntimeNavigationBarInfo | null;
560
- lxapp_dir: string;
561
- data_dir: string;
562
- cache_dir: string;
1456
+ inStack: boolean;
1457
+ isHome: boolean;
1458
+ currentPage: string | null;
1459
+ initialRoute: string;
1460
+ pagesCount: number;
1461
+ pageEntries: LxAppPageEntry[];
1462
+ pageStack: string[];
1463
+ tabBar: LxAppRuntimeTabBarInfo | null;
1464
+ navigationBar: LxAppRuntimeNavigationBarInfo | null;
1465
+ lxappDir: string;
1466
+ dataDir: string;
1467
+ cacheDir: string;
1468
+ /** Supported features keyed by Logic context id. */
1469
+ logicFeatures: Record<string, string[]>;
563
1470
  }
564
1471
 
565
1472
  /** Runtime NavigationBar state exposed for deterministic host-level assertions. */
566
1473
  export interface LxAppRuntimeNavigationBarInfo {
567
1474
  title: string;
568
- home_button: 'auto' | 'hidden';
569
- home_button_visible: boolean;
570
- runtime_style: {
571
- background_color: string | null;
572
- foreground_color: string | null;
573
- divider_color: string | null;
1475
+ homeButton: 'auto' | 'hidden';
1476
+ homeButtonVisible: boolean;
1477
+ runtimeStyle: {
1478
+ backgroundColor: string | null;
1479
+ foregroundColor: string | null;
1480
+ dividerColor: string | null;
574
1481
  };
575
1482
  }
576
1483
 
@@ -578,19 +1485,15 @@ export interface LxAppRuntimeNavigationBarInfo {
578
1485
  export interface LxAppRuntimeTabBarInfo {
579
1486
  presentation: 'standard' | 'immersive';
580
1487
  visibility: 'auto' | 'visible' | 'hidden';
581
- route_visible: boolean;
582
- effective_visible: boolean;
583
- selected_index: number;
584
- runtime_style: {
585
- foreground_color: string | null;
586
- selected_foreground_color: string | null;
587
- };
1488
+ routeVisible: boolean;
1489
+ effectiveVisible: boolean;
1490
+ selectedIndex: number;
588
1491
  items: Array<{
589
1492
  index: number;
590
1493
  text: string | null;
591
- icon_path: string | null;
1494
+ iconPath: string | null;
592
1495
  badge: string | null;
593
- red_dot: boolean;
1496
+ redDot: boolean;
594
1497
  }>;
595
1498
  }
596
1499
 
@@ -601,14 +1504,14 @@ export interface LxAppRef {
601
1504
  }
602
1505
 
603
1506
  export interface LxAppOpenOptions {
604
- appid: string;
1507
+ appId: string;
605
1508
  /** Initial page/path. */
606
1509
  path?: string;
607
1510
  channel?: 'release' | 'draft';
608
1511
  }
609
1512
 
610
1513
  export interface LxAppOpenResult {
611
- appid: string;
1514
+ appId: string;
612
1515
  path: string;
613
1516
  }
614
1517
 
@@ -623,17 +1526,18 @@ export interface ApplinkResult {
623
1526
  }
624
1527
 
625
1528
  /**
626
- * Cross-lxapp lifecycle and host-window access. Requires `host` outside dev.
1529
+ * Cross-lxapp lifecycle and host-window access. Requires the `host` privilege
1530
+ * from app Logic; a host automation run needs no grant.
627
1531
  *
628
1532
  * `close`, `restart`, and `uninstall` reject when they target the calling app
629
- * itself. Use `lx.app.exit()` to self-exit.
1533
+ * itself. Use `lx.host.exit()` to self-exit.
630
1534
  */
631
1535
  export interface LxAppManager {
632
1536
  list(): Promise<LxAppRuntimeInfo[]>;
633
1537
  current(): Promise<LxAppSummary>;
634
1538
  open(options: LxAppOpenOptions): Promise<LxAppOpenResult>;
635
1539
  /**
636
- * Inject an App Link (`lxdev app applink`). Warm `onShow`, `scene === 8003`.
1540
+ * Inject an App Link (`lxdev host applink`). Warm `onShow`, `scene === 8003`.
637
1541
  * Host must match `appLinks.hosts`. Resolves when accepted, not when
638
1542
  * navigation finishes.
639
1543
  */
@@ -676,9 +1580,6 @@ export interface DeviceState {
676
1580
  landscape: boolean;
677
1581
  /** Simulated system appearance of the device screen. */
678
1582
  appearance: "system" | "light" | "dark";
679
- /** Whether the simulated host capsule is enabled (the setting, not
680
- * per-device visibility: desktop presets draw no phone chrome either way). */
681
- capsule: boolean;
682
1583
  }
683
1584
 
684
1585
  export interface DeviceSetOptions {
@@ -689,8 +1590,6 @@ export interface DeviceSetOptions {
689
1590
  landscape?: boolean;
690
1591
  /** Simulated appearance; omit to keep. */
691
1592
  appearance?: "system" | "light" | "dark";
692
- /** Show (`true`) or hide (`false`) the simulated host capsule; omit to keep. */
693
- capsule?: boolean;
694
1593
  }
695
1594
 
696
1595
  /**
@@ -767,9 +1666,9 @@ export interface BrowserScrollOptions extends BrowserTabRef {
767
1666
  * A browser wait condition — pass **exactly one** of the condition fields.
768
1667
  * `navigation` may add `complete` to wait for load completion.
769
1668
  */
770
- export interface BrowserWaitOptions extends BrowserTabRef {
1669
+ interface BrowserWaitFields extends BrowserTabRef {
771
1670
  /** Wait for page load. */
772
- loaded?: boolean;
1671
+ loaded?: true;
773
1672
  /** Wait for a selector to exist. */
774
1673
  exists?: string;
775
1674
  /** Wait for a selector to be visible. */
@@ -785,7 +1684,7 @@ export interface BrowserWaitOptions extends BrowserTabRef {
785
1684
  /** Wait for the URL to contain this. */
786
1685
  urlContains?: string;
787
1686
  /** Wait for a navigation. */
788
- navigation?: boolean;
1687
+ navigation?: true;
789
1688
  /** With `navigation`: baseline URL to detect a change from (default: any
790
1689
  * navigation satisfies it). */
791
1690
  fromUrl?: string;
@@ -795,6 +1694,39 @@ export interface BrowserWaitOptions extends BrowserTabRef {
795
1694
  timeoutMs?: number;
796
1695
  }
797
1696
 
1697
+ type BrowserWaitConditionKey = 'loaded' | 'exists' | 'visible' | 'hidden' | 'editable' | 'js' | 'url' | 'urlContains' | 'navigation';
1698
+
1699
+ /** Exactly one condition; ambiguous and empty waits are rejected by the runtime. */
1700
+ export type BrowserWaitOptions = Pick<BrowserWaitFields, 'tab' | 'timeoutMs' | 'fromUrl' | 'complete'> & {
1701
+ [K in BrowserWaitConditionKey]: Required<Pick<BrowserWaitFields, K>> &
1702
+ Partial<Record<Exclude<BrowserWaitConditionKey, K>, never>>;
1703
+ }[BrowserWaitConditionKey];
1704
+
1705
+ /** Browser query payload; it does not carry lxapp-only identity/index fields. */
1706
+ export interface BrowserElementInfo {
1707
+ exists: boolean;
1708
+ visible: boolean;
1709
+ enabled: boolean;
1710
+ editable: boolean;
1711
+ text?: string;
1712
+ textTruncated?: boolean;
1713
+ value?: string;
1714
+ valueTruncated?: boolean;
1715
+ rect?: ElementRect;
1716
+ }
1717
+
1718
+ export interface BrowserWaitResult {
1719
+ elapsed_ms: number;
1720
+ current_url?: string;
1721
+ element?: BrowserElementInfo;
1722
+ value?: unknown;
1723
+ }
1724
+
1725
+ export interface BrowserEvalResult<T = unknown> {
1726
+ value: T;
1727
+ navigation: BrowserWaitResult;
1728
+ }
1729
+
798
1730
  export interface BrowserTab {
799
1731
  tab_id: string;
800
1732
  path: string;
@@ -868,16 +1800,22 @@ export interface BrowserDriver {
868
1800
  back(options?: BrowserTabRef): Promise<void>;
869
1801
  forward(options?: BrowserTabRef): Promise<void>;
870
1802
  /** Evaluate JS; with `waitNavigation` resolves to `{ value, navigation }`. */
871
- eval(options: BrowserEvalOptions): Promise<unknown>;
872
- query(options: BrowserQueryOptions): Promise<PageElement | PageElementMiss>;
1803
+ eval<T = unknown>(options: BrowserEvalOptions & {waitNavigation: true}): Promise<BrowserEvalResult<T>>;
1804
+ eval<T = unknown>(options: BrowserEvalOptions & {waitNavigation?: false}): Promise<T>;
1805
+ eval<T = unknown>(options: BrowserEvalOptions): Promise<T | BrowserEvalResult<T>>;
1806
+ query(options: BrowserQueryOptions): Promise<BrowserElementInfo>;
873
1807
  /** Wait for a condition (pass exactly one condition field). */
874
- wait(options: BrowserWaitOptions): Promise<unknown>;
1808
+ wait(options: BrowserWaitOptions): Promise<BrowserWaitResult>;
875
1809
  /** Click; with `waitNavigation` resolves to the navigation payload else `null`. */
876
- click(options: BrowserClickOptions): Promise<unknown>;
1810
+ click(options: BrowserClickOptions & { waitNavigation: true }): Promise<BrowserWaitResult>;
1811
+ click(options: BrowserClickOptions & { waitNavigation?: false }): Promise<null>;
1812
+ click(options: BrowserClickOptions): Promise<BrowserWaitResult | null>;
877
1813
  type(options: BrowserTypeOptions): Promise<void>;
878
1814
  fill(options: BrowserTypeOptions): Promise<void>;
879
1815
  /** Press; with `waitNavigation` resolves to the navigation payload else `null`. */
880
- press(options: BrowserPressOptions): Promise<unknown>;
1816
+ press(options: BrowserPressOptions & { waitNavigation: true }): Promise<BrowserWaitResult>;
1817
+ press(options: BrowserPressOptions & { waitNavigation?: false }): Promise<null>;
1818
+ press(options: BrowserPressOptions): Promise<BrowserWaitResult | null>;
881
1819
  scroll(options: BrowserScrollOptions): Promise<void>;
882
1820
  scrollTo(options: BrowserSelectorOptions): Promise<void>;
883
1821
  screenshot(options?: BrowserTabRef): Promise<Screenshot>;
@@ -971,7 +1909,7 @@ export interface PageKey {
971
1909
  press(options: KeyPressOptions): Promise<InputResult>;
972
1910
  }
973
1911
 
974
- // ======================= desktop (host, dev/test only) =======================
1912
+ // ======================= desktop (host) =======================
975
1913
 
976
1914
  /** A rectangle in backend-native global desktop coordinates. */
977
1915
  export interface DesktopRect {
@@ -1115,10 +2053,9 @@ export interface DesktopLaunchResult {
1115
2053
  * `match` (query `text | title: | class: | process: | pid:`, must resolve to
1116
2054
  * exactly one window).
1117
2055
  */
1118
- export interface DesktopWindowSel {
1119
- window?: string;
1120
- match?: string;
1121
- }
2056
+ export type DesktopWindowSel =
2057
+ | { window: string; match?: never }
2058
+ | { window?: never; match: string };
1122
2059
 
1123
2060
  export interface DesktopWindowsOptions {
1124
2061
  /** Match query (`text | title: | class: | process: | pid:`). */
@@ -1209,24 +2146,24 @@ export interface DesktopKey {
1209
2146
  up(options: DesktopKeyNameOptions): Promise<DesktopAck>;
1210
2147
  }
1211
2148
 
1212
- export interface DesktopWindowMoveOptions extends DesktopWindowSel {
2149
+ export type DesktopWindowMoveOptions = DesktopWindowSel & {
1213
2150
  /** Target position as `[x, y]` in desktop coordinates. */
1214
2151
  to: Point;
1215
- }
2152
+ };
1216
2153
 
1217
- export interface DesktopWindowResizeOptions extends DesktopWindowSel {
2154
+ export type DesktopWindowResizeOptions = DesktopWindowSel & {
1218
2155
  width: number;
1219
2156
  height: number;
1220
- }
2157
+ };
1221
2158
 
1222
- export interface DesktopWindowMoveDisplayOptions extends DesktopWindowSel {
2159
+ export type DesktopWindowMoveDisplayOptions = DesktopWindowSel & {
1223
2160
  /** Display id from `displays()`. */
1224
2161
  display: string;
1225
- }
2162
+ };
1226
2163
 
1227
- export interface DesktopWindowAlwaysOnTopOptions extends DesktopWindowSel {
2164
+ export type DesktopWindowAlwaysOnTopOptions = DesktopWindowSel & {
1228
2165
  on: boolean;
1229
- }
2166
+ };
1230
2167
 
1231
2168
  /** Window management; every verb resolves to the resulting window state. */
1232
2169
  export interface DesktopWindowDriver {
@@ -1340,10 +2277,11 @@ export interface DesktopAppLaunchOptions {
1340
2277
  }
1341
2278
 
1342
2279
  /** Quit target — exactly one of `match` / `pid` / `window`. */
1343
- export interface DesktopAppQuitOptions {
1344
- match?: string;
1345
- pid?: number;
1346
- window?: string;
2280
+ export type DesktopAppQuitOptions = (
2281
+ | { match: string; pid?: never; window?: never }
2282
+ | { match?: never; pid: number; window?: never }
2283
+ | { match?: never; pid?: never; window: string }
2284
+ ) & {
1347
2285
  /** Terminate instead of a graceful close. */
1348
2286
  force?: boolean;
1349
2287
  }