taphound 0.2.0-dev.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 +202 -0
- package/README.md +200 -0
- package/README.zh-CN.md +200 -0
- package/assets/brand/taphound-mark.svg +9 -0
- package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
- package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
- package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
- package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
- package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
- package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
- package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
- package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
- package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
- package/dist/adapters/adb/adb-adapter.d.ts +23 -0
- package/dist/adapters/adb/adb-adapter.js +218 -0
- package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
- package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
- package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
- package/dist/adapters/android-cli/layout-parser.js +174 -0
- package/dist/adapters/clock/system-clock.d.ts +5 -0
- package/dist/adapters/clock/system-clock.js +13 -0
- package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
- package/dist/adapters/filesystem/artifact-store.js +73 -0
- package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
- package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
- package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
- package/dist/adapters/filesystem/generation-session-store.js +1603 -0
- package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
- package/dist/adapters/filesystem/journey-writer.js +34 -0
- package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
- package/dist/adapters/filesystem/project-bound-file.js +240 -0
- package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
- package/dist/adapters/filesystem/project-file-inspector.js +198 -0
- package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
- package/dist/adapters/filesystem/skill-installer.js +39 -0
- package/dist/adapters/process/node-process-runner.d.ts +8 -0
- package/dist/adapters/process/node-process-runner.js +155 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
- package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
- package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
- package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
- package/dist/application/assertion/expectation-evaluator.js +207 -0
- package/dist/application/collector/logcat-collector.d.ts +39 -0
- package/dist/application/collector/logcat-collector.js +107 -0
- package/dist/application/context/context-validator.d.ts +27 -0
- package/dist/application/context/context-validator.js +120 -0
- package/dist/application/doctor/doctor-service.d.ts +35 -0
- package/dist/application/doctor/doctor-service.js +178 -0
- package/dist/application/generation/focused-input.d.ts +2 -0
- package/dist/application/generation/focused-input.js +9 -0
- package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
- package/dist/application/generation/generation-confirmation-service.js +383 -0
- package/dist/application/generation/generation-finalizer.d.ts +67 -0
- package/dist/application/generation/generation-finalizer.js +646 -0
- package/dist/application/generation/generation-publisher.d.ts +46 -0
- package/dist/application/generation/generation-publisher.js +178 -0
- package/dist/application/generation/generation-starter.d.ts +31 -0
- package/dist/application/generation/generation-starter.js +102 -0
- package/dist/application/generation/generation-step-executor.d.ts +52 -0
- package/dist/application/generation/generation-step-executor.js +668 -0
- package/dist/application/generation/proposed-step-validator.d.ts +11 -0
- package/dist/application/generation/proposed-step-validator.js +88 -0
- package/dist/application/generation/risk-evaluator.d.ts +9 -0
- package/dist/application/generation/risk-evaluator.js +14 -0
- package/dist/application/generation/runtime-observer.d.ts +39 -0
- package/dist/application/generation/runtime-observer.js +157 -0
- package/dist/application/init/init-service.d.ts +17 -0
- package/dist/application/init/init-service.js +44 -0
- package/dist/application/interaction/action-executor.d.ts +25 -0
- package/dist/application/interaction/action-executor.js +103 -0
- package/dist/application/interaction/fallback-resolver.d.ts +24 -0
- package/dist/application/interaction/fallback-resolver.js +48 -0
- package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
- package/dist/application/interaction/scroll-to-executor.js +191 -0
- package/dist/application/locator/locator-resolver.d.ts +20 -0
- package/dist/application/locator/locator-resolver.js +89 -0
- package/dist/application/project/project-describer.d.ts +14 -0
- package/dist/application/project/project-describer.js +8 -0
- package/dist/application/recorder/locator-selector.d.ts +10 -0
- package/dist/application/recorder/locator-selector.js +80 -0
- package/dist/application/recorder/recorder-service.d.ts +41 -0
- package/dist/application/recorder/recorder-service.js +331 -0
- package/dist/application/report/report-writer.d.ts +10 -0
- package/dist/application/report/report-writer.js +39 -0
- package/dist/application/runtime/activity-waiter.d.ts +33 -0
- package/dist/application/runtime/activity-waiter.js +73 -0
- package/dist/application/runtime/launch-failure.d.ts +2 -0
- package/dist/application/runtime/launch-failure.js +20 -0
- package/dist/application/runtime/process-waiter.d.ts +27 -0
- package/dist/application/runtime/process-waiter.js +73 -0
- package/dist/application/runtime/step-runner.d.ts +50 -0
- package/dist/application/runtime/step-runner.js +429 -0
- package/dist/application/runtime/verify-runtime.d.ts +44 -0
- package/dist/application/runtime/verify-runtime.js +350 -0
- package/dist/application/wait/idle-waiter.d.ts +30 -0
- package/dist/application/wait/idle-waiter.js +90 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +94 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +51 -0
- package/dist/cli/commands/generation.d.ts +3 -0
- package/dist/cli/commands/generation.js +518 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +104 -0
- package/dist/cli/commands/project.d.ts +3 -0
- package/dist/cli/commands/project.js +58 -0
- package/dist/cli/commands/record.d.ts +3 -0
- package/dist/cli/commands/record.js +88 -0
- package/dist/cli/commands/verify.d.ts +3 -0
- package/dist/cli/commands/verify.js +106 -0
- package/dist/cli/dependencies.d.ts +66 -0
- package/dist/cli/dependencies.js +204 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +73 -0
- package/dist/cli/output.d.ts +16 -0
- package/dist/cli/output.js +22 -0
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +43 -0
- package/dist/domain/activity.d.ts +7 -0
- package/dist/domain/activity.js +65 -0
- package/dist/domain/app-process.d.ts +7 -0
- package/dist/domain/app-process.js +10 -0
- package/dist/domain/config.d.ts +15 -0
- package/dist/domain/config.js +17 -0
- package/dist/domain/failure.d.ts +4 -0
- package/dist/domain/failure.js +43 -0
- package/dist/domain/generation.d.ts +912 -0
- package/dist/domain/generation.js +342 -0
- package/dist/domain/init.d.ts +42 -0
- package/dist/domain/init.js +114 -0
- package/dist/domain/journey.d.ts +673 -0
- package/dist/domain/journey.js +109 -0
- package/dist/domain/layout.d.ts +35 -0
- package/dist/domain/layout.js +33 -0
- package/dist/domain/locator.d.ts +8 -0
- package/dist/domain/locator.js +5 -0
- package/dist/domain/project-context.d.ts +101 -0
- package/dist/domain/project-context.js +64 -0
- package/dist/domain/proposed-step.d.ts +341 -0
- package/dist/domain/proposed-step.js +87 -0
- package/dist/domain/report.d.ts +349 -0
- package/dist/domain/report.js +135 -0
- package/dist/domain/runtime-snapshot.d.ts +16 -0
- package/dist/domain/runtime-snapshot.js +46 -0
- package/dist/ports/adb.d.ts +42 -0
- package/dist/ports/adb.js +1 -0
- package/dist/ports/android-cli.d.ts +21 -0
- package/dist/ports/android-cli.js +1 -0
- package/dist/ports/artifact-store.d.ts +12 -0
- package/dist/ports/artifact-store.js +1 -0
- package/dist/ports/clock.d.ts +4 -0
- package/dist/ports/clock.js +1 -0
- package/dist/ports/generation-meta-writer.d.ts +10 -0
- package/dist/ports/generation-meta-writer.js +1 -0
- package/dist/ports/generation-prompt.d.ts +19 -0
- package/dist/ports/generation-prompt.js +6 -0
- package/dist/ports/generation-session-store.d.ts +32 -0
- package/dist/ports/generation-session-store.js +26 -0
- package/dist/ports/init-prompt.d.ts +8 -0
- package/dist/ports/init-prompt.js +6 -0
- package/dist/ports/journey-writer.d.ts +10 -0
- package/dist/ports/journey-writer.js +1 -0
- package/dist/ports/process-runner.d.ts +32 -0
- package/dist/ports/process-runner.js +1 -0
- package/dist/ports/project-bound-file.d.ts +8 -0
- package/dist/ports/project-bound-file.js +1 -0
- package/dist/ports/project-file-inspector.d.ts +33 -0
- package/dist/ports/project-file-inspector.js +1 -0
- package/dist/ports/recorder-prompt.d.ts +29 -0
- package/dist/ports/recorder-prompt.js +1 -0
- package/dist/ports/skill-installer.d.ts +8 -0
- package/dist/ports/skill-installer.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { resolveLocator } from "../locator/locator-resolver.js";
|
|
2
|
+
function isCancelled(signal) {
|
|
3
|
+
return signal?.aborted === true;
|
|
4
|
+
}
|
|
5
|
+
function callbackFailure(error, swipesUsed) {
|
|
6
|
+
if (error !== null
|
|
7
|
+
&& typeof error === "object"
|
|
8
|
+
&& "code" in error
|
|
9
|
+
&& typeof error.code === "string"
|
|
10
|
+
&& [
|
|
11
|
+
"ACTIVITY_BEFORE_MISMATCH",
|
|
12
|
+
"ACTIVITY_AFTER_MISMATCH",
|
|
13
|
+
"ACTION_FAILED"
|
|
14
|
+
].includes(error.code)) {
|
|
15
|
+
return {
|
|
16
|
+
status: "failed",
|
|
17
|
+
code: error.code,
|
|
18
|
+
message: error instanceof Error ? error.message : error.code,
|
|
19
|
+
swipesUsed
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
export class ScrollToExecutor {
|
|
25
|
+
options;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
}
|
|
29
|
+
async execute(step, signal, initialLayout) {
|
|
30
|
+
let swipesUsed = 0;
|
|
31
|
+
let layout = initialLayout;
|
|
32
|
+
for (;;) {
|
|
33
|
+
if (isCancelled(signal)) {
|
|
34
|
+
return { status: "cancelled", swipesUsed };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
layout ??= this.options.readLayout === undefined
|
|
38
|
+
? await this.options.androidCli.layout({
|
|
39
|
+
deviceSerial: this.options.deviceSerial,
|
|
40
|
+
...(signal === undefined ? {} : { signal }),
|
|
41
|
+
timeoutMs: this.options.idle.timeoutMs
|
|
42
|
+
})
|
|
43
|
+
: await this.options.readLayout();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const failed = callbackFailure(error, swipesUsed);
|
|
47
|
+
if (failed !== undefined)
|
|
48
|
+
return failed;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
if (isCancelled(signal)) {
|
|
52
|
+
return { status: "cancelled", swipesUsed };
|
|
53
|
+
}
|
|
54
|
+
const target = resolveLocator(layout, step.locator, { requireEnabled: false });
|
|
55
|
+
if (target.status === "found") {
|
|
56
|
+
return { status: "found", swipesUsed };
|
|
57
|
+
}
|
|
58
|
+
if (target.code === "LOCATOR_AMBIGUOUS") {
|
|
59
|
+
return {
|
|
60
|
+
status: "failed",
|
|
61
|
+
code: "LOCATOR_AMBIGUOUS",
|
|
62
|
+
message: target.message,
|
|
63
|
+
swipesUsed
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (swipesUsed >= step.maxSwipes) {
|
|
67
|
+
return {
|
|
68
|
+
status: "failed",
|
|
69
|
+
code: "SCROLL_TARGET_NOT_FOUND",
|
|
70
|
+
message: `Target not visible after ${String(step.maxSwipes)} swipes`,
|
|
71
|
+
swipesUsed
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
let container = resolveLocator(layout, step.container, { requireEnabled: false });
|
|
75
|
+
if (container.status !== "found") {
|
|
76
|
+
return {
|
|
77
|
+
status: "failed",
|
|
78
|
+
code: container.code,
|
|
79
|
+
message: container.message,
|
|
80
|
+
swipesUsed
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (container.element.bounds === undefined) {
|
|
84
|
+
return {
|
|
85
|
+
status: "failed",
|
|
86
|
+
code: "ACTION_FAILED",
|
|
87
|
+
message: "scroll container has no bounds to swipe",
|
|
88
|
+
swipesUsed
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (this.options.beforeSwipe !== undefined) {
|
|
92
|
+
try {
|
|
93
|
+
layout = await this.options.beforeSwipe();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const failed = callbackFailure(error, swipesUsed);
|
|
97
|
+
if (failed !== undefined)
|
|
98
|
+
return failed;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
if (isCancelled(signal)) {
|
|
102
|
+
return { status: "cancelled", swipesUsed };
|
|
103
|
+
}
|
|
104
|
+
const liveTarget = resolveLocator(layout, step.locator, { requireEnabled: false });
|
|
105
|
+
if (liveTarget.status === "found") {
|
|
106
|
+
return { status: "found", swipesUsed };
|
|
107
|
+
}
|
|
108
|
+
if (liveTarget.code === "LOCATOR_AMBIGUOUS") {
|
|
109
|
+
return {
|
|
110
|
+
status: "failed",
|
|
111
|
+
code: "LOCATOR_AMBIGUOUS",
|
|
112
|
+
message: liveTarget.message,
|
|
113
|
+
swipesUsed
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
container = resolveLocator(layout, step.container, { requireEnabled: false });
|
|
117
|
+
if (container.status !== "found") {
|
|
118
|
+
return {
|
|
119
|
+
status: "failed",
|
|
120
|
+
code: container.code,
|
|
121
|
+
message: container.message,
|
|
122
|
+
swipesUsed
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (container.element.bounds === undefined) {
|
|
126
|
+
return {
|
|
127
|
+
status: "failed",
|
|
128
|
+
code: "ACTION_FAILED",
|
|
129
|
+
message: "scroll container has no bounds to swipe",
|
|
130
|
+
swipesUsed
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (isCancelled(signal)) {
|
|
135
|
+
return { status: "cancelled", swipesUsed };
|
|
136
|
+
}
|
|
137
|
+
if (this.options.requireLiveContainerCapability === true
|
|
138
|
+
&& (!container.element.enabled
|
|
139
|
+
|| container.element.scrollable !== true)) {
|
|
140
|
+
return {
|
|
141
|
+
status: "failed",
|
|
142
|
+
code: "ACTION_FAILED",
|
|
143
|
+
message: "scroll container lost enabled scrollable bounds",
|
|
144
|
+
swipesUsed
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await this.options.beforeMutation?.();
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const failed = callbackFailure(error, swipesUsed);
|
|
152
|
+
if (failed !== undefined)
|
|
153
|
+
return failed;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
if (isCancelled(signal)) {
|
|
157
|
+
return { status: "cancelled", swipesUsed };
|
|
158
|
+
}
|
|
159
|
+
const swipe = await this.options.actionExecutor.swipeBounds(container.element.bounds, step.direction, step.distancePercent, step.durationMs, signal);
|
|
160
|
+
if (isCancelled(signal)) {
|
|
161
|
+
return { status: "cancelled", swipesUsed };
|
|
162
|
+
}
|
|
163
|
+
if (swipe.status === "failed") {
|
|
164
|
+
return {
|
|
165
|
+
status: "failed",
|
|
166
|
+
code: swipe.code,
|
|
167
|
+
message: swipe.message,
|
|
168
|
+
swipesUsed
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const idle = await this.options.idleWaiter.waitUntilIdle(this.options.idle, signal);
|
|
172
|
+
if (isCancelled(signal)) {
|
|
173
|
+
return { status: "cancelled", swipesUsed };
|
|
174
|
+
}
|
|
175
|
+
if (idle.status === "cancelled") {
|
|
176
|
+
return { status: "cancelled", swipesUsed };
|
|
177
|
+
}
|
|
178
|
+
if (idle.status === "timeout") {
|
|
179
|
+
return {
|
|
180
|
+
status: "failed",
|
|
181
|
+
code: idle.code,
|
|
182
|
+
message: "Layout did not become stable before timeout",
|
|
183
|
+
swipesUsed,
|
|
184
|
+
idle: { polls: idle.polls, lastDiff: idle.lastDiff }
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
swipesUsed += 1;
|
|
188
|
+
layout = undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FailureCode } from "../../domain/failure.js";
|
|
2
|
+
import { type LocatorField } from "../../domain/locator.js";
|
|
3
|
+
import type { LayoutElement, Locator } from "../../domain/layout.js";
|
|
4
|
+
import type { Point } from "../../ports/android-cli.js";
|
|
5
|
+
export interface LocatedTarget {
|
|
6
|
+
status: "found";
|
|
7
|
+
element: LayoutElement;
|
|
8
|
+
point: Point;
|
|
9
|
+
matchedBy: LocatorField;
|
|
10
|
+
}
|
|
11
|
+
export interface LocatorFailure {
|
|
12
|
+
status: "failed";
|
|
13
|
+
code: Extract<FailureCode, "LOCATOR_NOT_FOUND" | "LOCATOR_AMBIGUOUS" | "ACTION_FAILED">;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
export type LocatorResolution = LocatedTarget | LocatorFailure;
|
|
17
|
+
export interface LocatorResolutionOptions {
|
|
18
|
+
requireEnabled?: boolean | undefined;
|
|
19
|
+
}
|
|
20
|
+
export declare function resolveLocator(roots: readonly LayoutElement[], locator: Locator, options?: LocatorResolutionOptions): LocatorResolution;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { LOCATOR_FIELDS } from "../../domain/locator.js";
|
|
2
|
+
function flatten(elements) {
|
|
3
|
+
return elements.flatMap((element) => [
|
|
4
|
+
element,
|
|
5
|
+
...flatten(element.children)
|
|
6
|
+
]);
|
|
7
|
+
}
|
|
8
|
+
function center(element) {
|
|
9
|
+
if (element.center !== undefined) {
|
|
10
|
+
return element.center;
|
|
11
|
+
}
|
|
12
|
+
const bounds = element.bounds;
|
|
13
|
+
if (bounds === undefined) {
|
|
14
|
+
throw new Error(`Layout element ${element.id} has no center or bounds`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
x: Math.round((bounds.left + bounds.right) / 2),
|
|
18
|
+
y: Math.round((bounds.top + bounds.bottom) / 2)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function resolveLocator(roots, locator, options = {}) {
|
|
22
|
+
const elements = flatten(roots);
|
|
23
|
+
let candidates;
|
|
24
|
+
let matchedBy;
|
|
25
|
+
for (const field of LOCATOR_FIELDS) {
|
|
26
|
+
const value = locator[field];
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (candidates === undefined) {
|
|
31
|
+
const matches = elements.filter((element) => element[field] === value);
|
|
32
|
+
if (matches.length === 0) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
candidates = matches;
|
|
36
|
+
matchedBy = field;
|
|
37
|
+
}
|
|
38
|
+
else if (candidates.length > 1) {
|
|
39
|
+
const narrowed = candidates.filter((element) => element[field] === value);
|
|
40
|
+
if (narrowed.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
status: "failed",
|
|
43
|
+
code: "LOCATOR_NOT_FOUND",
|
|
44
|
+
message: `Locator fields conflict at ${field}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
candidates = narrowed;
|
|
48
|
+
matchedBy = field;
|
|
49
|
+
}
|
|
50
|
+
if (candidates.length === 1) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (candidates === undefined || candidates.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
status: "failed",
|
|
57
|
+
code: "LOCATOR_NOT_FOUND",
|
|
58
|
+
message: "No Layout element matches the Locator"
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (candidates.length > 1) {
|
|
62
|
+
return {
|
|
63
|
+
status: "failed",
|
|
64
|
+
code: "LOCATOR_AMBIGUOUS",
|
|
65
|
+
message: `Locator matches ${String(candidates.length)} Layout elements`
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const element = candidates[0];
|
|
69
|
+
if (element === undefined || matchedBy === undefined) {
|
|
70
|
+
return {
|
|
71
|
+
status: "failed",
|
|
72
|
+
code: "LOCATOR_NOT_FOUND",
|
|
73
|
+
message: "No Layout element matches the Locator"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (options.requireEnabled !== false && !element.enabled) {
|
|
77
|
+
return {
|
|
78
|
+
status: "failed",
|
|
79
|
+
code: "ACTION_FAILED",
|
|
80
|
+
message: `Layout element ${element.id} is disabled`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
status: "found",
|
|
85
|
+
element,
|
|
86
|
+
point: center(element),
|
|
87
|
+
matchedBy
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TapHoundConfig } from "../../domain/config.js";
|
|
2
|
+
export interface ProjectDescribeInput {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
config: TapHoundConfig;
|
|
5
|
+
signal?: AbortSignal | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface ProjectDescription {
|
|
8
|
+
projectRoot: string;
|
|
9
|
+
packageName: string;
|
|
10
|
+
launchActivity: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class ProjectDescriber {
|
|
13
|
+
readonly describe: (input: ProjectDescribeInput) => Promise<ProjectDescription>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { normalizeActivity } from "../../domain/activity.js";
|
|
2
|
+
export class ProjectDescriber {
|
|
3
|
+
describe = async (input) => Promise.resolve({
|
|
4
|
+
projectRoot: input.projectRoot,
|
|
5
|
+
packageName: input.config.run.packageName,
|
|
6
|
+
launchActivity: normalizeActivity(input.config.run.packageName, input.config.run.activity)
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LayoutElement, Locator } from "../../domain/layout.js";
|
|
2
|
+
export interface RecorderTarget {
|
|
3
|
+
element: LayoutElement;
|
|
4
|
+
locator: Locator;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
export type RecorderTargetAction = "click" | "longClick" | "swipe";
|
|
8
|
+
export declare function selectUniqueLocator(target: LayoutElement, roots: readonly LayoutElement[]): Locator | undefined;
|
|
9
|
+
export declare function listRecorderTargets(roots: readonly LayoutElement[], action: RecorderTargetAction): RecorderTarget[];
|
|
10
|
+
export declare function listLocatableTargets(roots: readonly LayoutElement[]): RecorderTarget[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { LOCATOR_FIELDS } from "../../domain/locator.js";
|
|
2
|
+
function flatten(elements) {
|
|
3
|
+
return elements.flatMap((element) => [
|
|
4
|
+
element,
|
|
5
|
+
...flatten(element.children)
|
|
6
|
+
]);
|
|
7
|
+
}
|
|
8
|
+
export function selectUniqueLocator(target, roots) {
|
|
9
|
+
const elements = flatten(roots);
|
|
10
|
+
for (const field of LOCATOR_FIELDS) {
|
|
11
|
+
const value = target[field];
|
|
12
|
+
if (value !== undefined
|
|
13
|
+
&& value.length > 0
|
|
14
|
+
&& elements.filter((element) => element[field] === value).length === 1) {
|
|
15
|
+
return { [field]: value };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
function targetLabel(element, locator) {
|
|
21
|
+
const identity = LOCATOR_FIELDS.find((field) => locator[field] !== undefined);
|
|
22
|
+
if (identity === undefined) {
|
|
23
|
+
return element.id;
|
|
24
|
+
}
|
|
25
|
+
const value = locator[identity] ?? element.id;
|
|
26
|
+
return `${element.id} — ${identity}: ${value}`;
|
|
27
|
+
}
|
|
28
|
+
function supportsAction(element, action) {
|
|
29
|
+
return action === "click"
|
|
30
|
+
? element.clickable === true
|
|
31
|
+
: action === "longClick"
|
|
32
|
+
? element.longClickable === true
|
|
33
|
+
: element.scrollable === true && element.bounds !== undefined;
|
|
34
|
+
}
|
|
35
|
+
export function listRecorderTargets(roots, action) {
|
|
36
|
+
const primary = flatten(roots).flatMap((element) => {
|
|
37
|
+
if (!element.enabled || !supportsAction(element, action)) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const locator = selectUniqueLocator(element, roots);
|
|
41
|
+
if (locator === undefined) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return [{
|
|
45
|
+
element,
|
|
46
|
+
locator,
|
|
47
|
+
label: targetLabel(element, locator)
|
|
48
|
+
}];
|
|
49
|
+
});
|
|
50
|
+
// Relaxed content targets are click-only: for longClick the interactive
|
|
51
|
+
// target is often a small bounded view (e.g. a chat bubble), so a sibling
|
|
52
|
+
// label's center coordinate hit-tests to a neighbouring element instead.
|
|
53
|
+
if (action !== "click") {
|
|
54
|
+
return primary;
|
|
55
|
+
}
|
|
56
|
+
const hasOrphan = flatten(roots).some((element) => element.enabled
|
|
57
|
+
&& supportsAction(element, action)
|
|
58
|
+
&& selectUniqueLocator(element, roots) === undefined);
|
|
59
|
+
if (!hasOrphan) {
|
|
60
|
+
return primary;
|
|
61
|
+
}
|
|
62
|
+
const primaryIds = new Set(primary.map((target) => target.element.id));
|
|
63
|
+
const relaxed = listLocatableTargets(roots).filter((target) => !supportsAction(target.element, action)
|
|
64
|
+
&& !primaryIds.has(target.element.id)
|
|
65
|
+
&& (target.locator.text !== undefined
|
|
66
|
+
|| target.locator.contentDescription !== undefined));
|
|
67
|
+
return [...primary, ...relaxed];
|
|
68
|
+
}
|
|
69
|
+
export function listLocatableTargets(roots) {
|
|
70
|
+
return flatten(roots).flatMap((element) => {
|
|
71
|
+
if (!element.enabled) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const locator = selectUniqueLocator(element, roots);
|
|
75
|
+
if (locator === undefined) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
return [{ element, locator, label: targetLabel(element, locator) }];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TapHoundConfig } from "../../domain/config.js";
|
|
2
|
+
import { type Journey } from "../../domain/journey.js";
|
|
3
|
+
import type { AdbPort } from "../../ports/adb.js";
|
|
4
|
+
import type { AndroidCliPort } from "../../ports/android-cli.js";
|
|
5
|
+
import type { Clock } from "../../ports/clock.js";
|
|
6
|
+
import type { JourneyWriterPort } from "../../ports/journey-writer.js";
|
|
7
|
+
import type { RecorderPromptPort } from "../../ports/recorder-prompt.js";
|
|
8
|
+
export interface RecorderDependencies {
|
|
9
|
+
androidCli: AndroidCliPort;
|
|
10
|
+
adb: AdbPort;
|
|
11
|
+
clock: Clock;
|
|
12
|
+
prompt: RecorderPromptPort;
|
|
13
|
+
journeyWriter: JourneyWriterPort;
|
|
14
|
+
}
|
|
15
|
+
export interface RecordInput {
|
|
16
|
+
config: TapHoundConfig;
|
|
17
|
+
projectRoot: string;
|
|
18
|
+
deviceSerial: string;
|
|
19
|
+
journeyName: string;
|
|
20
|
+
outputPath: string;
|
|
21
|
+
signal?: AbortSignal | undefined;
|
|
22
|
+
}
|
|
23
|
+
export type RecordResult = {
|
|
24
|
+
status: "completed";
|
|
25
|
+
stepsRecorded: number;
|
|
26
|
+
journey: Journey;
|
|
27
|
+
} | {
|
|
28
|
+
status: "cancelled";
|
|
29
|
+
stepsRecorded: number;
|
|
30
|
+
} | {
|
|
31
|
+
status: "failed";
|
|
32
|
+
stepsRecorded: number;
|
|
33
|
+
message: string;
|
|
34
|
+
};
|
|
35
|
+
export declare class RecorderService {
|
|
36
|
+
private readonly dependencies;
|
|
37
|
+
constructor(dependencies: RecorderDependencies);
|
|
38
|
+
record(input: RecordInput): Promise<RecordResult>;
|
|
39
|
+
private prepareAction;
|
|
40
|
+
private prepareScrollTo;
|
|
41
|
+
}
|