@dyyz1993/agent-browser 0.13.2 → 0.23.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.
Files changed (121) hide show
  1. package/bin/agent-browser-linux-x64 +0 -0
  2. package/dist/__tests__/e2e/utils/test-helpers.d.ts +1 -0
  3. package/dist/__tests__/e2e/utils/test-helpers.d.ts.map +1 -1
  4. package/dist/__tests__/e2e/utils/test-helpers.js +14 -1
  5. package/dist/__tests__/e2e/utils/test-helpers.js.map +1 -1
  6. package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
  7. package/dist/__tests__/utils/parseCli.js +83 -1
  8. package/dist/__tests__/utils/parseCli.js.map +1 -1
  9. package/dist/actions.d.ts.map +1 -1
  10. package/dist/actions.js +269 -0
  11. package/dist/actions.js.map +1 -1
  12. package/dist/browser.d.ts +11 -1
  13. package/dist/browser.d.ts.map +1 -1
  14. package/dist/browser.js +63 -2
  15. package/dist/browser.js.map +1 -1
  16. package/dist/cli/commands.d.ts.map +1 -1
  17. package/dist/cli/commands.js +165 -1
  18. package/dist/cli/commands.js.map +1 -1
  19. package/dist/cli/connection.d.ts +13 -0
  20. package/dist/cli/connection.d.ts.map +1 -1
  21. package/dist/cli/connection.js +51 -1
  22. package/dist/cli/connection.js.map +1 -1
  23. package/dist/cli/help.d.ts.map +1 -1
  24. package/dist/cli/help.js +39 -0
  25. package/dist/cli/help.js.map +1 -1
  26. package/dist/cli.js +20 -1
  27. package/dist/cli.js.map +1 -1
  28. package/dist/daemon.d.ts +1 -0
  29. package/dist/daemon.d.ts.map +1 -1
  30. package/dist/daemon.js +22 -0
  31. package/dist/daemon.js.map +1 -1
  32. package/dist/diff.d.ts.map +1 -1
  33. package/dist/diff.js +1 -1
  34. package/dist/diff.js.map +1 -1
  35. package/dist/flow/exporters/index.d.ts +4 -0
  36. package/dist/flow/exporters/index.d.ts.map +1 -0
  37. package/dist/flow/exporters/index.js +3 -0
  38. package/dist/flow/exporters/index.js.map +1 -0
  39. package/dist/flow/exporters/playwright.d.ts +20 -0
  40. package/dist/flow/exporters/playwright.d.ts.map +1 -0
  41. package/dist/flow/exporters/playwright.js +175 -0
  42. package/dist/flow/exporters/playwright.js.map +1 -0
  43. package/dist/flow/exporters/python.d.ts +20 -0
  44. package/dist/flow/exporters/python.d.ts.map +1 -0
  45. package/dist/flow/exporters/python.js +163 -0
  46. package/dist/flow/exporters/python.js.map +1 -0
  47. package/dist/flow/exporters/types.d.ts +13 -0
  48. package/dist/flow/exporters/types.d.ts.map +1 -0
  49. package/dist/flow/exporters/types.js +2 -0
  50. package/dist/flow/exporters/types.js.map +1 -0
  51. package/dist/flow/flow-executor.d.ts +55 -0
  52. package/dist/flow/flow-executor.d.ts.map +1 -0
  53. package/dist/flow/flow-executor.js +1169 -0
  54. package/dist/flow/flow-executor.js.map +1 -0
  55. package/dist/flow/index.d.ts +15 -0
  56. package/dist/flow/index.d.ts.map +1 -0
  57. package/dist/flow/index.js +10 -0
  58. package/dist/flow/index.js.map +1 -0
  59. package/dist/flow/output.d.ts +11 -0
  60. package/dist/flow/output.d.ts.map +1 -0
  61. package/dist/flow/output.js +84 -0
  62. package/dist/flow/output.js.map +1 -0
  63. package/dist/flow/plugin-system.d.ts +48 -0
  64. package/dist/flow/plugin-system.d.ts.map +1 -0
  65. package/dist/flow/plugin-system.js +132 -0
  66. package/dist/flow/plugin-system.js.map +1 -0
  67. package/dist/flow/plugins/file-output-plugin.d.ts +8 -0
  68. package/dist/flow/plugins/file-output-plugin.d.ts.map +1 -0
  69. package/dist/flow/plugins/file-output-plugin.js +31 -0
  70. package/dist/flow/plugins/file-output-plugin.js.map +1 -0
  71. package/dist/flow/plugins/index.d.ts +4 -0
  72. package/dist/flow/plugins/index.d.ts.map +1 -0
  73. package/dist/flow/plugins/index.js +4 -0
  74. package/dist/flow/plugins/index.js.map +1 -0
  75. package/dist/flow/plugins/logging-plugin.d.ts +7 -0
  76. package/dist/flow/plugins/logging-plugin.d.ts.map +1 -0
  77. package/dist/flow/plugins/logging-plugin.js +40 -0
  78. package/dist/flow/plugins/logging-plugin.js.map +1 -0
  79. package/dist/flow/plugins/webhook-plugin.d.ts +7 -0
  80. package/dist/flow/plugins/webhook-plugin.d.ts.map +1 -0
  81. package/dist/flow/plugins/webhook-plugin.js +24 -0
  82. package/dist/flow/plugins/webhook-plugin.js.map +1 -0
  83. package/dist/flow/presets/index.d.ts +10 -0
  84. package/dist/flow/presets/index.d.ts.map +1 -0
  85. package/dist/flow/presets/index.js +29 -0
  86. package/dist/flow/presets/index.js.map +1 -0
  87. package/dist/flow/recorder-to-flow.d.ts +70 -0
  88. package/dist/flow/recorder-to-flow.d.ts.map +1 -0
  89. package/dist/flow/recorder-to-flow.js +392 -0
  90. package/dist/flow/recorder-to-flow.js.map +1 -0
  91. package/dist/flow/site-manager.d.ts +24 -0
  92. package/dist/flow/site-manager.d.ts.map +1 -0
  93. package/dist/flow/site-manager.js +125 -0
  94. package/dist/flow/site-manager.js.map +1 -0
  95. package/dist/flow/types.d.ts +181 -0
  96. package/dist/flow/types.d.ts.map +1 -0
  97. package/dist/flow/types.js +2 -0
  98. package/dist/flow/types.js.map +1 -0
  99. package/dist/flow/yaml-parser.d.ts +15 -0
  100. package/dist/flow/yaml-parser.d.ts.map +1 -0
  101. package/dist/flow/yaml-parser.js +214 -0
  102. package/dist/flow/yaml-parser.js.map +1 -0
  103. package/dist/protocol.d.ts.map +1 -1
  104. package/dist/protocol.js +15 -0
  105. package/dist/protocol.js.map +1 -1
  106. package/dist/recorder/inject.js +730 -332
  107. package/dist/snapshot-store.d.ts +77 -0
  108. package/dist/snapshot-store.d.ts.map +1 -0
  109. package/dist/snapshot-store.js +97 -0
  110. package/dist/snapshot-store.js.map +1 -0
  111. package/dist/snapshot.d.ts +6 -7
  112. package/dist/snapshot.d.ts.map +1 -1
  113. package/dist/snapshot.js +437 -1
  114. package/dist/snapshot.js.map +1 -1
  115. package/dist/types.d.ts +13 -1
  116. package/dist/types.d.ts.map +1 -1
  117. package/dist/types.js.map +1 -1
  118. package/dist/viewer-script.js +4 -4
  119. package/dist/viewer-script.js.map +1 -1
  120. package/package.json +7 -3
  121. package/skills/agent-browser/SKILL.md +102 -3
