cbrowser 10.5.0 → 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.
Files changed (67) hide show
  1. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  2. package/dist/analysis/accessibility-empathy.js +2 -2
  3. package/dist/analysis/accessibility-empathy.js.map +1 -1
  4. package/dist/analysis/agent-ready-audit.d.ts +1 -1
  5. package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
  6. package/dist/analysis/agent-ready-audit.js +2 -2
  7. package/dist/analysis/agent-ready-audit.js.map +1 -1
  8. package/dist/analysis/bug-hunter.js +2 -2
  9. package/dist/analysis/bug-hunter.js.map +1 -1
  10. package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
  11. package/dist/analysis/competitive-benchmark.js +1 -1
  12. package/dist/analysis/competitive-benchmark.js.map +1 -1
  13. package/dist/analysis/natural-language.js +1 -1
  14. package/dist/analysis/natural-language.js.map +1 -1
  15. package/dist/browser/index.d.ts +12 -0
  16. package/dist/browser/index.d.ts.map +1 -0
  17. package/dist/browser/index.js +9 -0
  18. package/dist/browser/index.js.map +1 -0
  19. package/dist/browser/overlay-handler.d.ts +42 -0
  20. package/dist/browser/overlay-handler.d.ts.map +1 -0
  21. package/dist/browser/overlay-handler.js +334 -0
  22. package/dist/browser/overlay-handler.js.map +1 -0
  23. package/dist/browser/selector-cache.d.ts +69 -0
  24. package/dist/browser/selector-cache.d.ts.map +1 -0
  25. package/dist/browser/selector-cache.js +193 -0
  26. package/dist/browser/selector-cache.js.map +1 -0
  27. package/dist/browser/session-manager.d.ts +60 -0
  28. package/dist/browser/session-manager.d.ts.map +1 -0
  29. package/dist/browser/session-manager.js +261 -0
  30. package/dist/browser/session-manager.js.map +1 -0
  31. package/dist/browser.d.ts +8 -39
  32. package/dist/browser.d.ts.map +1 -1
  33. package/dist/browser.js +65 -642
  34. package/dist/browser.js.map +1 -1
  35. package/dist/cli.js +2 -2
  36. package/dist/cli.js.map +1 -1
  37. package/dist/cognitive/focus-hierarchies.d.ts.map +1 -1
  38. package/dist/cognitive/focus-hierarchies.js +1 -1
  39. package/dist/cognitive/focus-hierarchies.js.map +1 -1
  40. package/dist/cognitive/index.d.ts.map +1 -1
  41. package/dist/cognitive/index.js +1 -1
  42. package/dist/cognitive/index.js.map +1 -1
  43. package/dist/daemon.js.map +1 -1
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +4 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp-server-remote.d.ts.map +1 -1
  49. package/dist/mcp-server-remote.js.map +1 -1
  50. package/dist/mcp-server.d.ts.map +1 -1
  51. package/dist/mcp-server.js.map +1 -1
  52. package/dist/performance/metrics.js +1 -1
  53. package/dist/performance/metrics.js.map +1 -1
  54. package/dist/personas.d.ts.map +1 -1
  55. package/dist/testing/coverage.d.ts.map +1 -1
  56. package/dist/testing/coverage.js +1 -1
  57. package/dist/testing/coverage.js.map +1 -1
  58. package/dist/testing/test-repair.js +2 -2
  59. package/dist/testing/test-repair.js.map +1 -1
  60. package/dist/version.js.map +1 -1
  61. package/dist/visual/cross-browser.js +1 -1
  62. package/dist/visual/cross-browser.js.map +1 -1
  63. package/dist/visual/regression.js +1 -1
  64. package/dist/visual/regression.js.map +1 -1
  65. package/dist/visual/responsive.js +1 -1
  66. package/dist/visual/responsive.js.map +1 -1
  67. 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 FAST_LAUNCH_ARGS = BROWSER_LAUNCH_ARGS.chromium;
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
- const page = await this.getPage();
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 networkResponse = {
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 timeout = options.timeout ?? 5000;
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 optionSelectors = [
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, options = {}) {
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 timeout = options.timeout ?? 5000;
1923
- const details = [];
1924
- const maxPasses = 5; // Prevent infinite loops
1925
- try {
1926
- // Multi-pass dismissal: some sites reveal new overlays after dismissing the first
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
- catch { }
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 title = await el.getAttribute("title").catch(() => "");
1912
+ const _title = await el.getAttribute("title").catch(() => "");
2192
1913
  const id = await el.getAttribute("id").catch(() => "");
2193
- const className = await el.getAttribute("class").catch(() => "");
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 the selector cache file path.
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
- // Reject empty or meaningless selectors
2295
- if (!working || working.trim() === "" || working === 'text=""' || working === "text=''") {
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
- const cache = this.loadSelectorCache();
2323
- const key = this.getSelectorCacheKey(original);
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
- const cache = this.loadSelectorCache();
2335
- const key = this.getSelectorCacheKey(original);
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
- const cache = this.loadSelectorCache();
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
- const cache = this.loadSelectorCache();
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
- const cache = this.loadSelectorCache();
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 testName = testDef.name.toLowerCase().replace(/\s+/g, "-");
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
- const page = await this.getPage();
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
- const context = this.context;
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
- const context = this.context;
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
- const files = readdirSync(this.paths.sessionsDir);
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
- const files = readdirSync(this.paths.sessionsDir);
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
- const sessionPath = join(this.paths.sessionsDir, `${name}.json`);
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
- const sessionPath = join(this.paths.sessionsDir, `${name}.json`);
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
- const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
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
- const sessionPath = join(this.paths.sessionsDir, `${name}.json`);
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
- if (!existsSync(inputPath))
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, i) => ({
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),