browser-commander 0.2.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 (82) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +296 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.jscpd.json +20 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +32 -0
  9. package/LICENSE +24 -0
  10. package/README.md +320 -0
  11. package/bunfig.toml +3 -0
  12. package/deno.json +7 -0
  13. package/eslint.config.js +125 -0
  14. package/examples/react-test-app/index.html +25 -0
  15. package/examples/react-test-app/package.json +19 -0
  16. package/examples/react-test-app/src/App.jsx +473 -0
  17. package/examples/react-test-app/src/main.jsx +10 -0
  18. package/examples/react-test-app/src/styles.css +323 -0
  19. package/examples/react-test-app/vite.config.js +9 -0
  20. package/package.json +89 -0
  21. package/scripts/changeset-version.mjs +38 -0
  22. package/scripts/create-github-release.mjs +93 -0
  23. package/scripts/create-manual-changeset.mjs +86 -0
  24. package/scripts/format-github-release.mjs +83 -0
  25. package/scripts/format-release-notes.mjs +216 -0
  26. package/scripts/instant-version-bump.mjs +121 -0
  27. package/scripts/merge-changesets.mjs +260 -0
  28. package/scripts/publish-to-npm.mjs +126 -0
  29. package/scripts/setup-npm.mjs +37 -0
  30. package/scripts/validate-changeset.mjs +262 -0
  31. package/scripts/version-and-commit.mjs +237 -0
  32. package/src/ARCHITECTURE.md +270 -0
  33. package/src/README.md +517 -0
  34. package/src/bindings.js +298 -0
  35. package/src/browser/launcher.js +93 -0
  36. package/src/browser/navigation.js +513 -0
  37. package/src/core/constants.js +24 -0
  38. package/src/core/engine-adapter.js +466 -0
  39. package/src/core/engine-detection.js +49 -0
  40. package/src/core/logger.js +21 -0
  41. package/src/core/navigation-manager.js +503 -0
  42. package/src/core/navigation-safety.js +160 -0
  43. package/src/core/network-tracker.js +373 -0
  44. package/src/core/page-session.js +299 -0
  45. package/src/core/page-trigger-manager.js +564 -0
  46. package/src/core/preferences.js +46 -0
  47. package/src/elements/content.js +197 -0
  48. package/src/elements/locators.js +243 -0
  49. package/src/elements/selectors.js +360 -0
  50. package/src/elements/visibility.js +166 -0
  51. package/src/exports.js +121 -0
  52. package/src/factory.js +192 -0
  53. package/src/high-level/universal-logic.js +206 -0
  54. package/src/index.js +17 -0
  55. package/src/interactions/click.js +684 -0
  56. package/src/interactions/fill.js +383 -0
  57. package/src/interactions/scroll.js +341 -0
  58. package/src/utilities/url.js +33 -0
  59. package/src/utilities/wait.js +135 -0
  60. package/tests/e2e/playwright.e2e.test.js +442 -0
  61. package/tests/e2e/puppeteer.e2e.test.js +408 -0
  62. package/tests/helpers/mocks.js +542 -0
  63. package/tests/unit/bindings.test.js +218 -0
  64. package/tests/unit/browser/navigation.test.js +345 -0
  65. package/tests/unit/core/constants.test.js +72 -0
  66. package/tests/unit/core/engine-adapter.test.js +170 -0
  67. package/tests/unit/core/engine-detection.test.js +81 -0
  68. package/tests/unit/core/logger.test.js +80 -0
  69. package/tests/unit/core/navigation-safety.test.js +202 -0
  70. package/tests/unit/core/network-tracker.test.js +198 -0
  71. package/tests/unit/core/page-trigger-manager.test.js +358 -0
  72. package/tests/unit/elements/content.test.js +318 -0
  73. package/tests/unit/elements/locators.test.js +236 -0
  74. package/tests/unit/elements/selectors.test.js +302 -0
  75. package/tests/unit/elements/visibility.test.js +234 -0
  76. package/tests/unit/factory.test.js +174 -0
  77. package/tests/unit/high-level/universal-logic.test.js +299 -0
  78. package/tests/unit/interactions/click.test.js +340 -0
  79. package/tests/unit/interactions/fill.test.js +378 -0
  80. package/tests/unit/interactions/scroll.test.js +330 -0
  81. package/tests/unit/utilities/url.test.js +63 -0
  82. package/tests/unit/utilities/wait.test.js +207 -0
