react-native-tapline 0.0.0-stage → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +86 -2
- package/TaplineNative.podspec +14 -0
- package/dist/actions.d.ts +160 -0
- package/dist/actions.js +1883 -0
- package/dist/actions.js.map +1 -0
- package/dist/agent.d.ts +79 -0
- package/dist/agent.js +255 -0
- package/dist/agent.js.map +1 -0
- package/dist/alerts.d.ts +35 -0
- package/dist/alerts.js +437 -0
- package/dist/alerts.js.map +1 -0
- package/dist/awake.d.ts +2 -0
- package/dist/awake.js +53 -0
- package/dist/awake.js.map +1 -0
- package/dist/checks.d.ts +19 -0
- package/dist/checks.js +318 -0
- package/dist/checks.js.map +1 -0
- package/dist/client.d.ts +111 -0
- package/dist/client.js +446 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +24 -0
- package/dist/commands.js +111 -0
- package/dist/commands.js.map +1 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.js +22 -0
- package/dist/events.js.map +1 -0
- package/dist/feed.d.ts +117 -0
- package/dist/feed.js +440 -0
- package/dist/feed.js.map +1 -0
- package/dist/fiber.d.ts +43 -0
- package/dist/fiber.js +100 -0
- package/dist/fiber.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +245 -0
- package/dist/inspect.js +1343 -0
- package/dist/inspect.js.map +1 -0
- package/dist/keyboard.d.ts +10 -0
- package/dist/keyboard.js +63 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/link.d.ts +21 -0
- package/dist/link.js +63 -0
- package/dist/link.js.map +1 -0
- package/dist/live.d.ts +30 -0
- package/dist/live.js +279 -0
- package/dist/live.js.map +1 -0
- package/dist/logs.d.ts +16 -0
- package/dist/logs.js +171 -0
- package/dist/logs.js.map +1 -0
- package/dist/methods.d.ts +3 -0
- package/dist/methods.js +47 -0
- package/dist/methods.js.map +1 -0
- package/dist/native.d.ts +34 -0
- package/dist/native.js +63 -0
- package/dist/native.js.map +1 -0
- package/dist/network.d.ts +27 -0
- package/dist/network.js +238 -0
- package/dist/network.js.map +1 -0
- package/dist/register.d.ts +24 -0
- package/dist/register.js +180 -0
- package/dist/register.js.map +1 -0
- package/dist/settle.d.ts +27 -0
- package/dist/settle.js +70 -0
- package/dist/settle.js.map +1 -0
- package/dist/storage.d.ts +10 -0
- package/dist/storage.js +45 -0
- package/dist/storage.js.map +1 -0
- package/dist/touches.d.ts +15 -0
- package/dist/touches.js +89 -0
- package/dist/touches.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.js +32 -0
- package/dist/types.js.map +1 -0
- package/dist/wait.d.ts +8 -0
- package/dist/wait.js +99 -0
- package/dist/wait.js.map +1 -0
- package/expo-module.config.json +7 -0
- package/ios/TaplineNativeModule.swift +79 -0
- package/metro.d.ts +10 -0
- package/metro.js +40 -0
- package/package.json +100 -4
- package/register.d.ts +1 -0
- package/register.js +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tapline contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-native-tapline
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The in-app agent of [Tapline](https://github.com/bo5amis/tapline#readme): lets an AI agent on your computer see and
|
|
4
|
+
drive **your own** React Native / Expo app on your test phone, on the same Wi-Fi as the
|
|
5
|
+
computer. TypeScript, plus an optional Expo native module that any new build of an Expo app
|
|
6
|
+
picks up by itself (faster live-view frames, and iOS alerts and the share sheet seen over the
|
|
7
|
+
app); no config plugin. Full setup:
|
|
8
|
+
[docs/setup.md](https://github.com/bo5amis/tapline/blob/main/docs/setup.md).
|
|
9
|
+
|
|
10
|
+
## Use
|
|
11
|
+
|
|
12
|
+
First line of the app's entry file (for Expo Router, your own `index.js` that then imports
|
|
13
|
+
`expo-router/entry`):
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import 'react-native-tapline/register';
|
|
17
|
+
import { startTapline } from 'react-native-tapline';
|
|
18
|
+
|
|
19
|
+
startTapline({ enabled: process.env.EXPO_PUBLIC_TAPLINE_ENABLED === '1' });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- The build carries no address and no token, only the switch. Set it in development and
|
|
23
|
+
test builds only. With it off, Tapline does nothing at all.
|
|
24
|
+
- With it on, the app pairs when a pairing link opens it
|
|
25
|
+
(`<scheme>://tapline/pair?url=…&code=…&name=…`, from the computer's QR code): it asks
|
|
26
|
+
"Connect for testing?", connects with the one-time code, and keeps the token the
|
|
27
|
+
computer sends back. From then on it reconnects to that computer whenever it's open
|
|
28
|
+
(a new try one second after each failed one). `<scheme>://tapline/forget` unpairs. It never throws
|
|
29
|
+
into the app.
|
|
30
|
+
- Expo Router: add `app/+native-intent.ts` so the router leaves pairing links alone:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { isTaplineLink } from 'react-native-tapline';
|
|
34
|
+
|
|
35
|
+
export function redirectSystemPath({ path }: { path: string; initial: boolean }) {
|
|
36
|
+
return isTaplineLink(path) ? null : path;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `expo-secure-store` (when installed) keeps the pairing in the keychain; without it the
|
|
41
|
+
pairing lasts until the app restarts, or pass `storage` (`getItem`, `setItem`,
|
|
42
|
+
`removeItem`, async). Each install also keeps a random id there, so the computer can
|
|
43
|
+
tell two phones apart.
|
|
44
|
+
- Optional: `app: { name, id, version, build }` names the app for the host; `url` and
|
|
45
|
+
`token` connect straight to a host without pairing (a simulator on the same computer);
|
|
46
|
+
`stopTapline()` shuts it down. `react-native-view-shot` for screenshots.
|
|
47
|
+
- The method functions (`tree`, `press`, `longPress`, `typeText`, `scroll`, `swipe`, `toggle`,
|
|
48
|
+
`openURL`, `getLogs`, `reload`, `screenshot`) are exported too, for tests or an in-app
|
|
49
|
+
debug screen.
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
- `register` installs a minimal React DevTools hook (chaining any existing one) that
|
|
54
|
+
records the app's React roots. It must run before React loads.
|
|
55
|
+
- The screen is read from the React tree: host views with text, an accessibility label,
|
|
56
|
+
a `testID` or a handler become elements. An element's type is the React Native
|
|
57
|
+
component around it (`Pressable`, `Button`, `TextInput`, `Switch`, `FlatList` …) or
|
|
58
|
+
your component's name. A `Text` that only repeats its button's label is folded into
|
|
59
|
+
the button.
|
|
60
|
+
- Positions come from `measureInWindow` (old architecture: the host instance; new
|
|
61
|
+
architecture: its public instance, else the Fabric UI manager), 500 ms per element at
|
|
62
|
+
most; an element that can't be measured is kept, without a frame.
|
|
63
|
+
- `press` calls the nearest `onPress` with a synthetic press event, and bubbles from a
|
|
64
|
+
non-actionable element to its nearest actionable parent. `type` calls the input's own
|
|
65
|
+
`onChange` and `onChangeText`, and sets the native text of uncontrolled inputs.
|
|
66
|
+
`scroll` uses the scroll view's `scrollTo` / `scrollToOffset` / `scrollToEnd`.
|
|
67
|
+
- Refs (`e1`, `e2` …) are given by `tree`; after an action, elements still on screen keep
|
|
68
|
+
theirs and new ones count on from the highest, so an old ref never points at another
|
|
69
|
+
element. Refs in an `ambiguous` error are valid too.
|
|
70
|
+
- `swipe` drags a synthetic finger through React Native's responder system (capture, then
|
|
71
|
+
bubble, with a touch history for `PanResponder`).
|
|
72
|
+
- Requests run one at a time; each has 30 s.
|
|
73
|
+
|
|
74
|
+
## Limits
|
|
75
|
+
|
|
76
|
+
- Only the app's own UI: no system dialogs, keyboard, other apps or home screen.
|
|
77
|
+
- Actions call handlers; they aren't touches. `swipe` reaches `PanResponder` and other
|
|
78
|
+
responder-based drags; gestures built on react-native-gesture-handler, pinch, and views
|
|
79
|
+
React doesn't render (sliders, maps, web view contents) are out of reach.
|
|
80
|
+
- Release builds shorten component names: element types read best in development builds,
|
|
81
|
+
and `testID`s keep targeting reliable.
|
|
82
|
+
- Checked on an iPhone with the new architecture (Expo 57, React Native 0.86) in a
|
|
83
|
+
development build. Not confirmed yet: the hook in release builds, and the scroll offset
|
|
84
|
+
after the user has scrolled a plain `ScrollView` by hand. On iOS the device model and name
|
|
85
|
+
aren't sent (the install id stands in).
|
|
86
|
+
- Turn it on only in builds you test with: the computer it's paired with can drive the
|
|
87
|
+
app while it's connected.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'TaplineNative'
|
|
3
|
+
s.version = '1.0.0'
|
|
4
|
+
s.summary = 'Tapline: frames without a forced render, and what iOS draws over the app'
|
|
5
|
+
s.license = { :type => 'MIT' }
|
|
6
|
+
s.author = 'Tapline'
|
|
7
|
+
s.homepage = 'https://github.com/tapline/tapline'
|
|
8
|
+
s.platforms = { :ios => '16.4' }
|
|
9
|
+
s.source = { :git => '' }
|
|
10
|
+
s.static_framework = true
|
|
11
|
+
s.dependency 'ExpoModulesCore'
|
|
12
|
+
s.swift_version = '5.9'
|
|
13
|
+
s.source_files = 'ios/**/*.swift'
|
|
14
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { type Entry, type Snapshot } from './inspect';
|
|
2
|
+
import { type Fiber } from './fiber';
|
|
3
|
+
import { type LogEntry, type TaplineElement } from './types';
|
|
4
|
+
export { tree } from './inspect';
|
|
5
|
+
/** Tune how long the app must be free of React commits after an action, and the longest wait (AGENT-TOOLS §6). */
|
|
6
|
+
export declare function configureActions(next: {
|
|
7
|
+
quietMs?: number;
|
|
8
|
+
capMs?: number;
|
|
9
|
+
}): void;
|
|
10
|
+
/** Runs `listener` when an action's handler has run, before the wait for the app to settle. */
|
|
11
|
+
export declare function onActed(listener: () => void): () => void;
|
|
12
|
+
/** What an action reports about its settle: the milliseconds, and the cap when the screen never went quiet. */
|
|
13
|
+
export interface SettleOutcome {
|
|
14
|
+
settledMs: number;
|
|
15
|
+
/** Set only when the wait ended at the cap with the screen still changing (AGENT-TOOLS v1.3, "The cap"). */
|
|
16
|
+
settleCapMs?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Waits until the app is still after an action; resolves with the milliseconds that took. */
|
|
19
|
+
export declare function settle(): Promise<number>;
|
|
20
|
+
/** Like `settle`, and says when the wait ended at the cap: spread it into an action's result. */
|
|
21
|
+
export declare function settleOutcome(): Promise<SettleOutcome>;
|
|
22
|
+
type ParsedTarget = {
|
|
23
|
+
kind: 'ref';
|
|
24
|
+
ref: string;
|
|
25
|
+
index?: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'testID';
|
|
28
|
+
value: string;
|
|
29
|
+
index?: number;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'label';
|
|
32
|
+
value: string;
|
|
33
|
+
index?: number;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'text';
|
|
36
|
+
value: string;
|
|
37
|
+
index?: number;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'point';
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
index?: number;
|
|
43
|
+
};
|
|
44
|
+
export declare function parseTarget(raw: unknown): ParsedTarget;
|
|
45
|
+
export interface Resolved {
|
|
46
|
+
snapshot: Snapshot;
|
|
47
|
+
entry: Entry;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What a `label` target matches: the element's own accessibility label, else its text. iOS forms
|
|
51
|
+
* the label of an element that has none from the text inside it, and Tapwright reads that label, so
|
|
52
|
+
* `{label: "OK"}` finds a button that only shows "OK" in both tools.
|
|
53
|
+
*/
|
|
54
|
+
export declare function labelOf(info: {
|
|
55
|
+
label: string | null;
|
|
56
|
+
text: string;
|
|
57
|
+
}): string;
|
|
58
|
+
/** What the caller means to do; a point target then picks only an element that does it. */
|
|
59
|
+
type ActionHint = 'press' | 'longPress' | 'toggle' | 'scroll' | 'type';
|
|
60
|
+
/** Resolve a protocol target to exactly one element, or throw not_found / ambiguous. */
|
|
61
|
+
export declare function resolveTarget(raw: unknown, hint?: ActionHint): Promise<Resolved>;
|
|
62
|
+
/** The nearest listed element at or above a host fiber, for naming what covers a target. */
|
|
63
|
+
export declare function describeHit(snapshot: Snapshot, hit: Fiber, actor?: Entry): string;
|
|
64
|
+
/** `back`: what the back button or gesture does, on the focused screen; with a sheet open, closes the sheet. */
|
|
65
|
+
export declare function back(rawParams: unknown): Promise<{
|
|
66
|
+
route: string[];
|
|
67
|
+
closed?: 'sheet';
|
|
68
|
+
} & SettleOutcome>;
|
|
69
|
+
/**
|
|
70
|
+
* What `press` and `longPress` answer: the element, `closedKeyboard: true` when the keyboard was
|
|
71
|
+
* closed first, and `forced` (the reason word) when `force` let through what the checks refuse.
|
|
72
|
+
*/
|
|
73
|
+
export type PressResult = {
|
|
74
|
+
element: TaplineElement;
|
|
75
|
+
closedKeyboard?: true;
|
|
76
|
+
forced?: string;
|
|
77
|
+
} & SettleOutcome;
|
|
78
|
+
export declare function press(params: unknown): Promise<PressResult>;
|
|
79
|
+
export declare function longPress(params: unknown): Promise<PressResult>;
|
|
80
|
+
/** What the SDK asks of the keyboard and the focused input; replaceable so tests need no native side. */
|
|
81
|
+
export interface KeyboardControl {
|
|
82
|
+
/** The instance of the text input that has focus, or null. */
|
|
83
|
+
focusedInput(): unknown;
|
|
84
|
+
/** Whether the software keyboard is up (true when React Native can't say). */
|
|
85
|
+
visible(): boolean;
|
|
86
|
+
/** Closes the keyboard without submitting (`Keyboard.dismiss()`). */
|
|
87
|
+
dismiss(): void;
|
|
88
|
+
/** Takes focus off `input`. */
|
|
89
|
+
blur(input: unknown): void;
|
|
90
|
+
/** The keyboard's top edge in window points, or null when it isn't known. */
|
|
91
|
+
top?(): number | null;
|
|
92
|
+
/**
|
|
93
|
+
* Gives a text input the focus, as a finger on it does. `instances` are the input's instances,
|
|
94
|
+
* public ones and its component's included (see `inputInstances`).
|
|
95
|
+
*/
|
|
96
|
+
focus?(instances: unknown[]): void;
|
|
97
|
+
}
|
|
98
|
+
/** Inject the keyboard and focus access (tests); pass null to restore the default. */
|
|
99
|
+
export declare function setKeyboardControl(next: KeyboardControl | null): void;
|
|
100
|
+
export declare function typeText(rawParams: unknown): Promise<{
|
|
101
|
+
element: TaplineElement;
|
|
102
|
+
value: string;
|
|
103
|
+
closedKeyboard?: true;
|
|
104
|
+
forced?: string;
|
|
105
|
+
} & SettleOutcome>;
|
|
106
|
+
/** What `scroll` answers; `target`, `reached` and `unchanged` only with `to: "target"`. */
|
|
107
|
+
export type ScrollResult = {
|
|
108
|
+
/** The scroll view that was scrolled. */
|
|
109
|
+
element: TaplineElement;
|
|
110
|
+
offset: {
|
|
111
|
+
x: number;
|
|
112
|
+
y: number;
|
|
113
|
+
};
|
|
114
|
+
moved: boolean;
|
|
115
|
+
/** The element that was scrolled to, where it is now. */
|
|
116
|
+
target?: TaplineElement;
|
|
117
|
+
/** False when the scroll views are as far as they go and the element still isn't clear. */
|
|
118
|
+
reached?: boolean;
|
|
119
|
+
/** The element was clear already: nothing was scrolled. */
|
|
120
|
+
unchanged?: true;
|
|
121
|
+
} & SettleOutcome;
|
|
122
|
+
export declare function scroll(rawParams: unknown): Promise<ScrollResult>;
|
|
123
|
+
export declare function toggle(rawParams: unknown): Promise<{
|
|
124
|
+
element: TaplineElement;
|
|
125
|
+
value: boolean;
|
|
126
|
+
closedKeyboard?: true;
|
|
127
|
+
unchanged?: true;
|
|
128
|
+
forced?: string;
|
|
129
|
+
} & SettleOutcome>;
|
|
130
|
+
export declare function openURL(rawParams: unknown): Promise<SettleOutcome>;
|
|
131
|
+
export declare function getLogs(rawParams: unknown): Promise<{
|
|
132
|
+
entries: LogEntry[];
|
|
133
|
+
}>;
|
|
134
|
+
export declare function reload(): Promise<Record<string, never>>;
|
|
135
|
+
export declare function screenshotAvailable(): boolean;
|
|
136
|
+
export declare function screenshot(rawParams: unknown): Promise<{
|
|
137
|
+
mime: string;
|
|
138
|
+
data: string;
|
|
139
|
+
width: number;
|
|
140
|
+
height: number;
|
|
141
|
+
}>;
|
|
142
|
+
/**
|
|
143
|
+
* `swipe`: a finger dragged from (x0, y0) to (x1, y1) over `seconds`, through React Native's own
|
|
144
|
+
* touch system: the views under the start point negotiate for it as they would for a finger
|
|
145
|
+
* (PanResponder, a sheet's swipe to close, a Pressable that gives way once the finger moves), and
|
|
146
|
+
* the winner gets the moves and the release with a real touch history. Gestures that
|
|
147
|
+
* react-native-gesture-handler or a native view handle (sliders, maps) don't go through it: they
|
|
148
|
+
* answer `no_action`, as does a start on a scroll view (`scroll` moves those).
|
|
149
|
+
*/
|
|
150
|
+
export declare function swipe(rawParams: unknown): Promise<{
|
|
151
|
+
element: TaplineElement | null;
|
|
152
|
+
from: {
|
|
153
|
+
x: number;
|
|
154
|
+
y: number;
|
|
155
|
+
};
|
|
156
|
+
to: {
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
};
|
|
160
|
+
} & SettleOutcome>;
|