cbrowser 10.4.5 → 10.5.1
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/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +2 -2
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/agent-ready-audit.d.ts +1 -1
- package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
- package/dist/analysis/agent-ready-audit.js +2 -2
- package/dist/analysis/agent-ready-audit.js.map +1 -1
- package/dist/analysis/bug-hunter.js +2 -2
- package/dist/analysis/bug-hunter.js.map +1 -1
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
- package/dist/analysis/competitive-benchmark.js +1 -1
- package/dist/analysis/competitive-benchmark.js.map +1 -1
- package/dist/analysis/natural-language.js +1 -1
- package/dist/analysis/natural-language.js.map +1 -1
- package/dist/browser/index.d.ts +12 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +9 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/overlay-handler.d.ts +42 -0
- package/dist/browser/overlay-handler.d.ts.map +1 -0
- package/dist/browser/overlay-handler.js +334 -0
- package/dist/browser/overlay-handler.js.map +1 -0
- package/dist/browser/selector-cache.d.ts +69 -0
- package/dist/browser/selector-cache.d.ts.map +1 -0
- package/dist/browser/selector-cache.js +193 -0
- package/dist/browser/selector-cache.js.map +1 -0
- package/dist/browser/session-manager.d.ts +60 -0
- package/dist/browser/session-manager.d.ts.map +1 -0
- package/dist/browser/session-manager.js +261 -0
- package/dist/browser/session-manager.js.map +1 -0
- package/dist/browser.d.ts +8 -39
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +65 -642
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/focus-hierarchies.d.ts.map +1 -1
- package/dist/cognitive/focus-hierarchies.js +1 -1
- package/dist/cognitive/focus-hierarchies.js.map +1 -1
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +1 -1
- package/dist/cognitive/index.js.map +1 -1
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server-remote.d.ts.map +1 -1
- package/dist/mcp-server-remote.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/performance/metrics.js +1 -1
- package/dist/performance/metrics.js.map +1 -1
- package/dist/personas.d.ts.map +1 -1
- package/dist/testing/coverage.d.ts.map +1 -1
- package/dist/testing/coverage.js +1 -1
- package/dist/testing/coverage.js.map +1 -1
- package/dist/testing/test-repair.js +2 -2
- package/dist/testing/test-repair.js.map +1 -1
- package/dist/types.d.ts +50 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +55 -0
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +32 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +63 -0
- package/dist/utils.js.map +1 -0
- package/dist/version.js.map +1 -1
- package/dist/visual/cross-browser.js +1 -1
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/regression.js +1 -1
- package/dist/visual/regression.js.map +1 -1
- package/dist/visual/responsive.js +1 -1
- package/dist/visual/responsive.js.map +1 -1
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -10,6 +10,9 @@ import { mergeConfig, getPaths, ensureDirectories } from "./config.js";
|
|
|
10
10
|
import { BUILTIN_PERSONAS, getPersona } from "./personas.js";
|
|
11
11
|
import { DEVICE_PRESETS, LOCATION_PRESETS } from "./types.js";
|
|
12
12
|
import { runCognitiveJourney, isApiKeyConfigured, } from "./cognitive/index.js";
|
|
13
|
+
import { SessionManager } from "./browser/session-manager.js";
|
|
14
|
+
import { SelectorCacheManager } from "./browser/selector-cache.js";
|
|
15
|
+
import { OverlayHandler } from "./browser/overlay-handler.js";
|
|
13
16
|
// Browser-specific fast launch args for performance optimization
|
|
14
17
|
const BROWSER_LAUNCH_ARGS = {
|
|
15
18
|
// Chromium args - reduces cold start time significantly
|
|
@@ -41,7 +44,7 @@ const BROWSER_LAUNCH_ARGS = {
|
|
|
41
44
|
webkit: [],
|
|
42
45
|
};
|
|
43
46
|
// Legacy alias for backward compatibility
|
|
44
|
-
const
|
|
47
|
+
const _FAST_LAUNCH_ARGS = BROWSER_LAUNCH_ARGS.chromium;
|
|
45
48
|
export class CBrowser {
|
|
46
49
|
config;
|
|
47
50
|
paths;
|
|
@@ -54,9 +57,27 @@ export class CBrowser {
|
|
|
54
57
|
harEntries = [];
|
|
55
58
|
isRecordingHar = false;
|
|
56
59
|
skipSessionRestore = false;
|
|
60
|
+
// Modular components (extracted for maintainability)
|
|
61
|
+
sessionManager;
|
|
62
|
+
selectorCacheManager;
|
|
63
|
+
overlayHandler;
|
|
57
64
|
constructor(userConfig = {}) {
|
|
58
65
|
this.config = mergeConfig(userConfig);
|
|
59
66
|
this.paths = ensureDirectories(getPaths(this.config.dataDir));
|
|
67
|
+
// Initialize modular components
|
|
68
|
+
this.sessionManager = new SessionManager({
|
|
69
|
+
sessionsDir: this.paths.sessionsDir,
|
|
70
|
+
viewportWidth: this.config.viewportWidth,
|
|
71
|
+
viewportHeight: this.config.viewportHeight,
|
|
72
|
+
verbose: this.config.verbose,
|
|
73
|
+
});
|
|
74
|
+
this.selectorCacheManager = new SelectorCacheManager({
|
|
75
|
+
dataDir: this.paths.dataDir,
|
|
76
|
+
verbose: this.config.verbose,
|
|
77
|
+
});
|
|
78
|
+
this.overlayHandler = new OverlayHandler({
|
|
79
|
+
verbose: this.config.verbose,
|
|
80
|
+
});
|
|
60
81
|
}
|
|
61
82
|
// =========================================================================
|
|
62
83
|
// Session State Persistence
|
|
@@ -380,7 +401,7 @@ export class CBrowser {
|
|
|
380
401
|
* Add a network mock at runtime.
|
|
381
402
|
*/
|
|
382
403
|
async addNetworkMock(mock) {
|
|
383
|
-
|
|
404
|
+
await this.getPage(); // Ensure page exists
|
|
384
405
|
await this.setupNetworkMocks([mock]);
|
|
385
406
|
}
|
|
386
407
|
/**
|
|
@@ -428,7 +449,7 @@ export class CBrowser {
|
|
|
428
449
|
});
|
|
429
450
|
this.page.on("response", async (response) => {
|
|
430
451
|
const key = response.url() + response.request().method();
|
|
431
|
-
const
|
|
452
|
+
const _networkResponse = {
|
|
432
453
|
url: response.url(),
|
|
433
454
|
status: response.status(),
|
|
434
455
|
statusText: response.statusText(),
|
|
@@ -1277,7 +1298,7 @@ export class CBrowser {
|
|
|
1277
1298
|
*/
|
|
1278
1299
|
async selectCustomDropdownOption(trigger, optionValue, options = {}) {
|
|
1279
1300
|
const page = await this.getPage();
|
|
1280
|
-
const
|
|
1301
|
+
const _timeout = options.timeout ?? 5000;
|
|
1281
1302
|
try {
|
|
1282
1303
|
// Click the trigger to open the dropdown
|
|
1283
1304
|
if (options.verbose)
|
|
@@ -1285,7 +1306,7 @@ export class CBrowser {
|
|
|
1285
1306
|
await trigger.click();
|
|
1286
1307
|
// Wait for dropdown options to appear
|
|
1287
1308
|
// Try multiple common patterns for option containers
|
|
1288
|
-
const
|
|
1309
|
+
const _optionSelectors = [
|
|
1289
1310
|
// Alpine.js / Headless UI patterns
|
|
1290
1311
|
'[x-show]:not([x-show="false"])',
|
|
1291
1312
|
'[role="listbox"]',
|
|
@@ -1379,7 +1400,6 @@ export class CBrowser {
|
|
|
1379
1400
|
* Prioritizes: exact text match > non-sticky elements > shorter text (closer match)
|
|
1380
1401
|
*/
|
|
1381
1402
|
async findBestClickCandidate(locator, searchText) {
|
|
1382
|
-
const page = await this.getPage();
|
|
1383
1403
|
const count = await locator.count();
|
|
1384
1404
|
if (count === 0)
|
|
1385
1405
|
return null;
|
|
@@ -1394,7 +1414,6 @@ export class CBrowser {
|
|
|
1394
1414
|
try {
|
|
1395
1415
|
const info = await el.evaluate((element, search) => {
|
|
1396
1416
|
const text = element.textContent?.trim() || '';
|
|
1397
|
-
const style = getComputedStyle(element);
|
|
1398
1417
|
// Check if element is in a sticky/fixed container
|
|
1399
1418
|
let inStickyContainer = false;
|
|
1400
1419
|
let parent = element;
|
|
@@ -1690,7 +1709,7 @@ export class CBrowser {
|
|
|
1690
1709
|
* Handle filling a custom input (hidden input with custom UI).
|
|
1691
1710
|
* Handles autocomplete, datepickers, custom text inputs, etc.
|
|
1692
1711
|
*/
|
|
1693
|
-
async handleCustomInput(hiddenInput, value,
|
|
1712
|
+
async handleCustomInput(hiddenInput, value, _options = {}) {
|
|
1694
1713
|
const page = await this.getPage();
|
|
1695
1714
|
try {
|
|
1696
1715
|
// Get input info to help find its visible counterpart
|
|
@@ -1861,320 +1880,22 @@ export class CBrowser {
|
|
|
1861
1880
|
}
|
|
1862
1881
|
// =========================================================================
|
|
1863
1882
|
// Overlay Detection & Dismissal (v7.4.14)
|
|
1883
|
+
// Delegated to OverlayHandler module for maintainability
|
|
1864
1884
|
// =========================================================================
|
|
1865
|
-
/** Common overlay patterns for detection */
|
|
1866
|
-
static OVERLAY_PATTERNS = [
|
|
1867
|
-
{
|
|
1868
|
-
type: "cookie",
|
|
1869
|
-
selectors: [
|
|
1870
|
-
'[class*="cookie"]', '[id*="cookie"]', '[class*="consent"]', '[id*="consent"]',
|
|
1871
|
-
'[class*="gdpr"]', '[id*="gdpr"]', '[class*="cc-"]', '[id*="cc-"]',
|
|
1872
|
-
'[class*="CookieBanner"]', '[class*="cookie-banner"]', '[class*="cookie_banner"]',
|
|
1873
|
-
'[aria-label*="cookie" i]', '[aria-label*="consent" i]',
|
|
1874
|
-
],
|
|
1875
|
-
closeButtons: [
|
|
1876
|
-
'button:has-text("Accept")', 'button:has-text("Accept All")', 'button:has-text("Accept all")',
|
|
1877
|
-
'button:has-text("I agree")', 'button:has-text("Agree")', 'button:has-text("Got it")',
|
|
1878
|
-
'button:has-text("OK")', 'button:has-text("Allow")', 'button:has-text("Allow All")',
|
|
1879
|
-
'button:has-text("Close")', '[class*="accept"]', '[class*="agree"]',
|
|
1880
|
-
'[id*="accept"]', '[data-action="accept"]',
|
|
1881
|
-
],
|
|
1882
|
-
},
|
|
1883
|
-
{
|
|
1884
|
-
type: "age-verify",
|
|
1885
|
-
selectors: [
|
|
1886
|
-
'[class*="age-verif"]', '[id*="age-verif"]', '[class*="age_verif"]',
|
|
1887
|
-
'[class*="age-gate"]', '[id*="age-gate"]', '[class*="agegate"]',
|
|
1888
|
-
'[class*="age-check"]', '[id*="age-check"]',
|
|
1889
|
-
'[role="dialog"]', '[aria-modal="true"]',
|
|
1890
|
-
],
|
|
1891
|
-
closeButtons: [
|
|
1892
|
-
'button:has-text("I am 18")', 'button:has-text("I am 21")',
|
|
1893
|
-
'button:has-text("I\'m 18")', 'button:has-text("I\'m 21")',
|
|
1894
|
-
'button:has-text("I am over")', 'button:has-text("I\'m over")',
|
|
1895
|
-
'button:has-text("Yes")', 'button:has-text("Enter")',
|
|
1896
|
-
'button:has-text("Confirm")', 'button:has-text("Verify")',
|
|
1897
|
-
'button:has-text("Enter Site")', 'button:has-text("Continue")',
|
|
1898
|
-
],
|
|
1899
|
-
},
|
|
1900
|
-
{
|
|
1901
|
-
type: "newsletter",
|
|
1902
|
-
selectors: [
|
|
1903
|
-
'[class*="newsletter"]', '[id*="newsletter"]',
|
|
1904
|
-
'[class*="popup"]', '[id*="popup"]',
|
|
1905
|
-
'[class*="subscribe"]', '[id*="subscribe"]',
|
|
1906
|
-
'[class*="signup-modal"]', '[class*="email-capture"]',
|
|
1907
|
-
],
|
|
1908
|
-
closeButtons: [
|
|
1909
|
-
'button:has-text("Close")', 'button:has-text("No thanks")',
|
|
1910
|
-
'button:has-text("Not now")', 'button:has-text("Maybe later")',
|
|
1911
|
-
'[class*="close"]', '[aria-label="Close"]', '[aria-label="close"]',
|
|
1912
|
-
'button[class*="dismiss"]',
|
|
1913
|
-
],
|
|
1914
|
-
},
|
|
1915
|
-
];
|
|
1916
1885
|
/**
|
|
1917
1886
|
* Detect and dismiss overlays (cookie consent, age verification, newsletter popups).
|
|
1918
1887
|
* Constitutional safety: Yellow zone - logs all dismissed overlays.
|
|
1919
1888
|
*/
|
|
1920
1889
|
async dismissOverlay(options = { type: "auto" }) {
|
|
1921
1890
|
const page = await this.getPage();
|
|
1922
|
-
const
|
|
1923
|
-
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
for (let pass = 0; pass < maxPasses; pass++) {
|
|
1928
|
-
const detected = await this.detectOverlays(page, options);
|
|
1929
|
-
// Custom selector fallback on first pass if nothing detected
|
|
1930
|
-
if (pass === 0 && detected.length === 0 && options.customSelector) {
|
|
1931
|
-
try {
|
|
1932
|
-
const customEl = page.locator(options.customSelector).first();
|
|
1933
|
-
if (await customEl.isVisible({ timeout: 2000 })) {
|
|
1934
|
-
await customEl.click();
|
|
1935
|
-
await page.waitForTimeout(500);
|
|
1936
|
-
this.audit("dismiss-overlay", options.customSelector, "yellow", "success");
|
|
1937
|
-
details.push({
|
|
1938
|
-
type: "custom",
|
|
1939
|
-
selector: options.customSelector,
|
|
1940
|
-
dismissed: true,
|
|
1941
|
-
closeMethod: "custom-selector-click",
|
|
1942
|
-
});
|
|
1943
|
-
continue; // Re-detect after custom dismiss
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
catch { }
|
|
1947
|
-
}
|
|
1948
|
-
if (detected.length === 0)
|
|
1949
|
-
break; // No more overlays
|
|
1950
|
-
// Attempt to dismiss each detected overlay
|
|
1951
|
-
let dismissedThisPass = false;
|
|
1952
|
-
for (const overlay of detected) {
|
|
1953
|
-
const result = await this.tryDismissOverlay(page, overlay, timeout);
|
|
1954
|
-
details.push(result);
|
|
1955
|
-
if (result.dismissed) {
|
|
1956
|
-
this.audit("dismiss-overlay", result.selector, "yellow", "success");
|
|
1957
|
-
dismissedThisPass = true;
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
if (!dismissedThisPass)
|
|
1961
|
-
break; // Can't dismiss anything, stop
|
|
1962
|
-
// Wait for any new overlays to appear
|
|
1963
|
-
await page.waitForTimeout(800);
|
|
1964
|
-
}
|
|
1965
|
-
const overlaysDismissed = details.filter(d => d.dismissed).length;
|
|
1966
|
-
const overlaysFound = details.length;
|
|
1967
|
-
return {
|
|
1968
|
-
dismissed: overlaysDismissed > 0,
|
|
1969
|
-
overlaysFound,
|
|
1970
|
-
overlaysDismissed,
|
|
1971
|
-
details,
|
|
1972
|
-
screenshot: await this.screenshot(),
|
|
1973
|
-
suggestion: overlaysDismissed === 0 && overlaysFound > 0
|
|
1974
|
-
? "Overlays detected but could not be dismissed automatically. Try providing a custom selector."
|
|
1975
|
-
: undefined,
|
|
1976
|
-
};
|
|
1977
|
-
}
|
|
1978
|
-
catch (error) {
|
|
1979
|
-
return {
|
|
1980
|
-
dismissed: false,
|
|
1981
|
-
overlaysFound: 0,
|
|
1982
|
-
overlaysDismissed: 0,
|
|
1983
|
-
details,
|
|
1984
|
-
screenshot: await this.screenshot(),
|
|
1985
|
-
suggestion: `Error during overlay detection: ${error instanceof Error ? error.message : String(error)}`,
|
|
1986
|
-
};
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
/** Text content keywords for overlay type classification */
|
|
1990
|
-
static OVERLAY_TEXT_PATTERNS = {
|
|
1991
|
-
"age-verify": ["age verif", "18+", "21+", "over 18", "over 21", "years or older", "age gate", "age check", "must be 18", "must be 21", "legal age", "adult content"],
|
|
1992
|
-
"cookie": ["cookie", "consent", "gdpr", "privacy policy", "we use cookies", "this site uses cookies", "accept cookies"],
|
|
1993
|
-
"newsletter": ["newsletter", "subscribe", "sign up for", "email updates", "stay updated", "join our mailing"],
|
|
1994
|
-
};
|
|
1995
|
-
/**
|
|
1996
|
-
* Classify an overlay by its text content.
|
|
1997
|
-
*/
|
|
1998
|
-
classifyOverlayType(text) {
|
|
1999
|
-
const lowerText = text.toLowerCase();
|
|
2000
|
-
for (const [type, keywords] of Object.entries(CBrowser.OVERLAY_TEXT_PATTERNS)) {
|
|
2001
|
-
if (keywords.some(kw => lowerText.includes(kw))) {
|
|
2002
|
-
return type;
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
return "unknown";
|
|
2006
|
-
}
|
|
2007
|
-
/**
|
|
2008
|
-
* Detect overlays on the page by analyzing position, z-index, pattern matching, and text content.
|
|
2009
|
-
* Returns overlays sorted by z-index (highest first) so the topmost blocking overlay is dismissed first.
|
|
2010
|
-
*/
|
|
2011
|
-
async detectOverlays(page, options) {
|
|
2012
|
-
const detected = [];
|
|
2013
|
-
try {
|
|
2014
|
-
// Unified detection: find all high-z-index fixed/absolute elements AND known pattern selectors
|
|
2015
|
-
const rawOverlays = await page.evaluate(() => {
|
|
2016
|
-
const results = [];
|
|
2017
|
-
const seen = new Set();
|
|
2018
|
-
const allElements = Array.from(document.querySelectorAll("*"));
|
|
2019
|
-
for (let i = 0; i < allElements.length; i++) {
|
|
2020
|
-
const el = allElements[i];
|
|
2021
|
-
if (seen.has(el))
|
|
2022
|
-
continue;
|
|
2023
|
-
const cs = window.getComputedStyle(el);
|
|
2024
|
-
const pos = cs.position;
|
|
2025
|
-
const zIndex = parseInt(cs.zIndex) || 0;
|
|
2026
|
-
const rect = el.getBoundingClientRect();
|
|
2027
|
-
const isDialog = el.getAttribute("role") === "dialog" || el.getAttribute("aria-modal") === "true";
|
|
2028
|
-
// Match: high z-index overlay OR dialog role
|
|
2029
|
-
if (((pos === "fixed" || pos === "absolute") && zIndex > 100 && rect.width > 200 && rect.height > 80) || isDialog) {
|
|
2030
|
-
if (rect.width < 50 || rect.height < 30)
|
|
2031
|
-
continue;
|
|
2032
|
-
if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0")
|
|
2033
|
-
continue;
|
|
2034
|
-
const text = (el.textContent || "").substring(0, 300).trim();
|
|
2035
|
-
let selector = el.tagName.toLowerCase();
|
|
2036
|
-
if (el.id)
|
|
2037
|
-
selector = `#${el.id}`;
|
|
2038
|
-
else if (el.className && typeof el.className === "string") {
|
|
2039
|
-
const cls = el.className.split(/\s+/).filter((c) => c.length > 0 && c.length < 40)[0];
|
|
2040
|
-
if (cls)
|
|
2041
|
-
selector = `.${cls}`;
|
|
2042
|
-
}
|
|
2043
|
-
seen.add(el);
|
|
2044
|
-
results.push({
|
|
2045
|
-
selector,
|
|
2046
|
-
text,
|
|
2047
|
-
zIndex: zIndex || (isDialog ? 99999 : 0),
|
|
2048
|
-
position: pos,
|
|
2049
|
-
role: el.getAttribute("role"),
|
|
2050
|
-
ariaModal: el.getAttribute("aria-modal"),
|
|
2051
|
-
width: Math.round(rect.width),
|
|
2052
|
-
height: Math.round(rect.height),
|
|
2053
|
-
});
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
return results;
|
|
2057
|
-
});
|
|
2058
|
-
// Classify and filter overlays
|
|
2059
|
-
for (const raw of rawOverlays) {
|
|
2060
|
-
const type = this.classifyOverlayType(raw.text);
|
|
2061
|
-
// Filter by requested type
|
|
2062
|
-
if (options.type && options.type !== "auto" && type !== options.type && type !== "unknown")
|
|
2063
|
-
continue;
|
|
2064
|
-
detected.push({
|
|
2065
|
-
type,
|
|
2066
|
-
selector: raw.selector,
|
|
2067
|
-
text: raw.text.substring(0, 200),
|
|
2068
|
-
zIndex: raw.zIndex,
|
|
2069
|
-
position: raw.position,
|
|
2070
|
-
});
|
|
1891
|
+
const result = await this.overlayHandler.dismissOverlays(page, options, () => this.screenshot());
|
|
1892
|
+
// Log dismissed overlays to audit trail
|
|
1893
|
+
for (const detail of result.details) {
|
|
1894
|
+
if (detail.dismissed) {
|
|
1895
|
+
this.audit("dismiss-overlay", detail.selector, "yellow", "success");
|
|
2071
1896
|
}
|
|
2072
1897
|
}
|
|
2073
|
-
|
|
2074
|
-
// Sort by z-index descending — dismiss topmost overlay first
|
|
2075
|
-
detected.sort((a, b) => b.zIndex - a.zIndex);
|
|
2076
|
-
// Deduplicate: keep only one per type (highest z-index)
|
|
2077
|
-
const seen = new Set();
|
|
2078
|
-
const deduped = [];
|
|
2079
|
-
for (const d of detected) {
|
|
2080
|
-
if (d.type !== "unknown" && seen.has(d.type))
|
|
2081
|
-
continue;
|
|
2082
|
-
seen.add(d.type);
|
|
2083
|
-
deduped.push(d);
|
|
2084
|
-
}
|
|
2085
|
-
return deduped;
|
|
2086
|
-
}
|
|
2087
|
-
/**
|
|
2088
|
-
* Try clicking a button, with force fallback if another element intercepts.
|
|
2089
|
-
*/
|
|
2090
|
-
async tryClickOverlayButton(btn, timeout) {
|
|
2091
|
-
try {
|
|
2092
|
-
await btn.click({ timeout: Math.min(timeout, 3000) });
|
|
2093
|
-
return true;
|
|
2094
|
-
}
|
|
2095
|
-
catch {
|
|
2096
|
-
// If normal click fails (e.g., another overlay intercepts), try force click
|
|
2097
|
-
try {
|
|
2098
|
-
await btn.click({ force: true, timeout: Math.min(timeout, 2000) });
|
|
2099
|
-
return true;
|
|
2100
|
-
}
|
|
2101
|
-
catch {
|
|
2102
|
-
return false;
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
/**
|
|
2107
|
-
* Attempt to dismiss a single detected overlay.
|
|
2108
|
-
*/
|
|
2109
|
-
async tryDismissOverlay(page, overlay, timeout) {
|
|
2110
|
-
// Collect all close button selectors to try: matched pattern first, then all patterns, then generic
|
|
2111
|
-
const closeButtonSets = [];
|
|
2112
|
-
// First: close buttons from the matched overlay type pattern
|
|
2113
|
-
const matchedPattern = CBrowser.OVERLAY_PATTERNS.find(p => p.type === overlay.type);
|
|
2114
|
-
if (matchedPattern) {
|
|
2115
|
-
closeButtonSets.push({ source: "matched-pattern", selectors: matchedPattern.closeButtons });
|
|
2116
|
-
}
|
|
2117
|
-
// Second: close buttons from all other patterns (in case classification was imperfect)
|
|
2118
|
-
for (const pattern of CBrowser.OVERLAY_PATTERNS) {
|
|
2119
|
-
if (pattern.type !== overlay.type) {
|
|
2120
|
-
closeButtonSets.push({ source: `${pattern.type}-pattern`, selectors: pattern.closeButtons });
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
// Third: generic close buttons
|
|
2124
|
-
closeButtonSets.push({
|
|
2125
|
-
source: "generic",
|
|
2126
|
-
selectors: [
|
|
2127
|
-
'button[aria-label="Close"]', 'button[aria-label="close"]',
|
|
2128
|
-
'button[aria-label="Dismiss"]', '[role="button"][aria-label="Close"]',
|
|
2129
|
-
'button[class*="close"]', '[class*="close-btn"]', '[class*="closeBtn"]',
|
|
2130
|
-
'button:has-text("×")', 'button:has-text("✕")', 'button:has-text("X")',
|
|
2131
|
-
'button:has-text("Close")', 'button:has-text("Dismiss")',
|
|
2132
|
-
'button:has-text("No thanks")', 'button:has-text("Not now")',
|
|
2133
|
-
],
|
|
2134
|
-
});
|
|
2135
|
-
// Try all close button sets
|
|
2136
|
-
for (const set of closeButtonSets) {
|
|
2137
|
-
for (const btnSelector of set.selectors) {
|
|
2138
|
-
try {
|
|
2139
|
-
const btn = page.locator(btnSelector).first();
|
|
2140
|
-
if (await btn.isVisible({ timeout: 800 })) {
|
|
2141
|
-
const clicked = await this.tryClickOverlayButton(btn, timeout);
|
|
2142
|
-
if (clicked) {
|
|
2143
|
-
await page.waitForTimeout(500);
|
|
2144
|
-
return {
|
|
2145
|
-
type: overlay.type,
|
|
2146
|
-
selector: overlay.selector,
|
|
2147
|
-
dismissed: true,
|
|
2148
|
-
closeMethod: `${set.source}: ${btnSelector}`,
|
|
2149
|
-
};
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
catch { }
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
// Last resort: Try pressing Escape
|
|
2157
|
-
try {
|
|
2158
|
-
await page.keyboard.press("Escape");
|
|
2159
|
-
await page.waitForTimeout(500);
|
|
2160
|
-
// Check if overlay is still visible
|
|
2161
|
-
const stillVisible = await page.$(overlay.selector);
|
|
2162
|
-
if (!stillVisible || !(await stillVisible.isVisible())) {
|
|
2163
|
-
return {
|
|
2164
|
-
type: overlay.type,
|
|
2165
|
-
selector: overlay.selector,
|
|
2166
|
-
dismissed: true,
|
|
2167
|
-
closeMethod: "escape-key",
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
catch { }
|
|
2172
|
-
return {
|
|
2173
|
-
type: overlay.type,
|
|
2174
|
-
selector: overlay.selector,
|
|
2175
|
-
dismissed: false,
|
|
2176
|
-
error: "Could not find a way to dismiss this overlay",
|
|
2177
|
-
};
|
|
1898
|
+
return result;
|
|
2178
1899
|
}
|
|
2179
1900
|
/**
|
|
2180
1901
|
* Find alternative selectors for an element.
|
|
@@ -2188,9 +1909,9 @@ export class CBrowser {
|
|
|
2188
1909
|
for (const el of elements.slice(0, 10)) {
|
|
2189
1910
|
const text = await el.textContent().catch(() => "");
|
|
2190
1911
|
const ariaLabel = await el.getAttribute("aria-label").catch(() => "");
|
|
2191
|
-
const
|
|
1912
|
+
const _title = await el.getAttribute("title").catch(() => "");
|
|
2192
1913
|
const id = await el.getAttribute("id").catch(() => "");
|
|
2193
|
-
const
|
|
1914
|
+
const _className = await el.getAttribute("class").catch(() => "");
|
|
2194
1915
|
// Check if text matches original selector
|
|
2195
1916
|
if (text && originalSelector.toLowerCase().includes(text.toLowerCase().trim().substring(0, 20))) {
|
|
2196
1917
|
alternatives.push({
|
|
@@ -2226,54 +1947,10 @@ export class CBrowser {
|
|
|
2226
1947
|
}
|
|
2227
1948
|
// =========================================================================
|
|
2228
1949
|
// Tier 5: Self-Healing Selector Cache (v5.0.0)
|
|
1950
|
+
// Delegated to SelectorCache module for maintainability
|
|
2229
1951
|
// =========================================================================
|
|
2230
|
-
selectorCache = null;
|
|
2231
1952
|
/**
|
|
2232
|
-
* Get
|
|
2233
|
-
*/
|
|
2234
|
-
getSelectorCachePath() {
|
|
2235
|
-
return join(this.paths.dataDir, "selector-cache.json");
|
|
2236
|
-
}
|
|
2237
|
-
/**
|
|
2238
|
-
* Load the selector cache from disk.
|
|
2239
|
-
*/
|
|
2240
|
-
loadSelectorCache() {
|
|
2241
|
-
if (this.selectorCache)
|
|
2242
|
-
return this.selectorCache;
|
|
2243
|
-
const cachePath = this.getSelectorCachePath();
|
|
2244
|
-
if (existsSync(cachePath)) {
|
|
2245
|
-
try {
|
|
2246
|
-
const data = readFileSync(cachePath, "utf-8");
|
|
2247
|
-
this.selectorCache = JSON.parse(data);
|
|
2248
|
-
return this.selectorCache;
|
|
2249
|
-
}
|
|
2250
|
-
catch (e) {
|
|
2251
|
-
if (this.config.verbose) {
|
|
2252
|
-
console.debug(`[CBrowser] Corrupted selector cache, starting fresh: ${e.message}`);
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
this.selectorCache = { version: 1, entries: {} };
|
|
2257
|
-
return this.selectorCache;
|
|
2258
|
-
}
|
|
2259
|
-
/**
|
|
2260
|
-
* Save the selector cache to disk.
|
|
2261
|
-
*/
|
|
2262
|
-
saveSelectorCache() {
|
|
2263
|
-
if (!this.selectorCache)
|
|
2264
|
-
return;
|
|
2265
|
-
const cachePath = this.getSelectorCachePath();
|
|
2266
|
-
writeFileSync(cachePath, JSON.stringify(this.selectorCache, null, 2));
|
|
2267
|
-
}
|
|
2268
|
-
/**
|
|
2269
|
-
* Get cache key for a selector (includes domain for context).
|
|
2270
|
-
*/
|
|
2271
|
-
getSelectorCacheKey(selector, domain) {
|
|
2272
|
-
const d = domain || this.getCurrentDomain();
|
|
2273
|
-
return `${d}::${selector.toLowerCase()}`;
|
|
2274
|
-
}
|
|
2275
|
-
/**
|
|
2276
|
-
* Get current page domain.
|
|
1953
|
+
* Get current page domain for cache key generation.
|
|
2277
1954
|
*/
|
|
2278
1955
|
getCurrentDomain() {
|
|
2279
1956
|
try {
|
|
@@ -2287,122 +1964,50 @@ export class CBrowser {
|
|
|
2287
1964
|
}
|
|
2288
1965
|
return "unknown";
|
|
2289
1966
|
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Sync the current domain to the selector cache manager.
|
|
1969
|
+
*/
|
|
1970
|
+
syncCacheDomain() {
|
|
1971
|
+
this.selectorCacheManager.setCurrentDomain(this.getCurrentDomain());
|
|
1972
|
+
}
|
|
2290
1973
|
/**
|
|
2291
1974
|
* Cache a working alternative selector for future use.
|
|
2292
1975
|
*/
|
|
2293
1976
|
cacheAlternativeSelector(original, working, reason = "Alternative found") {
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
if (this.config.verbose) {
|
|
2297
|
-
console.log(`⚠️ Rejected invalid selector for caching: "${working}"`);
|
|
2298
|
-
}
|
|
2299
|
-
return;
|
|
2300
|
-
}
|
|
2301
|
-
const cache = this.loadSelectorCache();
|
|
2302
|
-
const key = this.getSelectorCacheKey(original);
|
|
2303
|
-
const domain = this.getCurrentDomain();
|
|
2304
|
-
cache.entries[key] = {
|
|
2305
|
-
originalSelector: original,
|
|
2306
|
-
workingSelector: working,
|
|
2307
|
-
domain,
|
|
2308
|
-
successCount: 1,
|
|
2309
|
-
failCount: 0,
|
|
2310
|
-
lastUsed: new Date().toISOString(),
|
|
2311
|
-
reason,
|
|
2312
|
-
};
|
|
2313
|
-
this.saveSelectorCache();
|
|
2314
|
-
if (this.config.verbose) {
|
|
2315
|
-
console.log(`📦 Cached healed selector: "${original}" → "${working}"`);
|
|
2316
|
-
}
|
|
1977
|
+
this.syncCacheDomain();
|
|
1978
|
+
this.selectorCacheManager.cacheAlternative(original, working, reason);
|
|
2317
1979
|
}
|
|
2318
1980
|
/**
|
|
2319
1981
|
* Get a cached alternative selector if available.
|
|
2320
1982
|
*/
|
|
2321
1983
|
getCachedSelector(original) {
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
const entry = cache.entries[key] || null;
|
|
2325
|
-
if (entry && (entry.workingSelector === 'text=""' || entry.workingSelector === "text=''" || entry.workingSelector.trim() === "")) {
|
|
2326
|
-
return null; // Reject invalid cached selectors
|
|
2327
|
-
}
|
|
2328
|
-
return entry;
|
|
1984
|
+
this.syncCacheDomain();
|
|
1985
|
+
return this.selectorCacheManager.getCached(original);
|
|
2329
1986
|
}
|
|
2330
1987
|
/**
|
|
2331
1988
|
* Update cache entry statistics.
|
|
2332
1989
|
*/
|
|
2333
1990
|
updateCacheStats(original, success) {
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
const entry = cache.entries[key];
|
|
2337
|
-
if (entry) {
|
|
2338
|
-
if (success) {
|
|
2339
|
-
entry.successCount++;
|
|
2340
|
-
}
|
|
2341
|
-
else {
|
|
2342
|
-
entry.failCount++;
|
|
2343
|
-
}
|
|
2344
|
-
entry.lastUsed = new Date().toISOString();
|
|
2345
|
-
this.saveSelectorCache();
|
|
2346
|
-
}
|
|
1991
|
+
this.syncCacheDomain();
|
|
1992
|
+
this.selectorCacheManager.updateStats(original, success);
|
|
2347
1993
|
}
|
|
2348
1994
|
/**
|
|
2349
1995
|
* Get selector cache statistics.
|
|
2350
1996
|
*/
|
|
2351
1997
|
getSelectorCacheStats() {
|
|
2352
|
-
|
|
2353
|
-
const entries = Object.values(cache.entries);
|
|
2354
|
-
const byDomain = {};
|
|
2355
|
-
for (const entry of entries) {
|
|
2356
|
-
byDomain[entry.domain] = (byDomain[entry.domain] || 0) + 1;
|
|
2357
|
-
}
|
|
2358
|
-
const topHealedSelectors = entries
|
|
2359
|
-
.sort((a, b) => b.successCount - a.successCount)
|
|
2360
|
-
.slice(0, 10)
|
|
2361
|
-
.map(e => ({
|
|
2362
|
-
original: e.originalSelector,
|
|
2363
|
-
working: e.workingSelector,
|
|
2364
|
-
heals: e.successCount,
|
|
2365
|
-
}));
|
|
2366
|
-
return {
|
|
2367
|
-
totalEntries: entries.length,
|
|
2368
|
-
totalHeals: entries.reduce((sum, e) => sum + e.successCount, 0),
|
|
2369
|
-
byDomain,
|
|
2370
|
-
topHealedSelectors,
|
|
2371
|
-
};
|
|
1998
|
+
return this.selectorCacheManager.getStats();
|
|
2372
1999
|
}
|
|
2373
2000
|
/**
|
|
2374
2001
|
* Clear the selector cache.
|
|
2375
2002
|
*/
|
|
2376
2003
|
clearSelectorCache(domain) {
|
|
2377
|
-
|
|
2378
|
-
let cleared = 0;
|
|
2379
|
-
if (domain) {
|
|
2380
|
-
// Clear only for specific domain
|
|
2381
|
-
for (const [key, entry] of Object.entries(cache.entries)) {
|
|
2382
|
-
if (entry.domain === domain) {
|
|
2383
|
-
delete cache.entries[key];
|
|
2384
|
-
cleared++;
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
else {
|
|
2389
|
-
// Clear all
|
|
2390
|
-
cleared = Object.keys(cache.entries).length;
|
|
2391
|
-
cache.entries = {};
|
|
2392
|
-
}
|
|
2393
|
-
this.saveSelectorCache();
|
|
2394
|
-
return cleared;
|
|
2004
|
+
return this.selectorCacheManager.clear(domain);
|
|
2395
2005
|
}
|
|
2396
2006
|
/**
|
|
2397
2007
|
* List all cached selectors.
|
|
2398
2008
|
*/
|
|
2399
2009
|
listCachedSelectors(domain) {
|
|
2400
|
-
|
|
2401
|
-
let entries = Object.values(cache.entries);
|
|
2402
|
-
if (domain) {
|
|
2403
|
-
entries = entries.filter(e => e.domain === domain);
|
|
2404
|
-
}
|
|
2405
|
-
return entries.sort((a, b) => b.successCount - a.successCount);
|
|
2010
|
+
return this.selectorCacheManager.list(domain);
|
|
2406
2011
|
}
|
|
2407
2012
|
// =========================================================================
|
|
2408
2013
|
// Tier 5: AI Test Generation (v5.0.0)
|
|
@@ -2675,7 +2280,7 @@ export class CBrowser {
|
|
|
2675
2280
|
let code = `// Playwright Test Code\n// Generated for: ${url}\n// Date: ${new Date().toISOString()}\n\n`;
|
|
2676
2281
|
code += `import { test, expect } from '@playwright/test';\n\n`;
|
|
2677
2282
|
for (const testDef of tests) {
|
|
2678
|
-
const
|
|
2283
|
+
const _testName = testDef.name.toLowerCase().replace(/\s+/g, "-");
|
|
2679
2284
|
code += `test('${testDef.name}', async ({ page }) => {\n`;
|
|
2680
2285
|
code += ` // ${testDef.description}\n\n`;
|
|
2681
2286
|
for (const step of testDef.steps) {
|
|
@@ -2913,7 +2518,7 @@ export class CBrowser {
|
|
|
2913
2518
|
* Assert a condition using natural language.
|
|
2914
2519
|
*/
|
|
2915
2520
|
async assert(assertion) {
|
|
2916
|
-
|
|
2521
|
+
await this.getPage(); // Ensure page exists
|
|
2917
2522
|
try {
|
|
2918
2523
|
// Parse the assertion
|
|
2919
2524
|
const result = await this.evaluateAssertion(assertion);
|
|
@@ -3272,238 +2877,56 @@ export class CBrowser {
|
|
|
3272
2877
|
*/
|
|
3273
2878
|
async saveSession(name) {
|
|
3274
2879
|
const page = await this.getPage();
|
|
3275
|
-
|
|
3276
|
-
const cookies = await context.cookies();
|
|
3277
|
-
let localStorage = {};
|
|
3278
|
-
try {
|
|
3279
|
-
localStorage = await page.evaluate(() => {
|
|
3280
|
-
const data = {};
|
|
3281
|
-
for (let i = 0; i < window.localStorage.length; i++) {
|
|
3282
|
-
const key = window.localStorage.key(i);
|
|
3283
|
-
if (key)
|
|
3284
|
-
data[key] = window.localStorage.getItem(key) || "";
|
|
3285
|
-
}
|
|
3286
|
-
return data;
|
|
3287
|
-
});
|
|
3288
|
-
}
|
|
3289
|
-
catch {
|
|
3290
|
-
// localStorage may be inaccessible on about:blank or restricted pages
|
|
3291
|
-
}
|
|
3292
|
-
let sessionStorage = {};
|
|
3293
|
-
try {
|
|
3294
|
-
sessionStorage = await page.evaluate(() => {
|
|
3295
|
-
const data = {};
|
|
3296
|
-
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
3297
|
-
const key = window.sessionStorage.key(i);
|
|
3298
|
-
if (key)
|
|
3299
|
-
data[key] = window.sessionStorage.getItem(key) || "";
|
|
3300
|
-
}
|
|
3301
|
-
return data;
|
|
3302
|
-
});
|
|
3303
|
-
}
|
|
3304
|
-
catch {
|
|
3305
|
-
// sessionStorage may be inaccessible on about:blank or restricted pages
|
|
3306
|
-
}
|
|
3307
|
-
const url = page.url();
|
|
3308
|
-
const domain = new URL(url).hostname;
|
|
3309
|
-
const session = {
|
|
3310
|
-
name,
|
|
3311
|
-
created: new Date().toISOString(),
|
|
3312
|
-
lastUsed: new Date().toISOString(),
|
|
3313
|
-
domain,
|
|
3314
|
-
url,
|
|
3315
|
-
viewport: {
|
|
3316
|
-
width: this.config.viewportWidth,
|
|
3317
|
-
height: this.config.viewportHeight,
|
|
3318
|
-
},
|
|
3319
|
-
cookies: cookies,
|
|
3320
|
-
localStorage,
|
|
3321
|
-
sessionStorage,
|
|
3322
|
-
};
|
|
3323
|
-
const sessionPath = join(this.paths.sessionsDir, `${name}.json`);
|
|
3324
|
-
writeFileSync(sessionPath, JSON.stringify(session, null, 2));
|
|
2880
|
+
await this.sessionManager.save(name, page, this.context);
|
|
3325
2881
|
}
|
|
3326
2882
|
/**
|
|
3327
2883
|
* Load a saved session.
|
|
3328
2884
|
*/
|
|
3329
2885
|
async loadSession(name) {
|
|
3330
|
-
const sessionPath = join(this.paths.sessionsDir, `${name}.json`);
|
|
3331
|
-
if (!existsSync(sessionPath)) {
|
|
3332
|
-
return { success: false, name, cookiesRestored: 0, localStorageKeysRestored: 0, sessionStorageKeysRestored: 0 };
|
|
3333
|
-
}
|
|
3334
|
-
const session = JSON.parse(readFileSync(sessionPath, "utf-8"));
|
|
3335
2886
|
const page = await this.getPage();
|
|
3336
|
-
|
|
3337
|
-
const result = {
|
|
3338
|
-
success: true,
|
|
3339
|
-
name,
|
|
3340
|
-
cookiesRestored: session.cookies.length,
|
|
3341
|
-
localStorageKeysRestored: Object.keys(session.localStorage).length,
|
|
3342
|
-
sessionStorageKeysRestored: Object.keys(session.sessionStorage).length,
|
|
3343
|
-
};
|
|
3344
|
-
// Cross-domain warning
|
|
3345
|
-
const currentUrl = page.url();
|
|
3346
|
-
if (currentUrl && currentUrl !== "about:blank") {
|
|
3347
|
-
try {
|
|
3348
|
-
const currentDomain = new URL(currentUrl).hostname;
|
|
3349
|
-
if (session.domain && currentDomain !== session.domain) {
|
|
3350
|
-
result.warning = `Session '${name}' was saved for ${session.domain} but current page is ${currentDomain}. Some cookies may not apply.`;
|
|
3351
|
-
}
|
|
3352
|
-
}
|
|
3353
|
-
catch {
|
|
3354
|
-
// URL parsing failed, skip warning
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
// Restore cookies
|
|
3358
|
-
if (session.cookies.length > 0) {
|
|
3359
|
-
await context.addCookies(session.cookies);
|
|
3360
|
-
}
|
|
3361
|
-
// Navigate to saved URL
|
|
3362
|
-
await page.goto(session.url, { waitUntil: "networkidle" });
|
|
3363
|
-
// Restore localStorage
|
|
3364
|
-
await page.evaluate((data) => {
|
|
3365
|
-
for (const [key, value] of Object.entries(data)) {
|
|
3366
|
-
window.localStorage.setItem(key, value);
|
|
3367
|
-
}
|
|
3368
|
-
}, session.localStorage);
|
|
3369
|
-
// Restore sessionStorage
|
|
3370
|
-
await page.evaluate((data) => {
|
|
3371
|
-
for (const [key, value] of Object.entries(data)) {
|
|
3372
|
-
window.sessionStorage.setItem(key, value);
|
|
3373
|
-
}
|
|
3374
|
-
}, session.sessionStorage);
|
|
3375
|
-
// Refresh to apply storage
|
|
3376
|
-
await page.reload({ waitUntil: "networkidle" });
|
|
3377
|
-
// Update lastUsed
|
|
3378
|
-
session.lastUsed = new Date().toISOString();
|
|
3379
|
-
writeFileSync(sessionPath, JSON.stringify(session, null, 2));
|
|
3380
|
-
return result;
|
|
2887
|
+
return this.sessionManager.load(name, page, this.context);
|
|
3381
2888
|
}
|
|
3382
2889
|
/**
|
|
3383
2890
|
* List all saved session names.
|
|
3384
2891
|
*/
|
|
3385
2892
|
listSessions() {
|
|
3386
|
-
|
|
3387
|
-
return files.filter((f) => f.endsWith(".json") && f !== "last-session.json").map((f) => f.replace(".json", ""));
|
|
2893
|
+
return this.sessionManager.list();
|
|
3388
2894
|
}
|
|
3389
2895
|
/**
|
|
3390
2896
|
* List all saved sessions with rich metadata.
|
|
3391
2897
|
*/
|
|
3392
2898
|
listSessionsDetailed() {
|
|
3393
|
-
|
|
3394
|
-
const sessions = [];
|
|
3395
|
-
for (const file of files) {
|
|
3396
|
-
if (!file.endsWith(".json") || file === "last-session.json")
|
|
3397
|
-
continue;
|
|
3398
|
-
const filePath = join(this.paths.sessionsDir, file);
|
|
3399
|
-
try {
|
|
3400
|
-
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
3401
|
-
const stats = statSync(filePath);
|
|
3402
|
-
sessions.push({
|
|
3403
|
-
name: data.name || file.replace(".json", ""),
|
|
3404
|
-
created: data.created,
|
|
3405
|
-
lastUsed: data.lastUsed,
|
|
3406
|
-
domain: data.domain,
|
|
3407
|
-
url: data.url,
|
|
3408
|
-
cookies: data.cookies?.length || 0,
|
|
3409
|
-
localStorageKeys: Object.keys(data.localStorage || {}).length,
|
|
3410
|
-
sessionStorageKeys: Object.keys(data.sessionStorage || {}).length,
|
|
3411
|
-
sizeBytes: stats.size,
|
|
3412
|
-
});
|
|
3413
|
-
}
|
|
3414
|
-
catch (e) {
|
|
3415
|
-
if (this.config.verbose) {
|
|
3416
|
-
console.debug(`[CBrowser] Skipping malformed session file ${file}: ${e.message}`);
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
return sessions.sort((a, b) => new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime());
|
|
2899
|
+
return this.sessionManager.listDetailed();
|
|
3421
2900
|
}
|
|
3422
2901
|
/**
|
|
3423
2902
|
* Get detailed info for a single session.
|
|
3424
2903
|
*/
|
|
3425
2904
|
getSessionDetails(name) {
|
|
3426
|
-
|
|
3427
|
-
if (!existsSync(sessionPath))
|
|
3428
|
-
return null;
|
|
3429
|
-
try {
|
|
3430
|
-
return JSON.parse(readFileSync(sessionPath, "utf-8"));
|
|
3431
|
-
}
|
|
3432
|
-
catch (e) {
|
|
3433
|
-
if (this.config.verbose) {
|
|
3434
|
-
console.debug(`[CBrowser] Failed to load session ${name}: ${e.message}`);
|
|
3435
|
-
}
|
|
3436
|
-
return null;
|
|
3437
|
-
}
|
|
2905
|
+
return this.sessionManager.getDetails(name);
|
|
3438
2906
|
}
|
|
3439
2907
|
/**
|
|
3440
2908
|
* Delete a saved session.
|
|
3441
2909
|
*/
|
|
3442
2910
|
deleteSession(name) {
|
|
3443
|
-
|
|
3444
|
-
if (existsSync(sessionPath)) {
|
|
3445
|
-
unlinkSync(sessionPath);
|
|
3446
|
-
return true;
|
|
3447
|
-
}
|
|
3448
|
-
return false;
|
|
2911
|
+
return this.sessionManager.delete(name);
|
|
3449
2912
|
}
|
|
3450
2913
|
/**
|
|
3451
2914
|
* Delete sessions older than a given number of days.
|
|
3452
2915
|
*/
|
|
3453
2916
|
cleanupSessions(olderThanDays) {
|
|
3454
|
-
|
|
3455
|
-
const deleted = [];
|
|
3456
|
-
const kept = [];
|
|
3457
|
-
const files = readdirSync(this.paths.sessionsDir);
|
|
3458
|
-
for (const file of files) {
|
|
3459
|
-
if (!file.endsWith(".json") || file === "last-session.json")
|
|
3460
|
-
continue;
|
|
3461
|
-
const filePath = join(this.paths.sessionsDir, file);
|
|
3462
|
-
const name = file.replace(".json", "");
|
|
3463
|
-
try {
|
|
3464
|
-
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
3465
|
-
const lastUsed = new Date(data.lastUsed).getTime();
|
|
3466
|
-
if (lastUsed < cutoff) {
|
|
3467
|
-
unlinkSync(filePath);
|
|
3468
|
-
deleted.push(name);
|
|
3469
|
-
}
|
|
3470
|
-
else {
|
|
3471
|
-
kept.push(name);
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
catch {
|
|
3475
|
-
kept.push(name);
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
return { deleted, kept };
|
|
2917
|
+
return this.sessionManager.cleanup(olderThanDays);
|
|
3479
2918
|
}
|
|
3480
2919
|
/**
|
|
3481
2920
|
* Export a session to a portable JSON file.
|
|
3482
2921
|
*/
|
|
3483
2922
|
exportSession(name, outputPath) {
|
|
3484
|
-
|
|
3485
|
-
if (!existsSync(sessionPath))
|
|
3486
|
-
return false;
|
|
3487
|
-
const data = readFileSync(sessionPath, "utf-8");
|
|
3488
|
-
writeFileSync(outputPath, data);
|
|
3489
|
-
return true;
|
|
2923
|
+
return this.sessionManager.export(name, outputPath);
|
|
3490
2924
|
}
|
|
3491
2925
|
/**
|
|
3492
2926
|
* Import a session from a JSON file.
|
|
3493
2927
|
*/
|
|
3494
2928
|
importSession(inputPath, name) {
|
|
3495
|
-
|
|
3496
|
-
return false;
|
|
3497
|
-
try {
|
|
3498
|
-
const data = JSON.parse(readFileSync(inputPath, "utf-8"));
|
|
3499
|
-
data.name = name;
|
|
3500
|
-
const sessionPath = join(this.paths.sessionsDir, `${name}.json`);
|
|
3501
|
-
writeFileSync(sessionPath, JSON.stringify(data, null, 2));
|
|
3502
|
-
return true;
|
|
3503
|
-
}
|
|
3504
|
-
catch {
|
|
3505
|
-
return false;
|
|
3506
|
-
}
|
|
2929
|
+
return this.sessionManager.import(inputPath, name);
|
|
3507
2930
|
}
|
|
3508
2931
|
// =========================================================================
|
|
3509
2932
|
// Journeys
|
|
@@ -3537,7 +2960,7 @@ export class CBrowser {
|
|
|
3537
2960
|
return {
|
|
3538
2961
|
persona: personaName,
|
|
3539
2962
|
goal,
|
|
3540
|
-
steps: cognitiveResult.frictionPoints.map((fp,
|
|
2963
|
+
steps: cognitiveResult.frictionPoints.map((fp, _i) => ({
|
|
3541
2964
|
action: fp.type,
|
|
3542
2965
|
target: fp.element || 'page',
|
|
3543
2966
|
result: fp.monologue.substring(0, 100),
|