@@ -0,0 +1,197 @@
1
+ import { isNavigationError } from '../core/navigation-safety.js';
2
+ import { getLocatorOrElement } from './locators.js';
3
+ import { createEngineAdapter } from '../core/engine-adapter.js';
4
+
5
+ /**
6
+ * Get text content
7
+ * @param {Object} options - Configuration options
8
+ * @param {Object} options.page - Browser page object
9
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
10
+ * @param {string|Object} options.selector - CSS selector or element
11
+ * @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
12
+ * @returns {Promise<string|null>} - Text content or null
13
+ */
14
+ export async function textContent(options = {}) {
15
+ const { page, engine, selector, adapter: providedAdapter } = options;
16
+
17
+ if (!selector) {
18
+ throw new Error('selector is required in options');
19
+ }
20
+
21
+ try {
22
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
23
+ const locatorOrElement = await getLocatorOrElement({
24
+ page,
25
+ engine,
26
+ selector,
27
+ });
28
+ if (!locatorOrElement) {
29
+ return null;
30
+ }
31
+ return await adapter.getTextContent(locatorOrElement);
32
+ } catch (error) {
33
+ if (isNavigationError(error)) {
34
+ console.log('⚠️ Navigation detected during textContent, returning null');
35
+ return null;
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Get input value
43
+ * @param {Object} options - Configuration options
44
+ * @param {Object} options.page - Browser page object
45
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
46
+ * @param {string|Object} options.selector - CSS selector or element
47
+ * @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
48
+ * @returns {Promise<string>} - Input value
49
+ */
50
+ export async function inputValue(options = {}) {
51
+ const { page, engine, selector, adapter: providedAdapter } = options;
52
+
53
+ if (!selector) {
54
+ throw new Error('selector is required in options');
55
+ }
56
+
57
+ try {
58
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
59
+ const locatorOrElement = await getLocatorOrElement({
60
+ page,
61
+ engine,
62
+ selector,
63
+ });
64
+ if (!locatorOrElement) {
65
+ return '';
66
+ }
67
+ return await adapter.getInputValue(locatorOrElement);
68
+ } catch (error) {
69
+ if (isNavigationError(error)) {
70
+ console.log(
71
+ '⚠️ Navigation detected during inputValue, returning empty string'
72
+ );
73
+ return '';
74
+ }
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get element attribute
81
+ * @param {Object} options - Configuration options
82
+ * @param {Object} options.page - Browser page object
83
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
84
+ * @param {string|Object} options.selector - CSS selector or element
85
+ * @param {string} options.attribute - Attribute name
86
+ * @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
87
+ * @returns {Promise<string|null>} - Attribute value or null
88
+ */
89
+ export async function getAttribute(options = {}) {
90
+ const {
91
+ page,
92
+ engine,
93
+ selector,
94
+ attribute,
95
+ adapter: providedAdapter,
96
+ } = options;
97
+
98
+ if (!selector || !attribute) {
99
+ throw new Error('selector and attribute are required in options');
100
+ }
101
+
102
+ try {
103
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
104
+ const locatorOrElement = await getLocatorOrElement({
105
+ page,
106
+ engine,
107
+ selector,
108
+ });
109
+ if (!locatorOrElement) {
110
+ return null;
111
+ }
112
+ return await adapter.getAttribute(locatorOrElement, attribute);
113
+ } catch (error) {
114
+ if (isNavigationError(error)) {
115
+ console.log(
116
+ '⚠️ Navigation detected during getAttribute, returning null'
117
+ );
118
+ return null;
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get input value from element (helper)
126
+ * @param {Object} options - Configuration options
127
+ * @param {Object} options.page - Browser page object
128
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
129
+ * @param {Object} options.locatorOrElement - Element or locator
130
+ * @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
131
+ * @returns {Promise<string>}
132
+ */
133
+ export async function getInputValue(options = {}) {
134
+ const { page, engine, locatorOrElement, adapter: providedAdapter } = options;
135
+
136
+ if (!locatorOrElement) {
137
+ throw new Error('locatorOrElement is required in options');
138
+ }
139
+
140
+ try {
141
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
142
+ return await adapter.getInputValue(locatorOrElement);
143
+ } catch (error) {
144
+ if (isNavigationError(error)) {
145
+ console.log(
146
+ '⚠️ Navigation detected during getInputValue, returning empty string'
147
+ );
148
+ return '';
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Log element information for verbose debugging
156
+ * @param {Object} options - Configuration options
157
+ * @param {Object} options.page - Browser page object
158
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
159
+ * @param {Function} options.log - Logger instance
160
+ * @param {Object} options.locatorOrElement - Element or locator to log
161
+ * @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
162
+ * @returns {Promise<void>}
163
+ */
164
+ export async function logElementInfo(options = {}) {
165
+ const {
166
+ page,
167
+ engine,
168
+ log,
169
+ locatorOrElement,
170
+ adapter: providedAdapter,
171
+ } = options;
172
+
173
+ if (!locatorOrElement) {
174
+ return;
175
+ }
176
+
177
+ try {
178
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
179
+ const tagName = await adapter.evaluateOnElement(
180
+ locatorOrElement,
181
+ (el) => el.tagName
182
+ );
183
+ const text = await adapter.getTextContent(locatorOrElement);
184
+ log.debug(
185
+ () =>
186
+ `🔍 [VERBOSE] Target element: ${tagName}: "${text?.trim().substring(0, 30)}..."`
187
+ );
188
+ } catch (error) {
189
+ if (isNavigationError(error)) {
190
+ log.debug(
191
+ () => '⚠️ Navigation detected during logElementInfo, skipping'
192
+ );
193
+ return;
194
+ }
195
+ throw error;
196
+ }
197
+ }
@@ -0,0 +1,243 @@
1
+ import { TIMING } from '../core/constants.js';
2
+ import { isNavigationError } from '../core/navigation-safety.js';
3
+
4
+ /**
5
+ * Helper to create Playwright locator from selector string
6
+ * Handles :nth-of-type() pseudo-selectors which don't work in Playwright locators
7
+ * @param {Object} options - Configuration options
8
+ * @param {Object} options.page - Browser page object
9
+ * @param {string} options.selector - CSS selector
10
+ * @returns {Object} - Playwright locator
11
+ */
12
+ export function createPlaywrightLocator(options = {}) {
13
+ const { page, selector } = options;
14
+
15
+ if (!selector) {
16
+ throw new Error('selector is required in options');
17
+ }
18
+ // Check if selector has :nth-of-type(n) pattern
19
+ const nthOfTypeMatch = selector.match(/^(.+):nth-of-type\((\d+)\)$/);
20
+
21
+ if (nthOfTypeMatch) {
22
+ const baseSelector = nthOfTypeMatch[1];
23
+ const index = parseInt(nthOfTypeMatch[2], 10) - 1; // Convert to 0-based index
24
+ return page.locator(baseSelector).nth(index);
25
+ }
26
+
27
+ return page.locator(selector);
28
+ }
29
+
30
+ /**
31
+ * Get locator/element from selector (unified helper for both engines)
32
+ * Does NOT wait - use waitForLocatorOrElement() if you need to wait
33
+ * @param {Object} options - Configuration options
34
+ * @param {Object} options.page - Browser page object
35
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
36
+ * @param {string|Object} options.selector - CSS selector or element/locator
37
+ * @returns {Promise<Object|null>} - Locator for Playwright, Element for Puppeteer (can be null)
38
+ */
39
+ export async function getLocatorOrElement(options = {}) {
40
+ const { page, engine, selector } = options;
41
+
42
+ if (!selector) {
43
+ throw new Error('selector is required in options');
44
+ }
45
+ if (typeof selector !== 'string') {
46
+ return selector; // Already a locator/element
47
+ }
48
+
49
+ if (engine === 'playwright') {
50
+ return createPlaywrightLocator({ page, selector });
51
+ } else {
52
+ // For Puppeteer, return element (can be null if doesn't exist)
53
+ return await page.$(selector);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get locator/element and wait for it to be visible
59
+ * Unified waiting behavior for both engines
60
+ * @param {Object} options - Configuration options
61
+ * @param {Object} options.page - Browser page object
62
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
63
+ * @param {string|Object} options.selector - CSS selector or existing locator/element
64
+ * @param {number} options.timeout - Timeout in ms (default: TIMING.DEFAULT_TIMEOUT)
65
+ * @param {boolean} options.throwOnNavigation - Whether to throw on navigation error (default: true)
66
+ * @returns {Promise<Object|null>} - Locator for Playwright (first match), Element for Puppeteer, or null on navigation
67
+ * @throws {Error} - If element not found or not visible within timeout (unless navigation error and throwOnNavigation is false)
68
+ */
69
+ export async function waitForLocatorOrElement(options = {}) {
70
+ const {
71
+ page,
72
+ engine,
73
+ selector,
74
+ timeout = TIMING.DEFAULT_TIMEOUT,
75
+ throwOnNavigation = true,
76
+ } = options;
77
+
78
+ if (!selector) {
79
+ throw new Error('selector is required in options');
80
+ }
81
+
82
+ try {
83
+ if (engine === 'playwright') {
84
+ const locator = await getLocatorOrElement({ page, engine, selector });
85
+ // Use .first() to handle multiple matches (Playwright strict mode)
86
+ const firstLocator = locator.first();
87
+ await firstLocator.waitFor({ state: 'visible', timeout });
88
+ return firstLocator;
89
+ } else {
90
+ // Puppeteer: wait for selector to be visible (returns first match by default)
91
+ await page.waitForSelector(selector, { visible: true, timeout });
92
+ const element = await page.$(selector);
93
+ if (!element) {
94
+ throw new Error(`Element not found after waiting: ${selector}`);
95
+ }
96
+ return element;
97
+ }
98
+ } catch (error) {
99
+ if (isNavigationError(error)) {
100
+ console.log(
101
+ '⚠️ Navigation detected during waitForLocatorOrElement, recovering gracefully'
102
+ );
103
+ if (throwOnNavigation) {
104
+ throw error;
105
+ }
106
+ return null;
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Wait for element to be visible (works with existing locatorOrElement)
114
+ * @param {Object} options - Configuration options
115
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
116
+ * @param {Object} options.locatorOrElement - Element or locator to wait for
117
+ * @param {number} options.timeout - Timeout in ms (default: TIMING.DEFAULT_TIMEOUT)
118
+ * @returns {Promise<void>}
119
+ */
120
+ export async function waitForVisible(options = {}) {
121
+ const {
122
+ engine,
123
+ locatorOrElement,
124
+ timeout = TIMING.DEFAULT_TIMEOUT,
125
+ } = options;
126
+
127
+ if (!locatorOrElement) {
128
+ throw new Error('locatorOrElement is required in options');
129
+ }
130
+
131
+ if (engine === 'playwright') {
132
+ await locatorOrElement.waitFor({ state: 'visible', timeout });
133
+ } else {
134
+ // For Puppeteer, element is already fetched, just verify it exists
135
+ if (!locatorOrElement) {
136
+ throw new Error('Element not found');
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Create locator (Playwright-style fluent API)
143
+ * @param {Object} options - Configuration options
144
+ * @param {Object} options.page - Browser page object
145
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
146
+ * @param {string} options.selector - CSS selector
147
+ * @returns {Object} - Locator object (Playwright) or wrapper (Puppeteer)
148
+ */
149
+ export function locator(options = {}) {
150
+ const { page, engine, selector } = options;
151
+
152
+ if (!selector) {
153
+ throw new Error('selector is required in options');
154
+ }
155
+
156
+ if (engine === 'playwright') {
157
+ return createPlaywrightLocator({ page, selector });
158
+ } else {
159
+ // Return a wrapper that mimics Playwright locator API for Puppeteer
160
+ const createLocatorWrapper = (sel) => ({
161
+ selector: sel,
162
+ async count() {
163
+ const elements = await page.$$(sel);
164
+ return elements.length;
165
+ },
166
+ async click(options = {}) {
167
+ await page.click(sel, options);
168
+ },
169
+ async fill(text) {
170
+ await page.$eval(
171
+ sel,
172
+ (el, value) => {
173
+ el.value = value;
174
+ el.dispatchEvent(new Event('input', { bubbles: true }));
175
+ el.dispatchEvent(new Event('change', { bubbles: true }));
176
+ },
177
+ text
178
+ );
179
+ },
180
+ async type(text, options = {}) {
181
+ await page.type(sel, text, options);
182
+ },
183
+ async textContent() {
184
+ const element = await page.$(sel);
185
+ if (!element) {
186
+ return null;
187
+ }
188
+ return await page.evaluate((el) => el.textContent, element);
189
+ },
190
+ async inputValue() {
191
+ const element = await page.$(sel);
192
+ if (!element) {
193
+ return '';
194
+ }
195
+ return await page.evaluate((el) => el.value, element);
196
+ },
197
+ async getAttribute(name) {
198
+ const element = await page.$(sel);
199
+ if (!element) {
200
+ return null;
201
+ }
202
+ return await page.evaluate(
203
+ (el, attr) => el.getAttribute(attr),
204
+ element,
205
+ name
206
+ );
207
+ },
208
+ async isVisible() {
209
+ const element = await page.$(sel);
210
+ if (!element) {
211
+ return false;
212
+ }
213
+ return await page.evaluate(
214
+ (el) => el.offsetWidth > 0 && el.offsetHeight > 0,
215
+ element
216
+ );
217
+ },
218
+ async waitFor(options = {}) {
219
+ const { state = 'visible', timeout = TIMING.DEFAULT_TIMEOUT } = options;
220
+ const visible = state === 'visible';
221
+ await page.waitForSelector(sel, { visible, timeout });
222
+ },
223
+ nth(index) {
224
+ return createLocatorWrapper(`${sel}:nth-of-type(${index + 1})`);
225
+ },
226
+ first() {
227
+ return createLocatorWrapper(`${sel}:nth-of-type(1)`);
228
+ },
229
+ last() {
230
+ return createLocatorWrapper(`${sel}:last-of-type`);
231
+ },
232
+ async evaluate(fn, arg) {
233
+ const element = await page.$(sel);
234
+ if (!element) {
235
+ throw new Error(`Element not found: ${sel}`);
236
+ }
237
+ return await page.evaluate(fn, element, arg);
238
+ },
239
+ });
240
+
241
+ return createLocatorWrapper(selector);
242
+ }
243
+ }