@@ -0,0 +1,77 @@
1
+ /**
2
+ * SnapshotStore - In-memory store for snapshot data with stable selectors.
3
+ *
4
+ * Each snapshot gets a unique sequential ID (snap_1, snap_2, ...).
5
+ * Elements within each snapshot have stable CSS selectors and XPaths
6
+ * that can be retrieved later by snapshot_id + ref/index.
7
+ */
8
+ export interface SnapshotElement {
9
+ /** Ref ID (e.g., "e1") */
10
+ ref: string;
11
+ /** Display index (1-based, e.g., 1, 2, 3) */
12
+ index: number;
13
+ /** ARIA role (e.g., "button", "link", "textbox") */
14
+ role: string;
15
+ /** Accessible name (e.g., "Submit") */
16
+ name?: string;
17
+ /** Stable CSS selector (e.g., "#su", "input[name='wd']") */
18
+ cssSelector: string;
19
+ /** Stable XPath (e.g., "//*[@id='su']") */
20
+ xpath: string;
21
+ }
22
+ export interface SnapshotEntry {
23
+ /** Unique snapshot ID (e.g., "snap_3") */
24
+ id: string;
25
+ /** Creation timestamp */
26
+ timestamp: number;
27
+ /** Page URL at snapshot time */
28
+ url: string;
29
+ /** Iframe path if snapshot was taken inside an iframe */
30
+ framePath?: string;
31
+ /** Elements keyed by ref ID (e.g., "e1" -> SnapshotElement) */
32
+ elements: Map<string, SnapshotElement>;
33
+ /** Whether stable selectors have been generated for this snapshot */
34
+ selectorsGenerated: boolean;
35
+ }
36
+ export declare class SnapshotStore {
37
+ private snapshots;
38
+ private counter;
39
+ /**
40
+ * Create a new snapshot entry.
41
+ * @returns The generated snapshot ID (e.g., "snap_3")
42
+ */
43
+ create(url: string, elements: SnapshotElement[], framePath?: string): string;
44
+ /**
45
+ * Get a snapshot entry by ID.
46
+ */
47
+ get(id: string): SnapshotEntry | undefined;
48
+ /**
49
+ * Get a specific element from a snapshot by ref or index.
50
+ *
51
+ * @param snapId - Snapshot ID (e.g., "snap_3")
52
+ * @param refOrIndex - Either a ref like "e1" or "@e1", or an index like "1"
53
+ * @returns The SnapshotElement or undefined
54
+ */
55
+ getElement(snapId: string, refOrIndex: string): SnapshotElement | undefined;
56
+ /**
57
+ * Get all elements from a snapshot as an array, sorted by index.
58
+ */
59
+ getElements(snapId: string): SnapshotElement[] | undefined;
60
+ /**
61
+ * Get the current counter value (for testing/debugging).
62
+ */
63
+ getCounter(): number;
64
+ /**
65
+ * Check if a snapshot exists.
66
+ */
67
+ has(id: string): boolean;
68
+ /**
69
+ * Mark selectors as generated for a snapshot.
70
+ */
71
+ markSelectorsGenerated(id: string): void;
72
+ /**
73
+ * Check if selectors have been generated for a snapshot.
74
+ */
75
+ isSelectorsGenerated(id: string): boolean;
76
+ }
77
+ //# sourceMappingURL=snapshot-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-store.d.ts","sourceRoot":"","sources":["../src/snapshot-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,eAAe;IAC9B,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,qEAAqE;IACrE,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAyC;IAC1D,OAAO,CAAC,OAAO,CAAa;IAE5B;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM;IAc5E;;OAEG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI1C;;;;;;OAMG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAuB3E;;OAEG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,GAAG,SAAS;IAM1D;;OAEG;IACH,UAAU,IAAI,MAAM;IAIpB;;OAEG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB;;OAEG;IACH,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKxC;;OAEG;IACH,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;CAG1C"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * SnapshotStore - In-memory store for snapshot data with stable selectors.
3
+ *
4
+ * Each snapshot gets a unique sequential ID (snap_1, snap_2, ...).
5
+ * Elements within each snapshot have stable CSS selectors and XPaths
6
+ * that can be retrieved later by snapshot_id + ref/index.
7
+ */
8
+ export class SnapshotStore {
9
+ snapshots = new Map();
10
+ counter = 0;
11
+ /**
12
+ * Create a new snapshot entry.
13
+ * @returns The generated snapshot ID (e.g., "snap_3")
14
+ */
15
+ create(url, elements, framePath) {
16
+ const id = `snap_${++this.counter}`;
17
+ const elementMap = new Map(elements.map((e) => [e.ref, e]));
18
+ this.snapshots.set(id, {
19
+ id,
20
+ timestamp: Date.now(),
21
+ url,
22
+ framePath,
23
+ elements: elementMap,
24
+ selectorsGenerated: false,
25
+ });
26
+ return id;
27
+ }
28
+ /**
29
+ * Get a snapshot entry by ID.
30
+ */
31
+ get(id) {
32
+ return this.snapshots.get(id);
33
+ }
34
+ /**
35
+ * Get a specific element from a snapshot by ref or index.
36
+ *
37
+ * @param snapId - Snapshot ID (e.g., "snap_3")
38
+ * @param refOrIndex - Either a ref like "e1" or "@e1", or an index like "1"
39
+ * @returns The SnapshotElement or undefined
40
+ */
41
+ getElement(snapId, refOrIndex) {
42
+ const entry = this.snapshots.get(snapId);
43
+ if (!entry)
44
+ return undefined;
45
+ // Strip @ prefix if present (e.g., "@e1" -> "e1")
46
+ const cleaned = refOrIndex.startsWith('@') ? refOrIndex.slice(1) : refOrIndex;
47
+ // Try as ref first (e.g., "e1")
48
+ if (entry.elements.has(cleaned)) {
49
+ return entry.elements.get(cleaned);
50
+ }
51
+ // Try as index (e.g., "1")
52
+ const index = parseInt(cleaned, 10);
53
+ if (!isNaN(index)) {
54
+ for (const el of entry.elements.values()) {
55
+ if (el.index === index)
56
+ return el;
57
+ }
58
+ }
59
+ return undefined;
60
+ }
61
+ /**
62
+ * Get all elements from a snapshot as an array, sorted by index.
63
+ */
64
+ getElements(snapId) {
65
+ const entry = this.snapshots.get(snapId);
66
+ if (!entry)
67
+ return undefined;
68
+ return Array.from(entry.elements.values()).sort((a, b) => a.index - b.index);
69
+ }
70
+ /**
71
+ * Get the current counter value (for testing/debugging).
72
+ */
73
+ getCounter() {
74
+ return this.counter;
75
+ }
76
+ /**
77
+ * Check if a snapshot exists.
78
+ */
79
+ has(id) {
80
+ return this.snapshots.has(id);
81
+ }
82
+ /**
83
+ * Mark selectors as generated for a snapshot.
84
+ */
85
+ markSelectorsGenerated(id) {
86
+ const entry = this.snapshots.get(id);
87
+ if (entry)
88
+ entry.selectorsGenerated = true;
89
+ }
90
+ /**
91
+ * Check if selectors have been generated for a snapshot.
92
+ */
93
+ isSelectorsGenerated(id) {
94
+ return this.snapshots.get(id)?.selectorsGenerated ?? false;
95
+ }
96
+ }
97
+ //# sourceMappingURL=snapshot-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-store.js","sourceRoot":"","sources":["../src/snapshot-store.ts"],"names":[],"mappings":"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"}
@@ -37,25 +37,24 @@ export interface EnhancedSnapshot {
37
37
  refs: RefMap;
38
38
  }
