react-native-tapline 0.0.0-stage → 1.1.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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -2
  3. package/TaplineNative.podspec +14 -0
  4. package/dist/actions.d.ts +203 -0
  5. package/dist/actions.js +1991 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/agent.d.ts +117 -0
  8. package/dist/agent.js +352 -0
  9. package/dist/agent.js.map +1 -0
  10. package/dist/alerts.d.ts +35 -0
  11. package/dist/alerts.js +437 -0
  12. package/dist/alerts.js.map +1 -0
  13. package/dist/awake.d.ts +2 -0
  14. package/dist/awake.js +53 -0
  15. package/dist/awake.js.map +1 -0
  16. package/dist/checks.d.ts +35 -0
  17. package/dist/checks.js +501 -0
  18. package/dist/checks.js.map +1 -0
  19. package/dist/client.d.ts +111 -0
  20. package/dist/client.js +446 -0
  21. package/dist/client.js.map +1 -0
  22. package/dist/commands.d.ts +24 -0
  23. package/dist/commands.js +111 -0
  24. package/dist/commands.js.map +1 -0
  25. package/dist/events.d.ts +5 -0
  26. package/dist/events.js +22 -0
  27. package/dist/events.js.map +1 -0
  28. package/dist/feed.d.ts +117 -0
  29. package/dist/feed.js +440 -0
  30. package/dist/feed.js.map +1 -0
  31. package/dist/fiber.d.ts +43 -0
  32. package/dist/fiber.js +100 -0
  33. package/dist/fiber.js.map +1 -0
  34. package/dist/index.d.ts +50 -0
  35. package/dist/index.js +181 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/inspect.d.ts +249 -0
  38. package/dist/inspect.js +1354 -0
  39. package/dist/inspect.js.map +1 -0
  40. package/dist/keyboard.d.ts +10 -0
  41. package/dist/keyboard.js +63 -0
  42. package/dist/keyboard.js.map +1 -0
  43. package/dist/link.d.ts +21 -0
  44. package/dist/link.js +63 -0
  45. package/dist/link.js.map +1 -0
  46. package/dist/live.d.ts +30 -0
  47. package/dist/live.js +279 -0
  48. package/dist/live.js.map +1 -0
  49. package/dist/logs.d.ts +16 -0
  50. package/dist/logs.js +171 -0
  51. package/dist/logs.js.map +1 -0
  52. package/dist/methods.d.ts +3 -0
  53. package/dist/methods.js +49 -0
  54. package/dist/methods.js.map +1 -0
  55. package/dist/native.d.ts +34 -0
  56. package/dist/native.js +63 -0
  57. package/dist/native.js.map +1 -0
  58. package/dist/network.d.ts +27 -0
  59. package/dist/network.js +238 -0
  60. package/dist/network.js.map +1 -0
  61. package/dist/register.d.ts +24 -0
  62. package/dist/register.js +180 -0
  63. package/dist/register.js.map +1 -0
  64. package/dist/settle.d.ts +27 -0
  65. package/dist/settle.js +70 -0
  66. package/dist/settle.js.map +1 -0
  67. package/dist/source.d.ts +60 -0
  68. package/dist/source.js +214 -0
  69. package/dist/source.js.map +1 -0
  70. package/dist/storage.d.ts +10 -0
  71. package/dist/storage.js +45 -0
  72. package/dist/storage.js.map +1 -0
  73. package/dist/touches.d.ts +15 -0
  74. package/dist/touches.js +89 -0
  75. package/dist/touches.js.map +1 -0
  76. package/dist/types.d.ts +168 -0
  77. package/dist/types.js +32 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/wait.d.ts +8 -0
  80. package/dist/wait.js +99 -0
  81. package/dist/wait.js.map +1 -0
  82. package/expo-module.config.json +7 -0
  83. package/ios/TaplineNativeModule.swift +79 -0
  84. package/metro.d.ts +26 -0
  85. package/metro.js +146 -0
  86. package/package.json +100 -4
  87. package/register.d.ts +1 -0
  88. package/register.js +3 -0
