prowl-tools 0.1.5 → 0.1.6
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/README.md +251 -9
- package/dist/{chunk-2KD2XCTH.js → chunk-5KQR3IR3.js} +1182 -234
- package/dist/chunk-5KQR3IR3.js.map +1 -0
- package/dist/{chunk-ITOSUJCN.js → chunk-WHAMB4TY.js} +5 -1
- package/dist/chunk-WHAMB4TY.js.map +1 -0
- package/dist/index.cjs +1152 -204
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +173 -10
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +1220 -232
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +540 -53
- package/dist/lib.d.ts +540 -53
- package/dist/lib.js +74 -6
- package/dist/{loader-FCXPARP7.js → loader-PBBYV3U7.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-2KD2XCTH.js.map +0 -1
- package/dist/chunk-ITOSUJCN.js.map +0 -1
- /package/dist/{loader-FCXPARP7.js.map → loader-PBBYV3U7.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -59,7 +59,7 @@ var init_hunt_name = __esm({
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
// src/config/schema.ts
|
|
62
|
-
var import_zod, webTargetSchema, macosTargetSchema, androidTargetSchema, iosTargetSchema, targetSchema, configSchema, navigateStepSchema, clickStepSchema, singleKeyValueSchema, fillStepSchema, typeStepSchema, pressStepSchema, waitForSelectorStepSchema, waitStepSchema, waitForUrlStepSchema, waitForNetworkIdleStepSchema, selectOptionStepSchema, selectStepSchema, onDialogStepSchema, setInputFilesStepSchema, inlineAssertStepSchema, runHuntStepSchema, hoverStepSchema, scrollStepSchema, scrollToStepSchema, screenshotStepSchema, ifStepSchema, repeatStepSchema, mockRouteStepSchema, unmockRouteStepSchema, evalScriptStepSchema, runScriptStepSchema, assertScreenshotStepSchema, copyTextStepSchema, waitForDownloadStepSchema, stepSchema, assertionSchema, huntSchema;
|
|
62
|
+
var import_zod, webTargetSchema, macosTargetSchema, androidTargetSchema, iosTargetSchema, targetSchema, configSchema, navigateStepSchema, clickStepSchema, singleKeyValueSchema, fillStepSchema, typeStepSchema, pressStepSchema, waitForSelectorStepSchema, waitStepSchema, waitForUrlStepSchema, waitForNetworkIdleStepSchema, selectOptionStepSchema, selectStepSchema, onDialogStepSchema, setInputFilesStepSchema, inlineAssertStepSchema, runHuntStepSchema, hoverStepSchema, scrollStepSchema, scrollToStepSchema, screenshotStepSchema, ifStepSchema, repeatStepSchema, mockRouteStepSchema, unmockRouteStepSchema, evalScriptStepSchema, runScriptStepSchema, assertScreenshotStepSchema, assertWithAiStepSchema, copyTextStepSchema, waitForDownloadStepSchema, stepSchema, assertionSchema, huntSchema;
|
|
63
63
|
var init_schema = __esm({
|
|
64
64
|
"src/config/schema.ts"() {
|
|
65
65
|
"use strict";
|
|
@@ -311,6 +311,9 @@ var init_schema = __esm({
|
|
|
311
311
|
threshold: import_zod.z.number().min(0).max(1).optional()
|
|
312
312
|
}).strict()
|
|
313
313
|
}).strict();
|
|
314
|
+
assertWithAiStepSchema = import_zod.z.object({
|
|
315
|
+
assertWithAI: import_zod.z.string().min(1)
|
|
316
|
+
}).strict();
|
|
314
317
|
copyTextStepSchema = import_zod.z.object({
|
|
315
318
|
copyText: import_zod.z.object({
|
|
316
319
|
selector: import_zod.z.string().min(1),
|
|
@@ -353,6 +356,7 @@ var init_schema = __esm({
|
|
|
353
356
|
evalScriptStepSchema,
|
|
354
357
|
runScriptStepSchema,
|
|
355
358
|
assertScreenshotStepSchema,
|
|
359
|
+
assertWithAiStepSchema,
|
|
356
360
|
copyTextStepSchema,
|
|
357
361
|
waitForDownloadStepSchema
|
|
358
362
|
]);
|
|
@@ -716,7 +720,7 @@ var import_commander13 = require("commander");
|
|
|
716
720
|
// package.json
|
|
717
721
|
var package_default = {
|
|
718
722
|
name: "prowl-tools",
|
|
719
|
-
version: "0.1.
|
|
723
|
+
version: "0.1.6",
|
|
720
724
|
description: "CLI-first QA testing tool for deterministic Playwright flows.",
|
|
721
725
|
type: "module",
|
|
722
726
|
license: "Apache-2.0",
|
|
@@ -1105,6 +1109,11 @@ function interpolateStep(step, vars, stepPath2, redacted) {
|
|
|
1105
1109
|
}
|
|
1106
1110
|
};
|
|
1107
1111
|
}
|
|
1112
|
+
if ("assertWithAI" in step) {
|
|
1113
|
+
return {
|
|
1114
|
+
assertWithAI: interpolateString(step.assertWithAI, vars).value
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1108
1117
|
if ("copyText" in step) {
|
|
1109
1118
|
return {
|
|
1110
1119
|
copyText: {
|
|
@@ -1987,6 +1996,213 @@ var import_node_path5 = __toESM(require("path"), 1);
|
|
|
1987
1996
|
|
|
1988
1997
|
// src/browser/android-driver.ts
|
|
1989
1998
|
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
1999
|
+
|
|
2000
|
+
// src/analyzer/xml.ts
|
|
2001
|
+
var ENTITIES = {
|
|
2002
|
+
amp: "&",
|
|
2003
|
+
lt: "<",
|
|
2004
|
+
gt: ">",
|
|
2005
|
+
quot: '"',
|
|
2006
|
+
apos: "'"
|
|
2007
|
+
};
|
|
2008
|
+
function decodeXmlEntities(value) {
|
|
2009
|
+
if (!value.includes("&")) {
|
|
2010
|
+
return value;
|
|
2011
|
+
}
|
|
2012
|
+
return value.replace(/&(#(?:[xX][0-9a-fA-F]+|[0-9]+)|[a-zA-Z]+);/g, (match, code) => {
|
|
2013
|
+
if (code[0] === "#") {
|
|
2014
|
+
const hex = code[1] === "x" || code[1] === "X";
|
|
2015
|
+
const num2 = Number.parseInt(code.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
2016
|
+
return Number.isSafeInteger(num2) && num2 <= 1114111 ? String.fromCodePoint(num2) : match;
|
|
2017
|
+
}
|
|
2018
|
+
const named = ENTITIES[code];
|
|
2019
|
+
return named ?? match;
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
var ATTR_RE = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
2023
|
+
function parseTagBody(body) {
|
|
2024
|
+
const trimmed = body.trim();
|
|
2025
|
+
const nameMatch = /^([^\s/>]+)/.exec(trimmed);
|
|
2026
|
+
const tag = nameMatch ? nameMatch[1] : "";
|
|
2027
|
+
const attrs = {};
|
|
2028
|
+
const rest = trimmed.slice(tag.length);
|
|
2029
|
+
ATTR_RE.lastIndex = 0;
|
|
2030
|
+
let m;
|
|
2031
|
+
while ((m = ATTR_RE.exec(rest)) !== null) {
|
|
2032
|
+
const rawValue = m[3] !== void 0 ? m[3] : m[4] ?? "";
|
|
2033
|
+
attrs[m[1]] = decodeXmlEntities(rawValue);
|
|
2034
|
+
}
|
|
2035
|
+
return { tag, attrs };
|
|
2036
|
+
}
|
|
2037
|
+
function parseXml(input) {
|
|
2038
|
+
const n = input.length;
|
|
2039
|
+
const stack = [];
|
|
2040
|
+
let root = null;
|
|
2041
|
+
let i = 0;
|
|
2042
|
+
while (i < n) {
|
|
2043
|
+
const lt = input.indexOf("<", i);
|
|
2044
|
+
if (lt < 0) {
|
|
2045
|
+
break;
|
|
2046
|
+
}
|
|
2047
|
+
i = lt + 1;
|
|
2048
|
+
const ch = input[i];
|
|
2049
|
+
if (ch === "?") {
|
|
2050
|
+
const end = input.indexOf("?>", i);
|
|
2051
|
+
i = end < 0 ? n : end + 2;
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (ch === "!") {
|
|
2055
|
+
if (input.startsWith("!--", i)) {
|
|
2056
|
+
const end = input.indexOf("-->", i);
|
|
2057
|
+
i = end < 0 ? n : end + 3;
|
|
2058
|
+
} else {
|
|
2059
|
+
const end = input.indexOf(">", i);
|
|
2060
|
+
i = end < 0 ? n : end + 1;
|
|
2061
|
+
}
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
if (ch === "/") {
|
|
2065
|
+
const gt = input.indexOf(">", i);
|
|
2066
|
+
i = gt < 0 ? n : gt + 1;
|
|
2067
|
+
stack.pop();
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
let j = i;
|
|
2071
|
+
let quote2 = null;
|
|
2072
|
+
while (j < n) {
|
|
2073
|
+
const c = input[j];
|
|
2074
|
+
if (quote2 !== null) {
|
|
2075
|
+
if (c === quote2) {
|
|
2076
|
+
quote2 = null;
|
|
2077
|
+
}
|
|
2078
|
+
} else if (c === '"' || c === "'") {
|
|
2079
|
+
quote2 = c;
|
|
2080
|
+
} else if (c === ">") {
|
|
2081
|
+
break;
|
|
2082
|
+
}
|
|
2083
|
+
j += 1;
|
|
2084
|
+
}
|
|
2085
|
+
const inner = input.slice(i, j);
|
|
2086
|
+
i = j + 1;
|
|
2087
|
+
const selfClose = inner.endsWith("/");
|
|
2088
|
+
const body = selfClose ? inner.slice(0, -1) : inner;
|
|
2089
|
+
const { tag, attrs } = parseTagBody(body);
|
|
2090
|
+
if (tag.length === 0) {
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
const element = { tag, attrs, children: [] };
|
|
2094
|
+
const parent = stack[stack.length - 1];
|
|
2095
|
+
if (parent) {
|
|
2096
|
+
parent.children.push(element);
|
|
2097
|
+
}
|
|
2098
|
+
if (root === null) {
|
|
2099
|
+
root = element;
|
|
2100
|
+
}
|
|
2101
|
+
if (!selfClose) {
|
|
2102
|
+
stack.push(element);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
return root;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/selector/native.ts
|
|
2109
|
+
function unquoteSelectorValue(value) {
|
|
2110
|
+
const trimmed = value.trim();
|
|
2111
|
+
const first = trimmed[0];
|
|
2112
|
+
if ((first === '"' || first === "'") && trimmed.endsWith(first) && trimmed.length >= 2) {
|
|
2113
|
+
return trimmed.slice(1, -1);
|
|
2114
|
+
}
|
|
2115
|
+
return trimmed;
|
|
2116
|
+
}
|
|
2117
|
+
function quoteSelectorValue(value) {
|
|
2118
|
+
return `"${value}"`;
|
|
2119
|
+
}
|
|
2120
|
+
var ROLE_SELECTOR_RE = /^role=([A-Za-z][\w.$-]*)(?:\[name=(.+)\])?$/s;
|
|
2121
|
+
var NATIVE_SELECTOR_PREFIX_RE = /^(id|label|text|role)=/;
|
|
2122
|
+
function invalidSelectorMessage(selector, reason) {
|
|
2123
|
+
return `Invalid native selector ${JSON.stringify(selector)}: ${reason}.`;
|
|
2124
|
+
}
|
|
2125
|
+
function parseNativeSelector(selector) {
|
|
2126
|
+
const trimmed = selector.trim();
|
|
2127
|
+
if (trimmed.length === 0) {
|
|
2128
|
+
throw new Error(
|
|
2129
|
+
invalidSelectorMessage(
|
|
2130
|
+
selector,
|
|
2131
|
+
"selector is empty; use id=, label=, text=, role=, :focus, or a bare text value"
|
|
2132
|
+
)
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
if (trimmed === ":focus") {
|
|
2136
|
+
return { kind: "focused" };
|
|
2137
|
+
}
|
|
2138
|
+
const idMatch = /^id=(.+)$/s.exec(trimmed);
|
|
2139
|
+
if (idMatch) {
|
|
2140
|
+
return { kind: "id", value: unquoteSelectorValue(idMatch[1]) };
|
|
2141
|
+
}
|
|
2142
|
+
const roleMatch = ROLE_SELECTOR_RE.exec(trimmed);
|
|
2143
|
+
if (roleMatch) {
|
|
2144
|
+
const name = roleMatch[2] !== void 0 ? unquoteSelectorValue(roleMatch[2]) : void 0;
|
|
2145
|
+
return name !== void 0 && name.length > 0 ? { kind: "role", role: roleMatch[1], name } : { kind: "role", role: roleMatch[1] };
|
|
2146
|
+
}
|
|
2147
|
+
const labelMatch = /^label=(.+)$/s.exec(trimmed);
|
|
2148
|
+
if (labelMatch) {
|
|
2149
|
+
return { kind: "label", value: unquoteSelectorValue(labelMatch[1]) };
|
|
2150
|
+
}
|
|
2151
|
+
const textMatch = /^text=(.+)$/s.exec(trimmed);
|
|
2152
|
+
if (textMatch) {
|
|
2153
|
+
return { kind: "text", value: unquoteSelectorValue(textMatch[1]) };
|
|
2154
|
+
}
|
|
2155
|
+
const prefix = NATIVE_SELECTOR_PREFIX_RE.exec(trimmed);
|
|
2156
|
+
if (prefix) {
|
|
2157
|
+
throw new Error(
|
|
2158
|
+
invalidSelectorMessage(
|
|
2159
|
+
selector,
|
|
2160
|
+
`malformed ${prefix[1]}= selector; expected id=<value>, label=<value>, text=<value>, or role=<Type>[name=<value>]`
|
|
2161
|
+
)
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
return { kind: "text", value: trimmed };
|
|
2165
|
+
}
|
|
2166
|
+
function unwrapNativeTextSelector(selector) {
|
|
2167
|
+
const trimmed = selector.trim();
|
|
2168
|
+
if (!trimmed.startsWith("text=")) {
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
2171
|
+
return unquoteSelectorValue(trimmed.slice("text=".length));
|
|
2172
|
+
}
|
|
2173
|
+
function qualifyResourceId(value, appPackage) {
|
|
2174
|
+
if (value.includes(":") || !appPackage) {
|
|
2175
|
+
return value;
|
|
2176
|
+
}
|
|
2177
|
+
return `${appPackage}:id/${value}`;
|
|
2178
|
+
}
|
|
2179
|
+
function normalizeXcuiClassName(role) {
|
|
2180
|
+
return role.startsWith("XCUIElementType") ? role : `XCUIElementType${role}`;
|
|
2181
|
+
}
|
|
2182
|
+
function shortIosType(type) {
|
|
2183
|
+
return type.startsWith("XCUIElementType") ? type.slice("XCUIElementType".length) : type;
|
|
2184
|
+
}
|
|
2185
|
+
function rankNativeSelectors(fields) {
|
|
2186
|
+
const selectors = [];
|
|
2187
|
+
if (fields.id) {
|
|
2188
|
+
selectors.push(`id=${fields.id}`);
|
|
2189
|
+
}
|
|
2190
|
+
if (fields.label) {
|
|
2191
|
+
selectors.push(`label=${quoteSelectorValue(fields.label)}`);
|
|
2192
|
+
}
|
|
2193
|
+
if (fields.role && fields.name) {
|
|
2194
|
+
selectors.push(`role=${fields.role}[name=${quoteSelectorValue(fields.name)}]`);
|
|
2195
|
+
}
|
|
2196
|
+
if (fields.name) {
|
|
2197
|
+
selectors.push(`text=${quoteSelectorValue(fields.name)}`);
|
|
2198
|
+
}
|
|
2199
|
+
if (selectors.length === 0 && fields.role) {
|
|
2200
|
+
selectors.push(`role=${fields.role}`);
|
|
2201
|
+
}
|
|
2202
|
+
return selectors;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// src/browser/android-driver.ts
|
|
1990
2206
|
var ANDROID_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
1991
2207
|
"query",
|
|
1992
2208
|
"interact",
|
|
@@ -2020,50 +2236,29 @@ var ANDROID_KEYCODES = {
|
|
|
2020
2236
|
pageup: 92,
|
|
2021
2237
|
pagedown: 93
|
|
2022
2238
|
};
|
|
2023
|
-
function unquote2(value) {
|
|
2024
|
-
const trimmed = value.trim();
|
|
2025
|
-
const first = trimmed[0];
|
|
2026
|
-
if ((first === '"' || first === "'") && trimmed.endsWith(first) && trimmed.length >= 2) {
|
|
2027
|
-
return trimmed.slice(1, -1);
|
|
2028
|
-
}
|
|
2029
|
-
return trimmed;
|
|
2030
|
-
}
|
|
2031
2239
|
function escapeUiSelectorArg(value) {
|
|
2032
2240
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2033
2241
|
}
|
|
2034
|
-
function
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
return name !== void 0 && name.length > 0 ? { by: "role", role: roleMatch[1], name } : { by: "role", role: roleMatch[1] };
|
|
2047
|
-
}
|
|
2048
|
-
const labelMatch = /^label=(.+)$/s.exec(trimmed);
|
|
2049
|
-
if (labelMatch) {
|
|
2050
|
-
return { by: "accessibilityId", value: unquote2(labelMatch[1]) };
|
|
2051
|
-
}
|
|
2052
|
-
const textMatch = /^text=(.+)$/s.exec(trimmed);
|
|
2053
|
-
if (textMatch) {
|
|
2054
|
-
return { by: "text", value: unquote2(textMatch[1]) };
|
|
2242
|
+
function toAndroidQuery(selector) {
|
|
2243
|
+
switch (selector.kind) {
|
|
2244
|
+
case "focused":
|
|
2245
|
+
return { by: "focused" };
|
|
2246
|
+
case "id":
|
|
2247
|
+
return { by: "id", value: selector.value };
|
|
2248
|
+
case "role":
|
|
2249
|
+
return selector.name !== void 0 ? { by: "role", role: selector.role, name: selector.name } : { by: "role", role: selector.role };
|
|
2250
|
+
case "label":
|
|
2251
|
+
return { by: "accessibilityId", value: selector.value };
|
|
2252
|
+
case "text":
|
|
2253
|
+
return { by: "text", value: selector.value };
|
|
2055
2254
|
}
|
|
2056
|
-
|
|
2255
|
+
}
|
|
2256
|
+
function parseAndroidSelector(selector) {
|
|
2257
|
+
return toAndroidQuery(parseNativeSelector(selector));
|
|
2057
2258
|
}
|
|
2058
2259
|
function locator(strategy, selector) {
|
|
2059
2260
|
return { strategy, selector, context: "" };
|
|
2060
2261
|
}
|
|
2061
|
-
function qualifyResourceId(value, appPackage) {
|
|
2062
|
-
if (value.includes(":") || !appPackage) {
|
|
2063
|
-
return value;
|
|
2064
|
-
}
|
|
2065
|
-
return `${appPackage}:id/${value}`;
|
|
2066
|
-
}
|
|
2067
2262
|
function androidQueryToLocator(query, options = {}) {
|
|
2068
2263
|
switch (query.by) {
|
|
2069
2264
|
case "id":
|
|
@@ -2090,11 +2285,7 @@ function androidQueryToLocator(query, options = {}) {
|
|
|
2090
2285
|
}
|
|
2091
2286
|
}
|
|
2092
2287
|
function unwrapAndroidTextSelector(selector) {
|
|
2093
|
-
|
|
2094
|
-
if (!trimmed.startsWith("text=")) {
|
|
2095
|
-
return null;
|
|
2096
|
-
}
|
|
2097
|
-
return unquote2(trimmed.slice(5));
|
|
2288
|
+
return unwrapNativeTextSelector(selector);
|
|
2098
2289
|
}
|
|
2099
2290
|
function keyCodeFor(key) {
|
|
2100
2291
|
const code = ANDROID_KEYCODES[key.trim().toLowerCase()];
|
|
@@ -2369,22 +2560,63 @@ async function installApk(runner, serial, apkPath) {
|
|
|
2369
2560
|
);
|
|
2370
2561
|
}
|
|
2371
2562
|
}
|
|
2563
|
+
function formatUnknownError(error) {
|
|
2564
|
+
return error instanceof Error ? error.message : String(error);
|
|
2565
|
+
}
|
|
2566
|
+
function runnerPhaseError(message, error) {
|
|
2567
|
+
return new Error(`${message}: ${formatUnknownError(error)}`, { cause: error });
|
|
2568
|
+
}
|
|
2569
|
+
function parseResolvedComponent(stdout, pkg) {
|
|
2570
|
+
const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
2571
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
2572
|
+
if (/^[\w.]+\/[\w.$]+$/.test(lines[i]) && lines[i].startsWith(`${pkg}/`)) {
|
|
2573
|
+
return lines[i];
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
return null;
|
|
2577
|
+
}
|
|
2372
2578
|
async function launchPackage(runner, serial, pkg) {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2579
|
+
let resolved;
|
|
2580
|
+
try {
|
|
2581
|
+
resolved = await runner(
|
|
2582
|
+
withSerial(serial, [
|
|
2583
|
+
"shell",
|
|
2584
|
+
"cmd",
|
|
2585
|
+
"package",
|
|
2586
|
+
"resolve-activity",
|
|
2587
|
+
"--brief",
|
|
2588
|
+
"-c",
|
|
2589
|
+
"android.intent.category.LAUNCHER",
|
|
2590
|
+
pkg
|
|
2591
|
+
]),
|
|
2592
|
+
{ timeoutMs: 3e4 }
|
|
2593
|
+
);
|
|
2594
|
+
} catch (error) {
|
|
2595
|
+
throw runnerPhaseError(
|
|
2596
|
+
`Failed to resolve the launcher activity for Android package "${pkg}" via adb`,
|
|
2597
|
+
error
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
const component = parseResolvedComponent(resolved.stdout, pkg);
|
|
2601
|
+
if (resolved.code !== 0 || !component) {
|
|
2602
|
+
throw new Error(
|
|
2603
|
+
`Could not resolve a launcher activity for Android package "${pkg}" (exit ${resolved.code})${(resolved.stdout + resolved.stderr).trim() ? `: ${(resolved.stdout + resolved.stderr).trim().slice(0, 300)}` : ""}. Is it installed?`
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
let start;
|
|
2607
|
+
try {
|
|
2608
|
+
start = await runner(withSerial(serial, ["shell", "am", "start", "-n", component]), {
|
|
2609
|
+
timeoutMs: 3e4
|
|
2610
|
+
});
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
throw runnerPhaseError(
|
|
2613
|
+
`Failed to launch Android package "${pkg}" with \`am start\` (${component}) via adb`,
|
|
2614
|
+
error
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
if (start.code !== 0 || /error|does not exist|cannot start/i.test(start.stdout + start.stderr)) {
|
|
2386
2618
|
throw new Error(
|
|
2387
|
-
`Failed to launch Android package "${pkg}" (exit ${
|
|
2619
|
+
`Failed to launch Android package "${pkg}" (${component}, exit ${start.code}): ${(start.stdout + start.stderr).trim().slice(0, 400)}.`
|
|
2388
2620
|
);
|
|
2389
2621
|
}
|
|
2390
2622
|
}
|
|
@@ -2602,6 +2834,13 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
|
2602
2834
|
}
|
|
2603
2835
|
return Buffer.from(value, "base64");
|
|
2604
2836
|
},
|
|
2837
|
+
async source() {
|
|
2838
|
+
const value = await transport.request("GET", `${base}/source`);
|
|
2839
|
+
if (typeof value !== "string") {
|
|
2840
|
+
throw new Error("uiautomator2 /source did not return XML text; cannot analyze Android UI hierarchy");
|
|
2841
|
+
}
|
|
2842
|
+
return value;
|
|
2843
|
+
},
|
|
2605
2844
|
async close() {
|
|
2606
2845
|
await transport.request("DELETE", base).catch(() => void 0);
|
|
2607
2846
|
}
|
|
@@ -2745,6 +2984,7 @@ async function closeAndroidSession(session) {
|
|
|
2745
2984
|
|
|
2746
2985
|
// src/browser/ios-helper.ts
|
|
2747
2986
|
var import_node_module2 = require("module");
|
|
2987
|
+
var import_node_child_process6 = require("child_process");
|
|
2748
2988
|
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
2749
2989
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
2750
2990
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
@@ -2759,43 +2999,25 @@ var IOS_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
2759
2999
|
var WAIT_POLL_INTERVAL_MS2 = 250;
|
|
2760
3000
|
var DEFAULT_WAIT_TIMEOUT_MS2 = 5e3;
|
|
2761
3001
|
var IOS_PRESS_KEYS = ["backspace", "del", "delete", "enter", "home", "return"];
|
|
2762
|
-
function unquote3(value) {
|
|
2763
|
-
const trimmed = value.trim();
|
|
2764
|
-
const first = trimmed[0];
|
|
2765
|
-
if ((first === '"' || first === "'") && trimmed.endsWith(first) && trimmed.length >= 2) {
|
|
2766
|
-
return trimmed.slice(1, -1);
|
|
2767
|
-
}
|
|
2768
|
-
return trimmed;
|
|
2769
|
-
}
|
|
2770
3002
|
function escapePredicateArg(value) {
|
|
2771
3003
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2772
3004
|
}
|
|
2773
|
-
function
|
|
2774
|
-
|
|
3005
|
+
function toIosQuery(selector) {
|
|
3006
|
+
switch (selector.kind) {
|
|
3007
|
+
case "focused":
|
|
3008
|
+
return { by: "focused" };
|
|
3009
|
+
case "id":
|
|
3010
|
+
return { by: "accessibilityId", value: selector.value };
|
|
3011
|
+
case "role":
|
|
3012
|
+
return selector.name !== void 0 ? { by: "role", role: selector.role, name: selector.name } : { by: "role", role: selector.role };
|
|
3013
|
+
case "label":
|
|
3014
|
+
return { by: "label", value: selector.value };
|
|
3015
|
+
case "text":
|
|
3016
|
+
return { by: "text", value: selector.value };
|
|
3017
|
+
}
|
|
2775
3018
|
}
|
|
2776
3019
|
function parseIosSelector(selector) {
|
|
2777
|
-
|
|
2778
|
-
if (trimmed === ":focus") {
|
|
2779
|
-
return { by: "focused" };
|
|
2780
|
-
}
|
|
2781
|
-
const idMatch = /^id=(.+)$/s.exec(trimmed);
|
|
2782
|
-
if (idMatch) {
|
|
2783
|
-
return { by: "accessibilityId", value: unquote3(idMatch[1]) };
|
|
2784
|
-
}
|
|
2785
|
-
const roleMatch = /^role=([A-Za-z][\w.$-]*)(?:\[name=(.+)\])?$/s.exec(trimmed);
|
|
2786
|
-
if (roleMatch) {
|
|
2787
|
-
const name = roleMatch[2] !== void 0 ? unquote3(roleMatch[2]) : void 0;
|
|
2788
|
-
return name !== void 0 && name.length > 0 ? { by: "role", role: roleMatch[1], name } : { by: "role", role: roleMatch[1] };
|
|
2789
|
-
}
|
|
2790
|
-
const labelMatch = /^label=(.+)$/s.exec(trimmed);
|
|
2791
|
-
if (labelMatch) {
|
|
2792
|
-
return { by: "label", value: unquote3(labelMatch[1]) };
|
|
2793
|
-
}
|
|
2794
|
-
const textMatch = /^text=(.+)$/s.exec(trimmed);
|
|
2795
|
-
if (textMatch) {
|
|
2796
|
-
return { by: "text", value: unquote3(textMatch[1]) };
|
|
2797
|
-
}
|
|
2798
|
-
return { by: "text", value: trimmed };
|
|
3020
|
+
return toIosQuery(parseNativeSelector(selector));
|
|
2799
3021
|
}
|
|
2800
3022
|
function iosQueryToLocator(query) {
|
|
2801
3023
|
switch (query.by) {
|
|
@@ -2827,11 +3049,7 @@ function iosQueryToLocator(query) {
|
|
|
2827
3049
|
}
|
|
2828
3050
|
}
|
|
2829
3051
|
function unwrapIosTextSelector(selector) {
|
|
2830
|
-
|
|
2831
|
-
if (!trimmed.startsWith("text=")) {
|
|
2832
|
-
return null;
|
|
2833
|
-
}
|
|
2834
|
-
return unquote3(trimmed.slice(5));
|
|
3052
|
+
return unwrapNativeTextSelector(selector);
|
|
2835
3053
|
}
|
|
2836
3054
|
function delay2(ms) {
|
|
2837
3055
|
return new Promise((resolve) => {
|
|
@@ -2995,6 +3213,19 @@ var import_promises = require("fs/promises");
|
|
|
2995
3213
|
var import_node_net = __toESM(require("net"), 1);
|
|
2996
3214
|
var import_node_os = __toESM(require("os"), 1);
|
|
2997
3215
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
3216
|
+
var spawnXcrunProcess = (args, options) => {
|
|
3217
|
+
const child = (0, import_node_child_process5.spawn)("xcrun", args, {
|
|
3218
|
+
stdio: "ignore",
|
|
3219
|
+
env: options?.env ? { ...process.env, ...options.env } : process.env
|
|
3220
|
+
});
|
|
3221
|
+
child.on("error", () => {
|
|
3222
|
+
});
|
|
3223
|
+
return {
|
|
3224
|
+
kill: () => {
|
|
3225
|
+
child.kill();
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
3228
|
+
};
|
|
2998
3229
|
var DEFAULT_SIMULATOR_LOCK_ROOT = import_node_path6.default.join(import_node_os.default.tmpdir(), "prowl-ios-simulator-locks");
|
|
2999
3230
|
var SIMULATOR_LOCK_OWNER_FILE = "owner.json";
|
|
3000
3231
|
var execFileXcrunRunner = (args, options) => new Promise((resolve) => {
|
|
@@ -3431,6 +3662,13 @@ function createWdaAgentClient(transport, sessionId) {
|
|
|
3431
3662
|
async homescreen() {
|
|
3432
3663
|
await transport.request("POST", "/wda/homescreen", {});
|
|
3433
3664
|
},
|
|
3665
|
+
async source() {
|
|
3666
|
+
const value = await transport.request("GET", "/source");
|
|
3667
|
+
if (typeof value !== "string") {
|
|
3668
|
+
throw new Error("WebDriverAgent /source did not return XML text; cannot analyze iOS UI hierarchy");
|
|
3669
|
+
}
|
|
3670
|
+
return value;
|
|
3671
|
+
},
|
|
3434
3672
|
async close() {
|
|
3435
3673
|
await transport.request("DELETE", base).catch(() => void 0);
|
|
3436
3674
|
}
|
|
@@ -3440,8 +3678,8 @@ function createWdaAgentClient(transport, sessionId) {
|
|
|
3440
3678
|
// src/browser/ios-helper.ts
|
|
3441
3679
|
var import_meta3 = {};
|
|
3442
3680
|
var WDA_RUNNER_BUNDLE_ID = "com.facebook.WebDriverAgentRunner.xctrunner";
|
|
3443
|
-
var WDA_RUNNER_APP_NAME = "WebDriverAgentRunner-Runner.app";
|
|
3444
3681
|
var WDA_USE_PORT_ENV = "USE_PORT";
|
|
3682
|
+
var PREPARED_XCTESTRUN_PREFIX = "prowl-wda-xctestrun-";
|
|
3445
3683
|
var WDA_STARTUP_ATTEMPTS = 2;
|
|
3446
3684
|
var defaultIosAgentConnector = async ({
|
|
3447
3685
|
host,
|
|
@@ -3475,10 +3713,44 @@ function resolveWdaProject(requireFn = (0, import_node_module2.createRequire)(im
|
|
|
3475
3713
|
function wdaCacheDir(wdaVersion, xcode, homeDir = import_node_os2.default.homedir()) {
|
|
3476
3714
|
return import_node_path7.default.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
|
|
3477
3715
|
}
|
|
3478
|
-
function
|
|
3479
|
-
return import_node_path7.default.join(derivedDataPath, "Build", "Products"
|
|
3716
|
+
function productsDir(derivedDataPath) {
|
|
3717
|
+
return import_node_path7.default.join(derivedDataPath, "Build", "Products");
|
|
3480
3718
|
}
|
|
3481
|
-
|
|
3719
|
+
function findXctestrunIn(dir) {
|
|
3720
|
+
let entries;
|
|
3721
|
+
try {
|
|
3722
|
+
entries = import_node_fs7.default.readdirSync(dir);
|
|
3723
|
+
} catch {
|
|
3724
|
+
return null;
|
|
3725
|
+
}
|
|
3726
|
+
const match = entries.filter((name) => name.endsWith(".xctestrun") && !name.startsWith(PREPARED_XCTESTRUN_PREFIX)).sort()[0];
|
|
3727
|
+
return match ? import_node_path7.default.join(dir, match) : null;
|
|
3728
|
+
}
|
|
3729
|
+
function resolveOverrideXctestrun(override) {
|
|
3730
|
+
if (!import_node_fs7.default.existsSync(override)) {
|
|
3731
|
+
throw new Error(`PROWL_WDA_RUNNER points at a missing path: ${override}`);
|
|
3732
|
+
}
|
|
3733
|
+
if (override.endsWith(".xctestrun")) {
|
|
3734
|
+
return override;
|
|
3735
|
+
}
|
|
3736
|
+
const candidates = [];
|
|
3737
|
+
const stat = import_node_fs7.default.statSync(override);
|
|
3738
|
+
if (override.endsWith(".app")) {
|
|
3739
|
+
candidates.push(import_node_path7.default.dirname(import_node_path7.default.dirname(override)));
|
|
3740
|
+
} else if (stat.isDirectory()) {
|
|
3741
|
+
candidates.push(override, productsDir(override));
|
|
3742
|
+
}
|
|
3743
|
+
for (const dir of candidates) {
|
|
3744
|
+
const found = findXctestrunIn(dir);
|
|
3745
|
+
if (found) {
|
|
3746
|
+
return found;
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
throw new Error(
|
|
3750
|
+
`PROWL_WDA_RUNNER (${override}) does not resolve to a WebDriverAgent .xctestrun. Point it at the generated \`*.xctestrun\`, at the \`-derivedDataPath\` directory from \`xcodebuild build-for-testing\`, or at that build's runner \`.app\`.`
|
|
3751
|
+
);
|
|
3752
|
+
}
|
|
3753
|
+
async function resolveWdaTestRun(options = {}) {
|
|
3482
3754
|
const runner = options.runner ?? execFileXcrunRunner;
|
|
3483
3755
|
const env = options.env ?? process.env;
|
|
3484
3756
|
const homeDir = options.homeDir ?? import_node_os2.default.homedir();
|
|
@@ -3486,10 +3758,7 @@ async function resolveWdaRunner(options = {}) {
|
|
|
3486
3758
|
`));
|
|
3487
3759
|
const override = env.PROWL_WDA_RUNNER;
|
|
3488
3760
|
if (override) {
|
|
3489
|
-
|
|
3490
|
-
throw new Error(`PROWL_WDA_RUNNER points at a missing path: ${override}`);
|
|
3491
|
-
}
|
|
3492
|
-
return override;
|
|
3761
|
+
return resolveOverrideXctestrun(override);
|
|
3493
3762
|
}
|
|
3494
3763
|
const { projectPath, version } = resolveWdaProject(options.requireFn);
|
|
3495
3764
|
const xcode = await xcodeVersion(runner);
|
|
@@ -3499,9 +3768,9 @@ async function resolveWdaRunner(options = {}) {
|
|
|
3499
3768
|
);
|
|
3500
3769
|
}
|
|
3501
3770
|
const cacheDir = wdaCacheDir(version, xcode, homeDir);
|
|
3502
|
-
const
|
|
3503
|
-
if (
|
|
3504
|
-
return
|
|
3771
|
+
const cached = findXctestrunIn(productsDir(cacheDir));
|
|
3772
|
+
if (cached) {
|
|
3773
|
+
return cached;
|
|
3505
3774
|
}
|
|
3506
3775
|
log(
|
|
3507
3776
|
`Prowl: building WebDriverAgent for the iOS target (first run only; this can take a few minutes)\u2026`
|
|
@@ -3528,12 +3797,106 @@ async function resolveWdaRunner(options = {}) {
|
|
|
3528
3797
|
`Failed to build WebDriverAgent with \`xcodebuild build-for-testing\`. Ensure a full Xcode (not just the command-line tools) is installed and selected (\`xcode-select -p\`). Details: ${(result.stderr.trim() || result.stdout.trim()).slice(-800)}`
|
|
3529
3798
|
);
|
|
3530
3799
|
}
|
|
3531
|
-
|
|
3800
|
+
const built = findXctestrunIn(productsDir(cacheDir));
|
|
3801
|
+
if (!built) {
|
|
3532
3802
|
throw new Error(
|
|
3533
|
-
`WebDriverAgent build succeeded but
|
|
3803
|
+
`WebDriverAgent build succeeded but no .xctestrun was found under ${productsDir(cacheDir)}. This may indicate an Xcode layout change; set PROWL_WDA_RUNNER to a prebuilt runner/xctestrun.`
|
|
3534
3804
|
);
|
|
3535
3805
|
}
|
|
3536
|
-
return
|
|
3806
|
+
return built;
|
|
3807
|
+
}
|
|
3808
|
+
function injectUsePortIntoXctestrun(plist, port) {
|
|
3809
|
+
const clone = structuredClone(plist);
|
|
3810
|
+
let injected = 0;
|
|
3811
|
+
const visit = (node) => {
|
|
3812
|
+
if (Array.isArray(node)) {
|
|
3813
|
+
for (const entry of node) {
|
|
3814
|
+
visit(entry);
|
|
3815
|
+
}
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
if (!node || typeof node !== "object") {
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
const obj = node;
|
|
3822
|
+
const isTarget = typeof obj.TestBundlePath === "string" || typeof obj.TestHostPath === "string";
|
|
3823
|
+
const existingEnv = obj.EnvironmentVariables && typeof obj.EnvironmentVariables === "object" && !Array.isArray(obj.EnvironmentVariables) ? obj.EnvironmentVariables : void 0;
|
|
3824
|
+
if (isTarget || existingEnv) {
|
|
3825
|
+
const env = existingEnv ?? {};
|
|
3826
|
+
env[WDA_USE_PORT_ENV] = String(port);
|
|
3827
|
+
obj.EnvironmentVariables = env;
|
|
3828
|
+
injected += 1;
|
|
3829
|
+
}
|
|
3830
|
+
for (const value of Object.values(obj)) {
|
|
3831
|
+
visit(value);
|
|
3832
|
+
}
|
|
3833
|
+
};
|
|
3834
|
+
visit(clone);
|
|
3835
|
+
if (injected === 0) {
|
|
3836
|
+
throw new Error(
|
|
3837
|
+
"Could not find a test target in the WebDriverAgent .xctestrun to set USE_PORT. The xctestrun format may have changed; set PROWL_WDA_RUNNER to a compatible runner."
|
|
3838
|
+
);
|
|
3839
|
+
}
|
|
3840
|
+
return clone;
|
|
3841
|
+
}
|
|
3842
|
+
function runPlutil(args) {
|
|
3843
|
+
return new Promise((resolve, reject) => {
|
|
3844
|
+
(0, import_node_child_process6.execFile)(
|
|
3845
|
+
"plutil",
|
|
3846
|
+
args,
|
|
3847
|
+
{ encoding: "utf-8", maxBuffer: 32 * 1024 * 1024 },
|
|
3848
|
+
(error, stdout, stderr) => {
|
|
3849
|
+
if (error) {
|
|
3850
|
+
reject(
|
|
3851
|
+
new Error(
|
|
3852
|
+
`\`plutil ${args.join(" ")}\` failed: ${(stderr || error.message).slice(0, 400)}`
|
|
3853
|
+
)
|
|
3854
|
+
);
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
resolve(stdout ?? "");
|
|
3858
|
+
}
|
|
3859
|
+
);
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
var defaultWdaTestRunPreparer = async ({ xctestrunPath, port }) => {
|
|
3863
|
+
const json = await runPlutil(["-convert", "json", "-o", "-", xctestrunPath]);
|
|
3864
|
+
let parsed;
|
|
3865
|
+
try {
|
|
3866
|
+
parsed = JSON.parse(json);
|
|
3867
|
+
} catch {
|
|
3868
|
+
throw new Error(`Could not parse the WebDriverAgent .xctestrun as JSON: ${xctestrunPath}`);
|
|
3869
|
+
}
|
|
3870
|
+
const injected = injectUsePortIntoXctestrun(parsed, port);
|
|
3871
|
+
const stem = `${PREPARED_XCTESTRUN_PREFIX}${port}-${process.pid}`;
|
|
3872
|
+
const outPath = import_node_path7.default.join(import_node_path7.default.dirname(xctestrunPath), `${stem}.xctestrun`);
|
|
3873
|
+
const jsonPath = import_node_path7.default.join(import_node_os2.default.tmpdir(), `${stem}.json`);
|
|
3874
|
+
import_node_fs7.default.writeFileSync(jsonPath, JSON.stringify(injected));
|
|
3875
|
+
try {
|
|
3876
|
+
await runPlutil(["-convert", "xml1", jsonPath, "-o", outPath]);
|
|
3877
|
+
} finally {
|
|
3878
|
+
import_node_fs7.default.rmSync(jsonPath, { force: true });
|
|
3879
|
+
}
|
|
3880
|
+
return outPath;
|
|
3881
|
+
};
|
|
3882
|
+
function cleanupPreparedTestRun(preparedPath) {
|
|
3883
|
+
if (!preparedPath) {
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3886
|
+
if (!import_node_path7.default.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
import_node_fs7.default.rmSync(preparedPath, { force: true });
|
|
3890
|
+
}
|
|
3891
|
+
function wdaTestRunArgs(xctestrunPath, udid) {
|
|
3892
|
+
return [
|
|
3893
|
+
"xcodebuild",
|
|
3894
|
+
"test-without-building",
|
|
3895
|
+
"-xctestrun",
|
|
3896
|
+
xctestrunPath,
|
|
3897
|
+
"-destination",
|
|
3898
|
+
`id=${udid}`
|
|
3899
|
+
];
|
|
3537
3900
|
}
|
|
3538
3901
|
async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
|
|
3539
3902
|
if (!looksLikeIosAppPath(app)) {
|
|
@@ -3564,6 +3927,8 @@ async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
|
|
|
3564
3927
|
}
|
|
3565
3928
|
async function launchIosSession(options) {
|
|
3566
3929
|
const runner = options.runner ?? execFileXcrunRunner;
|
|
3930
|
+
const spawner = options.spawner ?? spawnXcrunProcess;
|
|
3931
|
+
const preparer = options.testRunPreparer ?? defaultWdaTestRunPreparer;
|
|
3567
3932
|
const portAllocator = options.portAllocator ?? findFreePort;
|
|
3568
3933
|
const connector = options.agentConnector ?? defaultIosAgentConnector;
|
|
3569
3934
|
const requestTimeoutMs = Math.max(options.timeoutMs ?? 1e4, DEFAULT_WDA_REQUEST_TIMEOUT_MS) + 5e3;
|
|
@@ -3587,14 +3952,14 @@ async function launchIosSession(options) {
|
|
|
3587
3952
|
options.coldStart ?? false,
|
|
3588
3953
|
options.allowedApps ?? []
|
|
3589
3954
|
);
|
|
3590
|
-
const
|
|
3591
|
-
await installApp(runner, udid, wdaRunnerApp);
|
|
3955
|
+
const baseTestRun = options.wdaTestRun ?? await resolveWdaTestRun({ runner, logger: options.logger });
|
|
3592
3956
|
for (let attempt = 1; attempt <= WDA_STARTUP_ATTEMPTS; attempt += 1) {
|
|
3593
3957
|
let client;
|
|
3958
|
+
let wdaProcess;
|
|
3959
|
+
let preparedTestRun;
|
|
3594
3960
|
let tornDown = false;
|
|
3595
|
-
let terminateWda = false;
|
|
3596
3961
|
let terminateTarget = false;
|
|
3597
|
-
let stage = "
|
|
3962
|
+
let stage = "prepare";
|
|
3598
3963
|
const teardownAttempt = async () => {
|
|
3599
3964
|
if (tornDown) {
|
|
3600
3965
|
return;
|
|
@@ -3603,18 +3968,18 @@ async function launchIosSession(options) {
|
|
|
3603
3968
|
if (client) {
|
|
3604
3969
|
await client.close().catch(() => void 0);
|
|
3605
3970
|
}
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
}
|
|
3971
|
+
wdaProcess?.kill();
|
|
3972
|
+
await terminateApp(runner, udid, WDA_RUNNER_BUNDLE_ID);
|
|
3609
3973
|
if (terminateTarget) {
|
|
3610
3974
|
await terminateApp(runner, udid, bundleId);
|
|
3611
3975
|
}
|
|
3976
|
+
cleanupPreparedTestRun(preparedTestRun);
|
|
3612
3977
|
};
|
|
3613
3978
|
try {
|
|
3614
3979
|
const port = await portAllocator();
|
|
3980
|
+
preparedTestRun = await preparer({ xctestrunPath: baseTestRun, port });
|
|
3615
3981
|
stage = "wda-launch";
|
|
3616
|
-
|
|
3617
|
-
await launchApp(runner, udid, WDA_RUNNER_BUNDLE_ID, { [WDA_USE_PORT_ENV]: String(port) });
|
|
3982
|
+
wdaProcess = spawner(wdaTestRunArgs(preparedTestRun, udid));
|
|
3618
3983
|
stage = "target-launch";
|
|
3619
3984
|
terminateTarget = true;
|
|
3620
3985
|
await launchApp(runner, udid, bundleId);
|
|
@@ -3793,14 +4158,254 @@ function createRunPolicy(driver, options) {
|
|
|
3793
4158
|
return { selector: healed.selector, healedFrom: healed.healedFrom };
|
|
3794
4159
|
}
|
|
3795
4160
|
return {
|
|
3796
|
-
assertWithinMaxSteps,
|
|
3797
|
-
ensureUrlAllowed,
|
|
3798
|
-
ensureAppAllowed,
|
|
3799
|
-
ensureLocationAllowed,
|
|
3800
|
-
assertAllowedSelector,
|
|
3801
|
-
resolveActionSelector
|
|
4161
|
+
assertWithinMaxSteps,
|
|
4162
|
+
ensureUrlAllowed,
|
|
4163
|
+
ensureAppAllowed,
|
|
4164
|
+
ensureLocationAllowed,
|
|
4165
|
+
assertAllowedSelector,
|
|
4166
|
+
resolveActionSelector
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
// src/generator/ai.ts
|
|
4171
|
+
var DEFAULT_BASE_URL = {
|
|
4172
|
+
anthropic: "https://api.anthropic.com",
|
|
4173
|
+
openai: "https://api.openai.com"
|
|
4174
|
+
};
|
|
4175
|
+
var AI_REQUEST_TIMEOUT_MS = 3e4;
|
|
4176
|
+
function apiRoot(config) {
|
|
4177
|
+
return config.baseUrl ?? DEFAULT_BASE_URL[config.provider];
|
|
4178
|
+
}
|
|
4179
|
+
function defaultModelFor(provider) {
|
|
4180
|
+
return provider === "anthropic" ? "claude-sonnet-4-5-20250929" : "gpt-4o";
|
|
4181
|
+
}
|
|
4182
|
+
function resolveAiEnv() {
|
|
4183
|
+
const provider = process.env.PROWL_AI_PROVIDER ?? "anthropic";
|
|
4184
|
+
if (provider !== "anthropic" && provider !== "openai") {
|
|
4185
|
+
throw new Error(`Unsupported AI provider: ${provider}. Use "anthropic" or "openai".`);
|
|
4186
|
+
}
|
|
4187
|
+
const model = process.env.PROWL_AI_MODEL ?? defaultModelFor(provider);
|
|
4188
|
+
const baseUrl = normalizeBaseUrl(process.env.PROWL_AI_BASE_URL) ?? DEFAULT_BASE_URL[provider];
|
|
4189
|
+
return { provider, model, baseUrl, apiKey: process.env.PROWL_AI_KEY };
|
|
4190
|
+
}
|
|
4191
|
+
function normalizeBaseUrl(value) {
|
|
4192
|
+
if (value === void 0) return void 0;
|
|
4193
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
4194
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
4195
|
+
}
|
|
4196
|
+
function resolveAiConfig() {
|
|
4197
|
+
const env = resolveAiEnv();
|
|
4198
|
+
if (!env.apiKey) {
|
|
4199
|
+
throw new Error(
|
|
4200
|
+
"PROWL_AI_KEY environment variable is required. Set it to your Anthropic or OpenAI API key."
|
|
4201
|
+
);
|
|
4202
|
+
}
|
|
4203
|
+
return { provider: env.provider, model: env.model, apiKey: env.apiKey, baseUrl: env.baseUrl };
|
|
4204
|
+
}
|
|
4205
|
+
function tryResolveAiConfig() {
|
|
4206
|
+
const env = resolveAiEnv();
|
|
4207
|
+
if (!env.apiKey) {
|
|
4208
|
+
return null;
|
|
4209
|
+
}
|
|
4210
|
+
return { provider: env.provider, model: env.model, apiKey: env.apiKey, baseUrl: env.baseUrl };
|
|
4211
|
+
}
|
|
4212
|
+
async function generateWithAi(prompt, config) {
|
|
4213
|
+
if (config.provider === "anthropic") {
|
|
4214
|
+
return generateWithAnthropic(prompt, config);
|
|
4215
|
+
}
|
|
4216
|
+
return generateWithOpenAi(prompt, config);
|
|
4217
|
+
}
|
|
4218
|
+
async function generateWithAnthropic(prompt, config) {
|
|
4219
|
+
const data = await postJson(
|
|
4220
|
+
"Anthropic",
|
|
4221
|
+
`${apiRoot(config)}/v1/messages`,
|
|
4222
|
+
anthropicHeaders(config),
|
|
4223
|
+
{
|
|
4224
|
+
model: config.model,
|
|
4225
|
+
max_tokens: 4096,
|
|
4226
|
+
messages: [
|
|
4227
|
+
{ role: "user", content: prompt }
|
|
4228
|
+
]
|
|
4229
|
+
}
|
|
4230
|
+
);
|
|
4231
|
+
return extractAnthropicText(data);
|
|
4232
|
+
}
|
|
4233
|
+
async function generateWithOpenAi(prompt, config) {
|
|
4234
|
+
const data = await postJson(
|
|
4235
|
+
"OpenAI",
|
|
4236
|
+
`${apiRoot(config)}/v1/chat/completions`,
|
|
4237
|
+
openAiHeaders(config),
|
|
4238
|
+
{
|
|
4239
|
+
model: config.model,
|
|
4240
|
+
messages: [
|
|
4241
|
+
{ role: "user", content: prompt }
|
|
4242
|
+
],
|
|
4243
|
+
max_tokens: 4096
|
|
4244
|
+
}
|
|
4245
|
+
);
|
|
4246
|
+
return extractOpenAiText(data);
|
|
4247
|
+
}
|
|
4248
|
+
function buildVisionPrompt(assertion) {
|
|
4249
|
+
return [
|
|
4250
|
+
"You are a meticulous QA reviewer. You are given a screenshot of an application",
|
|
4251
|
+
"and a single assertion describing what should be true about it.",
|
|
4252
|
+
"",
|
|
4253
|
+
"Assertion:",
|
|
4254
|
+
assertion,
|
|
4255
|
+
"",
|
|
4256
|
+
"Decide whether the assertion holds for the screenshot. Judge ONLY what is",
|
|
4257
|
+
"visible; do not assume behavior you cannot see. Be strict: if the assertion",
|
|
4258
|
+
"is not clearly satisfied, it fails.",
|
|
4259
|
+
"",
|
|
4260
|
+
"Reply with ONLY a single JSON object on one line, no markdown, no code fences:",
|
|
4261
|
+
'{"pass": <true|false>, "reason": "<one concise sentence explaining the verdict>"}'
|
|
4262
|
+
].join("\n");
|
|
4263
|
+
}
|
|
4264
|
+
function parseVisionVerdict(raw) {
|
|
4265
|
+
const cleaned = stripCodeFences(raw).trim();
|
|
4266
|
+
const candidate = extractJsonObject(cleaned);
|
|
4267
|
+
if (candidate === null) {
|
|
4268
|
+
throw new Error(
|
|
4269
|
+
`Could not parse a JSON verdict from the AI response: ${truncateForError(raw)}`
|
|
4270
|
+
);
|
|
4271
|
+
}
|
|
4272
|
+
let parsed;
|
|
4273
|
+
try {
|
|
4274
|
+
parsed = JSON.parse(candidate);
|
|
4275
|
+
} catch {
|
|
4276
|
+
throw new Error(
|
|
4277
|
+
`AI verdict was not valid JSON: ${truncateForError(raw)}`
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
4280
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
4281
|
+
throw new Error(`AI verdict was not a JSON object: ${truncateForError(raw)}`);
|
|
4282
|
+
}
|
|
4283
|
+
const record = parsed;
|
|
4284
|
+
if (typeof record.pass !== "boolean") {
|
|
4285
|
+
throw new Error(
|
|
4286
|
+
`AI verdict is missing a boolean "pass" field: ${truncateForError(raw)}`
|
|
4287
|
+
);
|
|
4288
|
+
}
|
|
4289
|
+
const reason = typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : record.pass ? "Assertion satisfied." : "Assertion not satisfied.";
|
|
4290
|
+
return { pass: record.pass, reason };
|
|
4291
|
+
}
|
|
4292
|
+
function stripCodeFences(text) {
|
|
4293
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
4294
|
+
return fence ? fence[1] : text;
|
|
4295
|
+
}
|
|
4296
|
+
function extractJsonObject(text) {
|
|
4297
|
+
const trimmed = text.trim();
|
|
4298
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
4299
|
+
return trimmed;
|
|
4300
|
+
}
|
|
4301
|
+
const first = trimmed.indexOf("{");
|
|
4302
|
+
const last = trimmed.lastIndexOf("}");
|
|
4303
|
+
if (first === -1 || last === -1 || last <= first) {
|
|
4304
|
+
return null;
|
|
4305
|
+
}
|
|
4306
|
+
return trimmed.slice(first, last + 1);
|
|
4307
|
+
}
|
|
4308
|
+
function truncateForError(text) {
|
|
4309
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
4310
|
+
return collapsed.length > 200 ? `${collapsed.slice(0, 199)}\u2026` : collapsed;
|
|
4311
|
+
}
|
|
4312
|
+
async function assertWithAiVision(input, config) {
|
|
4313
|
+
const raw = config.provider === "anthropic" ? await visionWithAnthropic(input, config) : await visionWithOpenAi(input, config);
|
|
4314
|
+
return parseVisionVerdict(raw);
|
|
4315
|
+
}
|
|
4316
|
+
async function visionWithAnthropic(input, config) {
|
|
4317
|
+
const data = await postJson(
|
|
4318
|
+
"Anthropic",
|
|
4319
|
+
`${apiRoot(config)}/v1/messages`,
|
|
4320
|
+
anthropicHeaders(config),
|
|
4321
|
+
{
|
|
4322
|
+
model: config.model,
|
|
4323
|
+
max_tokens: 1024,
|
|
4324
|
+
temperature: 0,
|
|
4325
|
+
messages: [
|
|
4326
|
+
{
|
|
4327
|
+
role: "user",
|
|
4328
|
+
content: [
|
|
4329
|
+
{
|
|
4330
|
+
type: "image",
|
|
4331
|
+
source: {
|
|
4332
|
+
type: "base64",
|
|
4333
|
+
media_type: input.mediaType,
|
|
4334
|
+
data: input.imageBase64
|
|
4335
|
+
}
|
|
4336
|
+
},
|
|
4337
|
+
{ type: "text", text: buildVisionPrompt(input.assertion) }
|
|
4338
|
+
]
|
|
4339
|
+
}
|
|
4340
|
+
]
|
|
4341
|
+
}
|
|
4342
|
+
);
|
|
4343
|
+
return extractAnthropicText(data);
|
|
4344
|
+
}
|
|
4345
|
+
async function visionWithOpenAi(input, config) {
|
|
4346
|
+
const data = await postJson(
|
|
4347
|
+
"OpenAI",
|
|
4348
|
+
`${apiRoot(config)}/v1/chat/completions`,
|
|
4349
|
+
openAiHeaders(config),
|
|
4350
|
+
{
|
|
4351
|
+
model: config.model,
|
|
4352
|
+
max_tokens: 1024,
|
|
4353
|
+
temperature: 0,
|
|
4354
|
+
messages: [
|
|
4355
|
+
{
|
|
4356
|
+
role: "user",
|
|
4357
|
+
content: [
|
|
4358
|
+
{ type: "text", text: buildVisionPrompt(input.assertion) },
|
|
4359
|
+
{
|
|
4360
|
+
type: "image_url",
|
|
4361
|
+
image_url: { url: `data:${input.mediaType};base64,${input.imageBase64}` }
|
|
4362
|
+
}
|
|
4363
|
+
]
|
|
4364
|
+
}
|
|
4365
|
+
]
|
|
4366
|
+
}
|
|
4367
|
+
);
|
|
4368
|
+
return extractOpenAiText(data);
|
|
4369
|
+
}
|
|
4370
|
+
async function postJson(provider, url, headers, body) {
|
|
4371
|
+
const response = await fetch(url, {
|
|
4372
|
+
method: "POST",
|
|
4373
|
+
headers,
|
|
4374
|
+
body: JSON.stringify(body),
|
|
4375
|
+
signal: AbortSignal.timeout(AI_REQUEST_TIMEOUT_MS)
|
|
4376
|
+
});
|
|
4377
|
+
if (!response.ok) {
|
|
4378
|
+
const responseBody = await response.text();
|
|
4379
|
+
throw new Error(`${provider} API error (${response.status}): ${responseBody}`);
|
|
4380
|
+
}
|
|
4381
|
+
return await response.json();
|
|
4382
|
+
}
|
|
4383
|
+
function anthropicHeaders(config) {
|
|
4384
|
+
return {
|
|
4385
|
+
"Content-Type": "application/json",
|
|
4386
|
+
"x-api-key": config.apiKey,
|
|
4387
|
+
"anthropic-version": "2023-06-01"
|
|
4388
|
+
};
|
|
4389
|
+
}
|
|
4390
|
+
function openAiHeaders(config) {
|
|
4391
|
+
return {
|
|
4392
|
+
"Content-Type": "application/json",
|
|
4393
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
3802
4394
|
};
|
|
3803
4395
|
}
|
|
4396
|
+
function extractAnthropicText(data) {
|
|
4397
|
+
const textBlock = data.content?.find((c) => c.type === "text");
|
|
4398
|
+
if (!textBlock?.text) {
|
|
4399
|
+
throw new Error("Anthropic API returned no text content");
|
|
4400
|
+
}
|
|
4401
|
+
return textBlock.text;
|
|
4402
|
+
}
|
|
4403
|
+
function extractOpenAiText(data) {
|
|
4404
|
+
if (!data.choices?.[0]?.message?.content) {
|
|
4405
|
+
throw new Error("OpenAI API returned no content");
|
|
4406
|
+
}
|
|
4407
|
+
return data.choices[0].message.content;
|
|
4408
|
+
}
|
|
3804
4409
|
|
|
3805
4410
|
// src/runner/steps.ts
|
|
3806
4411
|
function getStepType(step) {
|
|
@@ -3830,6 +4435,7 @@ function getStepType(step) {
|
|
|
3830
4435
|
if ("evalScript" in step) return "evalScript";
|
|
3831
4436
|
if ("runScript" in step) return "runScript";
|
|
3832
4437
|
if ("assertScreenshot" in step) return "assertScreenshot";
|
|
4438
|
+
if ("assertWithAI" in step) return "assertWithAI";
|
|
3833
4439
|
if ("copyText" in step) return "copyText";
|
|
3834
4440
|
if ("waitForDownload" in step) return "waitForDownload";
|
|
3835
4441
|
return "step";
|
|
@@ -3889,6 +4495,9 @@ function applyRuntimeVars(step, vars) {
|
|
|
3889
4495
|
}
|
|
3890
4496
|
};
|
|
3891
4497
|
}
|
|
4498
|
+
if ("assertWithAI" in step) {
|
|
4499
|
+
return { assertWithAI: sub(step.assertWithAI) };
|
|
4500
|
+
}
|
|
3892
4501
|
if ("copyText" in step) {
|
|
3893
4502
|
return { copyText: { selector: sub(step.copyText.selector), as: step.copyText.as } };
|
|
3894
4503
|
}
|
|
@@ -4822,6 +5431,48 @@ var STEP_HANDLERS = {
|
|
|
4822
5431
|
);
|
|
4823
5432
|
}
|
|
4824
5433
|
},
|
|
5434
|
+
assertWithAI: {
|
|
5435
|
+
capabilities: ["screenshot"],
|
|
5436
|
+
run: async (h) => {
|
|
5437
|
+
if (!("assertWithAI" in h.step)) unknownStep();
|
|
5438
|
+
const assertion = h.step.assertWithAI;
|
|
5439
|
+
const resolve = h.context.resolveAiConfig ?? tryResolveAiConfig;
|
|
5440
|
+
const aiConfig = resolve();
|
|
5441
|
+
if (!aiConfig) {
|
|
5442
|
+
return {
|
|
5443
|
+
kind: "result",
|
|
5444
|
+
result: {
|
|
5445
|
+
type: "assertWithAI",
|
|
5446
|
+
status: "warn",
|
|
5447
|
+
durationMs: Date.now() - h.stepStart,
|
|
5448
|
+
value: `skipped: no AI provider configured (set PROWL_AI_KEY to enable) \u2014 "${assertion}"`
|
|
5449
|
+
}
|
|
5450
|
+
};
|
|
5451
|
+
}
|
|
5452
|
+
const fileName = `assertWithAI_step_${h.index + 1}.png`;
|
|
5453
|
+
const relative = await h.addScreenshot(fileName);
|
|
5454
|
+
const screenshotFullPath = import_node_path9.default.join(h.context.runDir, relative);
|
|
5455
|
+
const imageBase64 = import_node_fs9.default.readFileSync(screenshotFullPath).toString("base64");
|
|
5456
|
+
const assertVision = h.context.assertVision ?? assertWithAiVision;
|
|
5457
|
+
const verdict = await assertVision(
|
|
5458
|
+
{ imageBase64, mediaType: "image/png", assertion },
|
|
5459
|
+
aiConfig
|
|
5460
|
+
);
|
|
5461
|
+
if (verdict.pass) {
|
|
5462
|
+
return {
|
|
5463
|
+
kind: "result",
|
|
5464
|
+
result: {
|
|
5465
|
+
type: "assertWithAI",
|
|
5466
|
+
status: "pass",
|
|
5467
|
+
durationMs: Date.now() - h.stepStart,
|
|
5468
|
+
value: verdict.reason,
|
|
5469
|
+
screenshot: relative
|
|
5470
|
+
}
|
|
5471
|
+
};
|
|
5472
|
+
}
|
|
5473
|
+
throw new Error(`AI assertion failed: ${verdict.reason}`);
|
|
5474
|
+
}
|
|
5475
|
+
},
|
|
4825
5476
|
copyText: {
|
|
4826
5477
|
capabilities: ["query"],
|
|
4827
5478
|
run: async (h) => {
|
|
@@ -5964,6 +6615,7 @@ function describeStep(step) {
|
|
|
5964
6615
|
const th = step.assertScreenshot.threshold !== void 0 ? ` (threshold: ${step.assertScreenshot.threshold})` : "";
|
|
5965
6616
|
return `assertScreenshot "${step.assertScreenshot.name}"${th}`;
|
|
5966
6617
|
}
|
|
6618
|
+
if ("assertWithAI" in step) return `assertWithAI "${truncate(step.assertWithAI, 60)}"`;
|
|
5967
6619
|
return "unknown step";
|
|
5968
6620
|
}
|
|
5969
6621
|
function truncate(text, max) {
|
|
@@ -5979,6 +6631,9 @@ function printStepResult(result, step, _index) {
|
|
|
5979
6631
|
const duration = import_chalk.default.gray(`(${result.durationMs}ms)`);
|
|
5980
6632
|
if (result.status === "pass") {
|
|
5981
6633
|
console.log(` ${import_chalk.default.green("\u2713")} ${label} ${duration}`);
|
|
6634
|
+
} else if (result.status === "warn") {
|
|
6635
|
+
const note = result.value ? import_chalk.default.gray(` \u2014 ${result.value}`) : "";
|
|
6636
|
+
console.log(` ${import_chalk.default.yellow("\u25CB")} ${label} ${duration}${note}`);
|
|
5982
6637
|
} else {
|
|
5983
6638
|
const error = result.error ? import_chalk.default.gray(` \u2014 ${result.error}`) : "";
|
|
5984
6639
|
console.log(` ${import_chalk.default.red("\u2717")} ${label} ${duration}${error}`);
|
|
@@ -7370,6 +8025,220 @@ async function readStatusMenu(client, menuTimeoutSeconds) {
|
|
|
7370
8025
|
}
|
|
7371
8026
|
}
|
|
7372
8027
|
|
|
8028
|
+
// src/analyzer/android.ts
|
|
8029
|
+
var ANDROID_INTERACTIVE_CLASSES = /* @__PURE__ */ new Set([
|
|
8030
|
+
"android.widget.Button",
|
|
8031
|
+
"android.widget.ImageButton",
|
|
8032
|
+
"android.widget.EditText",
|
|
8033
|
+
"android.widget.CheckBox",
|
|
8034
|
+
"android.widget.RadioButton",
|
|
8035
|
+
"android.widget.Switch",
|
|
8036
|
+
"android.widget.ToggleButton",
|
|
8037
|
+
"android.widget.Spinner",
|
|
8038
|
+
"android.widget.SeekBar",
|
|
8039
|
+
"android.widget.RatingBar",
|
|
8040
|
+
"android.widget.CompoundButton",
|
|
8041
|
+
"android.widget.AutoCompleteTextView",
|
|
8042
|
+
"android.widget.MultiAutoCompleteTextView",
|
|
8043
|
+
"android.widget.CheckedTextView",
|
|
8044
|
+
"androidx.appcompat.widget.SwitchCompat",
|
|
8045
|
+
"androidx.appcompat.widget.AppCompatButton",
|
|
8046
|
+
"androidx.appcompat.widget.AppCompatEditText"
|
|
8047
|
+
]);
|
|
8048
|
+
function str2(value) {
|
|
8049
|
+
if (value === void 0) {
|
|
8050
|
+
return void 0;
|
|
8051
|
+
}
|
|
8052
|
+
const trimmed = value.trim();
|
|
8053
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
8054
|
+
}
|
|
8055
|
+
function bool(value) {
|
|
8056
|
+
if (value === void 0) {
|
|
8057
|
+
return void 0;
|
|
8058
|
+
}
|
|
8059
|
+
return value === "true";
|
|
8060
|
+
}
|
|
8061
|
+
function toAndroidNode(element) {
|
|
8062
|
+
const a = element.attrs;
|
|
8063
|
+
return {
|
|
8064
|
+
className: str2(a["class"]),
|
|
8065
|
+
resourceId: str2(a["resource-id"]),
|
|
8066
|
+
contentDesc: str2(a["content-desc"]),
|
|
8067
|
+
text: str2(a.text),
|
|
8068
|
+
package: str2(a.package),
|
|
8069
|
+
clickable: bool(a.clickable),
|
|
8070
|
+
longClickable: bool(a["long-clickable"]),
|
|
8071
|
+
checkable: bool(a.checkable),
|
|
8072
|
+
checked: bool(a.checked),
|
|
8073
|
+
scrollable: bool(a.scrollable),
|
|
8074
|
+
focusable: bool(a.focusable),
|
|
8075
|
+
focused: bool(a.focused),
|
|
8076
|
+
enabled: bool(a.enabled),
|
|
8077
|
+
children: element.children.map(toAndroidNode)
|
|
8078
|
+
};
|
|
8079
|
+
}
|
|
8080
|
+
function parseAndroidHierarchy(xml) {
|
|
8081
|
+
const root = parseXml(xml);
|
|
8082
|
+
return root ? toAndroidNode(root) : null;
|
|
8083
|
+
}
|
|
8084
|
+
function isAndroidInteractive(node) {
|
|
8085
|
+
if (node.clickable || node.longClickable || node.checkable || node.scrollable) {
|
|
8086
|
+
return true;
|
|
8087
|
+
}
|
|
8088
|
+
return node.className !== void 0 && ANDROID_INTERACTIVE_CLASSES.has(node.className);
|
|
8089
|
+
}
|
|
8090
|
+
function rankAndroidSelectors(node) {
|
|
8091
|
+
return rankNativeSelectors({
|
|
8092
|
+
id: node.resourceId,
|
|
8093
|
+
label: node.contentDesc,
|
|
8094
|
+
role: node.className,
|
|
8095
|
+
name: node.text
|
|
8096
|
+
});
|
|
8097
|
+
}
|
|
8098
|
+
function toElement2(node) {
|
|
8099
|
+
return {
|
|
8100
|
+
className: node.className ?? "?",
|
|
8101
|
+
...node.resourceId ? { resourceId: node.resourceId } : {},
|
|
8102
|
+
...node.contentDesc ? { contentDesc: node.contentDesc } : {},
|
|
8103
|
+
...node.text ? { text: node.text } : {},
|
|
8104
|
+
...node.clickable !== void 0 ? { clickable: node.clickable } : {},
|
|
8105
|
+
...node.checkable !== void 0 ? { checkable: node.checkable } : {},
|
|
8106
|
+
...node.scrollable !== void 0 ? { scrollable: node.scrollable } : {},
|
|
8107
|
+
...node.enabled !== void 0 ? { enabled: node.enabled } : {},
|
|
8108
|
+
selectors: rankAndroidSelectors(node)
|
|
8109
|
+
};
|
|
8110
|
+
}
|
|
8111
|
+
function collectInteractive2(root) {
|
|
8112
|
+
const out = [];
|
|
8113
|
+
const visit = (node) => {
|
|
8114
|
+
if (isAndroidInteractive(node)) {
|
|
8115
|
+
out.push(toElement2(node));
|
|
8116
|
+
}
|
|
8117
|
+
for (const child of node.children) {
|
|
8118
|
+
visit(child);
|
|
8119
|
+
}
|
|
8120
|
+
};
|
|
8121
|
+
visit(root);
|
|
8122
|
+
return out;
|
|
8123
|
+
}
|
|
8124
|
+
async function analyzeAndroidApp(client, options) {
|
|
8125
|
+
const xml = await client.source();
|
|
8126
|
+
const root = parseAndroidHierarchy(xml);
|
|
8127
|
+
const elements = root ? collectInteractive2(root) : [];
|
|
8128
|
+
return { app: options.app, elements };
|
|
8129
|
+
}
|
|
8130
|
+
|
|
8131
|
+
// src/analyzer/ios.ts
|
|
8132
|
+
var IOS_INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
8133
|
+
"XCUIElementTypeButton",
|
|
8134
|
+
"XCUIElementTypeCell",
|
|
8135
|
+
"XCUIElementTypeTextField",
|
|
8136
|
+
"XCUIElementTypeSecureTextField",
|
|
8137
|
+
"XCUIElementTypeSearchField",
|
|
8138
|
+
"XCUIElementTypeSwitch",
|
|
8139
|
+
"XCUIElementTypeToggle",
|
|
8140
|
+
"XCUIElementTypeLink",
|
|
8141
|
+
"XCUIElementTypeMenuItem",
|
|
8142
|
+
"XCUIElementTypeSlider",
|
|
8143
|
+
"XCUIElementTypeStepper",
|
|
8144
|
+
"XCUIElementTypeTextView",
|
|
8145
|
+
"XCUIElementTypePickerWheel",
|
|
8146
|
+
"XCUIElementTypeTab",
|
|
8147
|
+
"XCUIElementTypeSegmentedControl",
|
|
8148
|
+
"XCUIElementTypeCheckBox",
|
|
8149
|
+
"XCUIElementTypeRadioButton",
|
|
8150
|
+
"XCUIElementTypeKey"
|
|
8151
|
+
]);
|
|
8152
|
+
var IOS_WINDOW_TYPE = "XCUIElementTypeWindow";
|
|
8153
|
+
function str3(value) {
|
|
8154
|
+
if (value === void 0) {
|
|
8155
|
+
return void 0;
|
|
8156
|
+
}
|
|
8157
|
+
const trimmed = value.trim();
|
|
8158
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
8159
|
+
}
|
|
8160
|
+
function bool2(value) {
|
|
8161
|
+
if (value === void 0) {
|
|
8162
|
+
return void 0;
|
|
8163
|
+
}
|
|
8164
|
+
return value === "true" || value === "1";
|
|
8165
|
+
}
|
|
8166
|
+
function toIosNode(element) {
|
|
8167
|
+
const a = element.attrs;
|
|
8168
|
+
return {
|
|
8169
|
+
type: str3(a.type) ?? str3(element.tag),
|
|
8170
|
+
name: str3(a.name),
|
|
8171
|
+
label: str3(a.label),
|
|
8172
|
+
value: str3(a.value),
|
|
8173
|
+
enabled: bool2(a.enabled),
|
|
8174
|
+
visible: bool2(a.visible),
|
|
8175
|
+
children: element.children.map(toIosNode)
|
|
8176
|
+
};
|
|
8177
|
+
}
|
|
8178
|
+
function parseIosHierarchy(xml) {
|
|
8179
|
+
const root = parseXml(xml);
|
|
8180
|
+
return root ? toIosNode(root) : null;
|
|
8181
|
+
}
|
|
8182
|
+
function hasAccessibilityId(node) {
|
|
8183
|
+
return node.name !== void 0 && node.name !== node.label;
|
|
8184
|
+
}
|
|
8185
|
+
function rankIosSelectors(node) {
|
|
8186
|
+
return rankNativeSelectors({
|
|
8187
|
+
id: hasAccessibilityId(node) ? node.name : void 0,
|
|
8188
|
+
label: node.label,
|
|
8189
|
+
role: node.type ? shortIosType(node.type) : void 0,
|
|
8190
|
+
name: node.label ?? node.value
|
|
8191
|
+
});
|
|
8192
|
+
}
|
|
8193
|
+
function toElement3(node) {
|
|
8194
|
+
return {
|
|
8195
|
+
type: node.type ?? "?",
|
|
8196
|
+
...node.name ? { name: node.name } : {},
|
|
8197
|
+
...node.label ? { label: node.label } : {},
|
|
8198
|
+
...node.value ? { value: node.value } : {},
|
|
8199
|
+
...node.enabled !== void 0 ? { enabled: node.enabled } : {},
|
|
8200
|
+
...node.visible !== void 0 ? { visible: node.visible } : {},
|
|
8201
|
+
selectors: rankIosSelectors(node)
|
|
8202
|
+
};
|
|
8203
|
+
}
|
|
8204
|
+
function toWindow2(node) {
|
|
8205
|
+
const [best] = rankIosSelectors(node);
|
|
8206
|
+
return {
|
|
8207
|
+
...node.name ? { name: node.name } : {},
|
|
8208
|
+
...node.label ? { label: node.label } : {},
|
|
8209
|
+
selector: best ?? `role=${shortIosType(IOS_WINDOW_TYPE)}`
|
|
8210
|
+
};
|
|
8211
|
+
}
|
|
8212
|
+
function isIosInteractive(node) {
|
|
8213
|
+
return node.type !== void 0 && IOS_INTERACTIVE_TYPES.has(node.type);
|
|
8214
|
+
}
|
|
8215
|
+
function collect(root) {
|
|
8216
|
+
const elements = [];
|
|
8217
|
+
const windows = [];
|
|
8218
|
+
const visit = (node) => {
|
|
8219
|
+
if (node.type === IOS_WINDOW_TYPE) {
|
|
8220
|
+
windows.push(toWindow2(node));
|
|
8221
|
+
}
|
|
8222
|
+
if (isIosInteractive(node)) {
|
|
8223
|
+
elements.push(toElement3(node));
|
|
8224
|
+
}
|
|
8225
|
+
for (const child of node.children) {
|
|
8226
|
+
visit(child);
|
|
8227
|
+
}
|
|
8228
|
+
};
|
|
8229
|
+
visit(root);
|
|
8230
|
+
return { elements, windows };
|
|
8231
|
+
}
|
|
8232
|
+
async function analyzeIosApp(client, options) {
|
|
8233
|
+
const xml = await client.source();
|
|
8234
|
+
const root = parseIosHierarchy(xml);
|
|
8235
|
+
if (!root) {
|
|
8236
|
+
return { app: options.app, elements: [], windows: [] };
|
|
8237
|
+
}
|
|
8238
|
+
const { elements, windows } = collect(root);
|
|
8239
|
+
return { app: options.app, elements, windows };
|
|
8240
|
+
}
|
|
8241
|
+
|
|
7373
8242
|
// src/cli/commands/analyze.ts
|
|
7374
8243
|
init_loader();
|
|
7375
8244
|
function parseViewportFlag2(value) {
|
|
@@ -7391,6 +8260,23 @@ function tryLoadConfig(configPath) {
|
|
|
7391
8260
|
throw new Error(`Failed to load config at ${configLocation}: ${message}`);
|
|
7392
8261
|
}
|
|
7393
8262
|
}
|
|
8263
|
+
function looksLikeApk2(app) {
|
|
8264
|
+
return /\.apk$/i.test(app.trim());
|
|
8265
|
+
}
|
|
8266
|
+
function pickNativePlatform(platformFlag, config, app) {
|
|
8267
|
+
if (platformFlag !== void 0) {
|
|
8268
|
+
const normalized = platformFlag.trim().toLowerCase();
|
|
8269
|
+
if (normalized === "macos" || normalized === "android" || normalized === "ios") {
|
|
8270
|
+
return normalized;
|
|
8271
|
+
}
|
|
8272
|
+
throw new Error(`Unknown --platform "${platformFlag}". Use one of: macos, android, ios.`);
|
|
8273
|
+
}
|
|
8274
|
+
const configType = config?.target.type;
|
|
8275
|
+
if (configType === "macos" || configType === "android" || configType === "ios") {
|
|
8276
|
+
return configType;
|
|
8277
|
+
}
|
|
8278
|
+
return looksLikeApk2(app) ? "android" : "macos";
|
|
8279
|
+
}
|
|
7394
8280
|
async function runWebAnalyze(url, options) {
|
|
7395
8281
|
const engine = parseBrowserEngine(options.browser);
|
|
7396
8282
|
const channel = options.channel;
|
|
@@ -7511,14 +8397,141 @@ async function runMacAnalyze(app, config, options) {
|
|
|
7511
8397
|
await session.client.close();
|
|
7512
8398
|
}
|
|
7513
8399
|
}
|
|
8400
|
+
function printAndroidElement(el) {
|
|
8401
|
+
const role = import_chalk11.default.cyan(el.className);
|
|
8402
|
+
const [best] = el.selectors;
|
|
8403
|
+
const selectorStr = best ? import_chalk11.default.yellow(best) : import_chalk11.default.gray("(no selector)");
|
|
8404
|
+
const label = el.contentDesc ?? el.text;
|
|
8405
|
+
const labelStr = label ? ` ${import_chalk11.default.gray(`"${label}"`)}` : "";
|
|
8406
|
+
const disabled = el.enabled === false ? import_chalk11.default.gray(" (disabled)") : "";
|
|
8407
|
+
console.log(` ${role} ${selectorStr}${labelStr}${disabled}`);
|
|
8408
|
+
}
|
|
8409
|
+
async function runAndroidAnalyze(app, config, options, deviceSerial) {
|
|
8410
|
+
const allowedApps = config?.guardrails.allowedApps ?? [];
|
|
8411
|
+
if (!looksLikeApk2(app)) {
|
|
8412
|
+
assertAndroidAppAllowed(allowedApps, app);
|
|
8413
|
+
}
|
|
8414
|
+
const session = await launchAndroidSession({
|
|
8415
|
+
app,
|
|
8416
|
+
...deviceSerial ? { deviceSerial } : {},
|
|
8417
|
+
timeoutMs: config?.browser.timeout,
|
|
8418
|
+
allowedApps
|
|
8419
|
+
});
|
|
8420
|
+
try {
|
|
8421
|
+
const sourceFn = session.client.source;
|
|
8422
|
+
if (typeof sourceFn !== "function") {
|
|
8423
|
+
throw new Error(
|
|
8424
|
+
"The Android agent client does not expose source(); a live uiautomator2 session is required to analyze."
|
|
8425
|
+
);
|
|
8426
|
+
}
|
|
8427
|
+
const result = await analyzeAndroidApp(
|
|
8428
|
+
{ source: () => sourceFn.call(session.client) },
|
|
8429
|
+
{ app: session.package }
|
|
8430
|
+
);
|
|
8431
|
+
if (options.json) {
|
|
8432
|
+
console.log(JSON.stringify(result, null, 2));
|
|
8433
|
+
} else {
|
|
8434
|
+
console.log(`
|
|
8435
|
+
${import_chalk11.default.bold("App Analysis:")} ${result.app}
|
|
8436
|
+
`);
|
|
8437
|
+
if (result.elements.length > 0) {
|
|
8438
|
+
console.log(import_chalk11.default.bold(" Interactive Elements:"));
|
|
8439
|
+
for (const el of result.elements) {
|
|
8440
|
+
printAndroidElement(el);
|
|
8441
|
+
}
|
|
8442
|
+
console.log();
|
|
8443
|
+
}
|
|
8444
|
+
console.log(import_chalk11.default.gray(` ${result.elements.length} elements
|
|
8445
|
+
`));
|
|
8446
|
+
}
|
|
8447
|
+
} finally {
|
|
8448
|
+
await session.teardown();
|
|
8449
|
+
}
|
|
8450
|
+
}
|
|
8451
|
+
function printIosElement(el) {
|
|
8452
|
+
const role = import_chalk11.default.cyan(el.type);
|
|
8453
|
+
const [best] = el.selectors;
|
|
8454
|
+
const selectorStr = best ? import_chalk11.default.yellow(best) : import_chalk11.default.gray("(no selector)");
|
|
8455
|
+
const label = el.label ?? el.value ?? el.name;
|
|
8456
|
+
const labelStr = label ? ` ${import_chalk11.default.gray(`"${label}"`)}` : "";
|
|
8457
|
+
const disabled = el.enabled === false ? import_chalk11.default.gray(" (disabled)") : "";
|
|
8458
|
+
console.log(` ${role} ${selectorStr}${labelStr}${disabled}`);
|
|
8459
|
+
}
|
|
8460
|
+
async function runIosAnalyze(app, config, options, udid) {
|
|
8461
|
+
const allowedApps = config?.guardrails.allowedApps ?? [];
|
|
8462
|
+
if (!/\//.test(app) && !/\.app$/i.test(app.trim())) {
|
|
8463
|
+
assertIosAppAllowed(allowedApps, app);
|
|
8464
|
+
}
|
|
8465
|
+
const session = await launchIosSession({
|
|
8466
|
+
app,
|
|
8467
|
+
...udid ? { udid } : {},
|
|
8468
|
+
timeoutMs: config?.browser.timeout,
|
|
8469
|
+
allowedApps
|
|
8470
|
+
});
|
|
8471
|
+
try {
|
|
8472
|
+
const sourceFn = session.client.source;
|
|
8473
|
+
if (typeof sourceFn !== "function") {
|
|
8474
|
+
throw new Error(
|
|
8475
|
+
"The iOS agent client does not expose source(); a live WebDriverAgent session is required to analyze."
|
|
8476
|
+
);
|
|
8477
|
+
}
|
|
8478
|
+
const result = await analyzeIosApp(
|
|
8479
|
+
{ source: () => sourceFn.call(session.client) },
|
|
8480
|
+
{ app: session.bundleId }
|
|
8481
|
+
);
|
|
8482
|
+
if (options.json) {
|
|
8483
|
+
console.log(JSON.stringify(result, null, 2));
|
|
8484
|
+
} else {
|
|
8485
|
+
console.log(`
|
|
8486
|
+
${import_chalk11.default.bold("App Analysis:")} ${result.app}
|
|
8487
|
+
`);
|
|
8488
|
+
if (result.windows.length > 0) {
|
|
8489
|
+
console.log(import_chalk11.default.bold(" Windows:"));
|
|
8490
|
+
for (const win of result.windows) {
|
|
8491
|
+
const title = win.label ?? win.name;
|
|
8492
|
+
const titleStr = title ? import_chalk11.default.gray(`"${title}"`) : import_chalk11.default.gray("(untitled)");
|
|
8493
|
+
console.log(` ${titleStr} ${import_chalk11.default.yellow(win.selector)}`);
|
|
8494
|
+
}
|
|
8495
|
+
console.log();
|
|
8496
|
+
}
|
|
8497
|
+
if (result.elements.length > 0) {
|
|
8498
|
+
console.log(import_chalk11.default.bold(" Interactive Elements:"));
|
|
8499
|
+
for (const el of result.elements) {
|
|
8500
|
+
printIosElement(el);
|
|
8501
|
+
}
|
|
8502
|
+
console.log();
|
|
8503
|
+
}
|
|
8504
|
+
console.log(import_chalk11.default.gray(` ${result.elements.length} elements, ${result.windows.length} windows
|
|
8505
|
+
`));
|
|
8506
|
+
}
|
|
8507
|
+
} finally {
|
|
8508
|
+
await session.teardown();
|
|
8509
|
+
}
|
|
8510
|
+
}
|
|
8511
|
+
async function runNativeAnalyze(platform, app, config, options, device) {
|
|
8512
|
+
if (platform === "android") {
|
|
8513
|
+
await runAndroidAnalyze(app, config, options, device.deviceSerial);
|
|
8514
|
+
return;
|
|
8515
|
+
}
|
|
8516
|
+
if (platform === "ios") {
|
|
8517
|
+
await runIosAnalyze(app, config, options, device.udid);
|
|
8518
|
+
return;
|
|
8519
|
+
}
|
|
8520
|
+
await runMacAnalyze(app, config, options);
|
|
8521
|
+
}
|
|
7514
8522
|
function buildAnalyzeCommand() {
|
|
7515
|
-
const command = new import_commander8.Command("analyze").argument("[url]", "URL to analyze (web target)").description("Analyze a page or
|
|
8523
|
+
const command = new import_commander8.Command("analyze").argument("[url]", "URL to analyze (web target)").description("Analyze a page or native app to discover interactive elements and selectors").option("--json", "Output as JSON").option("--app <app>", "Native app to analyze: bundle id / package / .app / .apk (macOS, Android, or iOS)").option("--platform <name>", "Native platform for --app: macos, android, or ios (else inferred)").option("--device <serial>", "Android adb device serial (native Android target)").option("--udid <udid>", "iOS simulator UDID (native iOS target)").option("--browser <engine>", "Browser engine: chromium, firefox, or webkit").option("--channel <name>", "Browser channel: chrome, msedge, etc.").option("--viewport <size>", "Viewport size: WxH or preset (mobile, tablet, desktop)").option("--headed", "Show browser window").option("--config <path>", "Custom config path").action(async (url, options) => {
|
|
7516
8524
|
try {
|
|
7517
8525
|
if (options.app && url) {
|
|
7518
|
-
throw new Error("Pass either a URL argument (web) or --app (
|
|
8526
|
+
throw new Error("Pass either a URL argument (web) or --app (native), not both.");
|
|
7519
8527
|
}
|
|
7520
8528
|
if (options.app) {
|
|
7521
|
-
|
|
8529
|
+
const config2 = tryLoadConfig(options.config);
|
|
8530
|
+
const platform = pickNativePlatform(options.platform, config2, options.app);
|
|
8531
|
+
await runNativeAnalyze(platform, options.app, config2, options, {
|
|
8532
|
+
deviceSerial: options.device,
|
|
8533
|
+
udid: options.udid
|
|
8534
|
+
});
|
|
7522
8535
|
return;
|
|
7523
8536
|
}
|
|
7524
8537
|
if (url) {
|
|
@@ -7526,12 +8539,21 @@ function buildAnalyzeCommand() {
|
|
|
7526
8539
|
return;
|
|
7527
8540
|
}
|
|
7528
8541
|
const config = tryLoadConfig(options.config);
|
|
7529
|
-
|
|
7530
|
-
|
|
8542
|
+
const target = config?.target;
|
|
8543
|
+
if (target?.type === "macos") {
|
|
8544
|
+
await runMacAnalyze(target.app, config, options);
|
|
8545
|
+
return;
|
|
8546
|
+
}
|
|
8547
|
+
if (target?.type === "android") {
|
|
8548
|
+
await runAndroidAnalyze(target.app, config, options, options.device ?? target.deviceSerial);
|
|
8549
|
+
return;
|
|
8550
|
+
}
|
|
8551
|
+
if (target?.type === "ios") {
|
|
8552
|
+
await runIosAnalyze(target.app, config, options, options.udid ?? target.udid);
|
|
7531
8553
|
return;
|
|
7532
8554
|
}
|
|
7533
8555
|
throw new Error(
|
|
7534
|
-
"analyze needs a target: pass a URL for the web target, or use --app <bundle-id|.app
|
|
8556
|
+
"analyze needs a target: pass a URL for the web target, or use --app <bundle-id|package|.app|.apk> (optionally with --platform), or set a native target.type in .prowl/config.yml."
|
|
7535
8557
|
);
|
|
7536
8558
|
} catch (error) {
|
|
7537
8559
|
const message = error instanceof Error ? error.message : "Analysis failed";
|
|
@@ -7601,6 +8623,7 @@ var STEP_REFERENCE = `
|
|
|
7601
8623
|
|
|
7602
8624
|
### Visual Regression
|
|
7603
8625
|
- assertScreenshot: { name: "baseline-name", threshold: 0.1 }
|
|
8626
|
+
- assertWithAI: "The login form should show email and password fields" \u2014 AI checks the screenshot against the claim
|
|
7604
8627
|
|
|
7605
8628
|
### Control Flow
|
|
7606
8629
|
- if: { visible: ".banner", then: [steps...] }
|
|
@@ -7642,81 +8665,6 @@ function extractYamlFromResponse(response) {
|
|
|
7642
8665
|
return response.trim();
|
|
7643
8666
|
}
|
|
7644
8667
|
|
|
7645
|
-
// src/generator/ai.ts
|
|
7646
|
-
function resolveAiConfig() {
|
|
7647
|
-
const provider = process.env.PROWL_AI_PROVIDER ?? "anthropic";
|
|
7648
|
-
if (provider !== "anthropic" && provider !== "openai") {
|
|
7649
|
-
throw new Error(`Unsupported AI provider: ${provider}. Use "anthropic" or "openai".`);
|
|
7650
|
-
}
|
|
7651
|
-
const apiKey = process.env.PROWL_AI_KEY;
|
|
7652
|
-
if (!apiKey) {
|
|
7653
|
-
throw new Error(
|
|
7654
|
-
"PROWL_AI_KEY environment variable is required. Set it to your Anthropic or OpenAI API key."
|
|
7655
|
-
);
|
|
7656
|
-
}
|
|
7657
|
-
const defaultModel = provider === "anthropic" ? "claude-sonnet-4-5-20250929" : "gpt-4o";
|
|
7658
|
-
const model = process.env.PROWL_AI_MODEL ?? defaultModel;
|
|
7659
|
-
return { provider, model, apiKey };
|
|
7660
|
-
}
|
|
7661
|
-
async function generateWithAi(prompt, config) {
|
|
7662
|
-
if (config.provider === "anthropic") {
|
|
7663
|
-
return generateWithAnthropic(prompt, config);
|
|
7664
|
-
}
|
|
7665
|
-
return generateWithOpenAi(prompt, config);
|
|
7666
|
-
}
|
|
7667
|
-
async function generateWithAnthropic(prompt, config) {
|
|
7668
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
7669
|
-
method: "POST",
|
|
7670
|
-
headers: {
|
|
7671
|
-
"Content-Type": "application/json",
|
|
7672
|
-
"x-api-key": config.apiKey,
|
|
7673
|
-
"anthropic-version": "2023-06-01"
|
|
7674
|
-
},
|
|
7675
|
-
body: JSON.stringify({
|
|
7676
|
-
model: config.model,
|
|
7677
|
-
max_tokens: 4096,
|
|
7678
|
-
messages: [
|
|
7679
|
-
{ role: "user", content: prompt }
|
|
7680
|
-
]
|
|
7681
|
-
})
|
|
7682
|
-
});
|
|
7683
|
-
if (!response.ok) {
|
|
7684
|
-
const body = await response.text();
|
|
7685
|
-
throw new Error(`Anthropic API error (${response.status}): ${body}`);
|
|
7686
|
-
}
|
|
7687
|
-
const data = await response.json();
|
|
7688
|
-
const textBlock = data.content.find((c) => c.type === "text");
|
|
7689
|
-
if (!textBlock?.text) {
|
|
7690
|
-
throw new Error("Anthropic API returned no text content");
|
|
7691
|
-
}
|
|
7692
|
-
return textBlock.text;
|
|
7693
|
-
}
|
|
7694
|
-
async function generateWithOpenAi(prompt, config) {
|
|
7695
|
-
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
7696
|
-
method: "POST",
|
|
7697
|
-
headers: {
|
|
7698
|
-
"Content-Type": "application/json",
|
|
7699
|
-
"Authorization": `Bearer ${config.apiKey}`
|
|
7700
|
-
},
|
|
7701
|
-
body: JSON.stringify({
|
|
7702
|
-
model: config.model,
|
|
7703
|
-
messages: [
|
|
7704
|
-
{ role: "user", content: prompt }
|
|
7705
|
-
],
|
|
7706
|
-
max_tokens: 4096
|
|
7707
|
-
})
|
|
7708
|
-
});
|
|
7709
|
-
if (!response.ok) {
|
|
7710
|
-
const body = await response.text();
|
|
7711
|
-
throw new Error(`OpenAI API error (${response.status}): ${body}`);
|
|
7712
|
-
}
|
|
7713
|
-
const data = await response.json();
|
|
7714
|
-
if (!data.choices?.[0]?.message?.content) {
|
|
7715
|
-
throw new Error("OpenAI API returned no content");
|
|
7716
|
-
}
|
|
7717
|
-
return data.choices[0].message.content;
|
|
7718
|
-
}
|
|
7719
|
-
|
|
7720
8668
|
// src/generator/index.ts
|
|
7721
8669
|
init_loader();
|
|
7722
8670
|
init_schema();
|