39
39
  export interface SnapshotOptions {
40
- /** Only include interactive elements (buttons, links, inputs, etc.) */
41
40
  interactive?: boolean;
42
- /** Include cursor-interactive elements (cursor:pointer, onclick, tabindex) */
43
41
  cursor?: boolean;
44
- /** Maximum depth of tree to include (0 = root only) */
45
42
  maxDepth?: number;
46
- /** Remove structural elements without meaningful content */
47
43
  compact?: boolean;
48
- /** CSS selector to scope the snapshot */
49
44
  selector?: string;
50
- /** Include xpath and cssPath in refs (requires selector) */
51
45
  path?: boolean;
52
- /** Include element attributes in refs (requires selector) */
53
46
  attrs?: boolean;
47
+ selectors?: boolean;
48
+ all?: boolean;
54
49
  }
55
50
  /**
56
51
  * Reset ref counter (call at start of each snapshot)
57
52
  */
58
53
  export declare function resetRefs(): void;
54
+ export declare function generateStableSelectors(page: Page | Frame, refs: RefMap): Promise<Record<string, {
55
+ cssSelector: string;
56
+ xpath: string;
57
+ }>>;
59
58
  /**
60
59
  * Get enhanced snapshot with refs and optional filtering
61
60
  */
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,KAAK,EAAW,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE1E,MAAM,WAAW,MAAM;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,0EAA0E;QAC1E,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,uCAAuC;QACvC,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,8CAA8C;QAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gDAAgD;QAChD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACrC,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAKD;;GAEG;AACH,wBAAgB,SAAS,IAAI,IAAI,CAEhC;AAwTD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,GAAG,KAAK,GAAG,YAAY,EACjC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CA+E3B;AAu0BD;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAcnD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB,CAUA;AA6BD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAYhE;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAqB5E;AAkDD,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAyC9E;AAyBD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS1E"}
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,KAAK,EAAW,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE1E,MAAM,WAAW,MAAM;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,0EAA0E;QAC1E,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,uCAAuC;QACvC,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,8CAA8C;QAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gDAAgD;QAChD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACrC,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAKD;;GAEG;AACH,wBAAgB,SAAS,IAAI,IAAI,CAEhC;AAwTD,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,IAAI,GAAG,KAAK,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CA4VjE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,GAAG,KAAK,GAAG,YAAY,EACjC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CAwF3B;AA44BD;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAcnD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB,CAUA;AA6BD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAYhE;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAqB5E;AAkDD,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAyC9E;AAyBD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS1E"}
package/dist/snapshot.js CHANGED
@@ -311,6 +311,369 @@ async function suggestSelectors(page) {
311
311
  }
