touchpress 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,853 @@
1
+ //#region src/core/screen.d.ts
2
+ type Platform = 'ios' | 'android';
3
+ type Rect = {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ readonly width: number;
7
+ readonly height: number;
8
+ };
9
+ /**
10
+ * One node of a parsed screen, not the driver's snapshot node: `label` becomes
11
+ * `name`, `identifier` becomes `testId`, and `type` is normalized into `role`
12
+ * with the platform spelling kept as `rawType`.
13
+ *
14
+ * Invariant: `parent` links form a forest rooted at nodes whose `parentIndex`
15
+ * was absent, built once at parse time so an ancestor walk stays O(depth).
16
+ */
17
+ type ScreenNode = {
18
+ readonly ref: string;
19
+ readonly index: number;
20
+ readonly parent: ScreenNode | null;
21
+ readonly depth: number;
22
+ readonly role: Role;
23
+ readonly rawType: string;
24
+ readonly name: string | null;
25
+ readonly value: string | null;
26
+ readonly testId: string | null;
27
+ readonly rect: Rect | null;
28
+ readonly enabled: boolean;
29
+ readonly selected: boolean;
30
+ readonly focused: boolean;
31
+ /**
32
+ * The driver's hints that a scroll container holds content out of the window.
33
+ * The only thing that says which way to scroll on a platform whose raw tree
34
+ * stops at the window.
35
+ */
36
+ readonly hiddenContentAbove: boolean;
37
+ readonly hiddenContentBelow: boolean;
38
+ };
39
+ /**
40
+ * A frozen observation of the device at one instant.
41
+ *
42
+ * Invariant: a screen is never refreshed in place. Every state-changing
43
+ * command advances the driver's ref generation, so a screen captured before
44
+ * one can only mint refs the driver will reject.
45
+ */
46
+ type Screen = {
47
+ readonly nodes: readonly ScreenNode[];
48
+ readonly generation: number | null;
49
+ readonly appId: string | null;
50
+ readonly truncated: boolean;
51
+ readonly capturedAt: number;
52
+ };
53
+ declare const pinnedRefBrand: unique symbol;
54
+ /**
55
+ * A ref pinned to the generation it was minted from, in the driver's
56
+ * `@e12~s776575` form. The driver rejects a pin from a superseded generation
57
+ * before dispatch, which turns a silent mistap into a recoverable failure.
58
+ */
59
+ type PinnedRef = string & {
60
+ readonly [pinnedRefBrand]: true;
61
+ };
62
+ type Resolution = {
63
+ readonly outcome: 'one';
64
+ readonly node: ScreenNode;
65
+ } | {
66
+ readonly outcome: 'none';
67
+ readonly nearest: readonly ScreenNode[];
68
+ } | {
69
+ readonly outcome: 'many';
70
+ readonly nodes: readonly ScreenNode[];
71
+ };
72
+ /**
73
+ * The structural shape of the driver's snapshot response. Declared here rather
74
+ * than imported so the core never depends on `agent-device`. The real
75
+ * `CaptureSnapshotResult` satisfies it and so does a JSON fixture.
76
+ */
77
+ type RawSnapshot = {
78
+ readonly nodes: ReadonlyArray<{
79
+ readonly ref: string;
80
+ readonly index: number;
81
+ readonly type?: string;
82
+ readonly role?: string;
83
+ readonly label?: string;
84
+ readonly value?: string;
85
+ readonly identifier?: string;
86
+ readonly rect?: Rect;
87
+ readonly enabled?: boolean;
88
+ readonly selected?: boolean;
89
+ readonly focused?: boolean;
90
+ readonly hintShowing?: boolean;
91
+ readonly hiddenContentAbove?: boolean;
92
+ readonly hiddenContentBelow?: boolean;
93
+ readonly depth?: number;
94
+ readonly parentIndex?: number;
95
+ readonly inheritsLabel?: true;
96
+ readonly inheritsIdentifier?: true;
97
+ }>;
98
+ readonly truncated?: boolean;
99
+ readonly appBundleId?: string;
100
+ readonly refsGeneration?: number;
101
+ };
102
+ /**
103
+ * The parse boundary. Everything past it trusts its types.
104
+ *
105
+ * `inheritsLabel` and `inheritsIdentifier` mean the driver omitted a value that
106
+ * string-equals the nearest ancestor's, so those are restored here rather than
107
+ * leaving a hole a matcher would read as absent.
108
+ */
109
+ declare function parseScreen(raw: RawSnapshot, platform: Platform): Screen;
110
+ /**
111
+ * The single resolver, shared by actions and assertions so the two can never
112
+ * disagree about which node was meant. Rule order: match every query field,
113
+ * apply every `.filter()`, absorb ancestors, then `index`.
114
+ *
115
+ * Ancestor absorption drops a match when a descendant match carries the same
116
+ * string the query matched on. On the sample app an `[other]` container and its
117
+ * `[text]` child both carry "Live from the cloud", which is one thing on screen.
118
+ * Matches in disjoint subtrees stay distinct, so "Explore" on the Explore screen
119
+ * is still the heading and the tab button.
120
+ */
121
+ declare function resolve(screen: Screen, query: Query): Resolution;
122
+ /**
123
+ * The one tree renderer, used by failure messages and by the `screen.txt`
124
+ * attachment so terminal and report agree. The vocabulary is the CLI's own
125
+ * `[role] "label"` form.
126
+ */
127
+ declare function renderScreen(screen: Screen, options?: {
128
+ readonly maxNodes?: number;
129
+ }): string;
130
+ //#endregion
131
+ //#region src/core/query.d.ts
132
+ /**
133
+ * Spelled the way `agent-device snapshot` prints it, so a failure listing and a
134
+ * manual snapshot read alike. iOS reports XCUIElement type names and Android
135
+ * reports widget class names, and both map here.
136
+ *
137
+ * Invariant: every raw type maps to exactly one role and an unrecognized type
138
+ * maps to `other`. `other` is a real role, not a failure signal, because React
139
+ * Native emits many labelled container views with no semantic type.
140
+ */
141
+ type Role = 'application' | 'window' | 'button' | 'text' | 'text-field' | 'secure-text-field' | 'link' | 'image' | 'switch' | 'slider' | 'tab-bar' | 'scroll-area' | 'cell' | 'alert' | 'other';
142
+ /** How one string field is compared. */
143
+ type TextMatch = {
144
+ readonly kind: 'substring';
145
+ readonly value: string;
146
+ } | {
147
+ readonly kind: 'exact';
148
+ readonly value: string;
149
+ } | {
150
+ readonly kind: 'regex';
151
+ readonly value: RegExp;
152
+ };
153
+ /** One `.filter()` call. Every field narrows, so an empty filter is a no-op. */
154
+ type Filter = {
155
+ readonly hasText?: TextMatch;
156
+ readonly hasNotText?: TextMatch;
157
+ readonly has?: Query;
158
+ readonly hasNot?: Query;
159
+ };
160
+ /**
161
+ * A pure conjunctive description of a node. Building one performs no I/O.
162
+ *
163
+ * Invariants: every field narrows, so an empty query matches every node. `name`
164
+ * is compared against the node's name and its value, which is what makes
165
+ * `getByText` behave like Playwright's. `filters` holds one entry per
166
+ * `.filter()` call, conjunctive within and across entries. `index` is the
167
+ * strictness opt-out set by `.first()` and `.nth(n)`, and without it more than
168
+ * one distinct match is an error.
169
+ */
170
+ type Query = {
171
+ readonly testId?: TextMatch;
172
+ readonly name?: TextMatch;
173
+ readonly value?: TextMatch;
174
+ readonly role?: Role;
175
+ readonly enabled?: boolean;
176
+ readonly selected?: boolean;
177
+ readonly focused?: boolean;
178
+ readonly where?: (node: ScreenNode) => boolean;
179
+ readonly filters?: readonly Filter[];
180
+ readonly index?: number;
181
+ };
182
+ /** Applied to both sides of every string comparison, so a label wrapped across lines still matches one typed on one line. */
183
+ declare function normalizeText(raw: string): string;
184
+ /**
185
+ * Playwright's default text semantics: case-insensitive substring after
186
+ * whitespace normalization. `exact` is whole-string and case-sensitive, still
187
+ * normalized.
188
+ */
189
+ declare function textMatch(value: string | RegExp, exact?: boolean): TextMatch;
190
+ /**
191
+ * Renders a query back into the factory call that produces it, so the `Locator:`
192
+ * line of a failure reads like the line the author wrote.
193
+ */
194
+ declare function describeQuery(query: Query): string;
195
+ //#endregion
196
+ //#region src/core/checks.d.ts
197
+ /**
198
+ * An assertion as data. Adapters turn each `name` into a matcher, and the poll
199
+ * and the message live in `probe`, so two runners produce identical failures.
200
+ */
201
+ type Check = {
202
+ readonly name: 'toBeVisible';
203
+ } | {
204
+ readonly name: 'toHaveText';
205
+ readonly expected: TextMatch;
206
+ } | {
207
+ readonly name: 'toHaveValue';
208
+ readonly expected: TextMatch;
209
+ } | {
210
+ readonly name: 'toBeEnabled';
211
+ } | {
212
+ readonly name: 'toBeSelected';
213
+ } | {
214
+ readonly name: 'toBeFocused';
215
+ } | {
216
+ readonly name: 'toHaveCount';
217
+ readonly expected: number;
218
+ };
219
+ type CheckName = Check['name'];
220
+ type Verdict = {
221
+ readonly pass: boolean;
222
+ /** What the screen actually held, for the `Received:` line. Null when nothing matched. */
223
+ readonly actual: string | null;
224
+ };
225
+ /**
226
+ * A `many` outcome never passes anything but `toHaveCount`. An ambiguous locator
227
+ * is a strictness violation, and it reports through the same message path as a
228
+ * plain mismatch rather than guessing which node was meant.
229
+ */
230
+ declare function evaluate(check: Check, resolution: Resolution): Verdict;
231
+ /** The `Expected:` line. */
232
+ declare function describeCheck(check: Check): string;
233
+ //#endregion
234
+ //#region src/core/driver.d.ts
235
+ type ScrollDirection = 'up' | 'down' | 'left' | 'right';
236
+ /**
237
+ * A session binds on its first command, so this rides on `open` and on every
238
+ * command after it. A call sent without selection lands on whichever device the
239
+ * daemon picks, which is not necessarily the one under test.
240
+ */
241
+ type DeviceSelection = {
242
+ readonly platform: Platform;
243
+ readonly name: string | null;
244
+ };
245
+ /** One device the driver can see. Only the fields preflight needs, so no agent-device device shape crosses here. */
246
+ type DeviceInfo = {
247
+ readonly id: string;
248
+ readonly name: string;
249
+ readonly booted: boolean;
250
+ };
251
+ /** The session name and the device selection ride on the driver itself, so an open cannot name a different one. */
252
+ type OpenRequest = {
253
+ readonly app: string;
254
+ readonly relaunch: boolean;
255
+ /** A deep link to launch the app with, or null to launch it plainly. */
256
+ readonly url: string | null;
257
+ };
258
+ /** Proof that a device is bound. Only `open` mints one, so a believed binding cannot drift from a real one. */
259
+ type Binding = {
260
+ readonly session: string;
261
+ readonly platform: Platform;
262
+ readonly deviceLabel: string;
263
+ readonly appId: string;
264
+ readonly stateDir: string | null;
265
+ };
266
+ type Settled = {
267
+ readonly settled: boolean;
268
+ readonly waitedMs: number;
269
+ };
270
+ /**
271
+ * Why an operation failed, in this library's vocabulary. Translated once, in
272
+ * `driver/agent-device.ts`, so a test author never reads a raw driver code.
273
+ */
274
+ type DeviceFailure = {
275
+ readonly kind: 'device-busy';
276
+ readonly owner: string | null;
277
+ readonly detail: string;
278
+ } | {
279
+ readonly kind: 'device-missing';
280
+ readonly detail: string;
281
+ } | {
282
+ readonly kind: 'app-missing';
283
+ readonly detail: string;
284
+ } | {
285
+ readonly kind: 'session-rebound';
286
+ readonly boundTo: string;
287
+ readonly detail: string;
288
+ } | {
289
+ readonly kind: 'stale-ref';
290
+ readonly detail: string;
291
+ } | {
292
+ readonly kind: 'ambiguous';
293
+ readonly detail: string;
294
+ } | {
295
+ readonly kind: 'timeout';
296
+ readonly detail: string;
297
+ } | {
298
+ readonly kind: 'unknown';
299
+ readonly code: string;
300
+ readonly detail: string;
301
+ readonly logPath: string | null;
302
+ };
303
+ type SettleOptions = {
304
+ readonly settleQuietMs: number;
305
+ readonly timeoutMs: number;
306
+ };
307
+ /**
308
+ * `default` is the driver's visible-first view, which every locator resolves
309
+ * against. `raw` is the full provider tree. On iOS it carries the nodes a scroll
310
+ * container moved out of the window, which is how a target is located before it
311
+ * is on screen. On Android it carries no off-screen content, only the wrappers
312
+ * the default view collapses away.
313
+ */
314
+ type Tree = 'default' | 'raw';
315
+ type CaptureOptions = {
316
+ readonly timeoutMs: number;
317
+ readonly tree: Tree;
318
+ };
319
+ /**
320
+ * Nothing crossing this port is a transport type, which is what lets the core be
321
+ * unit-tested against a captured snapshot.
322
+ *
323
+ * Contract every implementation owes the core. `open` converges when called on
324
+ * an already-open session. Mutations take a `PinnedRef` and never a selector, so
325
+ * the driver's own matcher is never a second opinion on which node was meant.
326
+ * Every failure throws a `TangereError` carrying a `DeviceFailure`.
327
+ */
328
+ type DeviceDriver = {
329
+ /** Takes no session, so it is the one call that binds nothing. */
330
+ listDevices(): Promise<readonly DeviceInfo[]>;
331
+ open(request: OpenRequest): Promise<Binding>;
332
+ capture(options: CaptureOptions): Promise<RawSnapshot>;
333
+ screenshot(path: string): Promise<string>;
334
+ tap(ref: PinnedRef, options: SettleOptions): Promise<Settled>;
335
+ longPress(ref: PinnedRef, durationMs: number, options: SettleOptions): Promise<Settled>;
336
+ fill(ref: PinnedRef, text: string, options: SettleOptions): Promise<Settled>;
337
+ /** Returns nothing because the driver's scroll response carries no settle observation. */
338
+ scroll(direction: ScrollDirection, options: SettleOptions): Promise<void>;
339
+ dismissDevOverlay(): Promise<void>;
340
+ /** Ends a session. Names one explicitly so a leftover owned by another run can be reclaimed. */
341
+ close(session: string): Promise<void>;
342
+ };
343
+ //#endregion
344
+ //#region src/core/probe.d.ts
345
+ /** What `probe` needs from a locator. A `Locator` supplies it, and so can a fake. */
346
+ type ProbeTarget = {
347
+ readonly query: Query;
348
+ readonly description: string;
349
+ capture(): Promise<Screen>;
350
+ /** The failure that broke the session, or null while it is usable. */
351
+ failure(): DeviceFailure | null;
352
+ };
353
+ type ProbeOptions = {
354
+ /** True when the caller wrote `.not`. The loop polls for the opposite condition rather than checking once. */
355
+ readonly negate: boolean;
356
+ readonly timeoutMs: number;
357
+ readonly intervalMs?: number;
358
+ };
359
+ type ProbeResult = {
360
+ /**
361
+ * Whether the check holds, always from the un-negated point of view. The
362
+ * adapter hands this to its assertion library, which inverts it for `.not`,
363
+ * so a `.not` that got what it wanted returns `false` here.
364
+ */
365
+ readonly pass: boolean;
366
+ /** Fully rendered, including the screen listing. Empty when the probe passed. */
367
+ readonly message: string;
368
+ readonly actual: string | null;
369
+ readonly expected: string;
370
+ };
371
+ /**
372
+ * Polls a fresh screen until the check agrees with `negate`, the budget runs
373
+ * out, or the session breaks. Never throws for a failed expectation, because the
374
+ * adapter hands `{ pass, message }` to its assertion library.
375
+ *
376
+ * Every early exit reports `pass: options.negate`, the value that fails the
377
+ * assertion whether or not the caller wrote `.not`. An ambiguous locator and a
378
+ * dead session are wrong under `.not` too, so neither may pass by inversion.
379
+ */
380
+ declare function probe(target: ProbeTarget, check: Check, options: ProbeOptions): Promise<ProbeResult>;
381
+ /**
382
+ * The screen listing comes from `renderScreen`, the same function that writes
383
+ * `screen.txt`, so terminal and report agree.
384
+ */
385
+ declare function formatFailure(input: {
386
+ readonly locator: string;
387
+ readonly check: Check;
388
+ readonly negate: boolean;
389
+ readonly resolution: Resolution;
390
+ readonly screen: Screen | null;
391
+ readonly timeoutMs: number;
392
+ readonly polls: number;
393
+ }): string;
394
+ //#endregion
395
+ //#region src/core/report.d.ts
396
+ /** A file the runner should surface with the test result. */
397
+ type EvidenceFile = {
398
+ readonly name: string;
399
+ readonly path: string;
400
+ readonly contentType: string;
401
+ } | {
402
+ readonly name: string;
403
+ readonly body: string;
404
+ readonly contentType: string;
405
+ };
406
+ /**
407
+ * The most a step title may say about text that was typed. A secure field never
408
+ * reports its contents, so a step that wrote to one says how many characters it
409
+ * typed and nothing else. A caller can ask for the same on a field the platform
410
+ * did not mark secure, which keeps a credential out of a terminal and a report.
411
+ */
412
+ type Typed = {
413
+ readonly kind: 'text';
414
+ readonly value: string;
415
+ } | {
416
+ readonly kind: 'hidden';
417
+ readonly length: number;
418
+ };
419
+ type ActionRecord = {
420
+ readonly kind: 'open';
421
+ readonly app: string;
422
+ readonly device: string;
423
+ readonly session: string;
424
+ } | {
425
+ readonly kind: 'tap';
426
+ readonly query: Query;
427
+ } | {
428
+ readonly kind: 'long-press';
429
+ readonly query: Query;
430
+ readonly durationMs: number;
431
+ } | {
432
+ readonly kind: 'fill';
433
+ readonly query: Query;
434
+ } | {
435
+ readonly kind: 'typed';
436
+ readonly typed: Typed;
437
+ } | {
438
+ readonly kind: 'scroll';
439
+ readonly direction: ScrollDirection;
440
+ } | {
441
+ readonly kind: 'scroll-into-view';
442
+ readonly query: Query;
443
+ } | {
444
+ readonly kind: 'relaunch';
445
+ readonly app: string;
446
+ } | {
447
+ readonly kind: 'dismiss-overlay';
448
+ } | {
449
+ readonly kind: 'screenshot';
450
+ readonly path: string;
451
+ };
452
+ /** A boxed step reports as one line rather than as something to open. */
453
+ type StepOptions = {
454
+ readonly box?: boolean;
455
+ };
456
+ /**
457
+ * A runner with no step concept calls `body()` directly, ignores `options`, and
458
+ * drops attachments.
459
+ *
460
+ * Invariant: `step` invokes `body` exactly once and propagates its result and
461
+ * its rejection unchanged. It reports, it never decides control flow.
462
+ */
463
+ type ActionSink = {
464
+ step<T>(title: string, body: () => Promise<T>, options?: StepOptions): Promise<T>;
465
+ attach(file: EvidenceFile): Promise<void>;
466
+ /** A key/value fact about the run, such as which device this worker bound to. */
467
+ note(key: string, value: string): void;
468
+ /** A unique path for this test and this retry attempt. */
469
+ outputPath(fileName: string): string;
470
+ };
471
+ /** Rendered here rather than in an adapter so every runner produces the same text. */
472
+ declare function renderTitle(record: ActionRecord): string;
473
+ /** Discards everything. The default for scripts, unit tests, and runners with no reporting. */
474
+ declare const silentSink: ActionSink;
475
+ //#endregion
476
+ //#region src/core/config.d.ts
477
+ /**
478
+ * The keys tangere adds to Playwright's `use`. Each is its own option fixture, so
479
+ * a project overrides one without restating the rest.
480
+ *
481
+ * Playwright types `use` as a partial of this, so a config may leave any key out.
482
+ * `parseDeviceOptions` is what makes `platform`, `app`, and `readyWhen` required.
483
+ * It runs once at worker start and nothing downstream re-validates.
484
+ */
485
+ type TangereOptions = {
486
+ /** Required. */
487
+ platform: Platform | undefined;
488
+ /** Required. Bundle id on iOS, package name on Android. Never a path to an artifact. */
489
+ app: string | undefined;
490
+ /**
491
+ * Required. `open` returns as soon as the native process launches, so without
492
+ * this gate the first assertion would race the JavaScript bundle.
493
+ */
494
+ readyWhen: ReadyQuery | undefined;
495
+ /** Device name. An array is a pool indexed by the runner's worker slot. Unset means the first booted device. */
496
+ deviceName: string | readonly string[] | undefined;
497
+ /**
498
+ * A deep link to open the app with, on the launch and on every relaunch.
499
+ * Unset launches the app plainly, which is what a release build wants. An
500
+ * Expo development client needs one, because launching it plainly shows its
501
+ * own server picker rather than the app.
502
+ */
503
+ launchUrl: string | undefined;
504
+ /** @default 'per-test' */
505
+ relaunch: 'per-test' | 'per-worker';
506
+ /** @default 'fail'. Sessions carrying this library's own prefix are always reclaimed. */
507
+ onDeviceInUse: 'fail' | 'reclaim';
508
+ /** @default 500 */
509
+ settleQuietMs: number;
510
+ /** @default 90_000. Covers `open` plus the ready gate. */
511
+ launchTimeout: number;
512
+ /** @default false. Sends agent-device's `react-native dismiss-overlay` after every launch. */
513
+ dismissDevOverlay: boolean;
514
+ /** @default 'on-failure' */
515
+ evidence: 'on-failure' | 'always' | 'off';
516
+ /** @default 'tangere'. Session names are `${prefix}-${project}-${parallelIndex}`. */
517
+ sessionPrefix: string;
518
+ };
519
+ /**
520
+ * The option fixtures declare these as their defaults and the parser falls back
521
+ * to the same values, so a caller that reaches the parser without the fixtures,
522
+ * such as `preflight`, resolves identically.
523
+ */
524
+ declare const TANGERE_DEFAULTS: Omit<TangereOptions, 'platform' | 'app' | 'readyWhen' | 'deviceName' | 'launchUrl'>;
525
+ /** The config-file friendly subset of a locator. Matched by the same rules as any other query. */
526
+ type ReadyQuery = {
527
+ text: string;
528
+ exact?: boolean;
529
+ } | {
530
+ testId: string;
531
+ } | {
532
+ role: Role;
533
+ name?: string;
534
+ };
535
+ /**
536
+ * The three cases are separate because only a pool can serve more than one
537
+ * worker slot. A single name and "whatever is booted" both resolve to the same
538
+ * device for every worker, and two workers on one device fight over the claim.
539
+ */
540
+ type DeviceChoice = {
541
+ readonly kind: 'first-booted';
542
+ } | {
543
+ readonly kind: 'named';
544
+ readonly name: string;
545
+ } | {
546
+ readonly kind: 'pool';
547
+ readonly names: readonly string[];
548
+ };
549
+ /** Every optional field resolved. Constructed only by `parseDeviceOptions`, so internal code trusts it. */
550
+ type ResolvedOptions = {
551
+ readonly platform: Platform;
552
+ readonly app: string;
553
+ readonly readyWhen: Query;
554
+ readonly device: DeviceChoice;
555
+ readonly launchUrl: string | null;
556
+ readonly relaunch: 'per-test' | 'per-worker';
557
+ readonly onDeviceInUse: 'fail' | 'reclaim';
558
+ readonly actionTimeout: number;
559
+ readonly settleQuietMs: number;
560
+ readonly launchTimeout: number;
561
+ readonly dismissDevOverlay: boolean;
562
+ readonly evidence: 'on-failure' | 'always' | 'off';
563
+ readonly sessionPrefix: string;
564
+ };
565
+ /**
566
+ * The config boundary. Options arrive as `unknown` because a project can omit
567
+ * any of them, or be written in JavaScript, and because `actionTimeout` rides
568
+ * along from Playwright's own options. Every message names the key to fix.
569
+ */
570
+ declare function parseDeviceOptions(raw: unknown): ResolvedOptions;
571
+ /**
572
+ * Anything but a pool serves slot 0 only. Two workers pointed at one device both
573
+ * try to claim it, and because leftovers carrying this library's session prefix
574
+ * are always reclaimed, the second worker would close the first worker's live
575
+ * session mid-test. That has to be a config error, not a race.
576
+ */
577
+ declare function deviceNameForSlot(options: ResolvedOptions, slot: number): string | null;
578
+ //#endregion
579
+ //#region src/core/session.d.ts
580
+ /**
581
+ * There is no unopened or opening variant because `openSession` is the only
582
+ * constructor and it returns after `open` succeeded and the ready gate passed.
583
+ * `broken` carries the first failure so a later call reports the root cause
584
+ * rather than a follow-on symptom.
585
+ */
586
+ type SessionState = {
587
+ readonly phase: 'ready';
588
+ readonly binding: Binding;
589
+ } | {
590
+ readonly phase: 'closed';
591
+ readonly reason: 'requested' | 'worker-exit';
592
+ } | {
593
+ readonly phase: 'broken';
594
+ readonly failure: DeviceFailure;
595
+ };
596
+ /**
597
+ * Every mutation takes the budget it has left rather than reading
598
+ * `actionTimeout` again, so waiting for a target and waiting for the screen to
599
+ * settle share one allowance instead of each getting a full one.
600
+ */
601
+ type SessionDevice = {
602
+ capture(): Promise<Screen>;
603
+ /**
604
+ * The driver's full provider tree. Only a scroll search reads it, because it
605
+ * carries nodes no locator should resolve against: on iOS the rows scrolled
606
+ * out of the window, and on both platforms the wrappers the default tree
607
+ * collapses away.
608
+ */
609
+ captureRaw(): Promise<Screen>;
610
+ tap(ref: PinnedRef, budgetMs: number): Promise<Settled>;
611
+ longPress(ref: PinnedRef, durationMs: number, budgetMs: number): Promise<Settled>;
612
+ fill(ref: PinnedRef, text: string, budgetMs: number): Promise<Settled>;
613
+ scroll(direction: ScrollDirection, budgetMs: number): Promise<void>;
614
+ };
615
+ type DeviceSession = {
616
+ readonly name: string;
617
+ readonly options: ResolvedOptions;
618
+ state(): SessionState;
619
+ /** The failure that broke this session, or null while it is usable. */
620
+ failure(): DeviceFailure | null;
621
+ /** Runs `body` as one unit on the session's queue. Nothing else touches the device while it runs. */
622
+ run<T>(body: (device: SessionDevice) => Promise<T>): Promise<T>;
623
+ screen(): Promise<Screen>;
624
+ screenshot(path: string): Promise<string>;
625
+ /** Relaunches the app and re-runs the ready gate. Reported as one step. */
626
+ relaunch(sink: ActionSink): Promise<void>;
627
+ dismissDevOverlay(): Promise<void>;
628
+ awaitReady(deadline: number): Promise<void>;
629
+ /** Idempotent. Never shuts the simulator down, and reaches `closed` even when the driver call fails. */
630
+ close(reason: 'requested' | 'worker-exit'): Promise<void>;
631
+ };
632
+ type OpenSessionInput = {
633
+ readonly options: ResolvedOptions;
634
+ /** The runner's stable worker slot. Playwright passes `parallelIndex`, which a replacement worker reuses. */
635
+ readonly slot: number;
636
+ /** The runner's project name. Part of the session name so two projects never share one. */
637
+ readonly scope: string;
638
+ readonly sink: ActionSink;
639
+ readonly createDriver: (session: string, selection: DeviceSelection) => DeviceDriver;
640
+ };
641
+ /**
642
+ * Convergent startup. Running it twice settles on one ready session. In order:
643
+ * reclaim a leftover session of the same name, open with the selection carried
644
+ * on that first command, recover once from a device claimed by a leftover or
645
+ * under `onDeviceInUse: 'reclaim'`, recover once from a session bound to another
646
+ * device, then hold until `readyWhen` resolves, because `open` returns while the
647
+ * JavaScript bundle is still loading.
648
+ */
649
+ declare function openSession(input: OpenSessionInput): Promise<DeviceSession>;
650
+ /** Deterministic, so a worker replaced after a failure reconnects to the session it left behind. */
651
+ declare function sessionName(options: ResolvedOptions, project: string, slot: number): string;
652
+ //#endregion
653
+ //#region src/core/device.d.ts
654
+ type TextOptions = {
655
+ exact?: boolean;
656
+ };
657
+ type RoleOptions = {
658
+ name?: string | RegExp;
659
+ exact?: boolean;
660
+ };
661
+ type ActionOptions = {
662
+ timeout?: number;
663
+ };
664
+ /**
665
+ * `hasText` matches the node's own text or any text in its subtree, while
666
+ * `has` needs a strict descendant. That asymmetry matches Playwright.
667
+ */
668
+ type FilterOptions = {
669
+ hasText?: string | RegExp;
670
+ hasNotText?: string | RegExp;
671
+ has?: Locator;
672
+ hasNot?: Locator;
673
+ };
674
+ /**
675
+ * `secret` reports only a character count, for a credential field the platform
676
+ * did not mark secure. The write is still confirmed character by character,
677
+ * because a plain field hands back its exact contents.
678
+ */
679
+ type FillOptions = ActionOptions & {
680
+ secret?: boolean;
681
+ };
682
+ type Device = {
683
+ /** Matches a node's accessibility name or its value. */
684
+ getByText(text: string | RegExp, options?: TextOptions): Locator;
685
+ getByRole(role: Role, options?: RoleOptions): Locator;
686
+ /** Matches the accessibility identifier, which is what a React Native `testID` becomes. */
687
+ getByTestId(testId: string): Locator;
688
+ locator(query: Query): Locator;
689
+ scroll(direction: ScrollDirection): Promise<void>;
690
+ /** Relaunches the app and waits for the ready gate again. */
691
+ relaunch(): Promise<void>;
692
+ /** Never automatic, because hiding the overlay would suppress a warning a test might want to see. */
693
+ dismissDevOverlay(): Promise<void>;
694
+ screen(): Promise<Screen>;
695
+ /** Returns the path. Nothing is attached to the report, so the caller decides whether to. */
696
+ screenshot(options?: {
697
+ path?: string;
698
+ }): Promise<string>;
699
+ };
700
+ /** A query bound to a session. Safe to hold across an action, because it stores a query and never a ref. */
701
+ type Locator = {
702
+ readonly query: Query;
703
+ /** The device this locator came from. A screenshot assertion needs the image and the tree, and a locator carries neither. */
704
+ readonly device: Device;
705
+ /** The factory call this locator renders back to, used in step titles and failure messages. */
706
+ readonly description: string;
707
+ first(): Locator;
708
+ /** Negative indexes count from the end, so `nth(-1)` is the last match. */
709
+ nth(index: number): Locator;
710
+ filter(options: FilterOptions): Locator;
711
+ tap(options?: ActionOptions): Promise<void>;
712
+ fill(text: string, options?: FillOptions): Promise<void>;
713
+ longPress(durationMs?: number, options?: ActionOptions): Promise<void>;
714
+ /** Actions scroll for themselves, so this is for seeing a node rather than acting on it. */
715
+ scrollIntoView(options?: ActionOptions): Promise<void>;
716
+ count(): Promise<number>;
717
+ /** The matched node's text off one fresh screen. Null when nothing matches. Ambiguity fails the way an action does. */
718
+ textContent(): Promise<string | null>;
719
+ expect(check: Check, options: ProbeOptions): Promise<ProbeResult>;
720
+ };
721
+ declare function createDevice(session: DeviceSession, sink: ActionSink): Device;
722
+ //#endregion
723
+ //#region src/core/scroll.d.ts
724
+ /** For a failure message. */
725
+ type ScrollTrail = {
726
+ readonly steps: number;
727
+ readonly direction: ScrollDirection;
728
+ };
729
+ /** What a scroll search needs from a session. A `SessionDevice` supplies it. */
730
+ type ScrollDevice = {
731
+ captureRaw(): Promise<Screen>;
732
+ scroll(direction: ScrollDirection, budgetMs: number): Promise<void>;
733
+ };
734
+ /**
735
+ * Two rules. A search never reverses, because a hint pointing back the way it
736
+ * came means the container ran out rather than that the target is behind, and
737
+ * reversing turns a finished search into an oscillation that burns the budget.
738
+ * And it stops at `maxSteps` whatever the clock says, so a screen that scrolls
739
+ * forever cannot run indefinitely.
740
+ */
741
+ type ScrollSearch = {
742
+ /** Null until the first step, then the one direction every later step uses. */
743
+ trail(): ScrollTrail | null;
744
+ /**
745
+ * Scrolls one step toward whatever `query` names. False means the search is
746
+ * over: nothing on screen says which way to go, the next hint would reverse,
747
+ * or the step cap is spent.
748
+ */
749
+ step(screen: Screen, query: Query, budgetMs: number): Promise<boolean>;
750
+ };
751
+ declare function createScrollSearch(device: ScrollDevice, maxSteps?: number): ScrollSearch;
752
+ /**
753
+ * Which way the target lies, or null when nothing on screen says.
754
+ *
755
+ * The raw tree places the target itself, so when it carries the node, its rect
756
+ * against the clipping container's rect is an answer rather than a guess. That
757
+ * is the iOS case. Android's raw tree stops at the window, so the only evidence
758
+ * left is the container reporting it holds content out of view.
759
+ */
760
+ declare function directionToward(screen: Screen, raw: Screen | null, query: Query): ScrollDirection | null;
761
+ //#endregion
762
+ //#region src/core/errors.d.ts
763
+ /**
764
+ * The most a failure may say about the text that was typed. A secure field
765
+ * reports one masking character per character it holds, so its length is all it
766
+ * can report, and a field marked secret discloses a length too. A password has
767
+ * no representation here, which keeps it out of a terminal and an HTML report.
768
+ */
769
+ type ExpectedValue = {
770
+ readonly kind: 'exact';
771
+ readonly value: string;
772
+ } | {
773
+ readonly kind: 'masked';
774
+ readonly length: number;
775
+ };
776
+ /**
777
+ * One class with a closed `info` union, so adapters and test authors switch on
778
+ * `info.kind` and the compiler names a missing case when a kind is added.
779
+ */
780
+ type ErrorInfo = {
781
+ readonly kind: 'config';
782
+ readonly field: string;
783
+ readonly detail: string;
784
+ } | {
785
+ readonly kind: 'device-in-use';
786
+ readonly owner: string | null;
787
+ readonly device: string;
788
+ readonly releaseCommand: string;
789
+ /** False once `onDeviceInUse: 'reclaim'` is already set, so the message stops suggesting it. */
790
+ readonly canReclaim: boolean;
791
+ } | {
792
+ readonly kind: 'launch-failed';
793
+ readonly app: string;
794
+ readonly device: string;
795
+ readonly failure: DeviceFailure;
796
+ } | {
797
+ readonly kind: 'not-ready';
798
+ readonly locator: string;
799
+ readonly timeoutMs: number;
800
+ readonly screen: string;
801
+ } | {
802
+ readonly kind: 'session-closed';
803
+ readonly command: string;
804
+ } | {
805
+ readonly kind: 'strict-mode';
806
+ readonly locator: string;
807
+ readonly matches: readonly string[];
808
+ readonly screen: string;
809
+ } | {
810
+ readonly kind: 'not-found';
811
+ readonly locator: string;
812
+ readonly timeoutMs: number;
813
+ readonly screen: string;
814
+ /** What the search scrolled looking for it, or null when it never scrolled. */
815
+ readonly scrolled: ScrollTrail | null;
816
+ } | {
817
+ readonly kind: 'fill-unconfirmed';
818
+ readonly locator: string;
819
+ readonly expected: ExpectedValue;
820
+ readonly actual: ExpectedValue | null;
821
+ readonly attempts: number;
822
+ readonly timeoutMs: number;
823
+ readonly screen: string;
824
+ } | {
825
+ readonly kind: 'driver';
826
+ readonly command: string;
827
+ readonly failure: DeviceFailure;
828
+ };
829
+ declare class TangereError extends Error {
830
+ readonly info: ErrorInfo;
831
+ constructor(info: ErrorInfo);
832
+ }
833
+ //#endregion
834
+ //#region src/preflight.d.ts
835
+ type PreflightDevice = {
836
+ readonly name: string;
837
+ readonly id: string;
838
+ };
839
+ type PreflightReport = {
840
+ readonly ok: true;
841
+ readonly device: PreflightDevice;
842
+ } | {
843
+ readonly ok: false;
844
+ readonly problems: readonly string[];
845
+ };
846
+ /**
847
+ * A device that is not booted is reported as problems rather than thrown, so a
848
+ * runner names every one of them at once. A malformed config still throws, the
849
+ * way it does everywhere else. `driver` is the seam tests inject.
850
+ */
851
+ declare function preflight(options: Partial<TangereOptions>, driver?: DeviceDriver): Promise<PreflightReport>;
852
+ //#endregion
853
+ export { Tree as $, deviceNameForSlot as A, ProbeResult as B, openSession as C, ResolvedOptions as D, ReadyQuery as E, StepOptions as F, CaptureOptions as G, formatFailure as H, Typed as I, DeviceInfo as J, DeviceDriver as K, renderTitle as L, ActionRecord as M, ActionSink as N, TANGERE_DEFAULTS as O, EvidenceFile as P, Settled as Q, silentSink as R, SessionState as S, DeviceChoice as T, probe as U, ProbeTarget as V, Binding as W, OpenRequest as X, DeviceSelection as Y, ScrollDirection as Z, RoleOptions as _, Screen as _t, ExpectedValue as a, Filter as at, DeviceSession as b, renderScreen as bt, ScrollSearch as c, TextMatch as ct, directionToward as d, textMatch as dt, Check as et, ActionOptions as f, PinnedRef as ft, Locator as g, Resolution as gt, FilterOptions as h, Rect as ht, ErrorInfo as i, evaluate as it, parseDeviceOptions as j, TangereOptions as k, ScrollTrail as l, describeQuery as lt, FillOptions as m, RawSnapshot as mt, PreflightReport as n, Verdict as nt, TangereError as o, Query as ot, Device as p, Platform as pt, DeviceFailure as q, preflight as r, describeCheck as rt, ScrollDevice as s, Role as st, PreflightDevice as t, CheckName as tt, createScrollSearch as u, normalizeText as ut, TextOptions as v, ScreenNode as vt, sessionName as w, OpenSessionInput as x, resolve as xt, createDevice as y, parseScreen as yt, ProbeOptions as z };