prowl-tools 0.1.3 → 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/LICENSE +1 -1
- package/NOTICE +11 -1
- package/README.md +390 -3
- package/dist/chunk-2KD2XCTH.js +6202 -0
- package/dist/chunk-2KD2XCTH.js.map +1 -0
- package/dist/{chunk-NXXGJOBG.js → chunk-ITOSUJCN.js} +74 -13
- package/dist/chunk-ITOSUJCN.js.map +1 -0
- package/dist/index.cjs +4249 -1116
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +184 -78
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +3988 -816
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +1052 -17
- package/dist/lib.d.ts +1052 -17
- package/dist/lib.js +152 -4
- package/dist/{loader-5RDNTJHH.js → loader-FCXPARP7.js} +2 -2
- package/package.json +6 -2
- package/dist/chunk-NXXGJOBG.js.map +0 -1
- package/dist/chunk-T7YLXF6X.js +0 -3158
- package/dist/chunk-T7YLXF6X.js.map +0 -1
- /package/dist/{loader-5RDNTJHH.js.map → loader-FCXPARP7.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_FLAKY_THRESHOLD,
|
|
4
|
+
analyzeMacApp,
|
|
4
5
|
analyzePage,
|
|
6
|
+
assertTargetAppAllowed,
|
|
7
|
+
closeBrowser,
|
|
8
|
+
createPlaywrightDriver,
|
|
5
9
|
generateHunt,
|
|
10
|
+
launchBrowser,
|
|
11
|
+
launchMacSession,
|
|
12
|
+
parseBrowserEngine,
|
|
6
13
|
printCiSummary,
|
|
7
14
|
rankFlaky,
|
|
8
15
|
readHuntHistory,
|
|
9
16
|
runHunt,
|
|
10
17
|
runSuite,
|
|
18
|
+
saveStorageState,
|
|
11
19
|
updateBacklogFromSuite
|
|
12
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-2KD2XCTH.js";
|
|
13
21
|
import {
|
|
14
22
|
CONFIG_DIR,
|
|
23
|
+
findConfigPath,
|
|
15
24
|
listHunts,
|
|
16
25
|
loadConfig,
|
|
17
26
|
loadHuntMeta,
|
|
18
27
|
loadHuntTags,
|
|
19
28
|
resolveViewport
|
|
20
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-ITOSUJCN.js";
|
|
21
30
|
|
|
22
31
|
// src/cli/program.ts
|
|
23
32
|
import { Command as Command13 } from "commander";
|
|
@@ -25,11 +34,11 @@ import { Command as Command13 } from "commander";
|
|
|
25
34
|
// package.json
|
|
26
35
|
var package_default = {
|
|
27
36
|
name: "prowl-tools",
|
|
28
|
-
version: "0.1.
|
|
37
|
+
version: "0.1.5",
|
|
29
38
|
description: "CLI-first QA testing tool for deterministic Playwright flows.",
|
|
30
39
|
type: "module",
|
|
31
40
|
license: "Apache-2.0",
|
|
32
|
-
author: "
|
|
41
|
+
author: "Prowl Tools",
|
|
33
42
|
repository: {
|
|
34
43
|
type: "git",
|
|
35
44
|
url: "https://github.com/prowl-tools/prowl.git"
|
|
@@ -95,6 +104,10 @@ var package_default = {
|
|
|
95
104
|
yaml: "^2.6.1",
|
|
96
105
|
zod: "^3.23.8"
|
|
97
106
|
},
|
|
107
|
+
optionalDependencies: {
|
|
108
|
+
"appium-uiautomator2-server": "10.6.2",
|
|
109
|
+
"appium-webdriveragent": "16.4.0"
|
|
110
|
+
},
|
|
98
111
|
devDependencies: {
|
|
99
112
|
"@types/node": "^22.13.1",
|
|
100
113
|
"@types/pngjs": "^6.0.5",
|
|
@@ -396,7 +409,6 @@ import path2 from "path";
|
|
|
396
409
|
import readline from "readline";
|
|
397
410
|
import chalk5 from "chalk";
|
|
398
411
|
import { Command as Command3 } from "commander";
|
|
399
|
-
import { chromium } from "playwright";
|
|
400
412
|
function resolvePath(configDir, inputPath) {
|
|
401
413
|
if (path2.isAbsolute(inputPath)) {
|
|
402
414
|
return inputPath;
|
|
@@ -415,30 +427,35 @@ function waitForEnter(prompt) {
|
|
|
415
427
|
}
|
|
416
428
|
function buildLoginCommand() {
|
|
417
429
|
const command = new Command3("login").option("--url <target>", "Override target URL").option("--config <path>", "Custom config path").action(async (options) => {
|
|
418
|
-
let
|
|
419
|
-
let context = null;
|
|
430
|
+
let session = null;
|
|
420
431
|
try {
|
|
421
432
|
const { config, configDir } = loadConfig(options.config);
|
|
433
|
+
if (config.target.type !== "web") {
|
|
434
|
+
throw new Error("`prowl login` captures browser auth state and only applies to web targets.");
|
|
435
|
+
}
|
|
422
436
|
const targetUrl = options.url ?? config.target.url;
|
|
423
437
|
const storageStatePath = config.auth.storageStatePath ? resolvePath(configDir, config.auth.storageStatePath) : resolvePath(configDir, ".prowl/auth-state.json");
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
438
|
+
session = await launchBrowser({
|
|
439
|
+
headless: false,
|
|
440
|
+
slowMo: 0,
|
|
441
|
+
timeout: config.browser.timeout,
|
|
442
|
+
trace: false,
|
|
443
|
+
recordHar: false,
|
|
444
|
+
runDir: configDir
|
|
445
|
+
});
|
|
446
|
+
const driver = createPlaywrightDriver(session.page);
|
|
447
|
+
await driver.goto(targetUrl);
|
|
428
448
|
console.log(chalk5.green("Browser opened. Log in manually."));
|
|
429
449
|
await waitForEnter("Press Enter to save auth state and close the browser... ");
|
|
430
|
-
await
|
|
450
|
+
await saveStorageState(session, storageStatePath);
|
|
431
451
|
console.log(chalk5.green(`Saved auth state to ${storageStatePath}`));
|
|
432
452
|
} catch (error) {
|
|
433
453
|
const message = error instanceof Error ? error.message : "Login failed";
|
|
434
454
|
console.error(chalk5.red(`Error: ${message}`));
|
|
435
455
|
process.exitCode = 1;
|
|
436
456
|
} finally {
|
|
437
|
-
if (
|
|
438
|
-
await
|
|
439
|
-
}
|
|
440
|
-
if (browser) {
|
|
441
|
-
await browser.close();
|
|
457
|
+
if (session) {
|
|
458
|
+
await closeBrowser(session);
|
|
442
459
|
}
|
|
443
460
|
}
|
|
444
461
|
});
|
|
@@ -763,8 +780,6 @@ function buildUpdateBaselinesCommand() {
|
|
|
763
780
|
// src/cli/commands/analyze.ts
|
|
764
781
|
import { Command as Command8 } from "commander";
|
|
765
782
|
import chalk10 from "chalk";
|
|
766
|
-
import { chromium as chromium2, firefox, webkit } from "playwright";
|
|
767
|
-
var ENGINES = { chromium: chromium2, firefox, webkit };
|
|
768
783
|
function parseViewportFlag(value) {
|
|
769
784
|
const match = /^(\d+)x(\d+)$/i.exec(value);
|
|
770
785
|
if (match) {
|
|
@@ -772,69 +787,160 @@ function parseViewportFlag(value) {
|
|
|
772
787
|
}
|
|
773
788
|
return value;
|
|
774
789
|
}
|
|
775
|
-
function
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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(`
|
|
795
825
|
${chalk10.bold("Page Analysis:")} ${result.title}`);
|
|
796
|
-
|
|
826
|
+
console.log(` ${chalk10.gray("URL:")} ${result.url}
|
|
797
827
|
`);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
|
832
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)}`);
|
|
894
|
+
}
|
|
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);
|
|
833
901
|
}
|
|
834
|
-
|
|
835
|
-
await context.close();
|
|
836
|
-
await browser.close();
|
|
902
|
+
console.log();
|
|
837
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.");
|
|
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
|
+
);
|
|
838
944
|
} catch (error) {
|
|
839
945
|
const message = error instanceof Error ? error.message : "Analysis failed";
|
|
840
946
|
if (options.json) {
|
|
@@ -908,7 +1014,7 @@ function buildGenerateCommand() {
|
|
|
908
1014
|
} else if (options.output) {
|
|
909
1015
|
let configDir;
|
|
910
1016
|
try {
|
|
911
|
-
const { loadConfig: loadConfig2 } = await import("./loader-
|
|
1017
|
+
const { loadConfig: loadConfig2 } = await import("./loader-FCXPARP7.js");
|
|
912
1018
|
const result = loadConfig2(options.config);
|
|
913
1019
|
configDir = result.configDir;
|
|
914
1020
|
} catch {
|