@@ -0,0 +1,15 @@
1
+ /** At most one touch is passed on per this many milliseconds. */
2
+ export declare const TOUCH_THROTTLE_MS = 500;
3
+ export type TouchListener = (at: number) => void;
4
+ /** Calls `listener` (with the touch time) for each real touch that passes the throttle; returns the unsubscribe. */
5
+ export declare function onRealTouch(listener: TouchListener): () => void;
6
+ /**
7
+ * A touch started on the app. Returns true when it was passed on to the listeners; false when
8
+ * nobody listens (Tapline stopped) or it came within `TOUCH_THROTTLE_MS` of the last passed one.
9
+ * A touch that is dropped is not lost: when its window ends, one `touch` report with the time of the
10
+ * latest dropped touch goes out (so a second touch a person made inside the window shows up too).
11
+ * Never throws.
12
+ */
13
+ export declare function noteRealTouch(now: number): boolean;
14
+ /** Forgets the throttle and every listener (tests). */
15
+ export declare function resetTouches(): void;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ // Real touches. Tapline never touches the screen itself (an agent's `tap` calls the element's handler),
3
+ // so any touch that reaches the app's root is a person's finger, or another tool that makes real
4
+ // touches. The wrapper around the app's root (alerts.tsx) calls `noteRealTouch`; the client listens
5
+ // and tells the host, which then warns the agent that a person used the app.
6
+ //
7
+ // Limit: a React Native `Modal` is its own native window, so touches inside one never reach the
8
+ // wrapper and are not seen.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.TOUCH_THROTTLE_MS = void 0;
11
+ exports.onRealTouch = onRealTouch;
12
+ exports.noteRealTouch = noteRealTouch;
13
+ exports.resetTouches = resetTouches;
14
+ /** At most one touch is passed on per this many milliseconds. */
15
+ exports.TOUCH_THROTTLE_MS = 500;
16
+ const listeners = new Set();
17
+ let lastPassedAt = null;
18
+ /** The time of the latest touch dropped in the current window; it is reported when the window ends. */
19
+ let droppedAt = null;
20
+ let trailing = null;
21
+ function cancelTrailing() {
22
+ if (trailing !== null)
23
+ clearTimeout(trailing);
24
+ trailing = null;
25
+ droppedAt = null;
26
+ }
27
+ function emit(at) {
28
+ for (const listener of [...listeners]) {
29
+ try {
30
+ listener(at);
31
+ }
32
+ catch {
33
+ // a listener must never reach the app
34
+ }
35
+ }
36
+ }
37
+ /** The window ended with touches dropped in it: report the latest of them once. */
38
+ function flushTrailing() {
39
+ trailing = null;
40
+ const at = droppedAt;
41
+ droppedAt = null;
42
+ if (at === null || lastPassedAt === null || listeners.size === 0)
43
+ return;
44
+ // The trailing report starts the next window, counted from where this one ended, so the rate
45
+ // stays at one per window however late the timer fires.
46
+ lastPassedAt += exports.TOUCH_THROTTLE_MS;
47
+ emit(at);
48
+ }
49
+ /** Calls `listener` (with the touch time) for each real touch that passes the throttle; returns the unsubscribe. */
50
+ function onRealTouch(listener) {
51
+ listeners.add(listener);
52
+ return () => {
53
+ listeners.delete(listener);
54
+ if (listeners.size === 0) {
55
+ lastPassedAt = null;
56
+ cancelTrailing();
57
+ }
58
+ };
59
+ }
60
+ /**
61
+ * A touch started on the app. Returns true when it was passed on to the listeners; false when
62
+ * nobody listens (Tapline stopped) or it came within `TOUCH_THROTTLE_MS` of the last passed one.
63
+ * A touch that is dropped is not lost: when its window ends, one `touch` report with the time of the
64
+ * latest dropped touch goes out (so a second touch a person made inside the window shows up too).
65
+ * Never throws.
66
+ */
67
+ function noteRealTouch(now) {
68
+ if (listeners.size === 0)
69
+ return false;
70
+ // A clock that went backwards must not silence touches until it catches up.
71
+ if (lastPassedAt !== null && now >= lastPassedAt && now - lastPassedAt < exports.TOUCH_THROTTLE_MS) {
72
+ droppedAt = now;
73
+ if (trailing === null)
74
+ trailing = setTimeout(flushTrailing, lastPassedAt + exports.TOUCH_THROTTLE_MS - now);
75
+ return false;
76
+ }
77
+ // A touch that passes covers every earlier one: nothing is left to report at the end of a window.
78
+ cancelTrailing();
79
+ lastPassedAt = now;
80
+ emit(now);
81
+ return true;
82
+ }
83
+ /** Forgets the throttle and every listener (tests). */
84
+ function resetTouches() {
85
+ listeners.clear();
86
+ lastPassedAt = null;
87
+ cancelTrailing();
88
+ }
89
+ //# sourceMappingURL=touches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"touches.js","sourceRoot":"","sources":["../src/touches.ts"],"names":[],"mappings":";AAAA,wGAAwG;AACxG,iGAAiG;AACjG,oGAAoG;AACpG,6EAA6E;AAC7E,EAAE;AACF,gGAAgG;AAChG,4BAA4B;;;AA0C5B,kCASC;AASD,sCAaC;AAGD,oCAIC;AA9ED,iEAAiE;AACpD,QAAA,iBAAiB,GAAG,GAAG,CAAC;AAIrC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAiB,CAAC;AAC3C,IAAI,YAAY,GAAkB,IAAI,CAAC;AACvC,uGAAuG;AACvG,IAAI,SAAS,GAAkB,IAAI,CAAC;AACpC,IAAI,QAAQ,GAAyC,IAAI,CAAC;AAE1D,SAAS,cAAc;IACrB,IAAI,QAAQ,KAAK,IAAI;QAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC9C,QAAQ,GAAG,IAAI,CAAC;IAChB,SAAS,GAAG,IAAI,CAAC;AACnB,CAAC;AAED,SAAS,IAAI,CAAC,EAAU;IACtB,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC;YACH,QAAQ,CAAC,EAAE,CAAC,CAAC;QACf,CAAC;QAAC,MAAM,CAAC;YACP,sCAAsC;QACxC,CAAC;IACH,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,SAAS,aAAa;IACpB,QAAQ,GAAG,IAAI,CAAC;IAChB,MAAM,EAAE,GAAG,SAAS,CAAC;IACrB,SAAS,GAAG,IAAI,CAAC;IACjB,IAAI,EAAE,KAAK,IAAI,IAAI,YAAY,KAAK,IAAI,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO;IACzE,6FAA6F;IAC7F,wDAAwD;IACxD,YAAY,IAAI,yBAAiB,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,CAAC;AACX,CAAC;AAED,oHAAoH;AACpH,SAAgB,WAAW,CAAC,QAAuB;IACjD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACxB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC3B,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACzB,YAAY,GAAG,IAAI,CAAC;YACpB,cAAc,EAAE,CAAC;QACnB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,GAAW;IACvC,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,4EAA4E;IAC5E,IAAI,YAAY,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,IAAI,GAAG,GAAG,YAAY,GAAG,yBAAiB,EAAE,CAAC;QAC3F,SAAS,GAAG,GAAG,CAAC;QAChB,IAAI,QAAQ,KAAK,IAAI;YAAE,QAAQ,GAAG,UAAU,CAAC,aAAa,EAAE,YAAY,GAAG,yBAAiB,GAAG,GAAG,CAAC,CAAC;QACpG,OAAO,KAAK,CAAC;IACf,CAAC;IACD,kGAAkG;IAClG,cAAc,EAAE,CAAC;IACjB,YAAY,GAAG,GAAG,CAAC;IACnB,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,IAAI,CAAC;AACd,CAAC;AAED,uDAAuD;AACvD,SAAgB,YAAY;IAC1B,SAAS,CAAC,KAAK,EAAE,CAAC;IAClB,YAAY,GAAG,IAAI,CAAC;IACpB,cAAc,EAAE,CAAC;AACnB,CAAC"}
@@ -0,0 +1,168 @@
1
+ /** Wire-protocol and shared types (see docs/PROTOCOL.md, version 1). */
2
+ export declare const PROTOCOL_VERSION = 1;
3
+ export type ErrorCode = 'not_found' | 'ambiguous' | 'not_actionable' | 'unsupported' | 'bad_params' | 'timeout' | 'internal';
4
+ export interface WireError {
5
+ code: ErrorCode;
6
+ message: string;
7
+ /** Candidate refs, only for `ambiguous`. */
8
+ refs?: string[];
9
+ /** The candidates as elements (role, id, actions, frame), only for `ambiguous`. */
10
+ candidates?: TaplineElement[];
11
+ }
12
+ /** An error that maps 1:1 onto a protocol error response. */
13
+ export declare class TaplineError extends Error {
14
+ readonly code: ErrorCode;
15
+ readonly refs: string[] | undefined;
16
+ readonly candidates: TaplineElement[] | undefined;
17
+ constructor(code: ErrorCode, message: string, refs?: string[], candidates?: TaplineElement[]);
18
+ toWire(): WireError;
19
+ }
20
+ export interface Frame {
21
+ x: number;
22
+ y: number;
23
+ width: number;
24
+ height: number;
25
+ }
26
+ export type Action = 'press' | 'longPress' | 'type' | 'submit' | 'toggle' | 'scroll';
27
+ export interface TaplineElement {
28
+ ref: string;
29
+ type: string;
30
+ role: string | null;
31
+ testID: string | null;
32
+ label: string | null;
33
+ /** The element's text, whitespace-collapsed; at most 2000 characters of it. */
34
+ text: string;
35
+ /** How many characters of the text were left out; absent when it is whole. */
36
+ textMore?: number;
37
+ /** TextInput content; null for everything else. */
38
+ value: string | null;
39
+ /** An empty TextInput's placeholder (AGENT-TOOLS v1.7); absent when the input has text or none. */
40
+ placeholder?: string;
41
+ /** Null only when the element could not be measured. */
42
+ frame: Frame | null;
43
+ actions: Action[];
44
+ disabled: boolean;
45
+ selected: boolean;
46
+ /** true / false / 'mixed' for checkable controls (Switch, checkbox role), else null. */
47
+ checked: boolean | 'mixed' | null;
48
+ }
49
+ export interface ScreenInfo {
50
+ width: number;
51
+ height: number;
52
+ scale: number;
53
+ }
54
+ export interface RouteInfo {
55
+ /** Route names from the outermost navigator to the focused screen, e.g. ["(tabs)", "nutrition"]. */
56
+ names: string[];
57
+ /** The focused screen's simple params (strings, numbers, booleans, null). */
58
+ params?: Record<string, string | number | boolean | null>;
59
+ }
60
+ /** What an element was before, for the fields that changed. */
61
+ export type ChangedFields = Partial<Pick<TaplineElement, 'text' | 'label' | 'value' | 'placeholder' | 'checked' | 'selected' | 'disabled' | 'actions'>>;
62
+ /** The difference from the previous `tree` call (only with `changes: true`). Refs of unchanged elements are kept. */
63
+ export interface TreeChanges {
64
+ /** Whether the focused route is the same as before. */
65
+ sameRoute: boolean;
66
+ /** Refs of elements that are new. */
67
+ added: string[];
68
+ /** Elements that are gone, as they were (their refs no longer work). */
69
+ removed: TaplineElement[];
70
+ /** Elements whose text, value, state or actions changed, with the old values. */
71
+ changed: Array<{
72
+ ref: string;
73
+ was: ChangedFields;
74
+ }>;
75
+ /** Refs of elements that only moved. */
76
+ moved: string[];
77
+ /**
78
+ * Elements off screen (above or below it, before and now) whose text, label, value or state
79
+ * changed, as they are now and without a ref, with the old values. At most 5; absent when none.
80
+ */
81
+ offScreen?: Array<{
82
+ element: TaplineElement;
83
+ was: ChangedFields;
84
+ }>;
85
+ /** How many more changed off screen than `offScreen` holds; absent when none. */
86
+ offScreenMore?: number;
87
+ /** The software keyboard came up or went down since then; absent when it didn't, or isn't known. */
88
+ keyboard?: 'shown' | 'hidden';
89
+ }
90
+ /** An alert the app showed and nobody has answered yet. */
91
+ export interface TreeAlert {
92
+ kind: 'alert' | 'prompt';
93
+ title: string;
94
+ message: string | null;
95
+ buttons: Array<{
96
+ text: string;
97
+ style: 'default' | 'cancel' | 'destructive';
98
+ }>;
99
+ /** Tapline's in-app stand-in (its buttons are elements to press) rather than the OS alert. */
100
+ standIn: boolean;
101
+ }
102
+ export interface TreeResult {
103
+ screen: ScreenInfo;
104
+ elements: TaplineElement[];
105
+ /** How many elements lie wholly above and below the screen (scroll to reach them); absent when none. */
106
+ offScreen?: {
107
+ above: number;
108
+ below: number;
109
+ };
110
+ /** Open alerts (Alert.alert / Alert.prompt) the app showed, oldest first. */
111
+ alerts?: TreeAlert[];
112
+ /** Network requests still running (fetch / XMLHttpRequest); absent when none. */
113
+ network?: {
114
+ inFlight: number;
115
+ };
116
+ /** True while the software keyboard is up; absent when it is down or isn't known. */
117
+ keyboard?: true;
118
+ /** The focused screen, when the app uses React Navigation (Expo Router included). */
119
+ route?: RouteInfo;
120
+ /** With `changes: true` and a previous tree: what changed since then. */
121
+ changes?: TreeChanges;
122
+ }
123
+ export type Target = {
124
+ ref: string;
125
+ index?: number;
126
+ } | {
127
+ testID: string;
128
+ index?: number;
129
+ } | {
130
+ label: string;
131
+ index?: number;
132
+ } | {
133
+ text: string;
134
+ index?: number;
135
+ } | {
136
+ x: number;
137
+ y: number;
138
+ index?: number;
139
+ };
140
+ export type LogLevel = 'log' | 'info' | 'warn' | 'error';
141
+ export interface LogEntry {
142
+ level: LogLevel;
143
+ message: string;
144
+ ts: number;
145
+ }
146
+ export interface ErrorEventData {
147
+ message: string;
148
+ stack: string;
149
+ fatal: boolean;
150
+ ts: number;
151
+ }
152
+ export interface AppInfo {
153
+ name?: string;
154
+ id?: string;
155
+ version?: string;
156
+ build?: string;
157
+ }
158
+ export interface DeviceInfo {
159
+ platform: string;
160
+ osVersion: string;
161
+ model?: string;
162
+ name?: string;
163
+ }
164
+ export type Json = string | number | boolean | null | Json[] | {
165
+ [key: string]: Json;
166
+ };
167
+ export type UnknownRecord = Record<string, unknown>;
168
+ export declare function isRecord(value: unknown): value is UnknownRecord;
package/dist/types.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ /** Wire-protocol and shared types (see docs/PROTOCOL.md, version 1). */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.TaplineError = exports.PROTOCOL_VERSION = void 0;
5
+ exports.isRecord = isRecord;
6
+ exports.PROTOCOL_VERSION = 1;
7
+ /** An error that maps 1:1 onto a protocol error response. */
8
+ class TaplineError extends Error {
9
+ code;
10
+ refs;
11
+ candidates;
12
+ constructor(code, message, refs, candidates) {
13
+ super(message);
14
+ this.name = 'TaplineError';
15
+ this.code = code;
16
+ this.refs = refs;
17
+ this.candidates = candidates;
18
+ }
19
+ toWire() {
20
+ const wire = { code: this.code, message: this.message };
21
+ if (this.refs)
22
+ wire.refs = this.refs;
23
+ if (this.candidates)
24
+ wire.candidates = this.candidates;
25
+ return wire;
26
+ }
27
+ }
28
+ exports.TaplineError = TaplineError;
29
+ function isRecord(value) {
30
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
31
+ }
32
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,wEAAwE;;;AAwLxE,4BAEC;AAxLY,QAAA,gBAAgB,GAAG,CAAC,CAAC;AAoBlC,6DAA6D;AAC7D,MAAa,YAAa,SAAQ,KAAK;IAC5B,IAAI,CAAY;IAChB,IAAI,CAAuB;IAC3B,UAAU,CAA+B;IAElD,YAAY,IAAe,EAAE,OAAe,EAAE,IAAe,EAAE,UAA6B;QAC1F,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED,MAAM;QACJ,MAAM,IAAI,GAAc,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;QACnE,IAAI,IAAI,CAAC,IAAI;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACrC,IAAI,IAAI,CAAC,UAAU;YAAE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAnBD,oCAmBC;AA8ID,SAAgB,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
package/dist/wait.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ type IdleProvider = () => boolean;
2
+ /** Where "the network is quiet" comes from (the network tracker; tests). Null restores "always quiet". */
3
+ export declare function setNetworkIdleProvider(provider: IdleProvider | null): void;
4
+ export declare function waitFor(raw: unknown): Promise<{
5
+ met: true;
6
+ waitedMs: number;
7
+ }>;
8
+ export {};
package/dist/wait.js ADDED
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setNetworkIdleProvider = setNetworkIdleProvider;
4
+ exports.waitFor = waitFor;
5
+ const inspect_1 = require("./inspect");
6
+ const types_1 = require("./types");
7
+ /**
8
+ * `waitFor`: polls the screen until an element appears (or is gone) and/or the network is
9
+ * quiet, so an agent waits for loading to finish instead of guessing a delay.
10
+ */
11
+ const POLL_MS = 250;
12
+ const DEFAULT_TIMEOUT_S = 10;
13
+ const MAX_TIMEOUT_S = 30;
14
+ let networkIdle = () => true;
15
+ /** Where "the network is quiet" comes from (the network tracker; tests). Null restores "always quiet". */
16
+ function setNetworkIdleProvider(provider) {
17
+ networkIdle = provider ?? (() => true);
18
+ }
19
+ function parse(raw) {
20
+ if (!(0, types_1.isRecord)(raw))
21
+ throw new types_1.TaplineError('bad_params', 'waitFor needs an object');
22
+ const pick = (key) => {
23
+ const value = raw[key];
24
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
25
+ };
26
+ const text = pick('text');
27
+ const testID = pick('testID');
28
+ const label = pick('label');
29
+ const wantsNetwork = raw.networkIdle === true;
30
+ if (text === undefined && testID === undefined && label === undefined && !wantsNetwork) {
31
+ throw new types_1.TaplineError('bad_params', 'waitFor needs text, testID, label or networkIdle');
32
+ }
33
+ const timeout = raw.timeout === undefined ? DEFAULT_TIMEOUT_S : raw.timeout;
34
+ if (typeof timeout !== 'number' || !(timeout > 0) || timeout > MAX_TIMEOUT_S) {
35
+ throw new types_1.TaplineError('bad_params', `timeout is in seconds, above 0 and at most ${MAX_TIMEOUT_S}`);
36
+ }
37
+ return {
38
+ ...(text !== undefined ? { text } : {}),
39
+ ...(testID !== undefined ? { testID } : {}),
40
+ ...(label !== undefined ? { label } : {}),
41
+ gone: raw.gone === true,
42
+ networkIdle: wantsNetwork,
43
+ timeoutMs: timeout * 1000,
44
+ };
45
+ }
46
+ function matches(entry, condition) {
47
+ const { info } = entry;
48
+ if (condition.testID !== undefined && info.testID !== condition.testID)
49
+ return false;
50
+ // As a `label` target matches: the element's own label, else its text.
51
+ if (condition.label !== undefined && (info.label ?? info.text).toLowerCase() !== condition.label.toLowerCase())
52
+ return false;
53
+ if (condition.text !== undefined && !info.text.toLowerCase().includes(condition.text.toLowerCase()))
54
+ return false;
55
+ return true;
56
+ }
57
+ function satisfied(condition) {
58
+ if (condition.text !== undefined || condition.testID !== undefined || condition.label !== undefined) {
59
+ const present = (0, inspect_1.takeSnapshot)().entries.some((entry) => entry.info.interesting && matches(entry, condition));
60
+ if (present === condition.gone)
61
+ return false;
62
+ }
63
+ if (condition.networkIdle) {
64
+ try {
65
+ if (!networkIdle())
66
+ return false;
67
+ }
68
+ catch {
69
+ // an unknown network state doesn't block the wait
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+ function describe(condition) {
75
+ const what = condition.text !== undefined
76
+ ? `text "${condition.text}"`
77
+ : condition.testID !== undefined
78
+ ? `id "${condition.testID}"`
79
+ : condition.label !== undefined
80
+ ? `label "${condition.label}"`
81
+ : null;
82
+ // The words Tapwright uses too (AGENT-TOOLS §5): `id` as the agent writes it, "to disappear".
83
+ const element = what === null ? null : `${what} ${condition.gone ? 'to disappear' : 'to appear'}`;
84
+ return [element, condition.networkIdle ? 'the network to be quiet' : null].filter((part) => part !== null).join(' and ');
85
+ }
86
+ async function waitFor(raw) {
87
+ const condition = parse(raw);
88
+ const started = Date.now();
89
+ for (;;) {
90
+ if (satisfied(condition))
91
+ return { met: true, waitedMs: Date.now() - started };
92
+ const waited = Date.now() - started;
93
+ if (waited >= condition.timeoutMs) {
94
+ throw new types_1.TaplineError('timeout', `Waited ${Math.round(waited / 100) / 10} s for ${describe(condition)}; it didn't happen.`);
95
+ }
96
+ await new Promise((resolve) => setTimeout(() => resolve(), Math.min(POLL_MS, condition.timeoutMs - waited)));
97
+ }
98
+ }
99
+ //# sourceMappingURL=wait.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait.js","sourceRoot":"","sources":["../src/wait.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ {
2
+ "platforms": ["apple"],
3
+ "apple": {
4
+ "podspecPath": "TaplineNative.podspec",
5
+ "modules": ["TaplineNativeModule"]
6
+ }
7
+ }
@@ -0,0 +1,79 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+
4
+ /// Tapline's native side (optional: the SDK works without it, through react-native-view-shot).
5
+ ///
6
+ /// - `capture`: a JPEG of the app's window. With `fresh: false` it draws what is on screen now
7
+ /// (`afterScreenUpdates: false`), so iOS isn't made to render a new frame for it: the live
8
+ /// viewer can capture often without slowing the app.
9
+ /// - `cover`: what UIKit presents over the app that React Native doesn't draw: an alert or action
10
+ /// sheet (`UIAlertController`, also the one `Alert.alert` shows) and the share sheet
11
+ /// (`UIActivityViewController`), with what an agent needs to name it.
12
+ private enum TaplineWindows {
13
+ static func keyWindow() -> UIWindow? {
14
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
15
+ let scene = scenes.first { $0.activationState == .foregroundActive } ?? scenes.first
16
+ guard let windows = scene?.windows else { return nil }
17
+ return windows.first { $0.isKeyWindow } ?? windows.first
18
+ }
19
+
20
+ /// The view controllers presented over the root, front last.
21
+ static func presented(from root: UIViewController?) -> [UIViewController] {
22
+ var chain: [UIViewController] = []
23
+ var current = root?.presentedViewController
24
+ while let controller = current {
25
+ chain.append(controller)
26
+ current = controller.presentedViewController
27
+ }
28
+ return chain
29
+ }
30
+ }
31
+
32
+ public final class TaplineNativeModule: Module {
33
+ public func definition() -> ModuleDefinition {
34
+ Name("TaplineNative")
35
+
36
+ /// Base64 JPEG of the key window at `pixelWidth` pixels wide (the height keeps the window's
37
+ /// shape), or an empty string when there is no window to draw.
38
+ /// Only the drawing holds the main thread; the JPEG is made on a background queue, so the app's
39
+ /// own frames wait for the draw alone.
40
+ AsyncFunction("capture") { (pixelWidth: Double, quality: Double, fresh: Bool, promise: Promise) in
41
+ guard let window = TaplineWindows.keyWindow(), window.bounds.width > 0, pixelWidth > 0 else {
42
+ promise.resolve("")
43
+ return
44
+ }
45
+ let bounds = window.bounds
46
+ let size = CGSize(width: pixelWidth.rounded(), height: (pixelWidth * bounds.height / bounds.width).rounded())
47
+ let format = UIGraphicsImageRendererFormat()
48
+ format.scale = 1
49
+ format.opaque = true
50
+ let image = UIGraphicsImageRenderer(size: size, format: format).image { _ in
51
+ window.drawHierarchy(in: CGRect(origin: .zero, size: size), afterScreenUpdates: fresh)
52
+ }
53
+ let clamped = CGFloat(min(max(quality, 0.1), 1))
54
+ DispatchQueue.global(qos: .userInitiated).async {
55
+ promise.resolve(image.jpegData(compressionQuality: clamped)?.base64EncodedString() ?? "")
56
+ }
57
+ }.runOnQueue(.main)
58
+
59
+ /// What UIKit shows in front of the app: `kind` is "alert", "actionSheet", "share" or "" (nothing).
60
+ AsyncFunction("cover") { () -> [String: Any] in
61
+ let chain = TaplineWindows.presented(from: TaplineWindows.keyWindow()?.rootViewController)
62
+ for controller in chain.reversed() {
63
+ if let alert = controller as? UIAlertController {
64
+ return [
65
+ "kind": alert.preferredStyle == .actionSheet ? "actionSheet" : "alert",
66
+ "title": alert.title ?? "",
67
+ "message": alert.message ?? "",
68
+ "buttons": alert.actions.map { $0.title ?? "" },
69
+ "destructive": alert.actions.map { $0.style == .destructive },
70
+ ]
71
+ }
72
+ if controller is UIActivityViewController {
73
+ return ["kind": "share", "title": "", "message": "", "buttons": [String](), "destructive": [Bool]()]
74
+ }
75
+ }
76
+ return ["kind": "", "title": "", "message": "", "buttons": [String](), "destructive": [Bool]()]
77
+ }.runOnQueue(.main)
78
+ }
79
+ }
package/metro.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { IncomingMessage, ServerResponse } from 'http';
2
+
3
+ export type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
4
+
5
+ export interface TaplineMetroConfig {
6
+ serializer?: { getModulesRunBeforeMainModule?: (entryFilePath: string) => string[] } & Record<string, unknown>;
7
+ server?: { enhanceMiddleware?: (middleware: Middleware, server: unknown) => Middleware } & Record<string, unknown>;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ /**
12
+ * Runs Tapline's `register` before the app's main module (only when Tapline is on:
13
+ * EXPO_PUBLIC_TAPLINE_ENABLED=1, or `enabled`), so no custom entry file is needed. It also answers
14
+ * `GET /tapline/offer` on Metro, so a development build finds the pairing offer a Tapline host on this
15
+ * computer opened (zero-QR pairing).
16
+ */
17
+ export function withTapline<C extends TaplineMetroConfig>(config: C, options?: { enabled?: boolean }): C;
18
+
19
+ /** The newest open offer in `dir` (`~/.tapline/offers`), or null. */
20
+ export function readOffer(dir: string): { code: string; name: string; port: number; file: string; claimedBy: string | undefined } | null;
21
+
22
+ /** The `/tapline/offer` middleware, reading offers from `dir`. */
23
+ export function offerMiddleware(dir: string): Middleware;
24
+
25
+ /** Private LAN, link-local or loopback. */
26
+ export function isLocalNetwork(address: string | undefined): boolean;