@aarwitz/tapp 0.15.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/AGENTS.md +123 -0
- package/Harness/OCQAHarness/AppDelegate.swift +21 -0
- package/Harness/OCQAHarness/Info.plist +26 -0
- package/Harness/OCQAHarness.xcodeproj/project.pbxproj +199 -0
- package/Harness/OCQAHarness.xcodeproj/xcshareddata/xcschemes/OCQAHarnessUITests.xcscheme +22 -0
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +4526 -0
- package/Harness/OCQAHarnessUITests/Info.plist +22 -0
- package/Harness/generate-harness-xcodeproj.rb +254 -0
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/bin/tapp.js +1382 -0
- package/browser/app.css +227 -0
- package/browser/app.js +675 -0
- package/browser/index.html +195 -0
- package/browser/product-contract.js +25 -0
- package/browser/view-model.js +16 -0
- package/docs/BROWSER-PRODUCT.md +72 -0
- package/docs/PRODUCT-ENGINE.md +102 -0
- package/docs/application-model.md +276 -0
- package/docs/scenarios.md +95 -0
- package/mcp-server/src/android-driver.js +287 -0
- package/mcp-server/src/android-explorer.js +197 -0
- package/mcp-server/src/android-flow.js +89 -0
- package/mcp-server/src/application-model.js +1597 -0
- package/mcp-server/src/browser-product.js +659 -0
- package/mcp-server/src/browser-workspaces.js +234 -0
- package/mcp-server/src/ci-report.js +557 -0
- package/mcp-server/src/ci-setup.js +359 -0
- package/mcp-server/src/contract-authoring.js +10 -0
- package/mcp-server/src/enrich.js +57 -0
- package/mcp-server/src/flow-runtime.js +127 -0
- package/mcp-server/src/html-report.js +124 -0
- package/mcp-server/src/index.js +3775 -0
- package/mcp-server/src/maintenance-proposal.js +178 -0
- package/mcp-server/src/managed-operation.js +61 -0
- package/mcp-server/src/pr-selection.js +841 -0
- package/mcp-server/src/product-execution.js +155 -0
- package/mcp-server/src/product-operations.js +526 -0
- package/mcp-server/src/project-config.js +101 -0
- package/mcp-server/src/release-contract.d.ts +81 -0
- package/mcp-server/src/release-contract.js +226 -0
- package/mcp-server/src/report.js +363 -0
- package/mcp-server/src/scenario-runtime.js +139 -0
- package/mcp-server/src/static-server.js +44 -0
- package/mcp-server/src/task-runtime.js +266 -0
- package/mcp-server/src/ui-map.js +661 -0
- package/mcp-server/src/web-explorer.js +493 -0
- package/mcp-server/src/web-flow.js +238 -0
- package/package.json +82 -0
- package/scripts/android-corpus-e2e.sh +30 -0
- package/scripts/ci-gate.sh +323 -0
- package/scripts/cleanup-xcode.sh +157 -0
- package/scripts/compile-contract.js +27 -0
- package/scripts/compile-flow.js +18 -0
- package/scripts/corpus-apps.txt +9 -0
- package/scripts/corpus-sweep.sh +121 -0
- package/scripts/coverage-eval.sh +92 -0
- package/scripts/coverage_eval_parse.py +95 -0
- package/scripts/deploy-and-build.sh +99 -0
- package/scripts/flow-platform.js +18 -0
- package/scripts/flow_ai_judge.py +102 -0
- package/scripts/flow_lib.py +154 -0
- package/scripts/mutation-recall-desktop.sh +186 -0
- package/scripts/mutation-recall.sh +121 -0
- package/scripts/mutation_lib.py +128 -0
- package/scripts/mutation_operators.py +144 -0
- package/scripts/platform-gate.js +186 -0
- package/scripts/pr-plan.js +68 -0
- package/scripts/quick-capture.sh +419 -0
- package/scripts/run-android-flow.js +27 -0
- package/scripts/run-flow.sh +90 -0
- package/scripts/run-web-flow.js +28 -0
- package/scripts/run-web-scenario.js +23 -0
- package/scripts/validation-matrix.sh +146 -0
- package/scripts/vision-fp-eval.sh +206 -0
- package/scripts/vision_escalation_responder.py +147 -0
- package/scripts/vision_fp_probe.py +221 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Deterministic committed Flow replay for browser apps. Playwright is only the
|
|
2
|
+
// driver; Tapp owns the shared Flow semantics and OCQA evidence protocol.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { FlowLog, flowVariables, normalizeFlowStep, substituteFlowValue } from "./flow-runtime.js";
|
|
6
|
+
import { loadPlaywright } from "./web-explorer.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT = 6000;
|
|
9
|
+
|
|
10
|
+
async function firstVisible(candidates) {
|
|
11
|
+
for (const locator of candidates) {
|
|
12
|
+
try {
|
|
13
|
+
const first = locator.first();
|
|
14
|
+
if ((await first.count()) > 0 && (await first.isVisible())) return first;
|
|
15
|
+
} catch { /* try the next semantic locator */ }
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cssId(value) {
|
|
21
|
+
return "#" + String(value).replace(/([^a-zA-Z0-9_-])/g, "\\$1");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function locateWebElement(page, target) {
|
|
25
|
+
const exact = { exact: true };
|
|
26
|
+
const candidates = [
|
|
27
|
+
page.getByTestId(target),
|
|
28
|
+
page.locator(cssId(target)),
|
|
29
|
+
page.getByLabel(target, exact),
|
|
30
|
+
page.getByRole("button", { name: target, exact: true }),
|
|
31
|
+
page.getByRole("link", { name: target, exact: true }),
|
|
32
|
+
page.getByText(target, exact),
|
|
33
|
+
page.getByLabel(target),
|
|
34
|
+
page.getByText(target),
|
|
35
|
+
];
|
|
36
|
+
return firstVisible(candidates);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function screenCandidates(page) {
|
|
40
|
+
const values = [];
|
|
41
|
+
try { values.push(await page.title()); } catch {}
|
|
42
|
+
try {
|
|
43
|
+
const h1 = page.getByRole("heading", { level: 1 }).first();
|
|
44
|
+
if ((await h1.count()) > 0 && (await h1.isVisible())) values.push(await h1.innerText());
|
|
45
|
+
} catch {}
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(page.url());
|
|
48
|
+
values.push(url.pathname, url.pathname.split("/").filter(Boolean).at(-1) || "Home");
|
|
49
|
+
} catch {}
|
|
50
|
+
return values.map((v) => String(v || "").trim()).filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function waitForScreen(page, target, timeout) {
|
|
54
|
+
const deadline = Date.now() + timeout;
|
|
55
|
+
const wanted = target.toLowerCase();
|
|
56
|
+
while (Date.now() < deadline) {
|
|
57
|
+
if ((await screenCandidates(page)).some((v) => v.toLowerCase() === wanted)) return true;
|
|
58
|
+
const el = await locateWebElement(page, target);
|
|
59
|
+
if (el) return true;
|
|
60
|
+
await page.waitForTimeout(150);
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function waitForExactScreen(page, target, timeout) {
|
|
66
|
+
const deadline = Date.now() + timeout;
|
|
67
|
+
const wanted = target.toLowerCase();
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
if ((await screenCandidates(page)).some((v) => v.toLowerCase() === wanted)) return true;
|
|
70
|
+
await page.waitForTimeout(150);
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function waitForElement(page, target, timeout) {
|
|
76
|
+
const deadline = Date.now() + timeout;
|
|
77
|
+
while (Date.now() < deadline) {
|
|
78
|
+
const element = await locateWebElement(page, target);
|
|
79
|
+
if (element) return element;
|
|
80
|
+
await page.waitForTimeout(150);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function settle(page) {
|
|
86
|
+
try { await page.waitForLoadState("domcontentloaded", { timeout: 3000 }); } catch {}
|
|
87
|
+
await page.waitForTimeout(150);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function substituteStructure(value, vars) {
|
|
91
|
+
if (typeof value === "string") return substituteFlowValue(value, vars);
|
|
92
|
+
if (Array.isArray(value)) return value.map((item) => substituteStructure(item, vars));
|
|
93
|
+
if (value && typeof value === "object") {
|
|
94
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, substituteStructure(item, vars)]));
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function runWebRequestStep({ step, startUrl, vars = {}, timeout = DEFAULT_TIMEOUT }) {
|
|
100
|
+
const request = step?.request;
|
|
101
|
+
if (!request || typeof request !== "object") throw new Error("request lifecycle step is invalid");
|
|
102
|
+
const targetUrl = new URL(substituteFlowValue(request.path, vars), startUrl);
|
|
103
|
+
const origin = new URL(startUrl).origin;
|
|
104
|
+
if (targetUrl.origin !== origin) throw new Error(`request must stay on the target origin (${origin})`);
|
|
105
|
+
const method = String(request.method || "POST").toUpperCase();
|
|
106
|
+
const headers = substituteStructure(request.headers || {}, vars);
|
|
107
|
+
const init = { method, headers, signal: AbortSignal.timeout(Number(request.timeoutMs) || timeout) };
|
|
108
|
+
if (request.body !== undefined) {
|
|
109
|
+
init.body = JSON.stringify(substituteStructure(request.body, vars));
|
|
110
|
+
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) headers["content-type"] = "application/json";
|
|
111
|
+
}
|
|
112
|
+
const response = await fetch(targetUrl, init);
|
|
113
|
+
const expected = Number(request.status ?? 200);
|
|
114
|
+
if (response.status !== expected) throw new Error(`${method} ${targetUrl.pathname} returned ${response.status}; expected ${expected}`);
|
|
115
|
+
return { action: "request", target: `${method} ${targetUrl.pathname}`, status: "pass", detail: "" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function executeWebFlowStep({ page, step, vars = {}, defaultTimeout = DEFAULT_TIMEOUT }) {
|
|
119
|
+
const raw = normalizeFlowStep(step);
|
|
120
|
+
const action = raw.action;
|
|
121
|
+
const target = substituteFlowValue(raw.target, vars);
|
|
122
|
+
const value = substituteFlowValue(raw.value, vars);
|
|
123
|
+
const timeout = Number(raw.params.timeoutMs) || defaultTimeout;
|
|
124
|
+
let status = "pass";
|
|
125
|
+
let detail = "";
|
|
126
|
+
try {
|
|
127
|
+
if (action === "tap") {
|
|
128
|
+
const el = await locateWebElement(page, target);
|
|
129
|
+
if (!el) throw new Error(`could not find ‘${target}’`);
|
|
130
|
+
await el.click();
|
|
131
|
+
} else if (action === "type") {
|
|
132
|
+
const el = await locateWebElement(page, target);
|
|
133
|
+
if (!el) throw new Error(`no field ‘${target}’ to type into`);
|
|
134
|
+
await el.fill(value);
|
|
135
|
+
} else if (action === "swipe") {
|
|
136
|
+
const dy = target.toLowerCase() === "down" ? -600 : 600;
|
|
137
|
+
await page.evaluate((y) => window.scrollBy({ top: y, behavior: "instant" }), dy);
|
|
138
|
+
} else if (action === "back") {
|
|
139
|
+
await page.goBack({ waitUntil: "domcontentloaded" });
|
|
140
|
+
} else if (action === "wait_for") {
|
|
141
|
+
if (!await waitForScreen(page, target, timeout)) throw new Error(`‘${target}’ never appeared within ${timeout}ms`);
|
|
142
|
+
} else if (action === "wait") {
|
|
143
|
+
await page.waitForTimeout(timeout);
|
|
144
|
+
} else if (action === "assert_screen") {
|
|
145
|
+
const wanted = value || target;
|
|
146
|
+
if (!await waitForExactScreen(page, wanted, timeout)) {
|
|
147
|
+
const candidates = await screenCandidates(page);
|
|
148
|
+
throw new Error(`expected screen ‘${wanted}’, saw ‘${candidates[0] || page.url()}’`);
|
|
149
|
+
}
|
|
150
|
+
} else if (action === "assert_exists") {
|
|
151
|
+
if (!await waitForElement(page, target, timeout)) throw new Error(`‘${target}’ not found`);
|
|
152
|
+
} else if (action === "assert_absent") {
|
|
153
|
+
if (await locateWebElement(page, target)) throw new Error(`‘${target}’ was present but should be absent`);
|
|
154
|
+
} else if (action === "assert_text") {
|
|
155
|
+
const of = substituteFlowValue(raw.params.of || target, vars);
|
|
156
|
+
const needle = substituteFlowValue(raw.params.contains || value, vars);
|
|
157
|
+
const deadline = Date.now() + timeout;
|
|
158
|
+
let matched = false;
|
|
159
|
+
while (Date.now() < deadline && !matched) {
|
|
160
|
+
const el = await locateWebElement(page, of);
|
|
161
|
+
matched = !!el && (await el.innerText()).toLowerCase().includes(needle.toLowerCase());
|
|
162
|
+
if (!matched) await page.waitForTimeout(150);
|
|
163
|
+
}
|
|
164
|
+
if (!matched) throw new Error(`‘${of}’ did not contain ‘${needle}’`);
|
|
165
|
+
} else if (action === "assert_ai") {
|
|
166
|
+
status = "skip";
|
|
167
|
+
detail = "AI assertions are advisory and are not part of deterministic browser replay";
|
|
168
|
+
} else {
|
|
169
|
+
throw new Error(`unknown action ‘${action}’`);
|
|
170
|
+
}
|
|
171
|
+
await settle(page);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
status = "fail";
|
|
174
|
+
detail = error.message || String(error);
|
|
175
|
+
}
|
|
176
|
+
return { action, target: target || value, status, detail, task: raw.task };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function runWebFlow({ flow, url, logPath, screenshotDir, playwright }) {
|
|
180
|
+
const startUrl = url || flow.url || (/^https?:\/\//i.test(flow.app || "") ? flow.app : "");
|
|
181
|
+
if (!startUrl) throw new Error("Web Flow needs `url:` (or an http(s) `app:` value)");
|
|
182
|
+
if (logPath) fs.rmSync(logPath, { force: true });
|
|
183
|
+
const setup = flow.setup || [];
|
|
184
|
+
const teardown = flow.teardown || [];
|
|
185
|
+
const loggedFlow = { ...flow, steps: [...setup, ...flow.steps, ...teardown] };
|
|
186
|
+
const log = new FlowLog({ logPath, flow: loggedFlow });
|
|
187
|
+
const pw = playwright || await loadPlaywright();
|
|
188
|
+
const browser = await pw.chromium.launch({ headless: true });
|
|
189
|
+
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
190
|
+
const page = await context.newPage();
|
|
191
|
+
const timeout = Number(flow.timeoutMs) || DEFAULT_TIMEOUT;
|
|
192
|
+
page.setDefaultTimeout(timeout);
|
|
193
|
+
const vars = flowVariables(flow);
|
|
194
|
+
let index = 0;
|
|
195
|
+
let failed = false;
|
|
196
|
+
|
|
197
|
+
const emit = (outcome) => {
|
|
198
|
+
index += 1;
|
|
199
|
+
log.step({ index, ...outcome });
|
|
200
|
+
if (outcome.status === "fail") failed = true;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const requestPhase = async (steps) => {
|
|
204
|
+
for (const step of steps) {
|
|
205
|
+
try {
|
|
206
|
+
emit(await runWebRequestStep({ step, startUrl, vars, timeout }));
|
|
207
|
+
} catch (error) {
|
|
208
|
+
emit({ action: "request", target: String(step?.request?.path || "request"), status: "fail", detail: error.message || String(error) });
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await requestPhase(setup);
|
|
216
|
+
if (!failed) {
|
|
217
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
218
|
+
await settle(page);
|
|
219
|
+
for (const step of flow.steps) {
|
|
220
|
+
const outcome = await executeWebFlowStep({ page, step, vars, defaultTimeout: timeout });
|
|
221
|
+
emit(outcome);
|
|
222
|
+
if (outcome.status === "fail" && screenshotDir) {
|
|
223
|
+
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
224
|
+
await page.screenshot({ path: path.join(screenshotDir, `flow-failure-${index}.png`), fullPage: true }).catch(() => {});
|
|
225
|
+
}
|
|
226
|
+
if (outcome.status === "fail" && !flow.continueOnFailure) break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (screenshotDir) {
|
|
230
|
+
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
231
|
+
await page.screenshot({ path: path.join(screenshotDir, "flow-final.png"), fullPage: true }).catch(() => {});
|
|
232
|
+
}
|
|
233
|
+
} finally {
|
|
234
|
+
await requestPhase(teardown);
|
|
235
|
+
await browser.close().catch(() => {});
|
|
236
|
+
}
|
|
237
|
+
return log.finish();
|
|
238
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aarwitz/tapp",
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"mcpName": "io.github.aarwitz/tapp",
|
|
5
|
+
"description": "Release contracts, autonomous QA, and evidence-backed CI gates for iOS, Android, and web.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"tapp": "bin/tapp.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./mcp-server/src/index.js",
|
|
13
|
+
"./product-operations": "./mcp-server/src/product-operations.js",
|
|
14
|
+
"./contracts": {
|
|
15
|
+
"types": "./mcp-server/src/release-contract.d.ts",
|
|
16
|
+
"import": "./mcp-server/src/contract-authoring.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"browser/",
|
|
22
|
+
"docs/BROWSER-PRODUCT.md",
|
|
23
|
+
"docs/PRODUCT-ENGINE.md",
|
|
24
|
+
"docs/application-model.md",
|
|
25
|
+
"docs/scenarios.md",
|
|
26
|
+
"mcp-server/src/",
|
|
27
|
+
"scripts/",
|
|
28
|
+
"Harness/OCQAHarness/",
|
|
29
|
+
"Harness/OCQAHarnessUITests/",
|
|
30
|
+
"Harness/OCQAHarness.xcodeproj/",
|
|
31
|
+
"Harness/generate-harness-xcodeproj.rb",
|
|
32
|
+
"AGENTS.md"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
36
|
+
"typescript": "^5.9.2"
|
|
37
|
+
},
|
|
38
|
+
"optionalDependencies": {
|
|
39
|
+
"playwright": "1.61.1"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/aarwitz/tapp.git"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/aarwitz/tapp#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/aarwitz/tapp/issues"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"tapp",
|
|
57
|
+
"cli",
|
|
58
|
+
"ios",
|
|
59
|
+
"android",
|
|
60
|
+
"simulator",
|
|
61
|
+
"mcp",
|
|
62
|
+
"mcp-server",
|
|
63
|
+
"xcuitest",
|
|
64
|
+
"e2e",
|
|
65
|
+
"testing",
|
|
66
|
+
"qa",
|
|
67
|
+
"playwright",
|
|
68
|
+
"release-contracts",
|
|
69
|
+
"release-gate",
|
|
70
|
+
"agent",
|
|
71
|
+
"claude",
|
|
72
|
+
"cursor",
|
|
73
|
+
"copilot",
|
|
74
|
+
"automation",
|
|
75
|
+
"mobile"
|
|
76
|
+
],
|
|
77
|
+
"scripts": {
|
|
78
|
+
"test": "node --test tests/report.test.js tests/regression.test.js tests/engine.test.js tests/project-config.test.js tests/application-model.test.js tests/ui-map.test.js tests/task-runtime.test.js tests/release-contract.test.js tests/pr-selection.test.js tests/flow-runtime.test.js tests/web-explorer.test.js tests/web-session.test.js tests/web-flow.test.js tests/scenario-runtime.test.js tests/android-driver.test.js tests/android-explorer.test.js tests/android-flow.test.js tests/managed-web.test.js tests/product-operations.test.js tests/browser-product.test.js tests/browser-onboarding.test.js tests/managed-operation.test.js tests/cloud-runner.test.js tests/ci-setup.test.js tests/ci-install.test.js tests/cli.test.js tests/action.test.js tests/ci-gate.test.js tests/ci-report.test.js tests/desktop-protocol.test.js tests/ios-flow-protocol.test.js",
|
|
79
|
+
"test:browser-journey": "node --test tests/browser-journey.test.js",
|
|
80
|
+
"test:browser-native": "TAPP_RUN_NATIVE_BROWSER=1 node --test tests/browser-native-journey.test.js"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build and exercise every native Android fixture through the same release gate
|
|
3
|
+
# customers use. A connected, booted emulator/device is required.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
+
CORPUS="$ROOT/AndroidCorpus"
|
|
8
|
+
SERIAL="${1:-${ANDROID_SERIAL:-}}"
|
|
9
|
+
SERIAL_ARGS=()
|
|
10
|
+
[[ -n "$SERIAL" ]] && SERIAL_ARGS=(--serial "$SERIAL")
|
|
11
|
+
|
|
12
|
+
"$CORPUS/gradlew" -p "$CORPUS" \
|
|
13
|
+
:demoapp:assembleDebug :logindemo:assembleDebug :shopdemo:assembleDebug --no-daemon --quiet
|
|
14
|
+
|
|
15
|
+
run_fixture() {
|
|
16
|
+
local module="$1" app_id="$2" flow="$3"
|
|
17
|
+
local apk="$CORPUS/$module/build/outputs/apk/debug/$module-debug.apk"
|
|
18
|
+
local output_dir="${RUNNER_TEMP:-/tmp}/tapp-android-corpus-$module"
|
|
19
|
+
mkdir -p "$output_dir"
|
|
20
|
+
echo "Testing Android corpus fixture: $module ($app_id)"
|
|
21
|
+
"$ROOT/scripts/ci-gate.sh" --platform android --apk "$apk" --app-id "$app_id" \
|
|
22
|
+
"${SERIAL_ARGS[@]}" --actions 12 --timeout 180 --flows "$CORPUS/$module/.autotap/flows/$flow" \
|
|
23
|
+
--project-dir "$CORPUS/$module" --json-out "$output_dir/report.json" --md-out "$output_dir/report.md"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
run_fixture demoapp io.tapp.corpus.demo smoke.yml
|
|
27
|
+
run_fixture logindemo io.tapp.corpus.login sign-in.yml
|
|
28
|
+
run_fixture shopdemo io.tapp.corpus.shop checkout.yml
|
|
29
|
+
|
|
30
|
+
echo "All Android corpus gates passed."
|