tapline-host 1.4.0 → 1.6.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/dist/accounts.d.ts +72 -0
- package/dist/accounts.js +208 -0
- package/dist/accounts.js.map +1 -0
- package/dist/actionlog.d.ts +20 -0
- package/dist/actionlog.js.map +1 -1
- package/dist/animations.d.ts +39 -0
- package/dist/animations.js +124 -0
- package/dist/animations.js.map +1 -0
- package/dist/api.d.ts +43 -0
- package/dist/api.js +154 -0
- package/dist/api.js.map +1 -0
- package/dist/audit.d.ts +3 -0
- package/dist/audit.js +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +19 -0
- package/dist/bin.js.map +1 -0
- package/dist/brief.d.ts +41 -0
- package/dist/brief.js +78 -0
- package/dist/brief.js.map +1 -0
- package/dist/cli.d.ts +41 -1
- package/dist/cli.js +100 -53
- package/dist/cli.js.map +1 -1
- package/dist/conditions.d.ts +36 -0
- package/dist/conditions.js +67 -0
- package/dist/conditions.js.map +1 -0
- package/dist/confirm.d.ts +21 -2
- package/dist/confirm.js +39 -6
- package/dist/confirm.js.map +1 -1
- package/dist/dispatch.d.ts +27 -0
- package/dist/dispatch.js +114 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +20 -0
- package/dist/doctor.js.map +1 -1
- package/dist/drive.d.ts +184 -0
- package/dist/drive.js +908 -0
- package/dist/drive.js.map +1 -0
- package/dist/elicit.d.ts +90 -0
- package/dist/elicit.js +179 -0
- package/dist/elicit.js.map +1 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.js +81 -24
- package/dist/format.js.map +1 -1
- package/dist/images.d.ts +35 -0
- package/dist/images.js +76 -6
- package/dist/images.js.map +1 -1
- package/dist/inspect.d.ts +1 -0
- package/dist/journey.d.ts +276 -0
- package/dist/journey.js +368 -0
- package/dist/journey.js.map +1 -0
- package/dist/journeyexport.d.ts +58 -0
- package/dist/journeyexport.js +318 -0
- package/dist/journeyexport.js.map +1 -0
- package/dist/journeygif.d.ts +62 -0
- package/dist/journeygif.js +192 -0
- package/dist/journeygif.js.map +1 -0
- package/dist/journeyreport.d.ts +202 -0
- package/dist/journeyreport.js +666 -0
- package/dist/journeyreport.js.map +1 -0
- package/dist/mapcmd.d.ts +200 -0
- package/dist/mapcmd.js +412 -0
- package/dist/mapcmd.js.map +1 -0
- package/dist/mapfile.d.ts +155 -0
- package/dist/mapfile.js +575 -0
- package/dist/mapfile.js.map +1 -0
- package/dist/mcp.d.ts +14 -11
- package/dist/mcp.js +50 -318
- package/dist/mcp.js.map +1 -1
- package/dist/media.d.ts +88 -0
- package/dist/media.js +263 -0
- package/dist/media.js.map +1 -0
- package/dist/nochange.d.ts +77 -0
- package/dist/nochange.js +77 -0
- package/dist/nochange.js.map +1 -0
- package/dist/places.d.ts +239 -0
- package/dist/places.js +823 -0
- package/dist/places.js.map +1 -0
- package/dist/policy.d.ts +18 -0
- package/dist/policy.js +52 -0
- package/dist/policy.js.map +1 -1
- package/dist/protocol.d.ts +27 -1
- package/dist/protocol.js +25 -0
- package/dist/protocol.js.map +1 -1
- package/dist/reflexes.d.ts +59 -0
- package/dist/reflexes.js +133 -0
- package/dist/reflexes.js.map +1 -0
- package/dist/report.d.ts +32 -2
- package/dist/report.js +154 -9
- package/dist/report.js.map +1 -1
- package/dist/routes.d.ts +115 -0
- package/dist/routes.js +287 -0
- package/dist/routes.js.map +1 -0
- package/dist/run.d.ts +28 -1
- package/dist/run.js +32 -3
- package/dist/run.js.map +1 -1
- package/dist/runtime.d.ts +60 -0
- package/dist/runtime.js +255 -0
- package/dist/runtime.js.map +1 -0
- package/dist/script.d.ts +114 -10
- package/dist/script.js +30 -2
- package/dist/script.js.map +1 -1
- package/dist/server.d.ts +48 -2
- package/dist/server.js +88 -13
- package/dist/server.js.map +1 -1
- package/dist/thincall.d.ts +13 -0
- package/dist/thincall.js +335 -0
- package/dist/thincall.js.map +1 -0
- package/dist/tool-text-data.d.ts +10 -5
- package/dist/tool-text-data.js +11 -6
- package/dist/tool-text-data.js.map +1 -1
- package/dist/tool-text.d.ts +1 -1
- package/dist/tools.d.ts +585 -5
- package/dist/tools.js +2926 -185
- package/dist/tools.js.map +1 -1
- package/dist/uxcheck.d.ts +103 -0
- package/dist/uxcheck.js +128 -0
- package/dist/uxcheck.js.map +1 -0
- package/package.json +3 -2
- package/skills/tapline/SKILL.md +57 -11
- package/skills/tapline/references/personas.md +45 -0
- package/skills/tapline/references/setup.md +6 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** AGENT-TOOLS 1.12 §17.3: `ux_check`, the screen against the basics, and the user's house rules. */
|
|
3
|
+
/** An action seen on a screen: its label, and its testID when it has one. */
|
|
4
|
+
export interface UxAction {
|
|
5
|
+
label: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
}
|
|
8
|
+
/** What a journey keeps and hands to `ux_check`. The journey module sets `tools.uxJourney` to one of these. */
|
|
9
|
+
export interface UxJourneyHook {
|
|
10
|
+
/**
|
|
11
|
+
* The named actions of the screens seen earlier in the running journey (not this screen's own), or null when
|
|
12
|
+
* no journey is running: naming is then not checked, and the answer says so.
|
|
13
|
+
*/
|
|
14
|
+
earlier(): UxAction[] | null;
|
|
15
|
+
/** Called after each `ux_check`, with the screen's title, its named actions and what was found. */
|
|
16
|
+
seen?(screen: string, actions: UxAction[], findings: UxFindingSummary[]): void;
|
|
17
|
+
}
|
|
18
|
+
export interface UxFindingSummary {
|
|
19
|
+
kind: string;
|
|
20
|
+
problem: string;
|
|
21
|
+
basis: string;
|
|
22
|
+
/** The element's ref, when the finding concerns one. */
|
|
23
|
+
ref?: string;
|
|
24
|
+
/** The element's frame in points, for the report's red box. */
|
|
25
|
+
frame?: {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare const uxResultSchema: z.ZodObject<{
|
|
33
|
+
issues: z.ZodArray<z.ZodObject<{
|
|
34
|
+
kind: z.ZodString;
|
|
35
|
+
element: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
36
|
+
ref: z.ZodString;
|
|
37
|
+
type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
38
|
+
role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
39
|
+
testID: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
40
|
+
label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
41
|
+
text: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
42
|
+
textMore: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
43
|
+
value: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
|
|
44
|
+
placeholder: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
45
|
+
frame: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
46
|
+
x: z.ZodNumber;
|
|
47
|
+
y: z.ZodNumber;
|
|
48
|
+
width: z.ZodNumber;
|
|
49
|
+
height: z.ZodNumber;
|
|
50
|
+
}, z.core.$strip>>>;
|
|
51
|
+
actions: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
52
|
+
disabled: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
53
|
+
selected: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
54
|
+
checked: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
|
|
55
|
+
row: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
56
|
+
}, z.core.$strip>>>;
|
|
57
|
+
where: z.ZodOptional<z.ZodObject<{
|
|
58
|
+
frame: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
x: z.ZodNumber;
|
|
60
|
+
y: z.ZodNumber;
|
|
61
|
+
width: z.ZodNumber;
|
|
62
|
+
height: z.ZodNumber;
|
|
63
|
+
}, z.core.$strip>>;
|
|
64
|
+
text: z.ZodOptional<z.ZodString>;
|
|
65
|
+
label: z.ZodOptional<z.ZodString>;
|
|
66
|
+
}, z.core.$strip>>;
|
|
67
|
+
problem: z.ZodString;
|
|
68
|
+
basis: z.ZodString;
|
|
69
|
+
at: z.ZodOptional<z.ZodObject<{
|
|
70
|
+
file: z.ZodString;
|
|
71
|
+
line: z.ZodNumber;
|
|
72
|
+
direct: z.ZodOptional<z.ZodBoolean>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
checked: z.ZodArray<z.ZodString>;
|
|
76
|
+
choices: z.ZodOptional<z.ZodNumber>;
|
|
77
|
+
actions: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
78
|
+
label: z.ZodString;
|
|
79
|
+
id: z.ZodOptional<z.ZodString>;
|
|
80
|
+
}, z.core.$strip>>>;
|
|
81
|
+
}, z.core.$strip>;
|
|
82
|
+
export type UxResult = z.infer<typeof uxResultSchema>;
|
|
83
|
+
export declare const MAX_UX_FINDINGS = 30;
|
|
84
|
+
export declare const JUDGMENT_LINE = "Add your own judgment from the screenshot, and label it as judgment.";
|
|
85
|
+
/** The report: header, numbered findings (each `at` on its own line), house rules to judge, what was checked, the ask. */
|
|
86
|
+
export declare function formatUxCheck(title: string, result: UxResult, options: {
|
|
87
|
+
textRules?: readonly string[];
|
|
88
|
+
journeyRunning: boolean;
|
|
89
|
+
eyes?: boolean;
|
|
90
|
+
}, pathOf?: (file: string) => string): string;
|
|
91
|
+
export interface UxRules {
|
|
92
|
+
/** Rules written as text: handed to the agent to judge. */
|
|
93
|
+
text: string[];
|
|
94
|
+
/** `{"way_back": true}`: every screen has a visible way back (checked by `ux_check`). */
|
|
95
|
+
wayBack: boolean;
|
|
96
|
+
/** `{"max_taps_to_goal": N}`: a journey that reaches its goal in more taps breaks it (checked by the journey at stop). */
|
|
97
|
+
maxTapsToGoal?: number;
|
|
98
|
+
}
|
|
99
|
+
export declare const NO_UX_RULES: UxRules;
|
|
100
|
+
/** Checks a parsed `--ux-rules` file; throws with a plain message when it isn't one. */
|
|
101
|
+
export declare function parseUxRules(json: unknown, label: string): UxRules;
|
|
102
|
+
/** Reads `--ux-rules FILE`; throws with a plain message when it isn't readable or isn't one. */
|
|
103
|
+
export declare function loadUxRules(path: string): UxRules;
|
package/dist/uxcheck.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { whereWords } from "./audit.js";
|
|
4
|
+
import { formatElement } from "./format.js";
|
|
5
|
+
import { shownPath } from "./inspect.js";
|
|
6
|
+
import { elementSchema } from "./protocol.js";
|
|
7
|
+
export const uxResultSchema = z.object({
|
|
8
|
+
issues: z.array(z.object({
|
|
9
|
+
kind: z.string(),
|
|
10
|
+
element: elementSchema.nullish(),
|
|
11
|
+
where: z
|
|
12
|
+
.object({
|
|
13
|
+
frame: z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() }).optional(),
|
|
14
|
+
text: z.string().optional(),
|
|
15
|
+
label: z.string().optional(),
|
|
16
|
+
})
|
|
17
|
+
.optional(),
|
|
18
|
+
problem: z.string(),
|
|
19
|
+
basis: z.string(),
|
|
20
|
+
at: z.object({ file: z.string(), line: z.number(), direct: z.boolean().optional() }).optional(),
|
|
21
|
+
})),
|
|
22
|
+
checked: z.array(z.string()),
|
|
23
|
+
choices: z.number().optional(),
|
|
24
|
+
actions: z.array(z.object({ label: z.string(), id: z.string().optional() })).default([]),
|
|
25
|
+
});
|
|
26
|
+
export const MAX_UX_FINDINGS = 30;
|
|
27
|
+
export const JUDGMENT_LINE = "Add your own judgment from the screenshot, and label it as judgment.";
|
|
28
|
+
const CHECK_WORDS = {
|
|
29
|
+
choices: "competing choices",
|
|
30
|
+
main_action: "main action",
|
|
31
|
+
thumb_reach: "thumb reach",
|
|
32
|
+
icon_only: "icon-only buttons",
|
|
33
|
+
cut_off_text: "cut-off text",
|
|
34
|
+
contrast: "contrast",
|
|
35
|
+
target_size: "target size",
|
|
36
|
+
naming: "naming across screens",
|
|
37
|
+
dead_end: "error and empty states",
|
|
38
|
+
way_back: "way back (house rule)",
|
|
39
|
+
};
|
|
40
|
+
/** The checks in the order the tail lists them. */
|
|
41
|
+
const CHECK_ORDER = Object.keys(CHECK_WORDS);
|
|
42
|
+
/** The report: header, numbered findings (each `at` on its own line), house rules to judge, what was checked, the ask. */
|
|
43
|
+
export function formatUxCheck(title, result, options, pathOf = shownPath) {
|
|
44
|
+
const n = result.issues.length;
|
|
45
|
+
const eyes = options.eyes === true;
|
|
46
|
+
const lines = [`UX check of ${JSON.stringify(title)}: ${n} finding${n === 1 ? "" : "s"}.`];
|
|
47
|
+
result.issues.slice(0, MAX_UX_FINDINGS).forEach((issue, i) => {
|
|
48
|
+
const what = issue.element ? formatElement(issue.element, { actions: false, eyes }) : whereWords(issue.where);
|
|
49
|
+
lines.push(`${i + 1}. ${issue.kind}: ${what}: ${issue.problem} (${issue.basis})`);
|
|
50
|
+
// In a user's eyes (AGENT-TOOLS 1.12 §17.1) the code behind the screen isn't shown: no file and line.
|
|
51
|
+
if (issue.at !== undefined && !eyes) {
|
|
52
|
+
const library = issue.at.direct === false ? " (a library draws it; this is the app's line around it)" : "";
|
|
53
|
+
lines.push(` at ${pathOf(issue.at.file)}:${issue.at.line}${library}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (n > MAX_UX_FINDINGS)
|
|
57
|
+
lines.push(`(+${n - MAX_UX_FINDINGS} more findings.)`);
|
|
58
|
+
const rules = options.textRules ?? [];
|
|
59
|
+
if (rules.length > 0)
|
|
60
|
+
lines.push(`House rules to judge: ${rules.map((r) => JSON.stringify(r)).join("; ")}.`);
|
|
61
|
+
const done = CHECK_ORDER.filter((k) => result.checked.includes(k));
|
|
62
|
+
const notes = [];
|
|
63
|
+
if (!result.checked.includes("naming")) {
|
|
64
|
+
notes.push(`${CHECK_WORDS["naming"]} (${options.journeyRunning ? "no earlier screens yet" : "no journey is running"})`);
|
|
65
|
+
}
|
|
66
|
+
lines.push(`Checked: ${done.map((k) => CHECK_WORDS[k]).join(", ")}.${notes.length > 0 ? ` Not checked here: ${notes.join(", ")}.` : ""}`);
|
|
67
|
+
lines.push(JUDGMENT_LINE);
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
export const NO_UX_RULES = { text: [], wayBack: false };
|
|
71
|
+
const KNOWN_KEYS = ["max_taps_to_goal", "way_back"];
|
|
72
|
+
/** Checks a parsed `--ux-rules` file; throws with a plain message when it isn't one. */
|
|
73
|
+
export function parseUxRules(json, label) {
|
|
74
|
+
const bad = (why) => new Error(`${label} isn't a set of UX rules: ${why}`);
|
|
75
|
+
if (typeof json !== "object" || json === null || Array.isArray(json))
|
|
76
|
+
throw bad('expected {"version": 1, "rules": [...]}');
|
|
77
|
+
const file = json;
|
|
78
|
+
for (const key of Object.keys(file))
|
|
79
|
+
if (key !== "version" && key !== "rules")
|
|
80
|
+
throw bad(`unknown key ${JSON.stringify(key)}`);
|
|
81
|
+
if (file["version"] !== 1)
|
|
82
|
+
throw bad("version must be 1");
|
|
83
|
+
const list = file["rules"];
|
|
84
|
+
if (!Array.isArray(list))
|
|
85
|
+
throw bad("rules must be a list");
|
|
86
|
+
const rules = { text: [], wayBack: false };
|
|
87
|
+
list.forEach((rule, i) => {
|
|
88
|
+
const at = `rule ${i + 1}`;
|
|
89
|
+
if (typeof rule === "string") {
|
|
90
|
+
if (rule.trim() === "")
|
|
91
|
+
throw bad(`${at} is empty`);
|
|
92
|
+
rules.text.push(rule.trim());
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (typeof rule !== "object" || rule === null || Array.isArray(rule))
|
|
96
|
+
throw bad(`${at} must be text or an object`);
|
|
97
|
+
const keys = Object.keys(rule);
|
|
98
|
+
const key = keys[0];
|
|
99
|
+
if (keys.length !== 1 || key === undefined)
|
|
100
|
+
throw bad(`${at} must have exactly one key`);
|
|
101
|
+
if (!KNOWN_KEYS.includes(key))
|
|
102
|
+
throw bad(`unknown key ${JSON.stringify(key)} in ${at} (known: ${KNOWN_KEYS.join(", ")})`);
|
|
103
|
+
const value = rule[key];
|
|
104
|
+
if (key === "way_back") {
|
|
105
|
+
if (typeof value !== "boolean")
|
|
106
|
+
throw bad(`way_back in ${at} must be true or false`);
|
|
107
|
+
rules.wayBack = value;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1)
|
|
111
|
+
throw bad(`max_taps_to_goal in ${at} must be a whole number of 1 or more`);
|
|
112
|
+
rules.maxTapsToGoal = value;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return rules;
|
|
116
|
+
}
|
|
117
|
+
/** Reads `--ux-rules FILE`; throws with a plain message when it isn't readable or isn't one. */
|
|
118
|
+
export function loadUxRules(path) {
|
|
119
|
+
let json;
|
|
120
|
+
try {
|
|
121
|
+
json = JSON.parse(readFileSync(path, "utf8"));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw new Error(`couldn't read the UX rules ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
return parseUxRules(json, path);
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=uxcheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uxcheck.js","sourceRoot":"","sources":["../src/uxcheck.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tapline-host",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Tapline host: lets AI agents see and drive your own React Native / Expo app on a test phone on the same Wi-Fi (MCP server and CLI).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"main": "dist/index.js",
|
|
27
27
|
"types": "dist/index.d.ts",
|
|
28
28
|
"bin": {
|
|
29
|
-
"tapline": "dist/
|
|
29
|
+
"tapline": "dist/bin.js"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
47
|
+
"gifenc": "1.0.3",
|
|
47
48
|
"jpeg-js": "^0.4.4",
|
|
48
49
|
"qrcode": "^1.5.4",
|
|
49
50
|
"ws": "^8.22.1",
|
package/skills/tapline/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tapline
|
|
3
|
-
description: Use when testing, exploring or driving the user's own React Native or Expo app on their real phone through Tapline (MCP tools such as look, tap, type_text, run, audit, inspect, wait_for, open_url, app_command, logs, crash, performance, session_report), when asked for QA, an accessibility check, a UI check or a bug hunt on that app, when asked where in the code a screen element comes from, when setting Tapline up for a user or adding its SDK to an app, or when a Tapline call answers not_connected, in_background, not_actionable, not_allowed, not_answering or reconnecting.
|
|
3
|
+
description: Use when testing, exploring or driving the user's own React Native or Expo app on their real phone through Tapline (MCP tools such as look, tap, type_text, run, drive, map, audit, inspect, wait_for, open_url, app_command, logs, crash, performance, session_report), when asked for QA, an accessibility check, a UI check or a bug hunt on that app, when asked where in the code a screen element comes from, when setting Tapline up for a user or adding its SDK to an app, or when a Tapline call answers not_connected, in_background, not_actionable, not_allowed, not_answering or reconnecting.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Needs the Tapline MCP server (`tapline mcp`, Node.js 20 or later) on the user's computer, and a development or TestFlight build of the app with Tapline's SDK switched on. Works from Windows, macOS or Linux.
|
|
6
6
|
metadata:
|
|
7
7
|
tool: tapline
|
|
8
|
-
interface: AGENT-TOOLS v1.
|
|
8
|
+
interface: AGENT-TOOLS v1.13
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Tapline: test your own app on a real phone
|
|
@@ -97,6 +97,39 @@ run {"steps": [
|
|
|
97
97
|
through the phone's Settings.
|
|
98
98
|
- After a long session, `session_report` shows where the calls went and which ones were wasted.
|
|
99
99
|
|
|
100
|
+
**Known ground: `drive`.** Tapline learns the app as you work: its places (screens by route
|
|
101
|
+
pattern), the roads between them, links and routes. The header's `Place:` line says where you are. Once
|
|
102
|
+
the map knows a place, go there in one call and read the answer, which has the screen:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
drive {"to": "language"}
|
|
106
|
+
|
|
107
|
+
Arrived at language by 3 taps in 1.9 s.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- On known ground, `drive` yourself in one call; don't tap step by step. On unknown ground, hand the
|
|
111
|
+
goal to one small, fast agent and keep one driver. Never two drivers on one phone.
|
|
112
|
+
- `map` lists what is known (places, roads, routes, reflexes) and touches nothing; `map {"to": "x"}`
|
|
113
|
+
shows the way. `drive` takes `by` (`taps`, `link`), `with` (a link's params, a route's variables),
|
|
114
|
+
`then` (`run` steps) and `dry_run`. It works as a `run` step too.
|
|
115
|
+
- `off_route` means a road didn't hold. Nothing more was touched: read the screen in the answer and
|
|
116
|
+
go on by hand. Don't repeat the drive.
|
|
117
|
+
- A route is a saved flow: `record` with `route`, then `drive {"route": "sign_in", "with": {...}}`.
|
|
118
|
+
- A reflex, `map {"reflex": {...}}`, closes a known interruption by itself (a rate prompt) and says
|
|
119
|
+
`Reflex: closed ...`. Only declared ones run. Add one only for something you have seen.
|
|
120
|
+
- Tapline never navigates from inside the app: a drive is the app's own taps or a deep link.
|
|
121
|
+
- `run` steps take `if` and `optional` for what may or may not show up; a skipped step is said.
|
|
122
|
+
`brief: true` shortens an action's answer to 12 element lines; `look` is never brief.
|
|
123
|
+
|
|
124
|
+
**Accounts by name.** When the user started Tapline with `--accounts`, `type_text` takes `account:
|
|
125
|
+
"tester2"` and the whole text `${account.username}` or `${account.password}`; `run` and `drive` take
|
|
126
|
+
`account` too. You never see the value, and you never put a password in a call, a note or a commit.
|
|
127
|
+
Use only accounts from that file.
|
|
128
|
+
|
|
129
|
+
**Speed.** `tapline call <tool> '<json>'` is one call to a running `tapline host`, for when no MCP
|
|
130
|
+
server is loaded; loading one per call costs about 2 s. `app_command animations {"speed": "fast"}`
|
|
131
|
+
makes transitions nearly instant; turn it off (`normal`) before a visual check. It is opt-in.
|
|
132
|
+
|
|
100
133
|
Name the element with one of, in this order of preference: `id` (the app's own identifier,
|
|
101
134
|
React Native's `testID`), `ref` (from your last list, like `e3`), `label`, `text`. Use `x`
|
|
102
135
|
and `y` only for things with no element.
|
|
@@ -109,15 +142,17 @@ and `y` only for things with no element.
|
|
|
109
142
|
(`[tap, longPress]`).
|
|
110
143
|
- **`Same screen. Changes:`** then `+` new, `-` gone, `~` changed (with what it was),
|
|
111
144
|
`↕` moved. When most of the screen changed, the full list comes instead.
|
|
112
|
-
- **`No visible change.`** means nothing changed, on screen or off it.
|
|
145
|
+
- **`No visible change.`** means nothing changed, on screen or off it. After a tap it is followed by
|
|
146
|
+
`Note: the tap was delivered, and nothing on screen changed within 0.5 s.` and, when known, why (the
|
|
147
|
+
handler threw, only a parent's took it, a request is still running). Read it: the tap may be a bug.
|
|
113
148
|
- **A sheet, an alert or a new screen answers with the full list:** the first line, then
|
|
114
149
|
`Screen …` and the elements that are in front now. What a sheet hides is left out until it
|
|
115
150
|
closes; acting on it answers `covered: … close that first.` When something out of view
|
|
116
151
|
changed as well, it follows the list under `Changes off screen:`.
|
|
117
152
|
- **The header counts what you can't see:**
|
|
118
153
|
`Screen 430x932 pt @3x · 26 elements (+41 below, +2 above)`. Those 41 are reached by
|
|
119
|
-
scrolling. They are not in the list
|
|
120
|
-
|
|
154
|
+
scrolling. They are not in the list, but `tap`, `long_press`, `toggle` and `type_text` scroll to one
|
|
155
|
+
themselves. `off_screen` means no scroll can bring it on screen: look at the screen, then target something else or report it. `Route:` names the screen you are on, and `Network:` says requests are still
|
|
121
156
|
running.
|
|
122
157
|
- **A change out of view has its own line, without a ref:**
|
|
123
158
|
```
|
|
@@ -148,7 +183,7 @@ and `y` only for things with no element.
|
|
|
148
183
|
|
|
149
184
|
| To | Call |
|
|
150
185
|
|---|---|
|
|
151
|
-
| reach an element that is off screen | `scroll` with it as `target` and `to: "target"`.
|
|
186
|
+
| reach an element that is off screen | `tap`, `long_press`, `toggle` and `type_text` scroll to it themselves. To only bring it into view: `scroll` with it as `target` and `to: "target"`. Don't scroll by guess |
|
|
152
187
|
| wait for a load, a spinner or a new screen | `wait_for` with `text`, `id` or `label` (`gone: true` for a spinner; `network_idle` for requests), not `wait` |
|
|
153
188
|
| set a switch, or read it | `toggle` with `value: true` or `false`. It does nothing if the switch is already there, and its answer says the switch's state either way |
|
|
154
189
|
| fill a field and send it | `type_text` with `submit: true` |
|
|
@@ -184,7 +219,7 @@ Every failure is `Error <code>: <message>`, and the message says what to do next
|
|
|
184
219
|
|---|---|
|
|
185
220
|
| `not_found` | `look`, then pick another target |
|
|
186
221
|
| `ambiguous` | use one of the listed refs, or add `index` |
|
|
187
|
-
| `not_actionable` | the first word is why: `disabled`, `off_screen`, `covered`, `under_tab_bar`, `behind_alert`, `no_action`. Fix the cause:
|
|
222
|
+
| `not_actionable` | the first word is why: `disabled`, `off_screen`, `covered`, `under_tab_bar`, `behind_alert`, `no_action`. Fix the cause: close what is in front, answer the alert; for `off_screen`, target something else or report it (nothing scrolls there) |
|
|
188
223
|
| `stale` | a person changed the screen since your list: `look` again |
|
|
189
224
|
| `busy` | a person is using the app: wait a few seconds, retry |
|
|
190
225
|
| `paused`, `in_background` | ask the user (to resume; to open the app again) |
|
|
@@ -194,7 +229,9 @@ Every failure is `Error <code>: <message>`, and the message says what to do next
|
|
|
194
229
|
| `timeout` | the action may have happened: `look` before you retry |
|
|
195
230
|
| `unsupported` | Tapline can't do this: use what the message names |
|
|
196
231
|
| `bad_params` | fix the call |
|
|
197
|
-
| `not_allowed` | the first word is why. `needs_confirmation`: a person must allow this touch in the live view; tell the user, and when they have, make the same call again. `blocked`: the safety policy forbids it; don't try another way |
|
|
232
|
+
| `not_allowed` | the first word is why. `needs_confirmation`: a person must allow this touch. Usually the host asks in a dialog and in the live view and your call waits (up to 120 s) for the answer; `They declined.` means ask your user, don't repeat the call. With no dialog: tell the user, and when they have allowed it in the live view, make the same call again. `blocked`: the safety policy forbids it; don't try another way |
|
|
233
|
+
| `off_route` | a `drive` road didn't hold; nothing more was touched. Read the screen and go on by hand |
|
|
234
|
+
| `Needs a person: ...` | the host's dialog (or a `Note:`) names what a person must do on the phone (open the app, tap an alert). The call goes on by itself when it clears; `They can't now.` means ask your user |
|
|
198
235
|
| `expect_failed` | a `run` step's `expect` didn't hold: `look`, then fix the step or report the bug |
|
|
199
236
|
| `internal` | a bug in the tool: report it with the call you made |
|
|
200
237
|
|
|
@@ -239,18 +276,27 @@ Evidence: screenshot moment, if any.
|
|
|
239
276
|
Then, apart from the bugs: what passed, what you couldn't test and why, and screens that
|
|
240
277
|
took more than a few seconds (`Settled in` gives the time).
|
|
241
278
|
|
|
279
|
+
**As a real user.** When the question is whether people can find, understand and finish a feature, not only
|
|
280
|
+
whether it works, run a `journey`: `start` with a `goal` and a `persona`, act as that person would (you see
|
|
281
|
+
only what is on the glass, and target by ref or visible text), then `stop` with `reached`. The tool measures
|
|
282
|
+
the taps, the time, the wrong turns and the hesitations, and writes a visual report. Build personas for this
|
|
283
|
+
app's own users and agree them with your user: [references/personas.md](references/personas.md) has the
|
|
284
|
+
recipe and examples. `ux_check` checks a screen against usability basics along the way.
|
|
285
|
+
|
|
242
286
|
## 7. Safety
|
|
243
287
|
|
|
244
288
|
- It is the user's own phone and their real app data. Don't buy, subscribe, send messages or
|
|
245
289
|
email, post, delete data, or change account, privacy or security settings, unless the user
|
|
246
290
|
asked for exactly that step. Ask when unsure.
|
|
247
|
-
-
|
|
248
|
-
you find in the app or its logs
|
|
291
|
+
- Test accounts come only from the user's accounts file (`--accounts`), used by name. Use the
|
|
292
|
+
test data the user gives you. Never sign in with credentials you find in the app or its logs, and
|
|
293
|
+
never print, repeat or commit one.
|
|
249
294
|
- Don't read or repeat private content you see (messages, photos, notifications) beyond
|
|
250
295
|
what the test needs.
|
|
251
296
|
- Stay in the app under test.
|
|
252
297
|
- Some touches need a person's OK first: a button whose label says Buy, Pay, Delete, Send,
|
|
253
|
-
Post, Sign out and the like
|
|
298
|
+
Post, Sign out and the like needs a person: the host's dialog and the live view ask them, and
|
|
299
|
+
a `drive` or `run` stops there like a single touch. A reflex never takes such a touch.
|
|
254
300
|
You can never give that OK yourself, and you never look for another way to the same result
|
|
255
301
|
(another button, a deep link, `x` and `y`). `dry_run: true` tells you beforehand.
|
|
256
302
|
- Give the live view's link to the user only. It carries a key. A pairing link lets a phone
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Personas for usability journeys
|
|
2
|
+
|
|
3
|
+
A persona is a short description of one kind of user, so a journey is judged by the right eyes. No tool enforces it:
|
|
4
|
+
you build it, agree it with your user, and keep to it while you test. A persona's conditions are values of
|
|
5
|
+
`language`, `region`, `text_size` and `network`; pass these as `conditions` on `journey { start }`; each tool applies
|
|
6
|
+
them its own way.
|
|
7
|
+
|
|
8
|
+
## Build one for the app at hand
|
|
9
|
+
|
|
10
|
+
Ask the user who really uses this app, then write five lines per persona:
|
|
11
|
+
|
|
12
|
+
1. **Who:** age, job, how often they use the app, what device they have.
|
|
13
|
+
2. **Knows:** what they already know about the app and about apps like it.
|
|
14
|
+
3. **Wants:** the one goal of this journey, in their words ("log my breakfast").
|
|
15
|
+
4. **Situation:** where they are and what else is going on (on a bus, one hand free, in a hurry).
|
|
16
|
+
5. **Conditions:** the `language`, `region`, `text_size` and `network` that fit, if any.
|
|
17
|
+
|
|
18
|
+
Two or three personas that differ a lot beat six that are alike. Name the persona in `journey { start }`, and read each
|
|
19
|
+
screen as they would: what they would notice first, what they would tap, where they would hesitate.
|
|
20
|
+
|
|
21
|
+
## Examples to copy, extend or ignore
|
|
22
|
+
|
|
23
|
+
**First-timer.** Has never seen the app; knows only what the store page told them. Wants to finish the first task
|
|
24
|
+
without reading help. Reads every label, hesitates at jargon and at icons without words, never scrolls to find a
|
|
25
|
+
hidden option. Conditions: none, a fresh install.
|
|
26
|
+
|
|
27
|
+
**Busy regular.** Uses the app daily, knows where things are, wants it done in as few taps as possible, one hand, while
|
|
28
|
+
walking. Notices every extra step and every changed button name. Conditions: `network: 3g`.
|
|
29
|
+
|
|
30
|
+
**Older user with large text.** Reads slowly, avoids small targets, does not guess what an unlabelled icon does. Wants
|
|
31
|
+
the same task with text at the largest size, so cut-off text and overlapping controls show up.
|
|
32
|
+
Conditions: `text_size: ax3`.
|
|
33
|
+
|
|
34
|
+
**Right-to-left speaker.** Reads Arabic; expects the layout mirrored, back arrows pointing the other way, numbers and
|
|
35
|
+
mixed text in order. Wants a task done in their own language. Conditions: `language: ar · region: QA`.
|
|
36
|
+
|
|
37
|
+
**Poor connection.** On a train or abroad, the network drops and crawls. Wants to know what happened when something
|
|
38
|
+
does not load, and to retry without losing what they typed. Conditions: `network: 3g`, and a `network` stress step to
|
|
39
|
+
`offline` at the worst moment (a field half filled, a form being sent).
|
|
40
|
+
|
|
41
|
+
**One-handed on a big phone.** Holds a large phone in one hand and can only reach the lower half with a thumb. Wants
|
|
42
|
+
to finish the task without stretching or using the other hand. Conditions: none, a large phone.
|
|
43
|
+
|
|
44
|
+
Each line above is a starting point: change the details to match the app's real users. Write the conditions on one line
|
|
45
|
+
the way the examples do, for example `language: ar · region: QA · text_size: xl · network: 3g`.
|
|
@@ -110,6 +110,11 @@ tapline mcp
|
|
|
110
110
|
- `--policy FILE` sets the words that need a person's OK or are blocked (JSON, per app);
|
|
111
111
|
`--no-policy` turns the policy off, only for a test phone with test data. `--no-log` turns
|
|
112
112
|
off the action log in `~/.tapline/log/`.
|
|
113
|
+
- `--no-dialog` keeps a person's OK to the live view (the host's own dialog is off), for a host that
|
|
114
|
+
hands dialogs to the model instead of a person. `--brief` makes actions answer briefly.
|
|
115
|
+
`--accounts FILE` loads test accounts (outside any git repository, or ignored by it; never print
|
|
116
|
+
it). `--maps DIR` keeps each app's map in `DIR/<app id>/`. `tapline host` takes the same options
|
|
117
|
+
and serves `tapline call`.
|
|
113
118
|
|
|
114
119
|
**Claude Code**
|
|
115
120
|
|
|
@@ -155,7 +160,7 @@ Run `tapline doctor` first.
|
|
|
155
160
|
| the phone can't connect, on Windows | the firewall: `tapline doctor` names the rule for ports 7210 to 7219 (it allows the local subnet and Tailscale) |
|
|
156
161
|
| the phone can't connect, on any computer | the phone and the computer are on different networks, or the Wi-Fi blocks devices from each other (guest networks do) |
|
|
157
162
|
| the app doesn't ask to connect after `pair` with no scheme | the offer lasts 10 minutes: call `pair` again. `metro.config.js` needs `withTapline`; same Wi-Fi; the code must load from this computer's Metro, not a tunnel. TestFlight and release builds need the QR code |
|
|
158
|
-
| `not_allowed: needs_confirmation` | a policy word (Buy, Delete, Send …) is on the element: the user allows it in the live view (`viewer`),
|
|
163
|
+
| `not_allowed: needs_confirmation` | a policy word (Buy, Delete, Send …) is on the element: the user allows it in the host's dialog or the live view (`viewer`); with no dialog, repeat the call within 120 s after they have. Use `--no-policy` only on a test phone, with the user's OK |
|
|
159
164
|
| the list is empty, "No React roots recorded" | the `register` import isn't the first line of the entry file |
|
|
160
165
|
| a pairing link opens a "not found" screen | Expo Router: `app/+native-intent.ts` is missing |
|
|
161
166
|
| every call answers `in_background` | the app isn't in front: the user opens it again |
|