312
312
  return selectors;
313
313
  }
314
+ export async function generateStableSelectors(page, refs) {
315
+ const result = {};
316
+ for (const [ref, data] of Object.entries(refs)) {
317
+ if (data.role === 'clickable' || data.role === 'focusable') {
318
+ if (data.selector && !data.selector.startsWith('getByRole')) {
319
+ result[ref] = { cssSelector: data.selector, xpath: '' };
320
+ }
321
+ continue;
322
+ }
323
+ try {
324
+ let locator;
325
+ if (data.name) {
326
+ locator = page.getByRole(data.role, { name: data.name, exact: true });
327
+ }
328
+ else {
329
+ locator = page.getByRole(data.role);
330
+ }
331
+ if (data.nth !== undefined) {
332
+ locator = locator.nth(data.nth);
333
+ }
334
+ const elementCount = await locator.count();
335
+ if (elementCount === 0)
336
+ continue;
337
+ const selectorData = await locator
338
+ .evaluate((el) => {
339
+ const UTILITY_CLASS_PATTERNS = [
340
+ /^_/,
341
+ /^css-/,
342
+ /^[a-z]{1,2}$/,
343
+ /^(active|disabled|hidden|visible|selected|hover|focus|current|open|closed)$/i,
344
+ /^(text-|font-|bg-|p-|m-|w-|h-|flex|grid|border|rounded|shadow|opacity|z-)/,
345
+ /^(sm:|md:|lg:|xl:|2xl:)/,
346
+ ];
347
+ const SEMANTIC_ATTRS = [
348
+ 'data-testid',
349
+ 'data-test',
350
+ 'data-cy',
351
+ 'name',
352
+ 'aria-label',
353
+ 'aria-labelledby',
354
+ 'role',
355
+ 'type',
356
+ 'placeholder',
357
+ 'title',
358
+ 'alt',
359
+ ];
360
+ function isHighEntropyClassName(className) {
361
+ if (!className || className.length < 4 || className.length > 15)
362
+ return false;
363
+ if (/^[a-zA-Z]+_[a-zA-Z]+_{2}[a-zA-Z0-9]+$/.test(className))
364
+ return true;
365
+ if (/^sc-[a-zA-Z0-9]+$/.test(className))
366
+ return true;
367
+ const hasUpper = /[A-Z]/.test(className);
368
+ const hasLower = /[a-z]/.test(className);
369
+ const hasDigit = /[0-9]/.test(className);
370
+ const hasSeparator = /[-_]/.test(className);
371
+ if (hasSeparator)
372
+ return false;
373
+ if (hasUpper && hasLower && hasDigit)
374
+ return true;
375
+ if (/^[A-Z][a-z0-9]+[A-Z]/.test(className) && className.length <= 12)
376
+ return true;
377
+ if (/^[a-z]/.test(className) && /[a-z][A-Z][a-z][A-Z]/.test(className))
378
+ return true;
379
+ return false;
380
+ }
381
+ function isUniqueSelector(selector) {
382
+ try {
383
+ return document.querySelectorAll(selector).length === 1;
384
+ }
385
+ catch {
386
+ return false;
387
+ }
388
+ }
389
+ function filterUsefulClasses(element) {
390
+ if (!element.className || typeof element.className !== 'string')
391
+ return [];
392
+ return element.className
393
+ .trim()
394
+ .split(/\s+/)
395
+ .filter((c) => {
396
+ if (!c)
397
+ return false;
398
+ if (UTILITY_CLASS_PATTERNS.some((p) => p.test(c)))
399
+ return false;
400
+ if (isHighEntropyClassName(c))
401
+ return false;
402
+ return true;
403
+ });
404
+ }
405
+ function tryIdSelector(element) {
406
+ if (element.id) {
407
+ const sel = '#' + CSS.escape(element.id);
408
+ if (isUniqueSelector(sel))
409
+ return sel;
410
+ }
411
+ return null;
412
+ }
413
+ function getMultiAttributeSelector(element) {
414
+ const tag = element.tagName.toLowerCase();
415
+ const attrs = [];
416
+ for (const attr of SEMANTIC_ATTRS) {
417
+ const value = element.getAttribute(attr);
418
+ if (value)
419
+ attrs.push({ attr, value });
420
+ }
421
+ if (attrs.length === 0)
422
+ return null;
423
+ for (const { attr, value } of attrs) {
424
+ const sel = tag + '[' + attr + '="' + CSS.escape(value) + '"]';
425
+ if (isUniqueSelector(sel))
426
+ return sel;
427
+ }
428
+ if (attrs.length >= 2) {
429
+ for (let i = 0; i < attrs.length; i++) {
430
+ for (let j = i + 1; j < attrs.length; j++) {
431
+ const sel = tag +
432
+ '[' +
433
+ attrs[i].attr +
434
+ '="' +
435
+ CSS.escape(attrs[i].value) +
436
+ '"]' +
437
+ '[' +
438
+ attrs[j].attr +
439
+ '="' +
440
+ CSS.escape(attrs[j].value) +
441
+ '"]';
442
+ if (isUniqueSelector(sel))
443
+ return sel;
444
+ }
445
+ }
446
+ }
447
+ return null;
448
+ }
449
+ function getAttributeClassComboSelector(element) {
450
+ const tag = element.tagName.toLowerCase();
451
+ const classes = filterUsefulClasses(element);
452
+ if (classes.length === 0)
453
+ return null;
454
+ classes.sort((a, b) => b.length - a.length);
455
+ const bestClass = classes[0];
456
+ for (const attr of SEMANTIC_ATTRS) {
457
+ const value = element.getAttribute(attr);
458
+ if (value) {
459
+ const sel = tag + '.' + CSS.escape(bestClass) + '[' + attr + '="' + CSS.escape(value) + '"]';
460
+ if (isUniqueSelector(sel))
461
+ return sel;
462
+ }
463
+ }
464
+ return null;
465
+ }
466
+ function getBestClassSelector(element) {
467
+ const classes = filterUsefulClasses(element);
468
+ if (classes.length === 0)
469
+ return null;
470
+ classes.sort((a, b) => b.length - a.length);
471
+ const tag = element.tagName.toLowerCase();
472
+ for (const cls of classes) {
473
+ const sel = tag + '.' + CSS.escape(cls);
474
+ if (isUniqueSelector(sel))
475
+ return sel;
476
+ }
477
+ for (let i = 2; i <= Math.min(3, classes.length); i++) {
478
+ const sel = tag +
479
+ '.' +
480
+ classes
481
+ .slice(0, i)
482
+ .map((c) => CSS.escape(c))
483
+ .join('.');
484
+ if (isUniqueSelector(sel))
485
+ return sel;
486
+ }
487
+ return null;
488
+ }
489
+ function getFeatureSelector(element) {
490
+ if (!element || element === document.body)
491
+ return null;
492
+ if (element.id)
493
+ return '#' + CSS.escape(element.id);
494
+ for (const attr of ['data-testid', 'data-test', 'name', 'role', 'aria-label']) {
495
+ const value = element.getAttribute(attr);
496
+ if (value)
497
+ return element.tagName.toLowerCase() + '[' + attr + '="' + CSS.escape(value) + '"]';
498
+ }
499
+ const classes = filterUsefulClasses(element);
500
+ if (classes.length > 0) {
501
+ classes.sort((a, b) => b.length - a.length);
502
+ const sel = element.tagName.toLowerCase() + '.' + CSS.escape(classes[0]);
503
+ if (isUniqueSelector(sel))
504
+ return sel;
505
+ }
506
+ return null;
507
+ }
508
+ function getBaseSelector(element) {
509
+ let sel = element.tagName.toLowerCase();
510
+ const classes = filterUsefulClasses(element);
511
+ if (classes.length > 0) {
512
+ classes.sort((a, b) => b.length - a.length);
513
+ sel +=
514
+ '.' +
515
+ classes
516
+ .slice(0, 2)
517
+ .map((c) => CSS.escape(c))
518
+ .join('.');
519
+ }
520
+ return sel;
521
+ }
522
+ function makeUniqueWithNth(element, baseSelector) {
523
+ const parent = element.parentElement;
524
+ if (!parent)
525
+ return baseSelector;
526
+ const siblings = Array.from(parent.children);
527
+ const sameTagSiblings = siblings.filter((s) => s.tagName === element.tagName);
528
+ if (sameTagSiblings.length === 1)
529
+ return baseSelector;
530
+ const index = siblings.indexOf(element) + 1;
531
+ return baseSelector + ':nth-child(' + index + ')';
532
+ }
533
+ function getSiblingBasedSelector(element) {
534
+ let prevSibling = element.previousElementSibling;
535
+ let attempts = 0;
536
+ while (prevSibling && attempts < 3) {
537
+ const siblingSelector = getFeatureSelector(prevSibling);
538
+ if (siblingSelector && isUniqueSelector(siblingSelector)) {
539
+ const elementSelector = getBaseSelector(element);
540
+ const combined = siblingSelector + ' + ' + elementSelector;
541
+ if (isUniqueSelector(combined))
542
+ return combined;
543
+ }
544
+ prevSibling = prevSibling.previousElementSibling;
545
+ attempts++;
546
+ }
547
+ return null;
548
+ }
549
+ function buildComposedSelector(element) {
550
+ const selfSelector = getBestClassSelector(element);
551
+ if (selfSelector && isUniqueSelector(selfSelector))
552
+ return selfSelector;
553
+ const parts = [];
554
+ let current = element;
555
+ let depth = 0;
556
+ const maxDepth = 3;
557
+ while (current && current !== document.body && depth < maxDepth) {
558
+ const featureSelector = getFeatureSelector(current);
559
+ if (featureSelector) {
560
+ parts.unshift(featureSelector);
561
+ const elementSelector = depth === 0 ? getBaseSelector(element) : getBaseSelector(current);
562
+ const fullSelector = parts.join(' > ') + (depth > 0 ? '' : ' > ' + elementSelector);
563
+ if (isUniqueSelector(fullSelector))
564
+ return fullSelector;
565
+ }
566
+ else {
567
+ const baseSelector = getBaseSelector(current);
568
+ const selector = makeUniqueWithNth(current, baseSelector);
569
+ parts.unshift(selector);
570
+ const fullSelector = parts.join(' > ');
571
+ if (isUniqueSelector(fullSelector))
572
+ return fullSelector;
573
+ }
574
+ current = current.parentElement;
575
+ depth++;
576
+ }
577
+ return parts.length > 0 ? parts.join(' > ') : null;
578
+ }
579
+ function tryNthChild(element) {
580
+ const baseSelector = getBaseSelector(element);
581
+ const uniqueSelector = makeUniqueWithNth(element, baseSelector);
582
+ try {
583
+ if (document.querySelectorAll(uniqueSelector).length === 1)
584
+ return uniqueSelector;
585
+ }
586
+ catch { }
587
+ return null;
588
+ }
589
+ function buildUniquePath(element) {
590
+ const parts = [];
591
+ let current = element;
592
+ let depth = 0;
593
+ while (current && current !== document.body && depth < 5) {
594
+ const baseSelector = getBaseSelector(current);
595
+ const selector = makeUniqueWithNth(current, baseSelector);
596
+ parts.unshift(selector);
597
+ const fullSelector = parts.join(' > ');
598
+ if (isUniqueSelector(fullSelector))
599
+ return fullSelector;
600
+ current = current.parentElement;
601
+ depth++;
602
+ }
603
+ return parts.length > 0 ? parts.join(' > ') : null;
604
+ }
605
+ function generateXPath(element) {
606
+ if (element.id)
607
+ return '//*[@id="' + element.id + '"]';
608
+ const testId = element.getAttribute('data-testid');
609
+ if (testId)
610
+ return '//*[@data-testid="' + testId + '"]';
611
+ const nameAttr = element.getAttribute('name');
612
+ if (nameAttr)
613
+ return '//' + element.tagName.toLowerCase() + '[@name="' + nameAttr + '"]';
614
+ const parts = [];
615
+ let current = element;
616
+ let depth = 0;
617
+ while (current && depth < 5) {
618
+ if (current.id) {
619
+ parts.unshift('//*[@id="' + current.id + '"]');
620
+ break;
621
+ }
622
+ const testId = current.getAttribute('data-testid');
623
+ if (testId) {
624
+ parts.unshift('//*[@data-testid="' + testId + '"]');
625
+ break;
626
+ }
627
+ const tagName = current.tagName.toLowerCase();
628
+ const parent = current.parentElement;
629
+ if (parent) {
630
+ const siblings = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
631
+ const index = siblings.indexOf(current) + 1;
632
+ parts.unshift(tagName + '[' + index + ']');
633
+ }
634
+ else {
635
+ parts.unshift(tagName);
636
+ }
637
+ current = current.parentElement;
638
+ depth++;
639
+ }
640
+ if (parts.length > 0 && !parts[0].startsWith('//'))
641
+ parts.unshift('//');
642
+ return parts.join('/');
643
+ }
644
+ let cssSelector = null;
645
+ cssSelector = tryIdSelector(el);
646
+ if (!cssSelector)
647
+ cssSelector = getMultiAttributeSelector(el);
648
+ if (!cssSelector)
649
+ cssSelector = getAttributeClassComboSelector(el);
650
+ if (!cssSelector)
651
+ cssSelector = getBestClassSelector(el);
652
+ if (!cssSelector)
653
+ cssSelector = getSiblingBasedSelector(el);
654
+ if (!cssSelector)
655
+ cssSelector = buildComposedSelector(el);
656
+ if (!cssSelector)
657
+ cssSelector = tryNthChild(el);
658
+ if (!cssSelector)
659
+ cssSelector = buildUniquePath(el);
660
+ if (!cssSelector)
661
+ cssSelector = el.tagName.toLowerCase();
662
+ const xpath = generateXPath(el);
663
+ return { cssSelector, xpath };
664
+ })
665
+ .catch(() => null);
666
+ if (selectorData) {
667
+ result[ref] = {
668
+ cssSelector: selectorData.cssSelector,
669
+ xpath: selectorData.xpath,
670
+ };
671
+ }
672
+ }
673
+ catch { }
674
+ }
675
+ return result;
676
+ }
314
677
  /**
315
678
  * Get enhanced snapshot with refs and optional filtering
316
679
  */
