zeno-mobile-runner 0.1.3 → 0.2.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/CHANGELOG.md +192 -2
- package/FEATURES.md +50 -7
- package/README.md +168 -120
- package/build.zig.zon +3 -3
- package/clients/README.md +60 -3
- package/clients/go/README.md +12 -0
- package/clients/go/zmr/client.go +142 -0
- package/clients/kotlin/README.md +18 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
- package/clients/python/README.md +19 -0
- package/clients/python/pyproject.toml +1 -1
- package/clients/python/zmr_client.py +33 -0
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/rust/README.md +25 -1
- package/clients/rust/src/lib.rs +201 -0
- package/clients/swift/README.md +18 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
- package/clients/typescript/README.md +16 -0
- package/clients/typescript/index.d.ts +12 -0
- package/clients/typescript/index.mjs +16 -0
- package/clients/typescript/package.json +1 -1
- package/docs/agent-discovery.md +151 -22
- package/docs/ai-agents.md +99 -11
- package/docs/benchmarking.md +49 -3
- package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
- package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
- package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
- package/docs/benchmarks/README.md +36 -0
- package/docs/benchmarks/benchmark-lab-v1.json +155 -0
- package/docs/benchmarks/benchmark-lab-v1.md +95 -0
- package/docs/clients.md +26 -6
- package/docs/demo.md +40 -1
- package/docs/expo-smoke.md +8 -8
- package/docs/frameworks.md +10 -0
- package/docs/install.md +3 -2
- package/docs/npm.md +100 -4
- package/docs/production-readiness.md +123 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +215 -16
- package/docs/scenario-authoring.md +18 -0
- package/docs/trace-privacy.md +9 -0
- package/docs/troubleshooting.md +7 -1
- package/examples/android-workflow.json +79 -0
- package/examples/ios-shim-workflow.json +79 -0
- package/examples/react-native-expo-workflow.json +75 -0
- package/npm/agents.mjs +16 -0
- package/npm/commands.mjs +9 -5
- package/package.json +6 -1
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +4 -0
- package/schemas/discover-output.schema.json +83 -0
- package/schemas/draft-output.schema.json +58 -0
- package/schemas/explore-output.schema.json +94 -0
- package/schemas/inspect-output.schema.json +88 -0
- package/schemas/run-output.schema.json +2 -0
- package/scripts/benchmark-lab.py +253 -0
- package/scripts/create-android-demo-app.sh +324 -29
- package/scripts/create-ios-demo-app.sh +174 -7
- package/scripts/create-react-native-expo-demo-app.sh +727 -0
- package/scripts/demo.sh +3 -0
- package/scripts/install-ios-shim.sh +2 -2
- package/scripts/release-readiness.py +43 -0
- package/scripts/run-android-pilot.sh +35 -9
- package/scripts/run-ios-pilot.sh +11 -4
- package/shims/ios/ZMRShim.swift +10 -0
- package/shims/ios/ZMRShimUITestCase.swift +42 -0
- package/shims/ios/protocol.md +1 -0
- package/skills/zmr-mobile-testing/SKILL.md +28 -3
- package/src/cli_discover.zig +239 -0
- package/src/cli_draft.zig +924 -0
- package/src/cli_explore.zig +136 -0
- package/src/cli_import.zig +31 -15
- package/src/cli_inspect.zig +310 -0
- package/src/cli_output.zig +26 -2
- package/src/cli_run.zig +28 -0
- package/src/cli_trace.zig +45 -15
- package/src/cli_validate.zig +12 -6
- package/src/errors.zig +9 -0
- package/src/ios.zig +49 -12
- package/src/ios_shim.zig +36 -2
- package/src/json_rpc_methods.zig +85 -11
- package/src/json_rpc_params.zig +8 -0
- package/src/json_rpc_protocol.zig +1 -1
- package/src/json_rpc_trace.zig +112 -0
- package/src/main.zig +27 -2
- package/src/mcp.zig +209 -6
- package/src/mcp_protocol.zig +29 -1
- package/src/mcp_trace.zig +126 -4
- package/src/report.zig +186 -0
- package/src/runner.zig +26 -4
- package/src/runner_actions.zig +10 -0
- package/src/runner_diagnostics.zig +31 -1
- package/src/runner_events.zig +70 -7
- package/src/runner_native.zig +17 -1
- package/src/runner_waits.zig +82 -19
- package/src/scaffold.zig +28 -12
- package/src/scenario.zig +32 -4
- package/src/schema_registry.zig +4 -0
- package/src/version.zig +1 -1
- package/viewer/app.js +23 -3
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
FIXTURE_STATUSES = {"planned", "fixture-available", "evidence-committed"}
|
|
9
|
+
ADAPTER_STATUSES = {"planned", "partial", "available", "evidence-committed"}
|
|
10
|
+
SLICE_STATUSES = {"next", "later", "done"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_args():
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Validate and render a ZMR benchmark lab manifest.",
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--manifest",
|
|
19
|
+
default="docs/benchmarks/benchmark-lab-v1.json",
|
|
20
|
+
help="Benchmark lab manifest path.",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--format",
|
|
24
|
+
choices=("json", "markdown"),
|
|
25
|
+
default="markdown",
|
|
26
|
+
help="Output format.",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument("--out", help="Optional output file. Defaults to stdout.")
|
|
29
|
+
return parser.parse_args()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def require_object(value, path):
|
|
33
|
+
if not isinstance(value, dict):
|
|
34
|
+
raise ValueError(f"{path} must be an object")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def require_array(value, path):
|
|
38
|
+
if not isinstance(value, list):
|
|
39
|
+
raise ValueError(f"{path} must be an array")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def require_string(value, path):
|
|
43
|
+
if not isinstance(value, str) or not value.strip():
|
|
44
|
+
raise ValueError(f"{path} must be a non-empty string")
|
|
45
|
+
return value.strip()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_unique_ids(items, path):
|
|
49
|
+
seen = set()
|
|
50
|
+
for index, item in enumerate(items):
|
|
51
|
+
require_object(item, f"{path}[{index}]")
|
|
52
|
+
item_id = require_string(item.get("id"), f"{path}[{index}].id")
|
|
53
|
+
if item_id in seen:
|
|
54
|
+
raise ValueError(f"{path}[{index}].id is duplicated: {item_id}")
|
|
55
|
+
seen.add(item_id)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_status(value, allowed, path):
|
|
59
|
+
actual = require_string(value, path)
|
|
60
|
+
if actual not in allowed:
|
|
61
|
+
raise ValueError(f"{path} must be one of: {', '.join(sorted(allowed))}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_manifest(data):
|
|
65
|
+
require_object(data, "$")
|
|
66
|
+
if data.get("schemaVersion") != 1:
|
|
67
|
+
raise ValueError("$.schemaVersion must be 1")
|
|
68
|
+
require_string(data.get("name"), "$.name")
|
|
69
|
+
require_string(data.get("purpose"), "$.purpose")
|
|
70
|
+
require_object(data.get("claimPolicy"), "$.claimPolicy")
|
|
71
|
+
|
|
72
|
+
policy = data["claimPolicy"]
|
|
73
|
+
if int(policy.get("minimumRuns", 0)) < 1:
|
|
74
|
+
raise ValueError("$.claimPolicy.minimumRuns must be positive")
|
|
75
|
+
if float(policy.get("candidatePassRate", -1)) < 0:
|
|
76
|
+
raise ValueError("$.claimPolicy.candidatePassRate must be non-negative")
|
|
77
|
+
if int(policy.get("candidateFailures", -1)) < 0:
|
|
78
|
+
raise ValueError("$.claimPolicy.candidateFailures must be non-negative")
|
|
79
|
+
if policy.get("requiresSameContext") is not True:
|
|
80
|
+
raise ValueError("$.claimPolicy.requiresSameContext must be true")
|
|
81
|
+
|
|
82
|
+
modes = data.get("modes")
|
|
83
|
+
fixtures = data.get("fixtures")
|
|
84
|
+
adapters = data.get("runnerAdapters")
|
|
85
|
+
slices = data.get("nextSlices")
|
|
86
|
+
require_array(modes, "$.modes")
|
|
87
|
+
require_array(fixtures, "$.fixtures")
|
|
88
|
+
require_array(adapters, "$.runnerAdapters")
|
|
89
|
+
require_array(slices, "$.nextSlices")
|
|
90
|
+
validate_unique_ids(modes, "$.modes")
|
|
91
|
+
validate_unique_ids(fixtures, "$.fixtures")
|
|
92
|
+
validate_unique_ids(adapters, "$.runnerAdapters")
|
|
93
|
+
validate_unique_ids(slices, "$.nextSlices")
|
|
94
|
+
|
|
95
|
+
mode_ids = {mode["id"] for mode in modes}
|
|
96
|
+
for index, mode in enumerate(modes):
|
|
97
|
+
require_string(mode.get("label"), f"$.modes[{index}].label")
|
|
98
|
+
require_string(mode.get("description"), f"$.modes[{index}].description")
|
|
99
|
+
|
|
100
|
+
for index, fixture in enumerate(fixtures):
|
|
101
|
+
require_string(fixture.get("label"), f"$.fixtures[{index}].label")
|
|
102
|
+
require_string(fixture.get("framework"), f"$.fixtures[{index}].framework")
|
|
103
|
+
validate_status(fixture.get("status"), FIXTURE_STATUSES, f"$.fixtures[{index}].status")
|
|
104
|
+
require_array(fixture.get("platforms"), f"$.fixtures[{index}].platforms")
|
|
105
|
+
require_array(fixture.get("workflow"), f"$.fixtures[{index}].workflow")
|
|
106
|
+
if fixture["status"] != "planned" and not fixture.get("scenario"):
|
|
107
|
+
raise ValueError(f"$.fixtures[{index}].scenario is required unless status is planned")
|
|
108
|
+
|
|
109
|
+
for index, adapter in enumerate(adapters):
|
|
110
|
+
require_string(adapter.get("label"), f"$.runnerAdapters[{index}].label")
|
|
111
|
+
validate_status(adapter.get("status"), ADAPTER_STATUSES, f"$.runnerAdapters[{index}].status")
|
|
112
|
+
require_string(adapter.get("collector"), f"$.runnerAdapters[{index}].collector")
|
|
113
|
+
require_array(adapter.get("modes"), f"$.runnerAdapters[{index}].modes")
|
|
114
|
+
for mode in adapter["modes"]:
|
|
115
|
+
if mode not in mode_ids:
|
|
116
|
+
raise ValueError(f"$.runnerAdapters[{index}].modes references unknown mode: {mode}")
|
|
117
|
+
|
|
118
|
+
for index, next_slice in enumerate(slices):
|
|
119
|
+
validate_status(next_slice.get("status"), SLICE_STATUSES, f"$.nextSlices[{index}].status")
|
|
120
|
+
require_string(next_slice.get("description"), f"$.nextSlices[{index}].description")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def read_manifest(path):
|
|
124
|
+
try:
|
|
125
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
126
|
+
except json.JSONDecodeError as exc:
|
|
127
|
+
raise ValueError(f"{path}:{exc.lineno}:{exc.colno}: invalid json: {exc.msg}") from exc
|
|
128
|
+
validate_manifest(data)
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def summary(data):
|
|
133
|
+
fixtures = data["fixtures"]
|
|
134
|
+
adapters = data["runnerAdapters"]
|
|
135
|
+
next_slices = data["nextSlices"]
|
|
136
|
+
return {
|
|
137
|
+
"ok": True,
|
|
138
|
+
"name": data["name"],
|
|
139
|
+
"schemaVersion": data["schemaVersion"],
|
|
140
|
+
"fixtureCount": len(fixtures),
|
|
141
|
+
"adapterCount": len(adapters),
|
|
142
|
+
"modeCount": len(data["modes"]),
|
|
143
|
+
"evidenceFixtures": [fixture["id"] for fixture in fixtures if fixture["status"] == "evidence-committed"],
|
|
144
|
+
"availableFixtures": [fixture["id"] for fixture in fixtures if fixture["status"] in ("fixture-available", "evidence-committed")],
|
|
145
|
+
"plannedFixtures": [fixture["id"] for fixture in fixtures if fixture["status"] == "planned"],
|
|
146
|
+
"nextSlices": [item["id"] for item in next_slices if item["status"] == "next"],
|
|
147
|
+
"minimumRuns": data["claimPolicy"]["minimumRuns"],
|
|
148
|
+
"candidatePassRate": data["claimPolicy"]["candidatePassRate"],
|
|
149
|
+
"candidateFailures": data["claimPolicy"]["candidateFailures"],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def markdown_table(headers, rows):
|
|
154
|
+
lines = [
|
|
155
|
+
"| " + " | ".join(headers) + " |",
|
|
156
|
+
"| " + " | ".join("---" for _ in headers) + " |",
|
|
157
|
+
]
|
|
158
|
+
for row in rows:
|
|
159
|
+
lines.append("| " + " | ".join(row) + " |")
|
|
160
|
+
return "\n".join(lines)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def render_markdown(data):
|
|
164
|
+
fixtures = [
|
|
165
|
+
[
|
|
166
|
+
fixture["id"],
|
|
167
|
+
fixture["framework"],
|
|
168
|
+
", ".join(fixture["platforms"]),
|
|
169
|
+
fixture["status"],
|
|
170
|
+
fixture.get("scenario", "pending"),
|
|
171
|
+
]
|
|
172
|
+
for fixture in data["fixtures"]
|
|
173
|
+
]
|
|
174
|
+
adapters = [
|
|
175
|
+
[
|
|
176
|
+
adapter["id"],
|
|
177
|
+
adapter["status"],
|
|
178
|
+
adapter["collector"],
|
|
179
|
+
", ".join(adapter["modes"]),
|
|
180
|
+
]
|
|
181
|
+
for adapter in data["runnerAdapters"]
|
|
182
|
+
]
|
|
183
|
+
modes = [
|
|
184
|
+
[mode["id"], mode["label"], mode["description"]]
|
|
185
|
+
for mode in data["modes"]
|
|
186
|
+
]
|
|
187
|
+
next_slices = [
|
|
188
|
+
[item["id"], item["status"], item["description"]]
|
|
189
|
+
for item in data["nextSlices"]
|
|
190
|
+
]
|
|
191
|
+
policy = data["claimPolicy"]
|
|
192
|
+
return "\n".join(
|
|
193
|
+
[
|
|
194
|
+
f"# {data['name']}",
|
|
195
|
+
"",
|
|
196
|
+
data["purpose"],
|
|
197
|
+
"",
|
|
198
|
+
"## Claim Policy",
|
|
199
|
+
"",
|
|
200
|
+
f"- Minimum runs: {policy['minimumRuns']}",
|
|
201
|
+
f"- Candidate pass rate: {policy['candidatePassRate']}%",
|
|
202
|
+
f"- Candidate failures: {policy['candidateFailures']}",
|
|
203
|
+
f"- Requires same context: {'yes' if policy['requiresSameContext'] else 'no'}",
|
|
204
|
+
f"- Requires committed rows: {'yes' if policy['requiresCommittedRows'] else 'no'}",
|
|
205
|
+
f"- Forbidden claim: {policy['forbiddenClaim']}",
|
|
206
|
+
"",
|
|
207
|
+
"## Fixtures",
|
|
208
|
+
"",
|
|
209
|
+
markdown_table(["Fixture", "Framework", "Platforms", "Status", "Scenario"], fixtures),
|
|
210
|
+
"",
|
|
211
|
+
"## Runner Adapters",
|
|
212
|
+
"",
|
|
213
|
+
markdown_table(["Adapter", "Status", "Collector", "Modes"], adapters),
|
|
214
|
+
"",
|
|
215
|
+
"## Modes",
|
|
216
|
+
"",
|
|
217
|
+
markdown_table(["Mode", "Label", "Description"], modes),
|
|
218
|
+
"",
|
|
219
|
+
"## Next Slices",
|
|
220
|
+
"",
|
|
221
|
+
markdown_table(["Slice", "Status", "Description"], next_slices),
|
|
222
|
+
"",
|
|
223
|
+
]
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def write_output(content, path):
|
|
228
|
+
if path:
|
|
229
|
+
out = Path(path)
|
|
230
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
out.write_text(content, encoding="utf-8")
|
|
232
|
+
else:
|
|
233
|
+
sys.stdout.write(content)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def main():
|
|
237
|
+
args = parse_args()
|
|
238
|
+
try:
|
|
239
|
+
data = read_manifest(args.manifest)
|
|
240
|
+
except ValueError as exc:
|
|
241
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
242
|
+
return 2
|
|
243
|
+
|
|
244
|
+
if args.format == "json":
|
|
245
|
+
content = json.dumps(summary(data), sort_keys=True, separators=(",", ":")) + "\n"
|
|
246
|
+
else:
|
|
247
|
+
content = render_markdown(data)
|
|
248
|
+
write_output(content, args.out)
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
raise SystemExit(main())
|
|
@@ -31,6 +31,7 @@ Options:
|
|
|
31
31
|
After generation:
|
|
32
32
|
adb install -r <dir>/build/app-debug.apk
|
|
33
33
|
zmr run <dir>/.zmr/android-smoke.json --device emulator-5554 --app-id com.example.mobiletest --trace-dir <dir>/traces/android-demo
|
|
34
|
+
zmr run <dir>/.zmr/android-workflow.json --device emulator-5554 --app-id com.example.mobiletest --trace-dir <dir>/traces/android-workflow
|
|
34
35
|
USAGE
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -199,6 +200,29 @@ write_file "$RES_DIR/values/ids.xml" "$(cat <<'EOF'
|
|
|
199
200
|
<item name="continue_button" type="id" />
|
|
200
201
|
<item name="demo_input" type="id" />
|
|
201
202
|
<item name="demo_status" type="id" />
|
|
203
|
+
<item name="profile_title" type="id" />
|
|
204
|
+
<item name="profile_name_input" type="id" />
|
|
205
|
+
<item name="profile_email_input" type="id" />
|
|
206
|
+
<item name="save_profile_button" type="id" />
|
|
207
|
+
<item name="catalog_title" type="id" />
|
|
208
|
+
<item name="catalog_list" type="id" />
|
|
209
|
+
<item name="catalog_item_trail_lamp" type="id" />
|
|
210
|
+
<item name="catalog_item_river_bottle" type="id" />
|
|
211
|
+
<item name="catalog_item_summit_shell" type="id" />
|
|
212
|
+
<item name="catalog_item_basecamp_roll" type="id" />
|
|
213
|
+
<item name="catalog_item_maple_organizer" type="id" />
|
|
214
|
+
<item name="catalog_item_canyon_sling" type="id" />
|
|
215
|
+
<item name="catalog_item_harbor_tote" type="id" />
|
|
216
|
+
<item name="catalog_item_north_ridge_pack" type="id" />
|
|
217
|
+
<item name="catalog_item_studio_stand" type="id" />
|
|
218
|
+
<item name="detail_title" type="id" />
|
|
219
|
+
<item name="detail_subtitle" type="id" />
|
|
220
|
+
<item name="detail_save_button" type="id" />
|
|
221
|
+
<item name="review_button" type="id" />
|
|
222
|
+
<item name="review_title" type="id" />
|
|
223
|
+
<item name="review_complete" type="id" />
|
|
224
|
+
<item name="review_item" type="id" />
|
|
225
|
+
<item name="workflow_status" type="id" />
|
|
202
226
|
</resources>
|
|
203
227
|
EOF
|
|
204
228
|
)"
|
|
@@ -217,66 +241,252 @@ import android.content.Context;
|
|
|
217
241
|
import android.widget.Button;
|
|
218
242
|
import android.widget.EditText;
|
|
219
243
|
import android.widget.LinearLayout;
|
|
244
|
+
import android.widget.ScrollView;
|
|
220
245
|
import android.widget.TextView;
|
|
221
246
|
|
|
222
247
|
public class MainActivity extends Activity {
|
|
223
|
-
private
|
|
224
|
-
private
|
|
248
|
+
private LinearLayout root;
|
|
249
|
+
private TextView demoStatus;
|
|
250
|
+
private TextView workflowStatus;
|
|
251
|
+
private String currentStatus = "Ready";
|
|
252
|
+
private CatalogItem selectedItem = new CatalogItem("north_ridge_pack", "North Ridge Pack", "Weatherproof day pack", R.id.catalog_item_north_ridge_pack);
|
|
253
|
+
|
|
254
|
+
private static class CatalogItem {
|
|
255
|
+
final String key;
|
|
256
|
+
final String title;
|
|
257
|
+
final String subtitle;
|
|
258
|
+
final int viewId;
|
|
259
|
+
|
|
260
|
+
CatalogItem(String key, String title, String subtitle, int viewId) {
|
|
261
|
+
this.key = key;
|
|
262
|
+
this.title = title;
|
|
263
|
+
this.subtitle = subtitle;
|
|
264
|
+
this.viewId = viewId;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private final CatalogItem[] catalogItems = new CatalogItem[] {
|
|
269
|
+
new CatalogItem("trail_lamp", "Trail Lamp", "Compact campsite light", R.id.catalog_item_trail_lamp),
|
|
270
|
+
new CatalogItem("river_bottle", "River Bottle", "Insulated hydration bottle", R.id.catalog_item_river_bottle),
|
|
271
|
+
new CatalogItem("north_ridge_pack", "North Ridge Pack", "Weatherproof day pack", R.id.catalog_item_north_ridge_pack),
|
|
272
|
+
new CatalogItem("summit_shell", "Summit Shell", "Lightweight rain layer", R.id.catalog_item_summit_shell),
|
|
273
|
+
new CatalogItem("basecamp_roll", "Basecamp Roll", "Modular storage roll", R.id.catalog_item_basecamp_roll),
|
|
274
|
+
new CatalogItem("maple_organizer", "Maple Organizer", "Cable and tool pouch", R.id.catalog_item_maple_organizer),
|
|
275
|
+
new CatalogItem("canyon_sling", "Canyon Sling", "Cross-body field bag", R.id.catalog_item_canyon_sling),
|
|
276
|
+
new CatalogItem("harbor_tote", "Harbor Tote", "Daily carry tote", R.id.catalog_item_harbor_tote),
|
|
277
|
+
new CatalogItem("studio_stand", "Studio Stand", "Fold-flat work stand", R.id.catalog_item_studio_stand)
|
|
278
|
+
};
|
|
225
279
|
|
|
226
280
|
@Override
|
|
227
281
|
protected void onCreate(Bundle savedInstanceState) {
|
|
228
282
|
super.onCreate(savedInstanceState);
|
|
229
283
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
284
|
+
root = new LinearLayout(this);
|
|
285
|
+
setContentView(root);
|
|
286
|
+
showWelcome();
|
|
287
|
+
|
|
288
|
+
Uri data = getIntent().getData();
|
|
289
|
+
if (data != null) {
|
|
290
|
+
setStatus("Deep link opened");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private void resetRoot() {
|
|
295
|
+
root.removeAllViews();
|
|
296
|
+
root.setOrientation(LinearLayout.VERTICAL);
|
|
297
|
+
root.setGravity(Gravity.CENTER_HORIZONTAL);
|
|
233
298
|
int padding = dp(24);
|
|
234
|
-
|
|
299
|
+
root.setPadding(padding, padding, padding, padding);
|
|
300
|
+
}
|
|
235
301
|
|
|
302
|
+
private TextView title(String text, int id) {
|
|
236
303
|
TextView title = new TextView(this);
|
|
237
|
-
title.setId(
|
|
238
|
-
title.setText(
|
|
304
|
+
title.setId(id);
|
|
305
|
+
title.setText(text);
|
|
239
306
|
title.setTextSize(24);
|
|
240
307
|
title.setTextColor(Color.rgb(17, 24, 39));
|
|
241
308
|
title.setGravity(Gravity.CENTER);
|
|
242
|
-
|
|
309
|
+
return title;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private EditText input(String hint, int id) {
|
|
313
|
+
EditText input = new EditText(this);
|
|
314
|
+
input.setId(id);
|
|
315
|
+
input.setHint(hint);
|
|
316
|
+
input.setSingleLine(true);
|
|
317
|
+
return input;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private Button button(String text, int id) {
|
|
321
|
+
Button button = new Button(this);
|
|
322
|
+
button.setId(id);
|
|
323
|
+
button.setText(text);
|
|
324
|
+
button.setAllCaps(false);
|
|
325
|
+
return button;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private void addStatusViews() {
|
|
329
|
+
demoStatus = new TextView(this);
|
|
330
|
+
demoStatus.setId(R.id.demo_status);
|
|
331
|
+
demoStatus.setText(currentStatus);
|
|
332
|
+
demoStatus.setTextSize(16);
|
|
333
|
+
demoStatus.setGravity(Gravity.CENTER);
|
|
334
|
+
root.addView(demoStatus, new LinearLayout.LayoutParams(-1, -2));
|
|
335
|
+
|
|
336
|
+
workflowStatus = new TextView(this);
|
|
337
|
+
workflowStatus.setId(R.id.workflow_status);
|
|
338
|
+
workflowStatus.setText(currentStatus);
|
|
339
|
+
workflowStatus.setTextSize(16);
|
|
340
|
+
workflowStatus.setGravity(Gravity.CENTER);
|
|
341
|
+
root.addView(workflowStatus, new LinearLayout.LayoutParams(-1, -2));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private void setStatus(String value) {
|
|
345
|
+
currentStatus = value;
|
|
346
|
+
if (demoStatus != null) {
|
|
347
|
+
demoStatus.setText(value);
|
|
348
|
+
}
|
|
349
|
+
if (workflowStatus != null) {
|
|
350
|
+
workflowStatus.setText(value);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private void showWelcome() {
|
|
355
|
+
resetRoot();
|
|
356
|
+
root.addView(title("ZMR Android Demo", R.id.demo_title), new LinearLayout.LayoutParams(-1, -2));
|
|
243
357
|
|
|
244
358
|
Button button = new Button(this);
|
|
245
359
|
button.setId(R.id.continue_button);
|
|
246
360
|
button.setText("Continue");
|
|
247
|
-
|
|
361
|
+
root.addView(button, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
362
|
+
addStatusViews();
|
|
248
363
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
364
|
+
button.setOnClickListener(new View.OnClickListener() {
|
|
365
|
+
@Override
|
|
366
|
+
public void onClick(View view) {
|
|
367
|
+
showProfile("Continue tapped");
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
254
371
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
status.setGravity(Gravity.CENTER);
|
|
260
|
-
layout.addView(status, new LinearLayout.LayoutParams(-1, -2));
|
|
372
|
+
private void showProfile(String statusText) {
|
|
373
|
+
currentStatus = statusText;
|
|
374
|
+
resetRoot();
|
|
375
|
+
root.addView(title("Profile", R.id.profile_title), new LinearLayout.LayoutParams(-1, -2));
|
|
261
376
|
|
|
262
|
-
|
|
377
|
+
final EditText quickInput = input("Type here", R.id.demo_input);
|
|
378
|
+
root.addView(quickInput, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
379
|
+
|
|
380
|
+
EditText profileName = input("Name", R.id.profile_name_input);
|
|
381
|
+
root.addView(profileName, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
382
|
+
|
|
383
|
+
EditText profileEmail = input("Email", R.id.profile_email_input);
|
|
384
|
+
root.addView(profileEmail, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
385
|
+
|
|
386
|
+
Button save = button("Save profile", R.id.save_profile_button);
|
|
387
|
+
root.addView(save, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
388
|
+
addStatusViews();
|
|
389
|
+
|
|
390
|
+
quickInput.requestFocus();
|
|
391
|
+
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
392
|
+
if (imm != null) {
|
|
393
|
+
imm.showSoftInput(quickInput, InputMethodManager.SHOW_IMPLICIT);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
save.setOnClickListener(new View.OnClickListener() {
|
|
263
397
|
@Override
|
|
264
398
|
public void onClick(View view) {
|
|
265
|
-
status.setText("Continue tapped");
|
|
266
|
-
input.requestFocus();
|
|
267
399
|
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
268
400
|
if (imm != null) {
|
|
269
|
-
imm.
|
|
401
|
+
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
|
270
402
|
}
|
|
403
|
+
showCatalog("Profile saved");
|
|
271
404
|
}
|
|
272
405
|
});
|
|
406
|
+
}
|
|
273
407
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
408
|
+
private void showCatalog(String statusText) {
|
|
409
|
+
currentStatus = statusText;
|
|
410
|
+
resetRoot();
|
|
411
|
+
root.addView(title("Catalog", R.id.catalog_title), new LinearLayout.LayoutParams(-1, -2));
|
|
412
|
+
|
|
413
|
+
ScrollView scrollView = new ScrollView(this);
|
|
414
|
+
scrollView.setId(R.id.catalog_list);
|
|
415
|
+
LinearLayout list = new LinearLayout(this);
|
|
416
|
+
list.setOrientation(LinearLayout.VERTICAL);
|
|
417
|
+
scrollView.addView(list, new ScrollView.LayoutParams(-1, -2));
|
|
418
|
+
|
|
419
|
+
for (final CatalogItem item : catalogItems) {
|
|
420
|
+
Button itemButton = button(item.title, item.viewId);
|
|
421
|
+
itemButton.setContentDescription(item.subtitle);
|
|
422
|
+
list.addView(itemButton, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
423
|
+
itemButton.setOnClickListener(new View.OnClickListener() {
|
|
424
|
+
@Override
|
|
425
|
+
public void onClick(View view) {
|
|
426
|
+
selectedItem = item;
|
|
427
|
+
showDetail("Selected " + item.title);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
277
430
|
}
|
|
278
431
|
|
|
279
|
-
|
|
432
|
+
root.addView(scrollView, new LinearLayout.LayoutParams(-1, 0, 1));
|
|
433
|
+
addStatusViews();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private void showDetail(String statusText) {
|
|
437
|
+
currentStatus = statusText;
|
|
438
|
+
resetRoot();
|
|
439
|
+
root.addView(title(selectedItem.title, R.id.detail_title), new LinearLayout.LayoutParams(-1, -2));
|
|
440
|
+
|
|
441
|
+
TextView subtitle = new TextView(this);
|
|
442
|
+
subtitle.setId(R.id.detail_subtitle);
|
|
443
|
+
subtitle.setText(selectedItem.subtitle);
|
|
444
|
+
subtitle.setTextSize(18);
|
|
445
|
+
subtitle.setGravity(Gravity.CENTER);
|
|
446
|
+
root.addView(subtitle, new LinearLayout.LayoutParams(-1, -2));
|
|
447
|
+
|
|
448
|
+
Button save = button("Save item", R.id.detail_save_button);
|
|
449
|
+
root.addView(save, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
450
|
+
|
|
451
|
+
Button review = button("Review order", R.id.review_button);
|
|
452
|
+
root.addView(review, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
453
|
+
addStatusViews();
|
|
454
|
+
|
|
455
|
+
save.setOnClickListener(new View.OnClickListener() {
|
|
456
|
+
@Override
|
|
457
|
+
public void onClick(View view) {
|
|
458
|
+
setStatus("Saved " + selectedItem.title);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
review.setOnClickListener(new View.OnClickListener() {
|
|
463
|
+
@Override
|
|
464
|
+
public void onClick(View view) {
|
|
465
|
+
showReview();
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private void showReview() {
|
|
471
|
+
currentStatus = "Workflow complete";
|
|
472
|
+
resetRoot();
|
|
473
|
+
root.addView(title("Review", R.id.review_title), new LinearLayout.LayoutParams(-1, -2));
|
|
474
|
+
|
|
475
|
+
TextView complete = new TextView(this);
|
|
476
|
+
complete.setId(R.id.review_complete);
|
|
477
|
+
complete.setText("Workflow complete");
|
|
478
|
+
complete.setTextSize(20);
|
|
479
|
+
complete.setGravity(Gravity.CENTER);
|
|
480
|
+
root.addView(complete, new LinearLayout.LayoutParams(-1, -2));
|
|
481
|
+
|
|
482
|
+
TextView item = new TextView(this);
|
|
483
|
+
item.setId(R.id.review_item);
|
|
484
|
+
item.setText(selectedItem.title);
|
|
485
|
+
item.setTextSize(18);
|
|
486
|
+
item.setGravity(Gravity.CENTER);
|
|
487
|
+
root.addView(item, new LinearLayout.LayoutParams(-1, -2));
|
|
488
|
+
|
|
489
|
+
addStatusViews();
|
|
280
490
|
}
|
|
281
491
|
|
|
282
492
|
private int dp(int value) {
|
|
@@ -291,6 +501,7 @@ write_file "$OUT/.zmr/android-smoke.json" "$(cat <<EOF
|
|
|
291
501
|
"name": "ZMR Android demo smoke",
|
|
292
502
|
"appId": "$APP_ID",
|
|
293
503
|
"steps": [
|
|
504
|
+
{ "action": "clearState" },
|
|
294
505
|
{ "action": "launch" },
|
|
295
506
|
{ "action": "waitVisible", "selector": { "text": "ZMR Android Demo" }, "timeoutMs": 30000 },
|
|
296
507
|
{ "action": "tap", "selector": { "resourceId": "$APP_ID:id/continue_button" } },
|
|
@@ -302,6 +513,90 @@ write_file "$OUT/.zmr/android-smoke.json" "$(cat <<EOF
|
|
|
302
513
|
EOF
|
|
303
514
|
)"
|
|
304
515
|
|
|
516
|
+
write_file "$OUT/.zmr/android-workflow.json" "$(cat <<EOF
|
|
517
|
+
{
|
|
518
|
+
"name": "ZMR Android workflow demo",
|
|
519
|
+
"appId": "$APP_ID",
|
|
520
|
+
"steps": [
|
|
521
|
+
{ "action": "stop" },
|
|
522
|
+
{ "action": "clearState" },
|
|
523
|
+
{ "action": "launch" },
|
|
524
|
+
{
|
|
525
|
+
"action": "waitVisible",
|
|
526
|
+
"selector": { "text": "ZMR Android Demo" },
|
|
527
|
+
"timeoutMs": 30000
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"action": "tap",
|
|
531
|
+
"selector": { "resourceId": "$APP_ID:id/continue_button" }
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"action": "waitVisible",
|
|
535
|
+
"selector": { "text": "Profile" },
|
|
536
|
+
"timeoutMs": 10000
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
"action": "typeText",
|
|
540
|
+
"selector": { "resourceId": "$APP_ID:id/profile_name_input" },
|
|
541
|
+
"text": "Riley"
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
"action": "typeText",
|
|
545
|
+
"selector": { "resourceId": "$APP_ID:id/profile_email_input" },
|
|
546
|
+
"text": "riley@example.test"
|
|
547
|
+
},
|
|
548
|
+
{ "action": "hideKeyboard" },
|
|
549
|
+
{
|
|
550
|
+
"action": "tap",
|
|
551
|
+
"selector": { "resourceId": "$APP_ID:id/save_profile_button" }
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
"action": "waitVisible",
|
|
555
|
+
"selector": { "text": "Catalog" },
|
|
556
|
+
"timeoutMs": 10000
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
"action": "scrollUntilVisible",
|
|
560
|
+
"selector": { "resourceId": "$APP_ID:id/catalog_item_north_ridge_pack" },
|
|
561
|
+
"direction": "down",
|
|
562
|
+
"timeoutMs": 10000
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
"action": "tap",
|
|
566
|
+
"selector": { "resourceId": "$APP_ID:id/catalog_item_north_ridge_pack" }
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
"action": "waitVisible",
|
|
570
|
+
"selector": { "text": "North Ridge Pack" },
|
|
571
|
+
"timeoutMs": 10000
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
"action": "tap",
|
|
575
|
+
"selector": { "resourceId": "$APP_ID:id/detail_save_button" }
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
"action": "waitVisible",
|
|
579
|
+
"selector": { "text": "Saved North Ridge Pack" },
|
|
580
|
+
"timeoutMs": 10000
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
"action": "tap",
|
|
584
|
+
"selector": { "resourceId": "$APP_ID:id/review_button" }
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
"action": "assertVisible",
|
|
588
|
+
"selector": {
|
|
589
|
+
"resourceId": "$APP_ID:id/workflow_status",
|
|
590
|
+
"text": "Workflow complete"
|
|
591
|
+
},
|
|
592
|
+
"timeoutMs": 10000
|
|
593
|
+
},
|
|
594
|
+
{ "action": "snapshot" }
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
EOF
|
|
598
|
+
)"
|
|
599
|
+
|
|
305
600
|
run "$AAPT2" compile --dir "$RES_DIR" -o "$COMPILED_RES"
|
|
306
601
|
run "$AAPT2" link -o "$UNSIGNED_APK" -I "$ANDROID_JAR" --manifest "$ANDROID_DIR/AndroidManifest.xml" -R "$COMPILED_RES" --java "$GEN_DIR" --custom-package dev.zmr.demo --auto-add-overlay
|
|
307
602
|
run javac -source 1.8 -target 1.8 -bootclasspath "$ANDROID_JAR" -d "$CLASSES_DIR" "$GEN_DIR/dev/zmr/demo/R.java" "$SRC_DIR/MainActivity.java"
|