@blario/react-native 0.0.0 → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/dist/BlarProvider.d.ts +74 -0
- package/dist/actions/batched.d.ts +1 -0
- package/dist/actions/click.d.ts +4 -0
- package/dist/actions/commit-watch.d.ts +15 -0
- package/dist/actions/events.d.ts +35 -0
- package/dist/actions/executor.d.ts +29 -0
- package/dist/actions/highlight.d.ts +4 -0
- package/dist/actions/interaction-guards.d.ts +9 -0
- package/dist/actions/landing.d.ts +11 -0
- package/dist/actions/navigation-driver.d.ts +22 -0
- package/dist/actions/parse.d.ts +4 -0
- package/dist/actions/result.d.ts +4 -0
- package/dist/actions/scrolling.d.ts +12 -0
- package/dist/actions/services.d.ts +20 -0
- package/dist/actions/settle.d.ts +13 -0
- package/dist/actions/switch.d.ts +3 -0
- package/dist/actions/target.d.ts +25 -0
- package/dist/actions/text-input.d.ts +9 -0
- package/dist/actions/types.d.ts +59 -0
- package/dist/agent/batch-commands.d.ts +17 -0
- package/dist/agent/compose.d.ts +23 -0
- package/dist/agent/controller.d.ts +28 -0
- package/dist/agent/frame-runner.d.ts +9 -0
- package/dist/agent/frames.d.ts +180 -0
- package/dist/agent/handlers/agent-command.d.ts +5 -0
- package/dist/agent/handlers/batch.d.ts +5 -0
- package/dist/agent/handlers/consent-screenshot.d.ts +4 -0
- package/dist/agent/handlers/context.d.ts +16 -0
- package/dist/agent/handlers/dom-state.d.ts +4 -0
- package/dist/agent/handlers/element-context.d.ts +2 -0
- package/dist/agent/handlers/screenshot.d.ts +3 -0
- package/dist/agent/handlers/text-dom.d.ts +5 -0
- package/dist/agent/stage.d.ts +13 -0
- package/dist/agent-mode/action-description.d.ts +3 -0
- package/dist/agent-mode/bubbles.d.ts +10 -0
- package/dist/agent-mode/colors.d.ts +12 -0
- package/dist/agent-mode/controller.d.ts +92 -0
- package/dist/agent-mode/cursor-follow.d.ts +12 -0
- package/dist/agent-mode/cursor.d.ts +37 -0
- package/dist/agent-mode/frames.d.ts +36 -0
- package/dist/agent-mode/haptics.d.ts +6 -0
- package/dist/agent-mode/keep-awake.d.ts +19 -0
- package/dist/agent-mode/layer-stacking.d.ts +7 -0
- package/dist/agent-mode/markdown.d.ts +19 -0
- package/dist/agent-mode/panel-geometry.d.ts +6 -0
- package/dist/agent-mode/resume.d.ts +13 -0
- package/dist/agent-mode/run-memory.d.ts +19 -0
- package/dist/agent-mode/spotlight-controller.d.ts +68 -0
- package/dist/agent-mode/spotlight.d.ts +6 -0
- package/dist/agent-mode/state.d.ts +9 -0
- package/dist/agent-mode/status.d.ts +11 -0
- package/dist/agent-mode/timers.d.ts +6 -0
- package/dist/agent-mode/ui/AgentDock.d.ts +37 -0
- package/dist/agent-mode/ui/AgentLayer.d.ts +19 -0
- package/dist/agent-mode/ui/AgentLayerHost.d.ts +8 -0
- package/dist/agent-mode/ui/AgentPanel.d.ts +19 -0
- package/dist/agent-mode/ui/AgentTap.d.ts +10 -0
- package/dist/agent-mode/ui/MarkdownText.d.ts +8 -0
- package/dist/agent-mode/ui/MessageBubbles.d.ts +9 -0
- package/dist/agent-mode/ui/PendingOverlay.d.ts +5 -0
- package/dist/agent-mode/ui/SpotlightLayer.d.ts +9 -0
- package/dist/agent-mode/ui/SpotlightOverlay.d.ts +7 -0
- package/dist/agent-mode/ui/StatusCard.d.ts +14 -0
- package/dist/agent-mode/ui/TargetPulse.d.ts +9 -0
- package/dist/agent-mode/ui/TouchShield.d.ts +5 -0
- package/dist/agent-mode/ui/WarningIcon.d.ts +6 -0
- package/dist/agent-mode/ui/full-window-overlay.d.ts +5 -0
- package/dist/agent-mode/ui/layer-host.d.ts +2 -0
- package/dist/backend/api-client.d.ts +38 -0
- package/dist/backend/api-types.d.ts +25 -0
- package/dist/backend/app-info.d.ts +14 -0
- package/dist/backend/backend-transport.d.ts +70 -0
- package/dist/backend/backoff.d.ts +9 -0
- package/dist/backend/base64.d.ts +1 -0
- package/dist/backend/errors.d.ts +17 -0
- package/dist/backend/foreground.d.ts +1 -0
- package/dist/backend/health.d.ts +17 -0
- package/dist/backend/identity-proof.d.ts +23 -0
- package/dist/backend/identity.d.ts +27 -0
- package/dist/backend/message-metadata.d.ts +19 -0
- package/dist/backend/presence.d.ts +20 -0
- package/dist/backend/response-cache.d.ts +9 -0
- package/dist/backend/session-change.d.ts +2 -0
- package/dist/backend/session-socket.d.ts +49 -0
- package/dist/backend/session.d.ts +39 -0
- package/dist/backend/socket-url.d.ts +9 -0
- package/dist/backend/storage.d.ts +13 -0
- package/dist/backend/tab-id.d.ts +2 -0
- package/dist/blar.d.ts +37 -0
- package/dist/chat/ChatLayer.d.ts +29 -0
- package/dist/chat/ChatSheet.d.ts +11 -0
- package/dist/chat/ChatView.d.ts +19 -0
- package/dist/chat/Launcher.d.ts +14 -0
- package/dist/chat/bridge.d.ts +22 -0
- package/dist/chat/chat-coordinator.d.ts +57 -0
- package/dist/chat/chat-state.d.ts +20 -0
- package/dist/chat/navigation.d.ts +5 -0
- package/dist/chat/options.d.ts +22 -0
- package/dist/chat/webview.d.ts +10 -0
- package/dist/chat/widget-bundle.generated.d.ts +2 -0
- package/dist/chat/widget-shell.d.ts +24 -0
- package/dist/confirmation/port.d.ts +12 -0
- package/dist/fiber/locate.d.ts +7 -0
- package/dist/fiber/narrow.d.ts +5 -0
- package/dist/fiber/reader.d.ts +10 -0
- package/dist/fiber/text.d.ts +4 -0
- package/dist/fiber/types.d.ts +21 -0
- package/dist/host/border-radius.d.ts +1 -0
- package/dist/host/instance.d.ts +9 -0
- package/dist/host/measure.d.ts +3 -0
- package/dist/host/rect.d.ts +22 -0
- package/dist/host/visible-area.d.ts +2 -0
- package/dist/index.d.ts +10 -0
- package/dist/nudge/NudgeBubble.d.ts +15 -0
- package/dist/nudge/nudge.d.ts +23 -0
- package/dist/optional/expo-haptics.d.ts +1 -0
- package/dist/optional/expo-keep-awake.d.ts +1 -0
- package/dist/optional/react-native-haptic-feedback.d.ts +1 -0
- package/dist/optional/react-native-keep-awake.d.ts +1 -0
- package/dist/overlay/AgentOverlay.d.ts +5 -0
- package/dist/screenshot/capture-backend.d.ts +6 -0
- package/dist/screenshot/screenshotter.d.ts +18 -0
- package/dist/screenshot/skia.d.ts +35 -0
- package/dist/screenshot/view-shot.d.ts +2 -0
- package/dist/snapshot/capture.d.ts +36 -0
- package/dist/snapshot/classify.d.ts +11 -0
- package/dist/snapshot/collect.d.ts +27 -0
- package/dist/snapshot/element-context.d.ts +9 -0
- package/dist/snapshot/icon.d.ts +2 -0
- package/dist/snapshot/kinds.d.ts +3 -0
- package/dist/snapshot/modals.d.ts +2 -0
- package/dist/snapshot/navigation.d.ts +14 -0
- package/dist/snapshot/ordering.d.ts +2 -0
- package/dist/snapshot/properties.d.ts +14 -0
- package/dist/snapshot/registry.d.ts +16 -0
- package/dist/snapshot/render.d.ts +10 -0
- package/dist/snapshot/route-summary.d.ts +3 -0
- package/dist/snapshot/scroll.d.ts +15 -0
- package/dist/snapshot/text-layout.d.ts +8 -0
- package/dist/snapshot/view.d.ts +30 -0
- package/dist/snapshot/visibility.d.ts +17 -0
- package/dist/snapshot/wrappers.d.ts +11 -0
- package/dist/telemetry/SdkErrorBoundary.d.ts +15 -0
- package/dist/telemetry/build-info.generated.d.ts +2 -0
- package/dist/telemetry/envelope.d.ts +53 -0
- package/dist/telemetry/reporter.d.ts +33 -0
- package/dist/telemetry/scrub.d.ts +1 -0
- package/dist/telemetry/sdk-reporter.d.ts +5 -0
- package/dist/transport/connection-options.d.ts +18 -0
- package/dist/transport/relay-transport.d.ts +17 -0
- package/dist/transport/transport.d.ts +7 -0
- package/dist/ui/insets.d.ts +8 -0
- package/dist/ui/strings.d.ts +33 -0
- package/dist/ui/useInsets.d.ts +2 -0
- package/dist/version.d.ts +1 -0
- package/package.json +87 -3
- package/src/BlarProvider.tsx +462 -0
- package/src/actions/batched.ts +16 -0
- package/src/actions/click.ts +54 -0
- package/src/actions/commit-watch.ts +86 -0
- package/src/actions/events.ts +58 -0
- package/src/actions/executor.ts +147 -0
- package/src/actions/highlight.ts +13 -0
- package/src/actions/interaction-guards.ts +44 -0
- package/src/actions/landing.ts +26 -0
- package/src/actions/navigation-driver.ts +131 -0
- package/src/actions/parse.ts +82 -0
- package/src/actions/result.ts +14 -0
- package/src/actions/scrolling.ts +112 -0
- package/src/actions/services.ts +24 -0
- package/src/actions/settle.ts +40 -0
- package/src/actions/switch.ts +31 -0
- package/src/actions/target.ts +77 -0
- package/src/actions/text-input.ts +82 -0
- package/src/actions/types.ts +52 -0
- package/src/agent/batch-commands.ts +33 -0
- package/src/agent/compose.ts +67 -0
- package/src/agent/controller.ts +106 -0
- package/src/agent/frame-runner.ts +27 -0
- package/src/agent/frames.ts +193 -0
- package/src/agent/handlers/agent-command.ts +74 -0
- package/src/agent/handlers/batch.ts +72 -0
- package/src/agent/handlers/consent-screenshot.ts +44 -0
- package/src/agent/handlers/context.ts +17 -0
- package/src/agent/handlers/dom-state.ts +19 -0
- package/src/agent/handlers/element-context.ts +9 -0
- package/src/agent/handlers/screenshot.ts +24 -0
- package/src/agent/handlers/text-dom.ts +17 -0
- package/src/agent/stage.ts +22 -0
- package/src/agent-mode/action-description.ts +25 -0
- package/src/agent-mode/bubbles.ts +31 -0
- package/src/agent-mode/colors.ts +40 -0
- package/src/agent-mode/controller.ts +459 -0
- package/src/agent-mode/cursor-follow.ts +43 -0
- package/src/agent-mode/cursor.ts +118 -0
- package/src/agent-mode/frames.ts +70 -0
- package/src/agent-mode/haptics.ts +49 -0
- package/src/agent-mode/keep-awake.ts +107 -0
- package/src/agent-mode/layer-stacking.ts +16 -0
- package/src/agent-mode/markdown.ts +84 -0
- package/src/agent-mode/panel-geometry.ts +27 -0
- package/src/agent-mode/resume.ts +16 -0
- package/src/agent-mode/run-memory.ts +65 -0
- package/src/agent-mode/spotlight-controller.ts +255 -0
- package/src/agent-mode/spotlight.ts +14 -0
- package/src/agent-mode/state.ts +36 -0
- package/src/agent-mode/status.ts +13 -0
- package/src/agent-mode/timers.ts +11 -0
- package/src/agent-mode/ui/AgentDock.tsx +285 -0
- package/src/agent-mode/ui/AgentLayer.tsx +123 -0
- package/src/agent-mode/ui/AgentLayerHost.tsx +37 -0
- package/src/agent-mode/ui/AgentPanel.tsx +187 -0
- package/src/agent-mode/ui/AgentTap.tsx +190 -0
- package/src/agent-mode/ui/MarkdownText.tsx +55 -0
- package/src/agent-mode/ui/MessageBubbles.tsx +58 -0
- package/src/agent-mode/ui/PendingOverlay.tsx +32 -0
- package/src/agent-mode/ui/SpotlightLayer.tsx +60 -0
- package/src/agent-mode/ui/SpotlightOverlay.tsx +35 -0
- package/src/agent-mode/ui/StatusCard.tsx +137 -0
- package/src/agent-mode/ui/TargetPulse.tsx +74 -0
- package/src/agent-mode/ui/TouchShield.tsx +22 -0
- package/src/agent-mode/ui/WarningIcon.tsx +32 -0
- package/src/agent-mode/ui/full-window-overlay.ts +16 -0
- package/src/agent-mode/ui/layer-host.ts +11 -0
- package/src/backend/api-client.ts +210 -0
- package/src/backend/api-types.ts +56 -0
- package/src/backend/app-info.ts +40 -0
- package/src/backend/backend-transport.ts +338 -0
- package/src/backend/backoff.ts +16 -0
- package/src/backend/base64.ts +25 -0
- package/src/backend/errors.ts +32 -0
- package/src/backend/foreground.ts +10 -0
- package/src/backend/health.ts +64 -0
- package/src/backend/identity-proof.ts +151 -0
- package/src/backend/identity.ts +31 -0
- package/src/backend/message-metadata.ts +28 -0
- package/src/backend/presence.ts +95 -0
- package/src/backend/response-cache.ts +45 -0
- package/src/backend/session-change.ts +14 -0
- package/src/backend/session-socket.ts +292 -0
- package/src/backend/session.ts +188 -0
- package/src/backend/socket-url.ts +25 -0
- package/src/backend/storage.ts +49 -0
- package/src/backend/tab-id.ts +24 -0
- package/src/blar.ts +100 -0
- package/src/chat/ChatLayer.tsx +109 -0
- package/src/chat/ChatSheet.tsx +84 -0
- package/src/chat/ChatView.tsx +133 -0
- package/src/chat/Launcher.tsx +72 -0
- package/src/chat/bridge.ts +56 -0
- package/src/chat/chat-coordinator.ts +189 -0
- package/src/chat/chat-state.ts +35 -0
- package/src/chat/navigation.ts +15 -0
- package/src/chat/options.ts +39 -0
- package/src/chat/webview.ts +23 -0
- package/src/chat/widget-bundle.generated.ts +2 -0
- package/src/chat/widget-shell.ts +170 -0
- package/src/confirmation/port.ts +15 -0
- package/src/fiber/locate.ts +41 -0
- package/src/fiber/narrow.ts +15 -0
- package/src/fiber/reader.ts +98 -0
- package/src/fiber/text.ts +52 -0
- package/src/fiber/types.ts +24 -0
- package/src/host/border-radius.ts +11 -0
- package/src/host/instance.ts +30 -0
- package/src/host/measure.ts +25 -0
- package/src/host/rect.ts +45 -0
- package/src/host/visible-area.ts +5 -0
- package/src/index.ts +10 -0
- package/src/nudge/NudgeBubble.tsx +117 -0
- package/src/nudge/nudge.ts +100 -0
- package/src/optional/expo-haptics.ts +11 -0
- package/src/optional/expo-keep-awake.ts +11 -0
- package/src/optional/react-native-haptic-feedback.ts +11 -0
- package/src/optional/react-native-keep-awake.ts +11 -0
- package/src/overlay/AgentOverlay.tsx +16 -0
- package/src/screenshot/capture-backend.ts +7 -0
- package/src/screenshot/screenshotter.ts +39 -0
- package/src/screenshot/skia.ts +107 -0
- package/src/screenshot/view-shot.ts +41 -0
- package/src/snapshot/capture.ts +163 -0
- package/src/snapshot/classify.ts +40 -0
- package/src/snapshot/collect.ts +136 -0
- package/src/snapshot/element-context.ts +66 -0
- package/src/snapshot/icon.ts +21 -0
- package/src/snapshot/kinds.ts +10 -0
- package/src/snapshot/modals.ts +21 -0
- package/src/snapshot/navigation.ts +47 -0
- package/src/snapshot/ordering.ts +26 -0
- package/src/snapshot/properties.ts +71 -0
- package/src/snapshot/registry.ts +33 -0
- package/src/snapshot/render.ts +140 -0
- package/src/snapshot/route-summary.ts +15 -0
- package/src/snapshot/scroll.ts +90 -0
- package/src/snapshot/text-layout.ts +59 -0
- package/src/snapshot/view.ts +87 -0
- package/src/snapshot/visibility.ts +63 -0
- package/src/snapshot/wrappers.ts +69 -0
- package/src/telemetry/SdkErrorBoundary.tsx +28 -0
- package/src/telemetry/build-info.generated.ts +2 -0
- package/src/telemetry/envelope.ts +83 -0
- package/src/telemetry/reporter.ts +170 -0
- package/src/telemetry/scrub.ts +10 -0
- package/src/telemetry/sdk-reporter.ts +37 -0
- package/src/transport/connection-options.ts +65 -0
- package/src/transport/relay-transport.ts +82 -0
- package/src/transport/transport.ts +8 -0
- package/src/ui/insets.ts +31 -0
- package/src/ui/strings.ts +102 -0
- package/src/ui/useInsets.ts +10 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { childrenOf, componentName, firstHostDescendant, isHostComponent, propsOf } from '../fiber/reader.ts';
|
|
2
|
+
import { collectText } from '../fiber/text.ts';
|
|
3
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
4
|
+
import { classify } from './classify.ts';
|
|
5
|
+
import type { ElementKind } from './kinds.ts';
|
|
6
|
+
import { isFocusedScreen, type ScreenRef, screenOf } from './navigation.ts';
|
|
7
|
+
import { roleOf } from './properties.ts';
|
|
8
|
+
|
|
9
|
+
export const AGENT_OVERLAY_TEST_ID = 'blar-agent-overlay';
|
|
10
|
+
|
|
11
|
+
export interface Candidate {
|
|
12
|
+
kind: ElementKind;
|
|
13
|
+
fiber: Fiber;
|
|
14
|
+
host: Fiber;
|
|
15
|
+
ancestors: Fiber[];
|
|
16
|
+
scrollContainers: Fiber[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MODAL_COMPONENT_NAME = 'Modal';
|
|
20
|
+
const TEXT_HOST_TYPE = 'RCTText';
|
|
21
|
+
const TEXT_BLOCK_LIMIT = 800;
|
|
22
|
+
const HEADING_ROLES: readonly string[] = ['header', 'heading'];
|
|
23
|
+
|
|
24
|
+
export interface TextBlock {
|
|
25
|
+
host: Fiber;
|
|
26
|
+
text: string;
|
|
27
|
+
heading: boolean;
|
|
28
|
+
ancestors: Fiber[];
|
|
29
|
+
scrollContainers: Fiber[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Collected {
|
|
33
|
+
candidates: Candidate[];
|
|
34
|
+
textBlocks: TextBlock[];
|
|
35
|
+
focusedScreens: ScreenRef[];
|
|
36
|
+
modalOpen: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function collect(root: Fiber): Collected {
|
|
40
|
+
const candidates: Candidate[] = [];
|
|
41
|
+
const textBlocks: TextBlock[] = [];
|
|
42
|
+
const focusedScreens = new Map<string, ScreenRef>();
|
|
43
|
+
const ancestors: Fiber[] = [];
|
|
44
|
+
const scrollContainers: Fiber[] = [];
|
|
45
|
+
const openModals: Fiber[] = [];
|
|
46
|
+
|
|
47
|
+
const visit = (fiber: Fiber): void => {
|
|
48
|
+
const props = propsOf(fiber);
|
|
49
|
+
if (props.testID === AGENT_OVERLAY_TEST_ID) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const screen = screenOf(props);
|
|
53
|
+
if (screen !== null) {
|
|
54
|
+
if (!isFocusedScreen(screen)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!focusedScreens.has(screen.key)) {
|
|
58
|
+
focusedScreens.set(screen.key, screen);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (isOpenModal(fiber)) {
|
|
62
|
+
openModals.push(fiber);
|
|
63
|
+
}
|
|
64
|
+
const kind = classify(fiber);
|
|
65
|
+
const host = kind === null ? null : firstHostDescendant(fiber);
|
|
66
|
+
if (kind !== null && host !== null) {
|
|
67
|
+
candidates.push({
|
|
68
|
+
kind,
|
|
69
|
+
fiber,
|
|
70
|
+
host,
|
|
71
|
+
ancestors: [...ancestors],
|
|
72
|
+
scrollContainers: [...scrollContainers],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const block = readTextBlock(fiber, ancestors, scrollContainers);
|
|
76
|
+
if (block !== null) {
|
|
77
|
+
textBlocks.push(block);
|
|
78
|
+
}
|
|
79
|
+
ancestors.push(fiber);
|
|
80
|
+
if (kind === 'scroll') {
|
|
81
|
+
scrollContainers.push(fiber);
|
|
82
|
+
}
|
|
83
|
+
for (const child of childrenOf(fiber)) {
|
|
84
|
+
visit(child);
|
|
85
|
+
}
|
|
86
|
+
if (kind === 'scroll') {
|
|
87
|
+
scrollContainers.pop();
|
|
88
|
+
}
|
|
89
|
+
ancestors.pop();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
visit(root);
|
|
93
|
+
const frontModal = openModals[openModals.length - 1];
|
|
94
|
+
const inFront = (entry: { ancestors: Fiber[] }): boolean => frontModal === undefined || entry.ancestors.includes(frontModal);
|
|
95
|
+
return {
|
|
96
|
+
candidates: candidates.filter(inFront),
|
|
97
|
+
textBlocks: textBlocks.filter(inFront),
|
|
98
|
+
focusedScreens: [...focusedScreens.values()],
|
|
99
|
+
modalOpen: frontModal !== undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readTextBlock(fiber: Fiber, ancestors: Fiber[], scrollContainers: Fiber[]): TextBlock | null {
|
|
104
|
+
if (!isHostComponent(fiber) || fiber.type !== TEXT_HOST_TYPE) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const text = collectText(fiber, TEXT_BLOCK_LIMIT);
|
|
108
|
+
if (text.length === 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const role = roleOf(propsOf(fiber));
|
|
112
|
+
return {
|
|
113
|
+
host: fiber,
|
|
114
|
+
text,
|
|
115
|
+
heading: role !== null && HEADING_ROLES.includes(role),
|
|
116
|
+
ancestors: [...ancestors],
|
|
117
|
+
scrollContainers: [...scrollContainers],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isOpenModal(fiber: Fiber): boolean {
|
|
122
|
+
return componentName(fiber) === MODAL_COMPONENT_NAME && fiber.child !== null && propsOf(fiber).visible !== false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function keepInnermostPerHost(candidates: Candidate[]): Candidate[] {
|
|
126
|
+
const actionsByHost = new Map<Fiber, Candidate>();
|
|
127
|
+
const scrollsByHost = new Map<Fiber, Candidate>();
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
const byHost = candidate.kind === 'scroll' ? scrollsByHost : actionsByHost;
|
|
130
|
+
const existing = byHost.get(candidate.host);
|
|
131
|
+
if (existing === undefined || candidate.ancestors.length > existing.ancestors.length) {
|
|
132
|
+
byHost.set(candidate.host, candidate);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return [...actionsByHost.values(), ...scrollsByHost.values()];
|
|
136
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ElementContextItem } from '../agent/frames.ts';
|
|
2
|
+
import type { TargetResolver } from '../actions/target.ts';
|
|
3
|
+
import { childrenOf, propsOf } from '../fiber/reader.ts';
|
|
4
|
+
import { collectText, precedingText } from '../fiber/text.ts';
|
|
5
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
6
|
+
import { measureHost } from '../host/measure.ts';
|
|
7
|
+
import { isVisibleIn, type Size } from '../host/rect.ts';
|
|
8
|
+
import { placeholderOf } from './properties.ts';
|
|
9
|
+
import { renderLine } from './render.ts';
|
|
10
|
+
import { buildView } from './view.ts';
|
|
11
|
+
|
|
12
|
+
const TEXT_LIMIT = 300;
|
|
13
|
+
const NEARBY_LIMIT = 60;
|
|
14
|
+
const ANCESTOR_LEVELS = 3;
|
|
15
|
+
const SIBLING_ENTRIES = 8;
|
|
16
|
+
|
|
17
|
+
const TAG_BY_KIND = { button: 'button', text_input: 'input', switch: 'input', scroll: 'scroll' } as const;
|
|
18
|
+
|
|
19
|
+
function siblingTexts(ancestors: Fiber[], fiber: Fiber): string[] {
|
|
20
|
+
const texts: string[] = [];
|
|
21
|
+
let branch = fiber;
|
|
22
|
+
for (let depth = ancestors.length - 1; depth >= Math.max(0, ancestors.length - ANCESTOR_LEVELS); depth -= 1) {
|
|
23
|
+
const parent = ancestors[depth];
|
|
24
|
+
if (parent === undefined) {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
for (const sibling of childrenOf(parent)) {
|
|
28
|
+
const text = sibling === branch ? '' : collectText(sibling, NEARBY_LIMIT);
|
|
29
|
+
if (text.length > 0 && !texts.includes(text)) {
|
|
30
|
+
texts.push(text);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
branch = parent;
|
|
34
|
+
}
|
|
35
|
+
return texts.slice(0, SIBLING_ENTRIES);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ElementContextReader {
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly resolver: TargetResolver,
|
|
41
|
+
private readonly getViewport: () => Size,
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
async read(agentId: string): Promise<ElementContextItem> {
|
|
45
|
+
const target = this.resolver.resolveAgentId(agentId);
|
|
46
|
+
if (target === null) {
|
|
47
|
+
return { agent_id: agentId, found: false };
|
|
48
|
+
}
|
|
49
|
+
const measured = target.host === null ? null : await measureHost(target.host);
|
|
50
|
+
const rect = measured ?? target.element.rect;
|
|
51
|
+
const view = buildView({ kind: target.element.kind, fiber: target.fiber, ancestors: target.ancestors, rect, scroll: null });
|
|
52
|
+
const placeholder = placeholderOf(propsOf(target.fiber));
|
|
53
|
+
return {
|
|
54
|
+
agent_id: agentId,
|
|
55
|
+
found: true,
|
|
56
|
+
tag_name: TAG_BY_KIND[target.element.kind],
|
|
57
|
+
visible: isVisibleIn(rect, this.getViewport()),
|
|
58
|
+
line: renderLine(view, agentId, 0),
|
|
59
|
+
bounding_box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
60
|
+
text_content: collectText(target.fiber, TEXT_LIMIT),
|
|
61
|
+
...(placeholder === null ? {} : { placeholder }),
|
|
62
|
+
nearest_label: precedingText(target.ancestors, target.fiber, NEARBY_LIMIT),
|
|
63
|
+
sibling_text: siblingTexts(target.ancestors, target.fiber),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { nonEmptyString } from '../fiber/narrow.ts';
|
|
2
|
+
import { childrenOf, propsOf } from '../fiber/reader.ts';
|
|
3
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
4
|
+
|
|
5
|
+
function isIconProps(props: Readonly<Record<string, unknown>>): boolean {
|
|
6
|
+
return nonEmptyString(props.name) !== null && (typeof props.size === 'number' || typeof props.color === 'string');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function iconNameIn(fiber: Fiber): string | null {
|
|
10
|
+
const props = propsOf(fiber);
|
|
11
|
+
if (typeof fiber.type !== 'string' && isIconProps(props)) {
|
|
12
|
+
return nonEmptyString(props.name);
|
|
13
|
+
}
|
|
14
|
+
for (const child of childrenOf(fiber)) {
|
|
15
|
+
const found = iconNameIn(child);
|
|
16
|
+
if (found !== null) {
|
|
17
|
+
return found;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ElementKind = 'button' | 'text_input' | 'switch' | 'scroll';
|
|
2
|
+
|
|
3
|
+
export type IdPrefix = 'btn' | 'inp' | 'scr';
|
|
4
|
+
|
|
5
|
+
export const ID_PREFIX_BY_KIND: Readonly<Record<ElementKind, IdPrefix>> = {
|
|
6
|
+
button: 'btn',
|
|
7
|
+
text_input: 'inp',
|
|
8
|
+
switch: 'inp',
|
|
9
|
+
scroll: 'scr',
|
|
10
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { childrenOf, propsOf } from '../fiber/reader.ts';
|
|
2
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
3
|
+
import { AGENT_OVERLAY_TEST_ID, isOpenModal } from './collect.ts';
|
|
4
|
+
|
|
5
|
+
export function countOpenModals(fiber: Fiber): number {
|
|
6
|
+
const pending = [fiber];
|
|
7
|
+
let count = 0;
|
|
8
|
+
while (pending.length > 0) {
|
|
9
|
+
const current = pending.pop();
|
|
10
|
+
if (current === undefined || propsOf(current).testID === AGENT_OVERLAY_TEST_ID) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (isOpenModal(current)) {
|
|
14
|
+
count += 1;
|
|
15
|
+
}
|
|
16
|
+
for (const child of childrenOf(current)) {
|
|
17
|
+
pending.push(child);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return count;
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isFunction, isRecord, nonEmptyString } from '../fiber/narrow.ts';
|
|
2
|
+
import type { Props } from '../fiber/types.ts';
|
|
3
|
+
|
|
4
|
+
export interface NavigationLike {
|
|
5
|
+
navigate: (...args: unknown[]) => void;
|
|
6
|
+
isFocused: () => boolean;
|
|
7
|
+
getState?: () => unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ScreenRef {
|
|
11
|
+
key: string;
|
|
12
|
+
name: string;
|
|
13
|
+
navigation: NavigationLike;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function screenOf(props: Props): ScreenRef | null {
|
|
17
|
+
const { route, navigation } = props;
|
|
18
|
+
if (!isRecord(route) || !isRecord(navigation)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const key = nonEmptyString(route.key);
|
|
22
|
+
const name = nonEmptyString(route.name);
|
|
23
|
+
if (key === null || name === null || !isFunction(navigation.navigate) || !isFunction(navigation.isFocused)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return { key, name, navigation: navigation as unknown as NavigationLike };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isFocusedScreen(screen: ScreenRef): boolean {
|
|
30
|
+
try {
|
|
31
|
+
return screen.navigation.isFocused();
|
|
32
|
+
} catch {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function routeNamesOf(navigation: NavigationLike): string[] {
|
|
38
|
+
try {
|
|
39
|
+
const state = navigation.getState?.();
|
|
40
|
+
if (!isRecord(state) || !Array.isArray(state.routeNames)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return state.routeNames.filter((name): name is string => typeof name === 'string');
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MeasuredCandidate } from './wrappers.ts';
|
|
2
|
+
|
|
3
|
+
export function orderForReading(measured: MeasuredCandidate[]): MeasuredCandidate[] {
|
|
4
|
+
const sorted = [...measured].sort((left, right) => left.rect.y - right.rect.y || left.rect.x - right.rect.x);
|
|
5
|
+
const scrollEntries = new Map(
|
|
6
|
+
sorted.filter((entry) => entry.candidate.kind === 'scroll').map((entry) => [entry.candidate.fiber, entry] as const),
|
|
7
|
+
);
|
|
8
|
+
const ordered: MeasuredCandidate[] = [];
|
|
9
|
+
const emitted = new Set<MeasuredCandidate>();
|
|
10
|
+
const emit = (entry: MeasuredCandidate): void => {
|
|
11
|
+
if (!emitted.has(entry)) {
|
|
12
|
+
emitted.add(entry);
|
|
13
|
+
ordered.push(entry);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
for (const entry of sorted) {
|
|
17
|
+
for (const container of entry.candidate.scrollContainers) {
|
|
18
|
+
const scrollEntry = scrollEntries.get(container);
|
|
19
|
+
if (scrollEntry !== undefined) {
|
|
20
|
+
emit(scrollEntry);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
emit(entry);
|
|
24
|
+
}
|
|
25
|
+
return ordered;
|
|
26
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isRecord, nonEmptyString } from '../fiber/narrow.ts';
|
|
2
|
+
import type { Props } from '../fiber/types.ts';
|
|
3
|
+
|
|
4
|
+
export interface AccessibilityFlags {
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
selected: boolean | null;
|
|
7
|
+
expanded: boolean | null;
|
|
8
|
+
checked: boolean | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function booleanFrom(...candidates: unknown[]): boolean | null {
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
if (typeof candidate === 'boolean') {
|
|
14
|
+
return candidate;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function explicitLabelOf(props: Props): string | null {
|
|
21
|
+
return nonEmptyString(props['aria-label']) ?? nonEmptyString(props.accessibilityLabel);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function testIdOf(props: Props): string | null {
|
|
25
|
+
return nonEmptyString(props.testID);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function roleOf(props: Props): string | null {
|
|
29
|
+
return nonEmptyString(props.role) ?? nonEmptyString(props.accessibilityRole);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function accessibilityFlagsOf(props: Props): AccessibilityFlags {
|
|
33
|
+
const state = isRecord(props.accessibilityState) ? props.accessibilityState : {};
|
|
34
|
+
return {
|
|
35
|
+
disabled: booleanFrom(props.disabled, props['aria-disabled'], state.disabled) === true,
|
|
36
|
+
selected: booleanFrom(props['aria-selected'], state.selected),
|
|
37
|
+
expanded: booleanFrom(props['aria-expanded'], state.expanded),
|
|
38
|
+
checked: booleanFrom(props['aria-checked'], state.checked),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function inputTypeOf(props: Props): string {
|
|
43
|
+
if (props.secureTextEntry === true) {
|
|
44
|
+
return 'password';
|
|
45
|
+
}
|
|
46
|
+
switch (props.keyboardType) {
|
|
47
|
+
case 'numeric':
|
|
48
|
+
case 'number-pad':
|
|
49
|
+
case 'decimal-pad':
|
|
50
|
+
return 'number';
|
|
51
|
+
case 'email-address':
|
|
52
|
+
return 'email';
|
|
53
|
+
case 'phone-pad':
|
|
54
|
+
return 'tel';
|
|
55
|
+
case 'url':
|
|
56
|
+
return 'url';
|
|
57
|
+
default:
|
|
58
|
+
return 'text';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function placeholderOf(props: Props): string | null {
|
|
63
|
+
return nonEmptyString(props.placeholder);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function textValueOf(props: Props): string | null {
|
|
67
|
+
if (props.secureTextEntry === true) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return nonEmptyString(props.value) ?? nonEmptyString(props.defaultValue);
|
|
71
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
2
|
+
import type { Rect } from '../host/rect.ts';
|
|
3
|
+
import { type ElementKind, ID_PREFIX_BY_KIND, type IdPrefix } from './kinds.ts';
|
|
4
|
+
|
|
5
|
+
export interface RegisteredElement {
|
|
6
|
+
agentId: string;
|
|
7
|
+
kind: ElementKind;
|
|
8
|
+
fiber: Fiber;
|
|
9
|
+
rect: Rect;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ElementRegistry {
|
|
13
|
+
private counters: Record<IdPrefix, number> = { btn: 0, inp: 0, scr: 0 };
|
|
14
|
+
|
|
15
|
+
private entries: Map<string, RegisteredElement> = new Map();
|
|
16
|
+
|
|
17
|
+
begin(): void {
|
|
18
|
+
this.entries = new Map();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
register(kind: ElementKind, fiber: Fiber, rect: Rect): RegisteredElement {
|
|
22
|
+
const prefix = ID_PREFIX_BY_KIND[kind];
|
|
23
|
+
const agentId = `${prefix}-${this.counters[prefix]}`;
|
|
24
|
+
this.counters[prefix] += 1;
|
|
25
|
+
const element: RegisteredElement = { agentId, kind, fiber, rect };
|
|
26
|
+
this.entries.set(agentId, element);
|
|
27
|
+
return element;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
find(agentId: string): RegisteredElement | null {
|
|
31
|
+
return this.entries.get(agentId) ?? null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ElementView } from './view.ts';
|
|
2
|
+
|
|
3
|
+
const ATTRIBUTE_LIMIT = 20;
|
|
4
|
+
const BUTTON_TEXT_LIMIT = 80;
|
|
5
|
+
|
|
6
|
+
type Attribute = readonly [string, string | null];
|
|
7
|
+
|
|
8
|
+
export function clip(value: string, limit: number): string {
|
|
9
|
+
const collapsed = value.replace(/\s+/g, ' ').replace(/"/g, "'").trim();
|
|
10
|
+
return collapsed.length > limit ? `${collapsed.slice(0, limit)}…` : collapsed;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function flag(name: string, enabled: boolean): Attribute {
|
|
14
|
+
return [name, enabled ? '' : null];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renderAttributes(attributes: Attribute[]): string {
|
|
18
|
+
return attributes
|
|
19
|
+
.filter((attribute): attribute is readonly [string, string] => attribute[1] !== null)
|
|
20
|
+
.map(([name, value]) => (value === '' ? name : `${name}=${clip(value, ATTRIBUTE_LIMIT)}`))
|
|
21
|
+
.filter((rendered) => rendered.length > 0)
|
|
22
|
+
.join(' ');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function openTag(tag: string, attributes: Attribute[]): string {
|
|
26
|
+
const rendered = renderAttributes(attributes);
|
|
27
|
+
return rendered.length > 0 ? `<${tag} ${rendered}` : `<${tag}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function textOrNull(value: string): string | null {
|
|
31
|
+
return value.length > 0 ? value : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function booleanText(value: boolean | null): string | null {
|
|
35
|
+
return value === null ? null : String(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function position(view: ElementView, twins: number): string | null {
|
|
39
|
+
return twins > 0 ? `${Math.round(view.rect.x)},${Math.round(view.rect.y)}` : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function commonStateAttributes(view: ElementView): Attribute[] {
|
|
43
|
+
return [
|
|
44
|
+
['aria-expanded', booleanText(view.expanded)],
|
|
45
|
+
['aria-selected', booleanText(view.selected)],
|
|
46
|
+
flag('disabled', view.disabled),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderButton(view: ElementView, twins: number): string {
|
|
51
|
+
const tag = openTag('button', [
|
|
52
|
+
['role', view.role],
|
|
53
|
+
['testID', view.testID],
|
|
54
|
+
['icon', view.icon],
|
|
55
|
+
['offscreen', view.offscreen],
|
|
56
|
+
...commonStateAttributes(view),
|
|
57
|
+
['twins', twins > 0 ? String(twins) : null],
|
|
58
|
+
['pos', position(view, twins)],
|
|
59
|
+
]);
|
|
60
|
+
return `${tag}>${clip(view.label, BUTTON_TEXT_LIMIT)} />`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderTextInput(view: ElementView, twins: number): string {
|
|
64
|
+
const label = view.placeholder !== null && view.label === view.placeholder ? '' : view.label;
|
|
65
|
+
const tag = openTag('input', [
|
|
66
|
+
['type', view.type],
|
|
67
|
+
['testID', view.testID],
|
|
68
|
+
['offscreen', view.offscreen],
|
|
69
|
+
['label', textOrNull(label)],
|
|
70
|
+
['placeholder', view.placeholder],
|
|
71
|
+
['value', view.value],
|
|
72
|
+
flag('disabled', view.disabled),
|
|
73
|
+
['twins', twins > 0 ? String(twins) : null],
|
|
74
|
+
['pos', position(view, twins)],
|
|
75
|
+
]);
|
|
76
|
+
return `${tag} />`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderSwitch(view: ElementView, twins: number): string {
|
|
80
|
+
const tag = openTag('input', [
|
|
81
|
+
['type', view.type],
|
|
82
|
+
['role', 'switch'],
|
|
83
|
+
['testID', view.testID],
|
|
84
|
+
['offscreen', view.offscreen],
|
|
85
|
+
['label', textOrNull(view.label)],
|
|
86
|
+
['checked', booleanText(view.checked)],
|
|
87
|
+
flag('disabled', view.disabled),
|
|
88
|
+
['twins', twins > 0 ? String(twins) : null],
|
|
89
|
+
['pos', position(view, twins)],
|
|
90
|
+
]);
|
|
91
|
+
return `${tag} />`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderScroll(view: ElementView): string {
|
|
95
|
+
const tag = openTag('scroll', [
|
|
96
|
+
['testID', view.testID],
|
|
97
|
+
['scroll', view.scroll],
|
|
98
|
+
]);
|
|
99
|
+
return `${tag} />`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderBody(view: ElementView, twins: number): string {
|
|
103
|
+
switch (view.kind) {
|
|
104
|
+
case 'button':
|
|
105
|
+
return renderButton(view, twins);
|
|
106
|
+
case 'text_input':
|
|
107
|
+
return renderTextInput(view, twins);
|
|
108
|
+
case 'switch':
|
|
109
|
+
return renderSwitch(view, twins);
|
|
110
|
+
case 'scroll':
|
|
111
|
+
return renderScroll(view);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function twinSignature(view: ElementView): string {
|
|
116
|
+
return [view.kind, view.label, view.icon ?? '', view.role ?? ''].join('|');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function countTwins(views: ElementView[]): Map<string, number> {
|
|
120
|
+
const counts = new Map<string, number>();
|
|
121
|
+
for (const view of views) {
|
|
122
|
+
const signature = twinSignature(view);
|
|
123
|
+
counts.set(signature, (counts.get(signature) ?? 0) + 1);
|
|
124
|
+
}
|
|
125
|
+
return counts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function renderLine(view: ElementView, agentId: string | null, twins: number): string {
|
|
129
|
+
const prefix = agentId === null ? '' : `[${agentId}]`;
|
|
130
|
+
return `${prefix}${renderBody(view, twins)}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface SnapshotHeader {
|
|
134
|
+
screen: string;
|
|
135
|
+
routes: string[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function renderSnapshotText(header: SnapshotHeader, lines: string[]): string {
|
|
139
|
+
return [`screen: ${header.screen}`, `routes: ${header.routes.join(', ')}`, '', ...lines].join('\n');
|
|
140
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { routeNamesOf, type ScreenRef } from './navigation.ts';
|
|
2
|
+
|
|
3
|
+
export function describeScreenPath(focusedScreens: ScreenRef[]): string {
|
|
4
|
+
return focusedScreens.map((screen) => screen.name).join('/');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function describeRoutes(focusedScreens: ScreenRef[], extraPaths: string[]): string[] {
|
|
8
|
+
const paths = new Set<string>();
|
|
9
|
+
focusedScreens.forEach((screen, index) => {
|
|
10
|
+
const prefix = focusedScreens.slice(0, index).map((owner) => owner.name);
|
|
11
|
+
routeNamesOf(screen.navigation).forEach((name) => paths.add([...prefix, name].join('/')));
|
|
12
|
+
});
|
|
13
|
+
extraPaths.forEach((path) => paths.add(path));
|
|
14
|
+
return [...paths];
|
|
15
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { childrenOf, firstHostDescendant, isHostComponent, propsOf } from '../fiber/reader.ts';
|
|
2
|
+
import type { Fiber } from '../fiber/types.ts';
|
|
3
|
+
import { measureHost } from '../host/measure.ts';
|
|
4
|
+
import type { Rect } from '../host/rect.ts';
|
|
5
|
+
|
|
6
|
+
export type ScrollDirection = 'up' | 'down' | 'left' | 'right';
|
|
7
|
+
|
|
8
|
+
export interface ScrollGeometry {
|
|
9
|
+
viewport: Rect;
|
|
10
|
+
content: Rect;
|
|
11
|
+
horizontal: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SCROLL_TOLERANCE = 1;
|
|
15
|
+
|
|
16
|
+
function findScrollHost(fiber: Fiber): Fiber | null {
|
|
17
|
+
const search = (node: Fiber): Fiber | null => {
|
|
18
|
+
if (isHostComponent(node) && typeof node.type === 'string' && node.type.includes('ScrollView')) {
|
|
19
|
+
return node;
|
|
20
|
+
}
|
|
21
|
+
for (const child of childrenOf(node)) {
|
|
22
|
+
const found = search(child);
|
|
23
|
+
if (found !== null) {
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
return search(fiber) ?? firstHostDescendant(fiber);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findContentHost(scrollHost: Fiber): Fiber | null {
|
|
33
|
+
for (const child of childrenOf(scrollHost)) {
|
|
34
|
+
const host = firstHostDescendant(child);
|
|
35
|
+
if (host !== null) {
|
|
36
|
+
return host;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isHorizontal(fiber: Fiber): boolean {
|
|
43
|
+
return propsOf(fiber).horizontal === true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function measureScrollGeometry(scrollFiber: Fiber): Promise<ScrollGeometry | null> {
|
|
47
|
+
const scrollHost = findScrollHost(scrollFiber);
|
|
48
|
+
if (scrollHost === null) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const contentHost = findContentHost(scrollHost);
|
|
52
|
+
const [viewport, content] = await Promise.all([
|
|
53
|
+
measureHost(scrollHost),
|
|
54
|
+
contentHost === null ? Promise.resolve(null) : measureHost(contentHost),
|
|
55
|
+
]);
|
|
56
|
+
if (viewport === null) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return { viewport, content: content ?? viewport, horizontal: isHorizontal(scrollFiber) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function scrollOffsetOf(geometry: ScrollGeometry): number {
|
|
63
|
+
return geometry.horizontal ? geometry.viewport.x - geometry.content.x : geometry.viewport.y - geometry.content.y;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function maxScrollOffsetOf(geometry: ScrollGeometry): number {
|
|
67
|
+
const content = geometry.horizontal ? geometry.content.width : geometry.content.height;
|
|
68
|
+
const viewport = geometry.horizontal ? geometry.viewport.width : geometry.viewport.height;
|
|
69
|
+
return Math.max(0, content - viewport);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function viewportExtentOf(geometry: ScrollGeometry): number {
|
|
73
|
+
return geometry.horizontal ? geometry.viewport.width : geometry.viewport.height;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function scrollDirectionsOf(geometry: ScrollGeometry): ScrollDirection[] {
|
|
77
|
+
const offset = scrollOffsetOf(geometry);
|
|
78
|
+
const canBackward = offset > SCROLL_TOLERANCE;
|
|
79
|
+
const canForward = maxScrollOffsetOf(geometry) - offset > SCROLL_TOLERANCE;
|
|
80
|
+
const [backward, forward]: [ScrollDirection, ScrollDirection] = geometry.horizontal ? ['left', 'right'] : ['up', 'down'];
|
|
81
|
+
return [...(canBackward ? [backward] : []), ...(canForward ? [forward] : [])];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function describeScrollDirections(geometry: ScrollGeometry | null): string {
|
|
85
|
+
if (geometry === null) {
|
|
86
|
+
return 'none';
|
|
87
|
+
}
|
|
88
|
+
const directions = scrollDirectionsOf(geometry);
|
|
89
|
+
return directions.length === 0 ? 'none' : directions.join(',');
|
|
90
|
+
}
|