cbrowser 19.5.0 → 20.0.0
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 +292 -232
- package/THIRD_PARTY_LICENSES.md +455 -7
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +2 -0
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
- package/dist/analysis/competitive-benchmark.js +70 -558
- package/dist/analysis/competitive-benchmark.js.map +1 -1
- package/dist/analysis/natural-language.d.ts.map +1 -1
- package/dist/analysis/natural-language.js +43 -0
- package/dist/analysis/natural-language.js.map +1 -1
- package/dist/analysis/persona-comparison.d.ts +7 -0
- package/dist/analysis/persona-comparison.d.ts.map +1 -1
- package/dist/analysis/persona-comparison.js +8 -1
- package/dist/analysis/persona-comparison.js.map +1 -1
- package/dist/browser-options.d.ts +23 -0
- package/dist/browser-options.d.ts.map +1 -0
- package/dist/browser-options.js +24 -0
- package/dist/browser-options.js.map +1 -0
- package/dist/browser.d.ts +36 -14
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +172 -202
- package/dist/browser.js.map +1 -1
- package/dist/cli-args.d.ts +9 -1
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +35 -7
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-browser-flags.d.ts +33 -0
- package/dist/cli-browser-flags.d.ts.map +1 -0
- package/dist/cli-browser-flags.js +89 -0
- package/dist/cli-browser-flags.js.map +1 -0
- package/dist/cli.js +1058 -478
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/index.d.ts +5 -13
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +44 -15
- package/dist/cognitive/index.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -6
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +9 -5
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +102 -36
- package/dist/daemon.js.map +1 -1
- package/dist/device-names.d.ts +10 -0
- package/dist/device-names.d.ts.map +1 -0
- package/dist/device-names.js +16 -0
- package/dist/device-names.js.map +1 -0
- package/dist/device-presets-devtools.d.ts +3 -0
- package/dist/device-presets-devtools.d.ts.map +1 -0
- package/dist/device-presets-devtools.js +823 -0
- package/dist/device-presets-devtools.js.map +1 -0
- package/dist/devices.d.ts +35 -0
- package/dist/devices.d.ts.map +1 -0
- package/dist/devices.js +214 -0
- package/dist/devices.js.map +1 -0
- package/dist/eval-script.d.ts +29 -0
- package/dist/eval-script.d.ts.map +1 -0
- package/dist/eval-script.js +46 -0
- package/dist/eval-script.js.map +1 -0
- package/dist/help-client.d.ts +48 -0
- package/dist/help-client.d.ts.map +1 -0
- package/dist/help-client.js +96 -0
- package/dist/help-client.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +165 -2281
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools/base/advanced-interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/advanced-interaction-tools.js +21 -21
- package/dist/mcp-tools/base/advanced-interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.js +6 -3
- package/dist/mcp-tools/base/assertion-tools.js.map +1 -1
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +15 -4
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.js +33 -27
- package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/bug-analysis-tools.js +9 -2
- package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/capture-tools.d.ts +60 -4
- package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/capture-tools.js +118 -14
- package/dist/mcp-tools/base/capture-tools.js.map +1 -1
- package/dist/mcp-tools/base/index.d.ts +6 -5
- package/dist/mcp-tools/base/index.d.ts.map +1 -1
- package/dist/mcp-tools/base/index.js +7 -6
- package/dist/mcp-tools/base/index.js.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.js +20 -6
- package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.d.ts +1 -1
- package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.js +83 -3
- package/dist/mcp-tools/base/navigation-tools.js.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.js +11 -23
- package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +13 -3
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/mcp-tools/enterprise-stubs.d.ts +2 -2
- package/dist/mcp-tools/enterprise-stubs.d.ts.map +1 -1
- package/dist/mcp-tools/enterprise-stubs.js +29 -16
- package/dist/mcp-tools/enterprise-stubs.js.map +1 -1
- package/dist/mcp-tools/index.d.ts +19 -14
- package/dist/mcp-tools/index.d.ts.map +1 -1
- package/dist/mcp-tools/index.js +2 -2
- package/dist/mcp-tools/index.js.map +1 -1
- package/dist/mcp-tools/red-zone-refusal.d.ts +33 -0
- package/dist/mcp-tools/red-zone-refusal.d.ts.map +1 -0
- package/dist/mcp-tools/red-zone-refusal.js +43 -0
- package/dist/mcp-tools/red-zone-refusal.js.map +1 -0
- package/dist/mcp-tools/tier-gate.js +1 -1
- package/dist/mcp-tools/tier-gate.js.map +1 -1
- package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
- package/dist/mcp-tools/tool-categories.js +2 -0
- package/dist/mcp-tools/tool-categories.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +18 -2
- package/dist/output.js.map +1 -1
- package/dist/performance/metrics.d.ts +8 -3
- package/dist/performance/metrics.d.ts.map +1 -1
- package/dist/performance/metrics.js +16 -5
- package/dist/performance/metrics.js.map +1 -1
- package/dist/recording/contact-sheet.js.map +1 -1
- package/dist/security/tool-permissions.d.ts.map +1 -1
- package/dist/security/tool-permissions.js +1 -0
- package/dist/security/tool-permissions.js.map +1 -1
- package/dist/testing/coverage.d.ts +2 -2
- package/dist/testing/coverage.d.ts.map +1 -1
- package/dist/testing/coverage.js +6 -2
- package/dist/testing/coverage.js.map +1 -1
- package/dist/testing/flaky-detection.d.ts +2 -2
- package/dist/testing/flaky-detection.d.ts.map +1 -1
- package/dist/testing/flaky-detection.js +11 -3
- package/dist/testing/flaky-detection.js.map +1 -1
- package/dist/testing/nl-test-suite.d.ts +20 -0
- package/dist/testing/nl-test-suite.d.ts.map +1 -1
- package/dist/testing/nl-test-suite.js +7 -1
- package/dist/testing/nl-test-suite.js.map +1 -1
- package/dist/testing/suite-results.d.ts +27 -0
- package/dist/testing/suite-results.d.ts.map +1 -0
- package/dist/testing/suite-results.js +54 -0
- package/dist/testing/suite-results.js.map +1 -0
- package/dist/testing/test-repair.d.ts +2 -2
- package/dist/testing/test-repair.d.ts.map +1 -1
- package/dist/testing/test-repair.js +6 -2
- package/dist/testing/test-repair.js.map +1 -1
- package/dist/types.d.ts +61 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -103
- package/dist/types.js.map +1 -1
- package/dist/visual/ab-comparison.d.ts.map +1 -1
- package/dist/visual/ab-comparison.js +7 -2
- package/dist/visual/ab-comparison.js.map +1 -1
- package/dist/visual/cross-browser.d.ts +7 -2
- package/dist/visual/cross-browser.d.ts.map +1 -1
- package/dist/visual/cross-browser.js +33 -5
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/distance-metrics.d.ts +7 -0
- package/dist/visual/distance-metrics.d.ts.map +1 -1
- package/dist/visual/distance-metrics.js.map +1 -1
- package/dist/visual/heatmap-overlay.js.map +1 -1
- package/dist/visual/llm-relevance.d.ts.map +1 -1
- package/dist/visual/llm-relevance.js.map +1 -1
- package/dist/visual/regression.d.ts +17 -7
- package/dist/visual/regression.d.ts.map +1 -1
- package/dist/visual/regression.js +77 -38
- package/dist/visual/regression.js.map +1 -1
- package/dist/visual/responsive.d.ts +22 -0
- package/dist/visual/responsive.d.ts.map +1 -1
- package/dist/visual/responsive.js +44 -7
- package/dist/visual/responsive.js.map +1 -1
- package/dist/visual/visual-overlays.js.map +1 -1
- package/dist/webhooks.d.ts +67 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +128 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +23 -24
package/dist/browser.js
CHANGED
|
@@ -15,7 +15,8 @@ import { join, dirname } from "path";
|
|
|
15
15
|
import { execSync } from "child_process";
|
|
16
16
|
import { mergeConfig, getPaths, ensureDirectories } from "./config.js";
|
|
17
17
|
import { getPersonaOrRefuse } from "./personas.js";
|
|
18
|
-
import {
|
|
18
|
+
import { LOCATION_PRESETS, CBrowserError, CBrowserErrorCode } from "./types.js";
|
|
19
|
+
import { DEVICE_PRESETS, resolveDevice } from "./devices.js";
|
|
19
20
|
import { runCognitiveJourney, isApiKeyConfigured, } from "./cognitive/index.js";
|
|
20
21
|
import { SessionManager } from "./browser/session-manager.js";
|
|
21
22
|
import { SelectorCacheManager } from "./browser/selector-cache.js";
|
|
@@ -258,6 +259,8 @@ export class CBrowser {
|
|
|
258
259
|
// =========================================================================
|
|
259
260
|
// Session State Persistence
|
|
260
261
|
// =========================================================================
|
|
262
|
+
/** True when this run was given --device or --viewport (see launch). */
|
|
263
|
+
explicitPageShape = false;
|
|
261
264
|
get sessionStateFile() {
|
|
262
265
|
return join(this.paths.dataDir, "browser-state", "last-session.json");
|
|
263
266
|
}
|
|
@@ -317,11 +320,22 @@ export class CBrowser {
|
|
|
317
320
|
// Don't save about:blank or empty URLs
|
|
318
321
|
if (!url || url === "about:blank" || url === "")
|
|
319
322
|
return;
|
|
323
|
+
// Device and location persist only when set on purpose (`device set`,
|
|
324
|
+
// `geo set`), so carry those over from the file. Saving this run's
|
|
325
|
+
// values made a one-off --device or --geo stick to every later command
|
|
326
|
+
// for an hour (audit 2026-10-21).
|
|
327
|
+
let previous = null;
|
|
328
|
+
try {
|
|
329
|
+
if (existsSync(this.sessionStateFile))
|
|
330
|
+
previous = JSON.parse(readFileSync(this.sessionStateFile, "utf-8"));
|
|
331
|
+
}
|
|
332
|
+
catch { /* unreadable: start fresh */ }
|
|
320
333
|
const state = {
|
|
321
334
|
url,
|
|
322
335
|
timestamp: Date.now(),
|
|
323
336
|
viewport,
|
|
324
|
-
device: device
|
|
337
|
+
device: device ?? previous?.device,
|
|
338
|
+
...(previous?.geolocation ? { geolocation: previous.geolocation } : {}),
|
|
325
339
|
};
|
|
326
340
|
const stateDir = join(this.paths.dataDir, "browser-state");
|
|
327
341
|
if (!existsSync(stateDir)) {
|
|
@@ -362,6 +376,30 @@ export class CBrowser {
|
|
|
362
376
|
console.error(`Failed to save device setting: ${e.message}`);
|
|
363
377
|
}
|
|
364
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Save a geolocation to session state, so later commands launch with it
|
|
381
|
+
* (`cbrowser geo set`). Returns false when the state could not be written.
|
|
382
|
+
* Before 2026-10-21, geo set changed a browser that was never launched and
|
|
383
|
+
* printed success, so nothing changed.
|
|
384
|
+
*/
|
|
385
|
+
saveGeolocationSetting(geo) {
|
|
386
|
+
try {
|
|
387
|
+
const stateDir = join(this.paths.dataDir, "browser-state");
|
|
388
|
+
if (!existsSync(stateDir))
|
|
389
|
+
mkdirSync(stateDir, { recursive: true });
|
|
390
|
+
const state = existsSync(this.sessionStateFile)
|
|
391
|
+
? JSON.parse(readFileSync(this.sessionStateFile, "utf-8"))
|
|
392
|
+
: { url: "", timestamp: Date.now() };
|
|
393
|
+
state.geolocation = geo;
|
|
394
|
+
state.timestamp = Date.now();
|
|
395
|
+
writeFileSync(this.sessionStateFile, JSON.stringify(state, null, 2));
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
console.error(`Failed to save geolocation: ${e.message}`);
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
365
403
|
loadSessionState() {
|
|
366
404
|
try {
|
|
367
405
|
if (!existsSync(this.sessionStateFile))
|
|
@@ -460,6 +498,31 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
460
498
|
async launch() {
|
|
461
499
|
if (this.browser || this.context)
|
|
462
500
|
return;
|
|
501
|
+
// An unknown device is an error, never a silent desktop render (reported
|
|
502
|
+
// 2026-10-21: "iphone-16" captured 1280x800 and called it success, and so did
|
|
503
|
+
// "Pixel 7" from the MCP tools' own descriptions). Names are normalised here,
|
|
504
|
+
// so "iPhone 16" and "iphone-16" are the same preset.
|
|
505
|
+
if (this.config.device) {
|
|
506
|
+
const resolved = resolveDevice(this.config.device);
|
|
507
|
+
if ("error" in resolved) {
|
|
508
|
+
const closest = resolved.suggestions.length > 0 ? ` Closest presets: ${resolved.suggestions.join(", ")}.` : "";
|
|
509
|
+
throw new CBrowserError(CBrowserErrorCode.CONFIG_INVALID, `${resolved.error}.${closest}`, {
|
|
510
|
+
device: this.config.device,
|
|
511
|
+
suggestions: resolved.suggestions,
|
|
512
|
+
howToFix: "Use a preset name ('cbrowser device list' shows them all; case and spaces do not matter), or set a custom size with --viewport WIDTHxHEIGHT.",
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
this.config.device = resolved.key;
|
|
516
|
+
}
|
|
517
|
+
// Was the page shape asked for on this run? Then the saved session's
|
|
518
|
+
// viewport must not overwrite it after launch.
|
|
519
|
+
this.explicitPageShape = !!this.config.device || !!this.config.viewportOverride;
|
|
520
|
+
// Restore a location saved by `cbrowser geo set` unless one was given.
|
|
521
|
+
if (this.config.persistent && !this.config.geolocation) {
|
|
522
|
+
const saved = this.loadSessionState();
|
|
523
|
+
if (saved?.geolocation)
|
|
524
|
+
this.config.geolocation = saved.geolocation;
|
|
525
|
+
}
|
|
463
526
|
// Load saved session state to restore device setting if not explicitly set
|
|
464
527
|
if (this.config.persistent && !this.config.device) {
|
|
465
528
|
const savedSession = this.loadSessionState();
|
|
@@ -489,7 +552,11 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
489
552
|
if (this.config.device && DEVICE_PRESETS[this.config.device]) {
|
|
490
553
|
const device = DEVICE_PRESETS[this.config.device];
|
|
491
554
|
contextOptions.viewport = device.viewport;
|
|
492
|
-
|
|
555
|
+
// The DevTools laptops have no user agent of their own and keep the
|
|
556
|
+
// browser's. Playwright 1.58 already treats "" as unset; this does not
|
|
557
|
+
// rely on that.
|
|
558
|
+
if (device.userAgent)
|
|
559
|
+
contextOptions.userAgent = device.userAgent;
|
|
493
560
|
contextOptions.deviceScaleFactor = device.deviceScaleFactor;
|
|
494
561
|
contextOptions.isMobile = device.isMobile;
|
|
495
562
|
contextOptions.hasTouch = device.hasTouch;
|
|
@@ -502,6 +569,12 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
502
569
|
contextOptions.isMobile = device.isMobile;
|
|
503
570
|
contextOptions.hasTouch = device.hasTouch;
|
|
504
571
|
}
|
|
572
|
+
// An explicit --viewport sets the size even with a device preset, which
|
|
573
|
+
// keeps its pixel ratio, touch and user agent. Before 2026-10-21 the preset
|
|
574
|
+
// silently won.
|
|
575
|
+
if (this.config.viewportOverride) {
|
|
576
|
+
contextOptions.viewport = { ...this.config.viewportOverride };
|
|
577
|
+
}
|
|
505
578
|
// Apply custom user agent if set (overrides device)
|
|
506
579
|
if (this.config.userAgent) {
|
|
507
580
|
contextOptions.userAgent = this.config.userAgent;
|
|
@@ -719,7 +792,8 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
719
792
|
await this.page.waitForLoadState("networkidle", { timeout: 10000 }).catch(() => { });
|
|
720
793
|
await this.page.waitForTimeout(1000);
|
|
721
794
|
// Apply saved viewport if available
|
|
722
|
-
|
|
795
|
+
// An explicit --device or --viewport wins over last run's size.
|
|
796
|
+
if (savedSession.viewport && !this.explicitPageShape) {
|
|
723
797
|
await this.page.setViewportSize(savedSession.viewport);
|
|
724
798
|
}
|
|
725
799
|
}
|
|
@@ -854,7 +928,7 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
854
928
|
// Wait for network idle and JS hydration
|
|
855
929
|
await this.page.waitForLoadState("networkidle", { timeout: 10000 }).catch(() => { });
|
|
856
930
|
await this.page.waitForTimeout(1000);
|
|
857
|
-
if (savedSession.viewport) {
|
|
931
|
+
if (savedSession.viewport && !this.explicitPageShape) {
|
|
858
932
|
await this.page.setViewportSize(savedSession.viewport);
|
|
859
933
|
}
|
|
860
934
|
}
|
|
@@ -1658,7 +1732,8 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
1658
1732
|
const filtered = cookies.filter((c) => {
|
|
1659
1733
|
if (c.name !== name)
|
|
1660
1734
|
return true;
|
|
1661
|
-
|
|
1735
|
+
// ".example.com" and "example.com" name the same cookie domain.
|
|
1736
|
+
if (domain && c.domain.replace(/^\./, "") !== domain.replace(/^\./, ""))
|
|
1662
1737
|
return true;
|
|
1663
1738
|
return false;
|
|
1664
1739
|
});
|
|
@@ -1700,11 +1775,42 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
1700
1775
|
* Set device emulation (requires browser restart).
|
|
1701
1776
|
*/
|
|
1702
1777
|
setDevice(deviceName) {
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
return
|
|
1778
|
+
const resolved = resolveDevice(deviceName);
|
|
1779
|
+
if ("error" in resolved)
|
|
1780
|
+
return false;
|
|
1781
|
+
this.config.device = resolved.key;
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Emulate a device preset and/or set the page size, relaunching if the
|
|
1786
|
+
* browser is already up (context options are fixed at launch). An unknown
|
|
1787
|
+
* device throws, as at launch. Returns the device key applied.
|
|
1788
|
+
*/
|
|
1789
|
+
async emulate(opts) {
|
|
1790
|
+
let key;
|
|
1791
|
+
if (opts.device) {
|
|
1792
|
+
const resolved = resolveDevice(opts.device);
|
|
1793
|
+
if ("error" in resolved) {
|
|
1794
|
+
const closest = resolved.suggestions.length > 0 ? ` Closest presets: ${resolved.suggestions.join(", ")}.` : "";
|
|
1795
|
+
throw new CBrowserError(CBrowserErrorCode.CONFIG_INVALID, `${resolved.error}.${closest}`, {
|
|
1796
|
+
device: opts.device,
|
|
1797
|
+
suggestions: resolved.suggestions,
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
key = resolved.key;
|
|
1706
1801
|
}
|
|
1707
|
-
|
|
1802
|
+
if (this.browser || this.context)
|
|
1803
|
+
await this.close();
|
|
1804
|
+
if (key)
|
|
1805
|
+
this.config.device = key;
|
|
1806
|
+
if (opts.viewport) {
|
|
1807
|
+
this.config.viewportOverride = { ...opts.viewport };
|
|
1808
|
+
if (!key) {
|
|
1809
|
+
this.config.viewportWidth = opts.viewport.width;
|
|
1810
|
+
this.config.viewportHeight = opts.viewport.height;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return { device: this.config.device ?? null };
|
|
1708
1814
|
}
|
|
1709
1815
|
/**
|
|
1710
1816
|
* List available device presets.
|
|
@@ -5507,202 +5613,57 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
5507
5613
|
// Journeys
|
|
5508
5614
|
// =========================================================================
|
|
5509
5615
|
/**
|
|
5510
|
-
* Run an autonomous journey.
|
|
5616
|
+
* Run an autonomous journey: a model-driven cognitive journey for one persona.
|
|
5511
5617
|
*
|
|
5512
|
-
*
|
|
5513
|
-
*
|
|
5618
|
+
* Needs an Anthropic API key. There is no keyless fallback any more (20.0.0):
|
|
5619
|
+
* the old heuristic explorer clicked the highest keyword match among the first
|
|
5620
|
+
* twenty links and called the goal reached when any goal word, "the"
|
|
5621
|
+
* included, appeared anywhere on the page. Measured 2026-10-21: "buy the blue
|
|
5622
|
+
* widget" on a page with no widget reported success for every persona, with
|
|
5623
|
+
* identical click paths. A result that is always yes is not a measurement.
|
|
5514
5624
|
*
|
|
5515
|
-
*
|
|
5516
|
-
*
|
|
5517
|
-
* @requires ANTHROPIC_API_KEY for cognitive mode (set via `npx cbrowser config set-api-key`)
|
|
5625
|
+
* @requires ANTHROPIC_API_KEY (set via `npx cbrowser config set-api-key`)
|
|
5518
5626
|
*/
|
|
5519
5627
|
async journey(options) {
|
|
5520
5628
|
const { persona: personaName, startUrl, goal, maxSteps = 20, maxTime = 120 } = options;
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
try {
|
|
5524
|
-
const cognitiveResult = await runCognitiveJourney({
|
|
5525
|
-
persona: personaName,
|
|
5526
|
-
startUrl,
|
|
5527
|
-
goal,
|
|
5528
|
-
maxSteps,
|
|
5529
|
-
maxTime,
|
|
5530
|
-
headless: this.config.headless,
|
|
5531
|
-
vision: false,
|
|
5532
|
-
verbose: false,
|
|
5533
|
-
});
|
|
5534
|
-
// Map cognitive journey result to JourneyResult format
|
|
5535
|
-
return {
|
|
5536
|
-
persona: personaName,
|
|
5537
|
-
goal,
|
|
5538
|
-
steps: cognitiveResult.frictionPoints.map((fp, _i) => ({
|
|
5539
|
-
action: fp.type,
|
|
5540
|
-
target: fp.element || 'page',
|
|
5541
|
-
result: fp.monologue.substring(0, 100),
|
|
5542
|
-
screenshot: fp.screenshot,
|
|
5543
|
-
timestamp: new Date().toISOString(),
|
|
5544
|
-
})),
|
|
5545
|
-
success: cognitiveResult.goalAchieved,
|
|
5546
|
-
frictionPoints: cognitiveResult.frictionPoints.map(fp => `${fp.type}: ${fp.monologue.substring(0, 80)}`),
|
|
5547
|
-
totalTime: cognitiveResult.totalTime * 1000, // Convert to ms
|
|
5548
|
-
consoleLogs: [],
|
|
5549
|
-
cognitive: {
|
|
5550
|
-
patienceRemaining: cognitiveResult.finalState.patienceRemaining,
|
|
5551
|
-
frustrationLevel: cognitiveResult.finalState.frustrationLevel,
|
|
5552
|
-
confusionLevel: cognitiveResult.finalState.confusionLevel,
|
|
5553
|
-
abandonmentReason: cognitiveResult.abandonmentReason,
|
|
5554
|
-
backtrackCount: cognitiveResult.summary.backtrackCount,
|
|
5555
|
-
monologue: cognitiveResult.fullMonologue,
|
|
5556
|
-
},
|
|
5557
|
-
};
|
|
5558
|
-
}
|
|
5559
|
-
catch (e) {
|
|
5560
|
-
console.warn(`Cognitive journey failed, falling back to heuristic: ${e.message}`);
|
|
5561
|
-
// Fall through to heuristic mode
|
|
5562
|
-
}
|
|
5563
|
-
}
|
|
5564
|
-
// Heuristic mode (no API key or cognitive journey failed)
|
|
5565
|
-
// NOTE: This is simplified exploration. For realistic user simulation,
|
|
5566
|
-
// configure an API key: npx cbrowser config set-api-key YOUR_KEY
|
|
5567
|
-
const persona = getPersonaOrRefuse(personaName);
|
|
5568
|
-
this.currentPersona = persona;
|
|
5569
|
-
// Set viewport based on persona
|
|
5570
|
-
if (persona.context?.viewport) {
|
|
5571
|
-
this.config.viewportWidth = persona.context.viewport[0];
|
|
5572
|
-
this.config.viewportHeight = persona.context.viewport[1];
|
|
5573
|
-
}
|
|
5574
|
-
const page = await this.getPage();
|
|
5575
|
-
const consoleLogs = [];
|
|
5576
|
-
const frictionPoints = [];
|
|
5577
|
-
const startTime = Date.now();
|
|
5578
|
-
// Capture console logs. Removed in the finally below (v8 B27, 2026-10-10):
|
|
5579
|
-
// it used to stay on the tab, which is the shared page in a hosted session,
|
|
5580
|
-
// so every heuristic journey left one more listener and log array behind
|
|
5581
|
-
// (measured 4 -> 7 console listeners over three runs). Not read from the
|
|
5582
|
-
// tab's console buffer instead: that keeps 500 entries and omits an empty
|
|
5583
|
-
// location, so a noisy page or an inline script would change consoleLogs.
|
|
5584
|
-
const onConsole = (msg) => {
|
|
5585
|
-
const entry = {
|
|
5586
|
-
type: msg.type(),
|
|
5587
|
-
text: msg.text().substring(0, 500),
|
|
5588
|
-
timestamp: new Date().toISOString(),
|
|
5589
|
-
url: msg.location()?.url,
|
|
5590
|
-
lineNumber: msg.location()?.lineNumber,
|
|
5591
|
-
};
|
|
5592
|
-
consoleLogs.push(entry);
|
|
5593
|
-
if (msg.type() === "error") {
|
|
5594
|
-
frictionPoints.push(`Console error: ${msg.text().substring(0, 100)}`);
|
|
5595
|
-
}
|
|
5596
|
-
};
|
|
5597
|
-
page.on("console", onConsole);
|
|
5598
|
-
try {
|
|
5599
|
-
return await this.heuristicJourney(page, persona, options, consoleLogs, frictionPoints, startTime);
|
|
5629
|
+
if (!isApiKeyConfigured()) {
|
|
5630
|
+
throw new Error("A journey needs an Anthropic API key (cbrowser config set-api-key <key>). The keyless heuristic explorer was removed in 20.0.0 because its results were not accurate.");
|
|
5600
5631
|
}
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
action: "navigate",
|
|
5613
|
-
target: startUrl,
|
|
5614
|
-
result: `Loaded: ${await page.title()}`,
|
|
5615
|
-
screenshot: await this.screenshot(),
|
|
5616
|
-
timestamp: new Date().toISOString(),
|
|
5617
|
-
});
|
|
5618
|
-
// Heuristic exploration (simplified - no API key available)
|
|
5619
|
-
// For realistic AI-driven exploration, set API key: npx cbrowser config set-api-key
|
|
5620
|
-
let stepCount = 0;
|
|
5621
|
-
let success = false;
|
|
5622
|
-
while (stepCount < maxSteps && !success) {
|
|
5623
|
-
stepCount++;
|
|
5624
|
-
// Wait with human-like timing
|
|
5625
|
-
const delay = this.getHumanDelay(persona);
|
|
5626
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
5627
|
-
// Try to find clickable elements
|
|
5628
|
-
const clickable = await page.evaluate((goalText) => {
|
|
5629
|
-
const elements = Array.from(document.querySelectorAll('a, button, [role="button"], input[type="submit"]'));
|
|
5630
|
-
// Score elements by relevance to goal (deterministic, not random)
|
|
5631
|
-
const scored = elements.slice(0, 20).map((el) => {
|
|
5632
|
-
const text = el.textContent?.trim().substring(0, 50) || '';
|
|
5633
|
-
const href = el.href || '';
|
|
5634
|
-
const textLower = text.toLowerCase();
|
|
5635
|
-
const goalWords = goalText.toLowerCase().split(' ').filter(w => w.length > 2);
|
|
5636
|
-
// Calculate relevance score
|
|
5637
|
-
let score = 0;
|
|
5638
|
-
for (const word of goalWords) {
|
|
5639
|
-
if (textLower.includes(word))
|
|
5640
|
-
score += 3;
|
|
5641
|
-
if (href.toLowerCase().includes(word))
|
|
5642
|
-
score += 2;
|
|
5643
|
-
}
|
|
5644
|
-
// Boost common action words
|
|
5645
|
-
if (textLower.match(/submit|continue|next|proceed|sign|login|register/))
|
|
5646
|
-
score += 1;
|
|
5647
|
-
return { tag: el.tagName, text, href, score };
|
|
5648
|
-
});
|
|
5649
|
-
// Sort by score (highest first) and return top 5
|
|
5650
|
-
return scored.sort((a, b) => b.score - a.score).slice(0, 5);
|
|
5651
|
-
}, goal);
|
|
5652
|
-
if (clickable.length === 0) {
|
|
5653
|
-
frictionPoints.push("No clickable elements found");
|
|
5654
|
-
break;
|
|
5655
|
-
}
|
|
5656
|
-
// Select highest-scored element (deterministic)
|
|
5657
|
-
const target = clickable[0];
|
|
5658
|
-
const result = await this.click(target.text || target.tag);
|
|
5659
|
-
steps.push({
|
|
5660
|
-
action: "click",
|
|
5661
|
-
target: target.text || target.tag,
|
|
5662
|
-
result: result.message,
|
|
5663
|
-
screenshot: result.screenshot,
|
|
5664
|
-
timestamp: new Date().toISOString(),
|
|
5665
|
-
});
|
|
5666
|
-
// Check if goal might be achieved
|
|
5667
|
-
const pageText = await page.evaluate(() => document.body.innerText.toLowerCase());
|
|
5668
|
-
if (goal.toLowerCase().split(" ").some((word) => pageText.includes(word))) {
|
|
5669
|
-
success = true;
|
|
5670
|
-
}
|
|
5671
|
-
}
|
|
5672
|
-
// Save final screenshot
|
|
5673
|
-
steps.push({
|
|
5674
|
-
action: "final",
|
|
5675
|
-
result: `Final page: ${await page.title()}`,
|
|
5676
|
-
screenshot: await this.screenshot(),
|
|
5677
|
-
timestamp: new Date().toISOString(),
|
|
5632
|
+
// Refuse an unknown persona before spending tokens.
|
|
5633
|
+
this.currentPersona = getPersonaOrRefuse(personaName);
|
|
5634
|
+
const cognitiveResult = await runCognitiveJourney({
|
|
5635
|
+
persona: personaName,
|
|
5636
|
+
startUrl,
|
|
5637
|
+
goal,
|
|
5638
|
+
maxSteps,
|
|
5639
|
+
maxTime,
|
|
5640
|
+
headless: this.config.headless,
|
|
5641
|
+
vision: false,
|
|
5642
|
+
verbose: false,
|
|
5678
5643
|
});
|
|
5679
|
-
|
|
5644
|
+
return {
|
|
5680
5645
|
persona: personaName,
|
|
5681
5646
|
goal,
|
|
5682
|
-
steps
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5647
|
+
steps: cognitiveResult.frictionPoints.map((fp) => ({
|
|
5648
|
+
action: fp.type,
|
|
5649
|
+
target: fp.element || 'page',
|
|
5650
|
+
result: fp.monologue.substring(0, 100),
|
|
5651
|
+
screenshot: fp.screenshot,
|
|
5652
|
+
timestamp: new Date().toISOString(),
|
|
5653
|
+
})),
|
|
5654
|
+
success: cognitiveResult.goalAchieved,
|
|
5655
|
+
frictionPoints: cognitiveResult.frictionPoints.map(fp => `${fp.type}: ${fp.monologue.substring(0, 80)}`),
|
|
5656
|
+
totalTime: cognitiveResult.totalTime * 1000, // Convert to ms
|
|
5657
|
+
consoleLogs: [],
|
|
5658
|
+
cognitive: {
|
|
5659
|
+
patienceRemaining: cognitiveResult.finalState.patienceRemaining,
|
|
5660
|
+
frustrationLevel: cognitiveResult.finalState.frustrationLevel,
|
|
5661
|
+
confusionLevel: cognitiveResult.finalState.confusionLevel,
|
|
5662
|
+
abandonmentReason: cognitiveResult.abandonmentReason,
|
|
5663
|
+
backtrackCount: cognitiveResult.summary.backtrackCount,
|
|
5664
|
+
monologue: cognitiveResult.fullMonologue,
|
|
5665
|
+
},
|
|
5687
5666
|
};
|
|
5688
|
-
// Save journey results
|
|
5689
|
-
const journeyFile = join(this.paths.dataDir, `journey-${Date.now()}.json`);
|
|
5690
|
-
writeFileSync(journeyFile, JSON.stringify(result, null, 2));
|
|
5691
|
-
return result;
|
|
5692
|
-
}
|
|
5693
|
-
/**
|
|
5694
|
-
* Get human-like delay based on persona.
|
|
5695
|
-
* Uses deterministic average of min/max (no random).
|
|
5696
|
-
* For realistic variable timing, use cognitive journeys with API key.
|
|
5697
|
-
*/
|
|
5698
|
-
getHumanDelay(persona) {
|
|
5699
|
-
const timing = persona.humanBehavior?.timing;
|
|
5700
|
-
if (!timing)
|
|
5701
|
-
return 500;
|
|
5702
|
-
const min = timing.reactionTime.min;
|
|
5703
|
-
const max = timing.reactionTime.max;
|
|
5704
|
-
// Use midpoint for deterministic behavior (no random)
|
|
5705
|
-
return Math.floor((min + max) / 2);
|
|
5706
5667
|
}
|
|
5707
5668
|
// =========================================================================
|
|
5708
5669
|
// Constitutional Safety
|
|
@@ -6825,12 +6786,18 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
6825
6786
|
*/
|
|
6826
6787
|
exportJUnit(suite, outputPath) {
|
|
6827
6788
|
const filename = outputPath || join(this.paths.dataDir, `junit-${Date.now()}.xml`);
|
|
6789
|
+
// Names and errors are page and test-file text: escaped, or a test named
|
|
6790
|
+
// "Search & filter" wrote XML no CI parser accepts. A failed test is a
|
|
6791
|
+
// failure with or without an error message; it used to pass without one.
|
|
6792
|
+
const esc = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6793
|
+
const failures = suite.tests.filter((t) => t.status === "failed").length;
|
|
6828
6794
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
|
6829
|
-
xml += `<testsuite name="${suite.name}" tests="${suite.tests.length}">\n`;
|
|
6795
|
+
xml += `<testsuite name="${esc(suite.name)}" tests="${suite.tests.length}" failures="${failures}">\n`;
|
|
6830
6796
|
for (const test of suite.tests) {
|
|
6831
|
-
xml += ` <testcase name="${test.name}" time="${(test.duration / 1000).toFixed(3)}">\n`;
|
|
6832
|
-
if (test.status === "failed"
|
|
6833
|
-
|
|
6797
|
+
xml += ` <testcase name="${esc(test.name)}" time="${(test.duration / 1000).toFixed(3)}">\n`;
|
|
6798
|
+
if (test.status === "failed") {
|
|
6799
|
+
const message = test.error || "failed";
|
|
6800
|
+
xml += ` <failure message="${esc(message)}">${esc(message)}</failure>\n`;
|
|
6834
6801
|
}
|
|
6835
6802
|
xml += ` </testcase>\n`;
|
|
6836
6803
|
}
|
|
@@ -6847,7 +6814,8 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
6847
6814
|
tap += `1..${suite.tests.length}\n`;
|
|
6848
6815
|
suite.tests.forEach((test, i) => {
|
|
6849
6816
|
const status = test.status === "passed" ? "ok" : "not ok";
|
|
6850
|
-
|
|
6817
|
+
// One line per test, and a "#" in a name is not read as a directive.
|
|
6818
|
+
tap += `${status} ${i + 1} ${test.name.replace(/[\r\n]+/g, " ").replace(/#/g, "\\#")}\n`;
|
|
6851
6819
|
});
|
|
6852
6820
|
writeFileSync(filename, tap);
|
|
6853
6821
|
return filename;
|
|
@@ -6894,9 +6862,11 @@ For more help: https://playwright.dev/docs/browsers
|
|
|
6894
6862
|
* Run the same task across multiple device configurations in parallel.
|
|
6895
6863
|
*/
|
|
6896
6864
|
static async parallelDevices(devices, run, options = {}) {
|
|
6865
|
+
// `config` is shared by every device (headless, engine); each task's own
|
|
6866
|
+
// device wins over one in it.
|
|
6897
6867
|
const tasks = devices.map(device => ({
|
|
6898
6868
|
name: device,
|
|
6899
|
-
config: { device },
|
|
6869
|
+
config: { ...options.config, device },
|
|
6900
6870
|
run: (browser) => run(browser, device),
|
|
6901
6871
|
}));
|
|
6902
6872
|
const results = await CBrowser.parallel(tasks, options);
|