@@ -378,7 +741,80 @@ export async function getEnhancedSnapshot(page, options = {}) {
378
741
  if (options.path || options.attrs) {
379
742
  await enrichRefsWithPathsAndAttrs(page, refs, options);
380
743
  }
381
- return { tree: enhancedTree, refs };
744
+ let finalTree = enhancedTree;
745
+ if (options.selectors && Object.keys(refs).length > 0) {
746
+ const selectorMap = await buildCompactSelectors(page, refs, options);
747
+ if (selectorMap) {
748
+ finalTree += '\n## Selectors\n' + selectorMap;
749
+ }
750
+ }
751
+ return { tree: finalTree, refs };
752
+ }
753
+ async function buildCompactSelectors(page, refs, options) {
754
+ const entries = Object.entries(refs);
755
+ const parts = [];
756
+ const includeAll = options?.all ?? false;
757
+ for (const [ref, data] of entries) {
758
+ if (data.role === 'clickable' || data.role === 'focusable')
759
+ continue;
760
+ try {
761
+ let locator;
762
+ if (data.name) {
763
+ locator = page.getByRole(data.role, { name: data.name, exact: true });
764
+ }
765
+ else {
766
+ locator = page.getByRole(data.role);
767
+ }
768
+ if (data.nth !== undefined)
769
+ locator = locator.nth(data.nth);
770
+ if (!includeAll) {
771
+ const isReallyVisible = await locator
772
+ .evaluate((el) => {
773
+ const style = getComputedStyle(el);
774
+ const rect = el.getBoundingClientRect();
775
+ return !(style.display === 'none' ||
776
+ style.visibility === 'hidden' ||
777
+ parseFloat(style.opacity) === 0 ||
778
+ (rect.width === 0 && rect.height === 0) ||
779
+ rect.x + rect.width < 0 ||
780
+ rect.y + rect.height < 0);
781
+ })
782
+ .catch(() => false);
783
+ if (!isReallyVisible)
784
+ continue;
785
+ }
786
+ const attrs = await locator
787
+ .evaluate((el) => {
788
+ const htmlEl = el;
789
+ const r = {};
790
+ if (htmlEl.dataset.testid)
791
+ r['testid'] = `[data-testid="${htmlEl.dataset.testid}"]`;
792
+ if (htmlEl.id && !htmlEl.id.match(/^[:]/))
793
+ r['id'] = '#' + CSS.escape(htmlEl.id);
794
+ const nameAttr = htmlEl.getAttribute('name');
795
+ if (nameAttr)
796
+ r['name'] = `${htmlEl.tagName.toLowerCase()}[name="${nameAttr}"]`;
797
+ return r;
798
+ })
799
+ .catch(() => null);
800
+ if (!attrs)
801
+ continue;
802
+ let bestSelector = '';
803
+ if (attrs.testid)
804
+ bestSelector = attrs.testid;
805
+ else if (attrs.id)
806
+ bestSelector = attrs.id;
807
+ else if (attrs.name)
808
+ bestSelector = attrs.name;
809
+ if (bestSelector) {
810
+ parts.push(`${ref}: ${bestSelector}`);
811
+ }
812
+ }
813
+ catch {
814
+ // skip
815
+ }
816
+ }
817
+ return parts.join(' | ');
382
818
  }
383
819
  async function enrichRefsWithPathsAndAttrs(page, refs, options) {
384
820
  if (Object.keys(refs).length === 0) {