@xbrowser/cli 1.9.5 → 1.9.7
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/dist/{browser-6WJHQDPV.js → browser-ALPLJNH2.js} +1 -1
- package/dist/{browser-4KRHALJ3.js → browser-JXIJF4VL.js} +1 -1
- package/dist/{browser-C63PFGPT.js → browser-ZPQP6VW7.js} +1 -1
- package/dist/{chunk-OOJ2H7IL.js → chunk-37SHZSUR.js} +1 -1
- package/dist/{chunk-MSQIPXKE.js → chunk-5JSOGINQ.js} +1 -1
- package/dist/{chunk-Z7WC4P2M.js → chunk-74S7UUI2.js} +1 -1
- package/dist/{chunk-TTZNR3QP.js → chunk-C4O73DQL.js} +165 -50
- package/dist/{chunk-YSCY52UJ.js → chunk-CH5ZQV64.js} +165 -50
- package/dist/{chunk-53LRNYPG.js → chunk-YAUG3NSI.js} +2 -2
- package/dist/cli.js +82 -18
- package/dist/{daemon-client-PZX2KOLQ.js → daemon-client-4HJENICO.js} +2 -2
- package/dist/{daemon-client-TOUDMIY5.js → daemon-client-CMRAWXTN.js} +1 -1
- package/dist/daemon-main.js +12 -8
- package/dist/index.d.ts +24 -3
- package/dist/index.js +81 -17
- package/dist/{session-recorder-UTULJFTE.js → session-recorder-7GXI42FO.js} +1 -1
- package/dist/{session-recorder-KPU4YQAE.js → session-recorder-RFOPX4SF.js} +1 -1
- package/dist/{session-replayer-RWFOWH3F.js → session-replayer-3HEIJBJ2.js} +65 -33
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1812,6 +1812,13 @@ interface RecordingStep {
|
|
|
1812
1812
|
networkId: number;
|
|
1813
1813
|
paramName: string;
|
|
1814
1814
|
}>;
|
|
1815
|
+
signals: ActionSignal[];
|
|
1816
|
+
}
|
|
1817
|
+
/** Success / verification signal attached to a recording step */
|
|
1818
|
+
interface ActionSignal {
|
|
1819
|
+
type: 'network_success' | 'url_change' | 'dialog' | 'dom_change';
|
|
1820
|
+
value?: string;
|
|
1821
|
+
label: string;
|
|
1815
1822
|
}
|
|
1816
1823
|
interface RecordingSummary {
|
|
1817
1824
|
startUrl: string;
|
|
@@ -1871,12 +1878,17 @@ declare class SessionRecorder {
|
|
|
1871
1878
|
private lastActionTs;
|
|
1872
1879
|
private activePages;
|
|
1873
1880
|
private lastKnownUrl;
|
|
1874
|
-
/** Dedup
|
|
1875
|
-
|
|
1881
|
+
/** Dedup map: key = normalizedType|tag|value → expiration timestamp.
|
|
1882
|
+
* Replaces old single-entry cdpActionDedup for bidirectional dedup. */
|
|
1883
|
+
private dedupMap;
|
|
1884
|
+
private dedupActionCount;
|
|
1876
1885
|
/** Network dedup: last request key for short-window dedup */
|
|
1877
1886
|
private _lastRequestKey;
|
|
1878
1887
|
private _lastRequestTs;
|
|
1879
1888
|
private _isRecording;
|
|
1889
|
+
private streamMode;
|
|
1890
|
+
/** Ambient action types: filtered out in clean mode */
|
|
1891
|
+
private static readonly AMBIENT_TYPES;
|
|
1880
1892
|
constructor(context: XBContext, page: XBPage, sessionName: string);
|
|
1881
1893
|
get isRecording(): boolean;
|
|
1882
1894
|
get actionCount(): number;
|
|
@@ -1888,6 +1900,8 @@ declare class SessionRecorder {
|
|
|
1888
1900
|
url?: string;
|
|
1889
1901
|
element?: UserAction['element'];
|
|
1890
1902
|
}): Promise<void>;
|
|
1903
|
+
/** Generate dedup key for an action (normalized type + selector/tag + value). */
|
|
1904
|
+
private dedupKey;
|
|
1891
1905
|
get networkCount(): number;
|
|
1892
1906
|
getLiveData(): RecordingData;
|
|
1893
1907
|
addManualCheckpoint(type: string, hint: string, selector?: string): CheckpointEntry;
|
|
@@ -1898,7 +1912,9 @@ declare class SessionRecorder {
|
|
|
1898
1912
|
get controlFilePath(): string;
|
|
1899
1913
|
/** Path to the stop signal file (written by `record stop`). */
|
|
1900
1914
|
get stopSignalPath(): string;
|
|
1901
|
-
start(url?: string
|
|
1915
|
+
start(url?: string, options?: {
|
|
1916
|
+
stream?: 'clean' | 'raw';
|
|
1917
|
+
}): Promise<void>;
|
|
1902
1918
|
stop(): Promise<{
|
|
1903
1919
|
data: RecordingData;
|
|
1904
1920
|
summary: RecordingSummary;
|
|
@@ -1932,6 +1948,11 @@ declare class SessionRecorder {
|
|
|
1932
1948
|
*/
|
|
1933
1949
|
private captureClickContext;
|
|
1934
1950
|
private detectCheckpoints;
|
|
1951
|
+
/**
|
|
1952
|
+
* After a key action (click/submit/input), check for success signals:
|
|
1953
|
+
* URL changes, recent successful network responses, dialog appearances.
|
|
1954
|
+
*/
|
|
1955
|
+
private verifyAction;
|
|
1935
1956
|
private flushToDisk;
|
|
1936
1957
|
private writeFinalOutput;
|
|
1937
1958
|
private buildData;
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
startDaemonProcess,
|
|
45
45
|
stopDaemonProcess,
|
|
46
46
|
version
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-YAUG3NSI.js";
|
|
48
48
|
import {
|
|
49
49
|
generateBashScript,
|
|
50
50
|
generateJSScript,
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
} from "./chunk-ANVL2ID2.js";
|
|
62
62
|
import {
|
|
63
63
|
SessionRecorder
|
|
64
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-CH5ZQV64.js";
|
|
65
65
|
import {
|
|
66
66
|
addKnownIssue,
|
|
67
67
|
getKnowledgePath,
|
|
@@ -86,7 +86,7 @@ import {
|
|
|
86
86
|
resolveLaunchOpts,
|
|
87
87
|
saveSessionDiskMeta,
|
|
88
88
|
setActivePage
|
|
89
|
-
} from "./chunk-
|
|
89
|
+
} from "./chunk-5JSOGINQ.js";
|
|
90
90
|
import "./chunk-VJNMAWPZ.js";
|
|
91
91
|
import "./chunk-TNEN6VQ2.js";
|
|
92
92
|
import {
|
|
@@ -7406,7 +7406,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7406
7406
|
}
|
|
7407
7407
|
let targetPageOverride = null;
|
|
7408
7408
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
7409
|
-
const { findTargetPage } = await import("./browser-
|
|
7409
|
+
const { findTargetPage } = await import("./browser-ZPQP6VW7.js");
|
|
7410
7410
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
7411
7411
|
if (!targetPageOverride) {
|
|
7412
7412
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -7423,7 +7423,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7423
7423
|
params = result.data;
|
|
7424
7424
|
}
|
|
7425
7425
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
7426
|
-
const { forwardExec } = await import("./daemon-client-
|
|
7426
|
+
const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
|
|
7427
7427
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
7428
7428
|
if (result) return result;
|
|
7429
7429
|
}
|
|
@@ -8868,7 +8868,7 @@ var PluginInstaller = class {
|
|
|
8868
8868
|
import { outputFormatter, OutputFormatter } from "@dyyz1993/xcli-core";
|
|
8869
8869
|
var formatter = new OutputFormatter();
|
|
8870
8870
|
function outputResult(result, mode = "text") {
|
|
8871
|
-
if (typeof result === "object" && result !== null) {
|
|
8871
|
+
if (mode !== "json" && mode !== "yaml" && typeof result === "object" && result !== null) {
|
|
8872
8872
|
const r = result;
|
|
8873
8873
|
if (r.success === false) {
|
|
8874
8874
|
outputError(r.message ? String(r.message) : "Unknown error");
|
|
@@ -10711,6 +10711,50 @@ async function infoFromMarketplacePlugin(slug, loader) {
|
|
|
10711
10711
|
}
|
|
10712
10712
|
return null;
|
|
10713
10713
|
}
|
|
10714
|
+
function normalizePluginName(name) {
|
|
10715
|
+
return name.replace(/^@[^/]+\//, "").replace(/^xbrowser-plugin-/, "").toLowerCase();
|
|
10716
|
+
}
|
|
10717
|
+
async function searchLocalPlugins(query) {
|
|
10718
|
+
let installed;
|
|
10719
|
+
try {
|
|
10720
|
+
installed = await new PluginInstaller().list();
|
|
10721
|
+
} catch {
|
|
10722
|
+
return [];
|
|
10723
|
+
}
|
|
10724
|
+
if (!installed || installed.length === 0) return [];
|
|
10725
|
+
const q = normalizePluginName(query);
|
|
10726
|
+
if (!q) return [];
|
|
10727
|
+
const results = [];
|
|
10728
|
+
for (const p of installed) {
|
|
10729
|
+
const plugin = p;
|
|
10730
|
+
const name = String(plugin.name ?? "");
|
|
10731
|
+
const norm = normalizePluginName(name);
|
|
10732
|
+
if (norm === q) {
|
|
10733
|
+
results.push({ plugin, rank: 0 });
|
|
10734
|
+
} else if (norm.startsWith(q) || q.startsWith(norm)) {
|
|
10735
|
+
results.push({ plugin, rank: 1 });
|
|
10736
|
+
} else if (norm.includes(q) || q.includes(norm)) {
|
|
10737
|
+
results.push({ plugin, rank: 2 });
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
10740
|
+
results.sort((a, b) => a.rank - b.rank);
|
|
10741
|
+
const runtimeInfo = await buildRuntimePluginInfo().catch(() => /* @__PURE__ */ new Map());
|
|
10742
|
+
return results.map(({ plugin }) => {
|
|
10743
|
+
const name = String(plugin.name ?? "");
|
|
10744
|
+
const metadata = plugin.metadata;
|
|
10745
|
+
const staticCommands = metadata?.commands;
|
|
10746
|
+
const rt = runtimeInfo.get(name);
|
|
10747
|
+
const commands = rt?.commands || staticCommands;
|
|
10748
|
+
return {
|
|
10749
|
+
name: "@xbrowser/" + name,
|
|
10750
|
+
slug: name,
|
|
10751
|
+
description: metadata?.description,
|
|
10752
|
+
version: metadata?.version,
|
|
10753
|
+
commands,
|
|
10754
|
+
source: "local"
|
|
10755
|
+
};
|
|
10756
|
+
});
|
|
10757
|
+
}
|
|
10714
10758
|
async function handleSearch(args, options, mode) {
|
|
10715
10759
|
const query = args[0] || "";
|
|
10716
10760
|
applyRegistryOverride(options);
|
|
@@ -10718,6 +10762,8 @@ async function handleSearch(args, options, mode) {
|
|
|
10718
10762
|
const searchLimit = options.limit ? Number(options.limit) : 20;
|
|
10719
10763
|
const searchOpts = { query, tag: options.tag, site: options.site, limit: searchLimit };
|
|
10720
10764
|
const results = [];
|
|
10765
|
+
const localResults = await searchLocalPlugins(query);
|
|
10766
|
+
results.push(...localResults);
|
|
10721
10767
|
const loader = await getPluginLoader();
|
|
10722
10768
|
const pluginResults = await searchFromMarketplacePlugin2(searchOpts, loader);
|
|
10723
10769
|
results.push(...pluginResults);
|
|
@@ -10730,15 +10776,24 @@ async function handleSearch(args, options, mode) {
|
|
|
10730
10776
|
} catch {
|
|
10731
10777
|
}
|
|
10732
10778
|
}
|
|
10779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10780
|
+
const deduped = [];
|
|
10781
|
+
for (const r of results) {
|
|
10782
|
+
const key = normalizePluginName(String(r.name ?? ""));
|
|
10783
|
+
if (key && !seen.has(key)) {
|
|
10784
|
+
seen.add(key);
|
|
10785
|
+
deduped.push(r);
|
|
10786
|
+
}
|
|
10787
|
+
}
|
|
10733
10788
|
if (mode === "json") {
|
|
10734
|
-
outputEnvelope({ success: true, data: { results, total:
|
|
10789
|
+
outputEnvelope({ success: true, data: { results: deduped, total: deduped.length } }, { command: "plugin search" }, mode);
|
|
10735
10790
|
} else {
|
|
10736
|
-
if (
|
|
10791
|
+
if (deduped.length === 0) {
|
|
10737
10792
|
console.log("No plugins found");
|
|
10738
10793
|
return;
|
|
10739
10794
|
}
|
|
10740
|
-
for (const r of
|
|
10741
|
-
const src = r.source === "marketplace" ? "[marketplace]" : "[npm]";
|
|
10795
|
+
for (const r of deduped) {
|
|
10796
|
+
const src = r.source === "marketplace" ? "[marketplace]" : r.source === "local" ? "[local]" : "[npm]";
|
|
10742
10797
|
const slug = r.slug ? ` (${r.slug})` : "";
|
|
10743
10798
|
console.log(` ${src} ${r.name}${slug}`);
|
|
10744
10799
|
if (r.description) console.log(` ${r.description}`);
|
|
@@ -10746,7 +10801,7 @@ async function handleSearch(args, options, mode) {
|
|
|
10746
10801
|
if (r.downloads) console.log(` Downloads: ${r.downloads}`);
|
|
10747
10802
|
console.log("");
|
|
10748
10803
|
}
|
|
10749
|
-
console.log(`Total: ${
|
|
10804
|
+
console.log(`Total: ${deduped.length} plugins`);
|
|
10750
10805
|
}
|
|
10751
10806
|
}
|
|
10752
10807
|
async function handlePluginInfo(args, options, mode) {
|
|
@@ -10882,7 +10937,7 @@ async function handlePlugin(args, options, mode) {
|
|
|
10882
10937
|
} catch {
|
|
10883
10938
|
}
|
|
10884
10939
|
try {
|
|
10885
|
-
const { daemonPing } = await import("./daemon-client-
|
|
10940
|
+
const { daemonPing } = await import("./daemon-client-CMRAWXTN.js");
|
|
10886
10941
|
if (await daemonPing()) {
|
|
10887
10942
|
await fetch("http://localhost:9224/rpc", {
|
|
10888
10943
|
method: "POST",
|
|
@@ -11040,7 +11095,8 @@ async function handleRecord(args, options, mode) {
|
|
|
11040
11095
|
const url = options.url;
|
|
11041
11096
|
const sessionName = options.session || "default";
|
|
11042
11097
|
const cdpEndpoint = options.cdp;
|
|
11043
|
-
const
|
|
11098
|
+
const stream = options.stream || "clean";
|
|
11099
|
+
const result = await forwardRecordStart(sessionName, url, cdpEndpoint, stream);
|
|
11044
11100
|
if (!result.ok) {
|
|
11045
11101
|
outputError(String(result.error || "Failed to start recording"));
|
|
11046
11102
|
return;
|
|
@@ -11335,7 +11391,15 @@ async function handleReplay(args, options, mode) {
|
|
|
11335
11391
|
const absPath = await import("path").then((p) => p.resolve(filePath));
|
|
11336
11392
|
const result = await forwardReplay(absPath, sessionName, slowMo);
|
|
11337
11393
|
if (!result.ok) {
|
|
11338
|
-
|
|
11394
|
+
if (mode === "json" || mode === "yaml") {
|
|
11395
|
+
outputResult({
|
|
11396
|
+
...result,
|
|
11397
|
+
success: false,
|
|
11398
|
+
message: result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"
|
|
11399
|
+
}, mode);
|
|
11400
|
+
} else {
|
|
11401
|
+
outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
|
|
11402
|
+
}
|
|
11339
11403
|
return;
|
|
11340
11404
|
}
|
|
11341
11405
|
outputResult(result, mode);
|
|
@@ -11439,7 +11503,7 @@ async function handleFilter(args, _mode, options) {
|
|
|
11439
11503
|
}
|
|
11440
11504
|
}
|
|
11441
11505
|
async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
|
|
11442
|
-
const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-
|
|
11506
|
+
const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RFOPX4SF.js");
|
|
11443
11507
|
const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
|
|
11444
11508
|
const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync13 } = await import("fs");
|
|
11445
11509
|
const { join: join14 } = await import("path");
|
|
@@ -13376,7 +13440,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
13376
13440
|
}
|
|
13377
13441
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
13378
13442
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
13379
|
-
const { forwardExec } = await import("./daemon-client-
|
|
13443
|
+
const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
|
|
13380
13444
|
const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
|
|
13381
13445
|
const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
|
|
13382
13446
|
const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
|
|
@@ -13416,7 +13480,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
13416
13480
|
const targetPage = pages[cmdTabIndex];
|
|
13417
13481
|
await targetPage.bringToFront().catch(() => {
|
|
13418
13482
|
});
|
|
13419
|
-
const { setActivePage: setActivePage2 } = await import("./browser-
|
|
13483
|
+
const { setActivePage: setActivePage2 } = await import("./browser-ZPQP6VW7.js");
|
|
13420
13484
|
setActivePage2(session, targetPage);
|
|
13421
13485
|
}
|
|
13422
13486
|
}
|
|
@@ -56,15 +56,17 @@ var SessionReplayer = class {
|
|
|
56
56
|
await this.replayTrajectory(action.trajectory);
|
|
57
57
|
}
|
|
58
58
|
await this.replayAction(action);
|
|
59
|
+
if (action.type !== "resize" && action.type !== "clipboard" && action.type !== "visibility") {
|
|
60
|
+
try {
|
|
61
|
+
await this.page.waitForLoadState("domcontentloaded", this.opts.stepTimeout);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
59
65
|
success++;
|
|
60
66
|
} catch (e) {
|
|
61
67
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
62
68
|
this.opts.onError?.(action, err);
|
|
63
|
-
|
|
64
|
-
skipped++;
|
|
65
|
-
} else {
|
|
66
|
-
failed++;
|
|
67
|
-
}
|
|
69
|
+
failed++;
|
|
68
70
|
}
|
|
69
71
|
if (i < actions.length - 1) {
|
|
70
72
|
await new Promise((r) => setTimeout(r, this.opts.stepDelay));
|
|
@@ -78,41 +80,36 @@ var SessionReplayer = class {
|
|
|
78
80
|
const timeout = this.opts.stepTimeout;
|
|
79
81
|
switch (action.type) {
|
|
80
82
|
case "navigation":
|
|
81
|
-
await page.goto(action.url, { waitUntil: "
|
|
83
|
+
await page.goto(action.url, { waitUntil: "load", timeout });
|
|
82
84
|
break;
|
|
83
85
|
case "goto":
|
|
84
|
-
await page.goto(action.url, { waitUntil: "
|
|
86
|
+
await page.goto(action.url, { waitUntil: "load", timeout });
|
|
85
87
|
break;
|
|
86
88
|
case "click":
|
|
87
89
|
case "cdp-click": {
|
|
88
|
-
const selector = this.
|
|
89
|
-
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
90
|
+
const selector = await this.resolveAndWait(action);
|
|
90
91
|
await page.click(selector, { timeout });
|
|
91
92
|
break;
|
|
92
93
|
}
|
|
93
94
|
case "input": {
|
|
94
|
-
const selector = this.
|
|
95
|
-
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
95
|
+
const selector = await this.resolveAndWait(action);
|
|
96
96
|
await page.fill(selector, action.value ?? "", { timeout });
|
|
97
97
|
break;
|
|
98
98
|
}
|
|
99
99
|
case "cdp-fill": {
|
|
100
|
-
const selector = this.
|
|
101
|
-
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
100
|
+
const selector = await this.resolveAndWait(action);
|
|
102
101
|
await page.fill(selector, action.value ?? "", { timeout });
|
|
103
102
|
break;
|
|
104
103
|
}
|
|
105
104
|
case "change": {
|
|
106
|
-
const selector = this.
|
|
107
|
-
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
105
|
+
const selector = await this.resolveAndWait(action);
|
|
108
106
|
if (action.value) {
|
|
109
107
|
await page.selectOption(selector, action.value);
|
|
110
108
|
}
|
|
111
109
|
break;
|
|
112
110
|
}
|
|
113
111
|
case "filechooser": {
|
|
114
|
-
const selector = this.
|
|
115
|
-
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
112
|
+
const selector = await this.resolveAndWait(action);
|
|
116
113
|
const files = this.resolveFiles(action);
|
|
117
114
|
if (files.length > 0) {
|
|
118
115
|
await page.setInputFiles(selector, files);
|
|
@@ -155,11 +152,8 @@ var SessionReplayer = class {
|
|
|
155
152
|
break;
|
|
156
153
|
}
|
|
157
154
|
case "hover": {
|
|
158
|
-
const selector = this.
|
|
159
|
-
|
|
160
|
-
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
161
|
-
await page.hover(selector);
|
|
162
|
-
}
|
|
155
|
+
const selector = await this.resolveAndWait(action);
|
|
156
|
+
await page.hover(selector);
|
|
163
157
|
break;
|
|
164
158
|
}
|
|
165
159
|
case "drag": {
|
|
@@ -199,10 +193,9 @@ var SessionReplayer = class {
|
|
|
199
193
|
break;
|
|
200
194
|
}
|
|
201
195
|
case "focus": {
|
|
202
|
-
const selector = this.
|
|
203
|
-
if (
|
|
204
|
-
await page.
|
|
205
|
-
await page.click(selector, { timeout });
|
|
196
|
+
const selector = await this.resolveAndWait(action);
|
|
197
|
+
if (action.focus?.focusType === "focus") {
|
|
198
|
+
await page.locator(selector).focus();
|
|
206
199
|
}
|
|
207
200
|
break;
|
|
208
201
|
}
|
|
@@ -210,10 +203,16 @@ var SessionReplayer = class {
|
|
|
210
203
|
break;
|
|
211
204
|
}
|
|
212
205
|
case "submit": {
|
|
213
|
-
const selector = this.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
206
|
+
const selector = await this.resolveAndWait(action);
|
|
207
|
+
await page.evaluate((sel) => {
|
|
208
|
+
const form = document.querySelector(sel);
|
|
209
|
+
if (!form) return;
|
|
210
|
+
if (typeof form.requestSubmit === "function") {
|
|
211
|
+
form.requestSubmit();
|
|
212
|
+
} else {
|
|
213
|
+
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
|
|
214
|
+
}
|
|
215
|
+
}, selector);
|
|
217
216
|
break;
|
|
218
217
|
}
|
|
219
218
|
case "scroll": {
|
|
@@ -239,12 +238,10 @@ var SessionReplayer = class {
|
|
|
239
238
|
await page.mouse.move(x, y);
|
|
240
239
|
}
|
|
241
240
|
}
|
|
242
|
-
/** Resolve the best selector for an action */
|
|
241
|
+
/** Resolve the best selector for an action (primary selector only) */
|
|
243
242
|
resolveSelector(action) {
|
|
244
243
|
const el = action.element;
|
|
245
244
|
if (!el) return "";
|
|
246
|
-
if (el.textFallback) {
|
|
247
|
-
}
|
|
248
245
|
if (el.selector) {
|
|
249
246
|
return el.selector;
|
|
250
247
|
}
|
|
@@ -253,6 +250,41 @@ var SessionReplayer = class {
|
|
|
253
250
|
}
|
|
254
251
|
return "";
|
|
255
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* X2: Wait for an element using the best available selector, with
|
|
255
|
+
* confidence-based fallback support.
|
|
256
|
+
*
|
|
257
|
+
* Returns the first matching selector, or throws if none match.
|
|
258
|
+
* Fallback order:
|
|
259
|
+
* 1. Primary CSS selector (always tried first)
|
|
260
|
+
* 2. textFallback selector (for low-confidence selectors when primary fails)
|
|
261
|
+
* 3. Tag-based fallback (last resort)
|
|
262
|
+
*/
|
|
263
|
+
async resolveAndWait(action) {
|
|
264
|
+
const el = action.element;
|
|
265
|
+
if (!el) throw new Error("No element metadata");
|
|
266
|
+
const page = this.page;
|
|
267
|
+
const timeout = this.opts.stepTimeout;
|
|
268
|
+
const candidates = [];
|
|
269
|
+
if (el.selector) candidates.push(el.selector);
|
|
270
|
+
if (el.confidence === "low" && el.textFallback?.selector) {
|
|
271
|
+
if (!candidates.includes(el.textFallback.selector)) {
|
|
272
|
+
candidates.push(el.textFallback.selector);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (el.tag && !candidates.includes(el.tag)) {
|
|
276
|
+
candidates.push(el.tag);
|
|
277
|
+
}
|
|
278
|
+
if (candidates.length === 0) throw new Error("No selector available for element");
|
|
279
|
+
for (const sel of candidates) {
|
|
280
|
+
try {
|
|
281
|
+
await page.waitForSelector(sel, { state: "visible", timeout });
|
|
282
|
+
return sel;
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
throw new Error(`Element not found, tried: ${candidates.join(", ")}`);
|
|
287
|
+
}
|
|
256
288
|
/** Resolve file payloads from a filechooser action */
|
|
257
289
|
resolveFiles(action) {
|
|
258
290
|
if (!action.files?.fileData) return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbrowser/cli",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.7",
|
|
4
4
|
"description": "Browser automation CLI for web scraping, headless browsing, SEO analysis, and AI agent workflows. A command-line alternative to Playwright, Puppeteer, and Selenium.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|