prowl-tools 0.1.4 → 0.1.5
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/NOTICE +10 -0
- package/README.md +277 -2
- package/dist/{chunk-ZEFVTKQT.js → chunk-2KD2XCTH.js} +2240 -213
- package/dist/chunk-2KD2XCTH.js.map +1 -0
- package/dist/{chunk-MBAIGNVO.js → chunk-ITOSUJCN.js} +39 -3
- package/dist/chunk-ITOSUJCN.js.map +1 -0
- package/dist/index.cjs +2411 -313
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +162 -67
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +2375 -244
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +742 -4
- package/dist/lib.d.ts +742 -4
- package/dist/lib.js +126 -2
- package/dist/{loader-XMTDB6OL.js → loader-FCXPARP7.js} +2 -2
- package/package.json +5 -1
- package/dist/chunk-MBAIGNVO.js.map +0 -1
- package/dist/chunk-ZEFVTKQT.js.map +0 -1
- /package/dist/{loader-XMTDB6OL.js.map → loader-FCXPARP7.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_FLAKY_THRESHOLD,
|
|
4
|
+
analyzeMacApp,
|
|
4
5
|
analyzePage,
|
|
6
|
+
assertTargetAppAllowed,
|
|
5
7
|
closeBrowser,
|
|
6
8
|
createPlaywrightDriver,
|
|
7
9
|
generateHunt,
|
|
8
10
|
launchBrowser,
|
|
11
|
+
launchMacSession,
|
|
9
12
|
parseBrowserEngine,
|
|
10
13
|
printCiSummary,
|
|
11
14
|
rankFlaky,
|
|
@@ -14,15 +17,16 @@ import {
|
|
|
14
17
|
runSuite,
|
|
15
18
|
saveStorageState,
|
|
16
19
|
updateBacklogFromSuite
|
|
17
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-2KD2XCTH.js";
|
|
18
21
|
import {
|
|
19
22
|
CONFIG_DIR,
|
|
23
|
+
findConfigPath,
|
|
20
24
|
listHunts,
|
|
21
25
|
loadConfig,
|
|
22
26
|
loadHuntMeta,
|
|
23
27
|
loadHuntTags,
|
|
24
28
|
resolveViewport
|
|
25
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-ITOSUJCN.js";
|
|
26
30
|
|
|
27
31
|
// src/cli/program.ts
|
|
28
32
|
import { Command as Command13 } from "commander";
|
|
@@ -30,7 +34,7 @@ import { Command as Command13 } from "commander";
|
|
|
30
34
|
// package.json
|
|
31
35
|
var package_default = {
|
|
32
36
|
name: "prowl-tools",
|
|
33
|
-
version: "0.1.
|
|
37
|
+
version: "0.1.5",
|
|
34
38
|
description: "CLI-first QA testing tool for deterministic Playwright flows.",
|
|
35
39
|
type: "module",
|
|
36
40
|
license: "Apache-2.0",
|
|
@@ -100,6 +104,10 @@ var package_default = {
|
|
|
100
104
|
yaml: "^2.6.1",
|
|
101
105
|
zod: "^3.23.8"
|
|
102
106
|
},
|
|
107
|
+
optionalDependencies: {
|
|
108
|
+
"appium-uiautomator2-server": "10.6.2",
|
|
109
|
+
"appium-webdriveragent": "16.4.0"
|
|
110
|
+
},
|
|
103
111
|
devDependencies: {
|
|
104
112
|
"@types/node": "^22.13.1",
|
|
105
113
|
"@types/pngjs": "^6.0.5",
|
|
@@ -422,7 +430,7 @@ function buildLoginCommand() {
|
|
|
422
430
|
let session = null;
|
|
423
431
|
try {
|
|
424
432
|
const { config, configDir } = loadConfig(options.config);
|
|
425
|
-
if (config.target.type
|
|
433
|
+
if (config.target.type !== "web") {
|
|
426
434
|
throw new Error("`prowl login` captures browser auth state and only applies to web targets.");
|
|
427
435
|
}
|
|
428
436
|
const targetUrl = options.url ?? config.target.url;
|
|
@@ -779,73 +787,160 @@ function parseViewportFlag(value) {
|
|
|
779
787
|
}
|
|
780
788
|
return value;
|
|
781
789
|
}
|
|
782
|
-
function
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
790
|
+
function tryLoadConfig(configPath) {
|
|
791
|
+
const configLocation = configPath !== void 0 ? configPath : findConfigPath(process.cwd());
|
|
792
|
+
if (configLocation === null) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
return loadConfig(configLocation).config;
|
|
797
|
+
} catch (error) {
|
|
798
|
+
const message = error instanceof Error ? error.message : "Unknown configuration error";
|
|
799
|
+
throw new Error(`Failed to load config at ${configLocation}: ${message}`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function runWebAnalyze(url, options) {
|
|
803
|
+
const engine = parseBrowserEngine(options.browser);
|
|
804
|
+
const channel = options.channel;
|
|
805
|
+
const viewport = options.viewport ? resolveViewport(parseViewportFlag(options.viewport)) : { width: 1280, height: 720 };
|
|
806
|
+
const session = await launchBrowser({
|
|
807
|
+
headless: !options.headed,
|
|
808
|
+
slowMo: 0,
|
|
809
|
+
timeout: 3e4,
|
|
810
|
+
trace: false,
|
|
811
|
+
recordHar: false,
|
|
812
|
+
runDir: process.cwd(),
|
|
813
|
+
engine,
|
|
814
|
+
channel,
|
|
815
|
+
viewport
|
|
816
|
+
});
|
|
817
|
+
const driver = createPlaywrightDriver(session.page);
|
|
818
|
+
try {
|
|
819
|
+
await driver.goto(url, { waitUntil: "networkidle" });
|
|
820
|
+
const result = await analyzePage(driver);
|
|
821
|
+
if (options.json) {
|
|
822
|
+
console.log(JSON.stringify(result, null, 2));
|
|
823
|
+
} else {
|
|
824
|
+
console.log(`
|
|
807
825
|
${chalk10.bold("Page Analysis:")} ${result.title}`);
|
|
808
|
-
|
|
826
|
+
console.log(` ${chalk10.gray("URL:")} ${result.url}
|
|
809
827
|
`);
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
828
|
+
if (result.forms.length > 0) {
|
|
829
|
+
console.log(chalk10.bold(" Forms:"));
|
|
830
|
+
for (const form of result.forms) {
|
|
831
|
+
const method = form.method ? chalk10.cyan(form.method) : "";
|
|
832
|
+
const action = form.action ? chalk10.gray(form.action) : "";
|
|
833
|
+
console.log(` [${form.index}] ${method} ${action} (${form.fieldCount} fields)`);
|
|
834
|
+
}
|
|
835
|
+
console.log();
|
|
836
|
+
}
|
|
837
|
+
if (result.elements.length > 0) {
|
|
838
|
+
console.log(chalk10.bold(" Interactive Elements:"));
|
|
839
|
+
for (const el of result.elements) {
|
|
840
|
+
const tag = chalk10.cyan(el.tag);
|
|
841
|
+
const type = el.type ? chalk10.gray(`[${el.type}]`) : "";
|
|
842
|
+
const bestSelector = el.selectors.testId ?? el.selectors.label ?? el.selectors.ariaLabel ?? el.selectors.css ?? el.selectors.name ?? "";
|
|
843
|
+
const selectorStr = bestSelector ? chalk10.yellow(bestSelector) : chalk10.gray("(no selector)");
|
|
844
|
+
const req = el.required ? chalk10.red(" *") : "";
|
|
845
|
+
const form = el.formGroup !== void 0 ? chalk10.gray(` form[${el.formGroup}]`) : "";
|
|
846
|
+
console.log(` ${tag}${type} ${selectorStr}${req}${form}`);
|
|
847
|
+
}
|
|
848
|
+
console.log();
|
|
849
|
+
}
|
|
850
|
+
if (result.links.length > 0) {
|
|
851
|
+
console.log(chalk10.bold(" Links:"));
|
|
852
|
+
for (const link of result.links.slice(0, 20)) {
|
|
853
|
+
const text = link.text || chalk10.gray("(no text)");
|
|
854
|
+
console.log(` ${text} ${chalk10.gray("\u2192")} ${chalk10.blue(link.href)}`);
|
|
855
|
+
}
|
|
856
|
+
if (result.links.length > 20) {
|
|
857
|
+
console.log(chalk10.gray(` ... and ${result.links.length - 20} more`));
|
|
858
|
+
}
|
|
859
|
+
console.log();
|
|
860
|
+
}
|
|
861
|
+
console.log(chalk10.gray(` ${result.elements.length} elements, ${result.forms.length} forms, ${result.links.length} links
|
|
844
862
|
`));
|
|
863
|
+
}
|
|
864
|
+
} finally {
|
|
865
|
+
await closeBrowser(session);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function printMacElement(el) {
|
|
869
|
+
const role = chalk10.cyan(el.role);
|
|
870
|
+
const [best] = el.selectors;
|
|
871
|
+
const selectorStr = best ? chalk10.yellow(best) : chalk10.gray("(no selector)");
|
|
872
|
+
const label = el.title ?? el.description ?? el.value;
|
|
873
|
+
const labelStr = label ? ` ${chalk10.gray(`"${label}"`)}` : "";
|
|
874
|
+
const disabled = el.enabled === false ? chalk10.gray(" (disabled)") : "";
|
|
875
|
+
console.log(` ${role} ${selectorStr}${labelStr}${disabled}`);
|
|
876
|
+
}
|
|
877
|
+
async function runMacAnalyze(app, config, options) {
|
|
878
|
+
const allowedApps = config?.guardrails.allowedApps ?? [];
|
|
879
|
+
assertTargetAppAllowed(allowedApps, app);
|
|
880
|
+
const session = await launchMacSession({ app, timeoutMs: config?.browser.timeout });
|
|
881
|
+
try {
|
|
882
|
+
const result = await analyzeMacApp(session.client, { app: session.bundleId });
|
|
883
|
+
if (options.json) {
|
|
884
|
+
console.log(JSON.stringify(result, null, 2));
|
|
885
|
+
} else {
|
|
886
|
+
console.log(`
|
|
887
|
+
${chalk10.bold("App Analysis:")} ${result.app}
|
|
888
|
+
`);
|
|
889
|
+
if (result.windows.length > 0) {
|
|
890
|
+
console.log(chalk10.bold(" Windows:"));
|
|
891
|
+
for (const win of result.windows) {
|
|
892
|
+
const title = win.title ? chalk10.gray(`"${win.title}"`) : chalk10.gray("(untitled)");
|
|
893
|
+
console.log(` ${title} ${chalk10.yellow(win.selector)}`);
|
|
845
894
|
}
|
|
846
|
-
|
|
847
|
-
|
|
895
|
+
console.log();
|
|
896
|
+
}
|
|
897
|
+
if (result.elements.length > 0) {
|
|
898
|
+
console.log(chalk10.bold(" Interactive Elements:"));
|
|
899
|
+
for (const el of result.elements) {
|
|
900
|
+
printMacElement(el);
|
|
901
|
+
}
|
|
902
|
+
console.log();
|
|
903
|
+
}
|
|
904
|
+
if (result.menuItems.length > 0) {
|
|
905
|
+
console.log(chalk10.bold(" Menu Bar:"));
|
|
906
|
+
for (const el of result.menuItems) {
|
|
907
|
+
printMacElement(el);
|
|
908
|
+
}
|
|
909
|
+
console.log();
|
|
910
|
+
}
|
|
911
|
+
console.log(
|
|
912
|
+
chalk10.gray(
|
|
913
|
+
` ${result.elements.length} elements, ${result.windows.length} windows, ${result.menuItems.length} menu items
|
|
914
|
+
`
|
|
915
|
+
)
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
} finally {
|
|
919
|
+
await session.client.close();
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function buildAnalyzeCommand() {
|
|
923
|
+
const command = new Command8("analyze").argument("[url]", "URL to analyze (web target)").description("Analyze a page or macOS app to discover interactive elements and selectors").option("--json", "Output as JSON").option("--app <app>", "macOS app to analyze: bundle id or /path/to/App.app (macOS 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
|
+
try {
|
|
925
|
+
if (options.app && url) {
|
|
926
|
+
throw new Error("Pass either a URL argument (web) or --app (macOS), not both.");
|
|
848
927
|
}
|
|
928
|
+
if (options.app) {
|
|
929
|
+
await runMacAnalyze(options.app, tryLoadConfig(options.config), options);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (url) {
|
|
933
|
+
await runWebAnalyze(url, options);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const config = tryLoadConfig(options.config);
|
|
937
|
+
if (config?.target.type === "macos") {
|
|
938
|
+
await runMacAnalyze(config.target.app, config, options);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
throw new Error(
|
|
942
|
+
"analyze needs a target: pass a URL for the web target, or use --app <bundle-id|.app-path> (or set target.type: macos in .prowl/config.yml) for the macOS target."
|
|
943
|
+
);
|
|
849
944
|
} catch (error) {
|
|
850
945
|
const message = error instanceof Error ? error.message : "Analysis failed";
|
|
851
946
|
if (options.json) {
|
|
@@ -919,7 +1014,7 @@ function buildGenerateCommand() {
|
|
|
919
1014
|
} else if (options.output) {
|
|
920
1015
|
let configDir;
|
|
921
1016
|
try {
|
|
922
|
-
const { loadConfig: loadConfig2 } = await import("./loader-
|
|
1017
|
+
const { loadConfig: loadConfig2 } = await import("./loader-FCXPARP7.js");
|
|
923
1018
|
const result = loadConfig2(options.config);
|
|
924
1019
|
configDir = result.configDir;
|
|
925
1020
|
} catch {
|