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.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_FLAKY_THRESHOLD,
|
|
4
|
+
analyzeAndroidApp,
|
|
5
|
+
analyzeIosApp,
|
|
4
6
|
analyzeMacApp,
|
|
5
7
|
analyzePage,
|
|
8
|
+
assertAndroidAppAllowed,
|
|
9
|
+
assertIosAppAllowed,
|
|
6
10
|
assertTargetAppAllowed,
|
|
7
11
|
closeBrowser,
|
|
8
12
|
createPlaywrightDriver,
|
|
9
13
|
generateHunt,
|
|
14
|
+
launchAndroidSession,
|
|
10
15
|
launchBrowser,
|
|
16
|
+
launchIosSession,
|
|
11
17
|
launchMacSession,
|
|
12
18
|
parseBrowserEngine,
|
|
13
19
|
printCiSummary,
|
|
@@ -17,7 +23,7 @@ import {
|
|
|
17
23
|
runSuite,
|
|
18
24
|
saveStorageState,
|
|
19
25
|
updateBacklogFromSuite
|
|
20
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-5KQR3IR3.js";
|
|
21
27
|
import {
|
|
22
28
|
CONFIG_DIR,
|
|
23
29
|
findConfigPath,
|
|
@@ -26,7 +32,7 @@ import {
|
|
|
26
32
|
loadHuntMeta,
|
|
27
33
|
loadHuntTags,
|
|
28
34
|
resolveViewport
|
|
29
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-WHAMB4TY.js";
|
|
30
36
|
|
|
31
37
|
// src/cli/program.ts
|
|
32
38
|
import { Command as Command13 } from "commander";
|
|
@@ -34,7 +40,7 @@ import { Command as Command13 } from "commander";
|
|
|
34
40
|
// package.json
|
|
35
41
|
var package_default = {
|
|
36
42
|
name: "prowl-tools",
|
|
37
|
-
version: "0.1.
|
|
43
|
+
version: "0.1.6",
|
|
38
44
|
description: "CLI-first QA testing tool for deterministic Playwright flows.",
|
|
39
45
|
type: "module",
|
|
40
46
|
license: "Apache-2.0",
|
|
@@ -198,6 +204,7 @@ function describeStep(step) {
|
|
|
198
204
|
const th = step.assertScreenshot.threshold !== void 0 ? ` (threshold: ${step.assertScreenshot.threshold})` : "";
|
|
199
205
|
return `assertScreenshot "${step.assertScreenshot.name}"${th}`;
|
|
200
206
|
}
|
|
207
|
+
if ("assertWithAI" in step) return `assertWithAI "${truncate(step.assertWithAI, 60)}"`;
|
|
201
208
|
return "unknown step";
|
|
202
209
|
}
|
|
203
210
|
function truncate(text, max) {
|
|
@@ -213,6 +220,9 @@ function printStepResult(result, step, _index) {
|
|
|
213
220
|
const duration = chalk.gray(`(${result.durationMs}ms)`);
|
|
214
221
|
if (result.status === "pass") {
|
|
215
222
|
console.log(` ${chalk.green("\u2713")} ${label} ${duration}`);
|
|
223
|
+
} else if (result.status === "warn") {
|
|
224
|
+
const note = result.value ? chalk.gray(` \u2014 ${result.value}`) : "";
|
|
225
|
+
console.log(` ${chalk.yellow("\u25CB")} ${label} ${duration}${note}`);
|
|
216
226
|
} else {
|
|
217
227
|
const error = result.error ? chalk.gray(` \u2014 ${result.error}`) : "";
|
|
218
228
|
console.log(` ${chalk.red("\u2717")} ${label} ${duration}${error}`);
|
|
@@ -799,6 +809,23 @@ function tryLoadConfig(configPath) {
|
|
|
799
809
|
throw new Error(`Failed to load config at ${configLocation}: ${message}`);
|
|
800
810
|
}
|
|
801
811
|
}
|
|
812
|
+
function looksLikeApk(app) {
|
|
813
|
+
return /\.apk$/i.test(app.trim());
|
|
814
|
+
}
|
|
815
|
+
function pickNativePlatform(platformFlag, config, app) {
|
|
816
|
+
if (platformFlag !== void 0) {
|
|
817
|
+
const normalized = platformFlag.trim().toLowerCase();
|
|
818
|
+
if (normalized === "macos" || normalized === "android" || normalized === "ios") {
|
|
819
|
+
return normalized;
|
|
820
|
+
}
|
|
821
|
+
throw new Error(`Unknown --platform "${platformFlag}". Use one of: macos, android, ios.`);
|
|
822
|
+
}
|
|
823
|
+
const configType = config?.target.type;
|
|
824
|
+
if (configType === "macos" || configType === "android" || configType === "ios") {
|
|
825
|
+
return configType;
|
|
826
|
+
}
|
|
827
|
+
return looksLikeApk(app) ? "android" : "macos";
|
|
828
|
+
}
|
|
802
829
|
async function runWebAnalyze(url, options) {
|
|
803
830
|
const engine = parseBrowserEngine(options.browser);
|
|
804
831
|
const channel = options.channel;
|
|
@@ -919,14 +946,141 @@ async function runMacAnalyze(app, config, options) {
|
|
|
919
946
|
await session.client.close();
|
|
920
947
|
}
|
|
921
948
|
}
|
|
949
|
+
function printAndroidElement(el) {
|
|
950
|
+
const role = chalk10.cyan(el.className);
|
|
951
|
+
const [best] = el.selectors;
|
|
952
|
+
const selectorStr = best ? chalk10.yellow(best) : chalk10.gray("(no selector)");
|
|
953
|
+
const label = el.contentDesc ?? el.text;
|
|
954
|
+
const labelStr = label ? ` ${chalk10.gray(`"${label}"`)}` : "";
|
|
955
|
+
const disabled = el.enabled === false ? chalk10.gray(" (disabled)") : "";
|
|
956
|
+
console.log(` ${role} ${selectorStr}${labelStr}${disabled}`);
|
|
957
|
+
}
|
|
958
|
+
async function runAndroidAnalyze(app, config, options, deviceSerial) {
|
|
959
|
+
const allowedApps = config?.guardrails.allowedApps ?? [];
|
|
960
|
+
if (!looksLikeApk(app)) {
|
|
961
|
+
assertAndroidAppAllowed(allowedApps, app);
|
|
962
|
+
}
|
|
963
|
+
const session = await launchAndroidSession({
|
|
964
|
+
app,
|
|
965
|
+
...deviceSerial ? { deviceSerial } : {},
|
|
966
|
+
timeoutMs: config?.browser.timeout,
|
|
967
|
+
allowedApps
|
|
968
|
+
});
|
|
969
|
+
try {
|
|
970
|
+
const sourceFn = session.client.source;
|
|
971
|
+
if (typeof sourceFn !== "function") {
|
|
972
|
+
throw new Error(
|
|
973
|
+
"The Android agent client does not expose source(); a live uiautomator2 session is required to analyze."
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
const result = await analyzeAndroidApp(
|
|
977
|
+
{ source: () => sourceFn.call(session.client) },
|
|
978
|
+
{ app: session.package }
|
|
979
|
+
);
|
|
980
|
+
if (options.json) {
|
|
981
|
+
console.log(JSON.stringify(result, null, 2));
|
|
982
|
+
} else {
|
|
983
|
+
console.log(`
|
|
984
|
+
${chalk10.bold("App Analysis:")} ${result.app}
|
|
985
|
+
`);
|
|
986
|
+
if (result.elements.length > 0) {
|
|
987
|
+
console.log(chalk10.bold(" Interactive Elements:"));
|
|
988
|
+
for (const el of result.elements) {
|
|
989
|
+
printAndroidElement(el);
|
|
990
|
+
}
|
|
991
|
+
console.log();
|
|
992
|
+
}
|
|
993
|
+
console.log(chalk10.gray(` ${result.elements.length} elements
|
|
994
|
+
`));
|
|
995
|
+
}
|
|
996
|
+
} finally {
|
|
997
|
+
await session.teardown();
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function printIosElement(el) {
|
|
1001
|
+
const role = chalk10.cyan(el.type);
|
|
1002
|
+
const [best] = el.selectors;
|
|
1003
|
+
const selectorStr = best ? chalk10.yellow(best) : chalk10.gray("(no selector)");
|
|
1004
|
+
const label = el.label ?? el.value ?? el.name;
|
|
1005
|
+
const labelStr = label ? ` ${chalk10.gray(`"${label}"`)}` : "";
|
|
1006
|
+
const disabled = el.enabled === false ? chalk10.gray(" (disabled)") : "";
|
|
1007
|
+
console.log(` ${role} ${selectorStr}${labelStr}${disabled}`);
|
|
1008
|
+
}
|
|
1009
|
+
async function runIosAnalyze(app, config, options, udid) {
|
|
1010
|
+
const allowedApps = config?.guardrails.allowedApps ?? [];
|
|
1011
|
+
if (!/\//.test(app) && !/\.app$/i.test(app.trim())) {
|
|
1012
|
+
assertIosAppAllowed(allowedApps, app);
|
|
1013
|
+
}
|
|
1014
|
+
const session = await launchIosSession({
|
|
1015
|
+
app,
|
|
1016
|
+
...udid ? { udid } : {},
|
|
1017
|
+
timeoutMs: config?.browser.timeout,
|
|
1018
|
+
allowedApps
|
|
1019
|
+
});
|
|
1020
|
+
try {
|
|
1021
|
+
const sourceFn = session.client.source;
|
|
1022
|
+
if (typeof sourceFn !== "function") {
|
|
1023
|
+
throw new Error(
|
|
1024
|
+
"The iOS agent client does not expose source(); a live WebDriverAgent session is required to analyze."
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
const result = await analyzeIosApp(
|
|
1028
|
+
{ source: () => sourceFn.call(session.client) },
|
|
1029
|
+
{ app: session.bundleId }
|
|
1030
|
+
);
|
|
1031
|
+
if (options.json) {
|
|
1032
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1033
|
+
} else {
|
|
1034
|
+
console.log(`
|
|
1035
|
+
${chalk10.bold("App Analysis:")} ${result.app}
|
|
1036
|
+
`);
|
|
1037
|
+
if (result.windows.length > 0) {
|
|
1038
|
+
console.log(chalk10.bold(" Windows:"));
|
|
1039
|
+
for (const win of result.windows) {
|
|
1040
|
+
const title = win.label ?? win.name;
|
|
1041
|
+
const titleStr = title ? chalk10.gray(`"${title}"`) : chalk10.gray("(untitled)");
|
|
1042
|
+
console.log(` ${titleStr} ${chalk10.yellow(win.selector)}`);
|
|
1043
|
+
}
|
|
1044
|
+
console.log();
|
|
1045
|
+
}
|
|
1046
|
+
if (result.elements.length > 0) {
|
|
1047
|
+
console.log(chalk10.bold(" Interactive Elements:"));
|
|
1048
|
+
for (const el of result.elements) {
|
|
1049
|
+
printIosElement(el);
|
|
1050
|
+
}
|
|
1051
|
+
console.log();
|
|
1052
|
+
}
|
|
1053
|
+
console.log(chalk10.gray(` ${result.elements.length} elements, ${result.windows.length} windows
|
|
1054
|
+
`));
|
|
1055
|
+
}
|
|
1056
|
+
} finally {
|
|
1057
|
+
await session.teardown();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async function runNativeAnalyze(platform, app, config, options, device) {
|
|
1061
|
+
if (platform === "android") {
|
|
1062
|
+
await runAndroidAnalyze(app, config, options, device.deviceSerial);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (platform === "ios") {
|
|
1066
|
+
await runIosAnalyze(app, config, options, device.udid);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
await runMacAnalyze(app, config, options);
|
|
1070
|
+
}
|
|
922
1071
|
function buildAnalyzeCommand() {
|
|
923
|
-
const command = new Command8("analyze").argument("[url]", "URL to analyze (web target)").description("Analyze a page or
|
|
1072
|
+
const command = new Command8("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) => {
|
|
924
1073
|
try {
|
|
925
1074
|
if (options.app && url) {
|
|
926
|
-
throw new Error("Pass either a URL argument (web) or --app (
|
|
1075
|
+
throw new Error("Pass either a URL argument (web) or --app (native), not both.");
|
|
927
1076
|
}
|
|
928
1077
|
if (options.app) {
|
|
929
|
-
|
|
1078
|
+
const config2 = tryLoadConfig(options.config);
|
|
1079
|
+
const platform = pickNativePlatform(options.platform, config2, options.app);
|
|
1080
|
+
await runNativeAnalyze(platform, options.app, config2, options, {
|
|
1081
|
+
deviceSerial: options.device,
|
|
1082
|
+
udid: options.udid
|
|
1083
|
+
});
|
|
930
1084
|
return;
|
|
931
1085
|
}
|
|
932
1086
|
if (url) {
|
|
@@ -934,12 +1088,21 @@ function buildAnalyzeCommand() {
|
|
|
934
1088
|
return;
|
|
935
1089
|
}
|
|
936
1090
|
const config = tryLoadConfig(options.config);
|
|
937
|
-
|
|
938
|
-
|
|
1091
|
+
const target = config?.target;
|
|
1092
|
+
if (target?.type === "macos") {
|
|
1093
|
+
await runMacAnalyze(target.app, config, options);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (target?.type === "android") {
|
|
1097
|
+
await runAndroidAnalyze(target.app, config, options, options.device ?? target.deviceSerial);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (target?.type === "ios") {
|
|
1101
|
+
await runIosAnalyze(target.app, config, options, options.udid ?? target.udid);
|
|
939
1102
|
return;
|
|
940
1103
|
}
|
|
941
1104
|
throw new Error(
|
|
942
|
-
"analyze needs a target: pass a URL for the web target, or use --app <bundle-id|.app
|
|
1105
|
+
"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."
|
|
943
1106
|
);
|
|
944
1107
|
} catch (error) {
|
|
945
1108
|
const message = error instanceof Error ? error.message : "Analysis failed";
|
|
@@ -1014,7 +1177,7 @@ function buildGenerateCommand() {
|
|
|
1014
1177
|
} else if (options.output) {
|
|
1015
1178
|
let configDir;
|
|
1016
1179
|
try {
|
|
1017
|
-
const { loadConfig: loadConfig2 } = await import("./loader-
|
|
1180
|
+
const { loadConfig: loadConfig2 } = await import("./loader-PBBYV3U7.js");
|
|
1018
1181
|
const result = loadConfig2(options.config);
|
|
1019
1182
|
configDir = result.configDir;
|
|
1020
1183
|
} catch {
|