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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +296 -0
- package/.husky/pre-commit +1 -0
- package/.jscpd.json +20 -0
- package/.prettierignore +7 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +32 -0
- package/LICENSE +24 -0
- package/README.md +320 -0
- package/bunfig.toml +3 -0
- package/deno.json +7 -0
- package/eslint.config.js +125 -0
- package/examples/react-test-app/index.html +25 -0
- package/examples/react-test-app/package.json +19 -0
- package/examples/react-test-app/src/App.jsx +473 -0
- package/examples/react-test-app/src/main.jsx +10 -0
- package/examples/react-test-app/src/styles.css +323 -0
- package/examples/react-test-app/vite.config.js +9 -0
- package/package.json +89 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +86 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +216 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/merge-changesets.mjs +260 -0
- package/scripts/publish-to-npm.mjs +126 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +262 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/ARCHITECTURE.md +270 -0
- package/src/README.md +517 -0
- package/src/bindings.js +298 -0
- package/src/browser/launcher.js +93 -0
- package/src/browser/navigation.js +513 -0
- package/src/core/constants.js +24 -0
- package/src/core/engine-adapter.js +466 -0
- package/src/core/engine-detection.js +49 -0
- package/src/core/logger.js +21 -0
- package/src/core/navigation-manager.js +503 -0
- package/src/core/navigation-safety.js +160 -0
- package/src/core/network-tracker.js +373 -0
- package/src/core/page-session.js +299 -0
- package/src/core/page-trigger-manager.js +564 -0
- package/src/core/preferences.js +46 -0
- package/src/elements/content.js +197 -0
- package/src/elements/locators.js +243 -0
- package/src/elements/selectors.js +360 -0
- package/src/elements/visibility.js +166 -0
- package/src/exports.js +121 -0
- package/src/factory.js +192 -0
- package/src/high-level/universal-logic.js +206 -0
- package/src/index.js +17 -0
- package/src/interactions/click.js +684 -0
- package/src/interactions/fill.js +383 -0
- package/src/interactions/scroll.js +341 -0
- package/src/utilities/url.js +33 -0
- package/src/utilities/wait.js +135 -0
- package/tests/e2e/playwright.e2e.test.js +442 -0
- package/tests/e2e/puppeteer.e2e.test.js +408 -0
- package/tests/helpers/mocks.js +542 -0
- package/tests/unit/bindings.test.js +218 -0
- package/tests/unit/browser/navigation.test.js +345 -0
- package/tests/unit/core/constants.test.js +72 -0
- package/tests/unit/core/engine-adapter.test.js +170 -0
- package/tests/unit/core/engine-detection.test.js +81 -0
- package/tests/unit/core/logger.test.js +80 -0
- package/tests/unit/core/navigation-safety.test.js +202 -0
- package/tests/unit/core/network-tracker.test.js +198 -0
- package/tests/unit/core/page-trigger-manager.test.js +358 -0
- package/tests/unit/elements/content.test.js +318 -0
- package/tests/unit/elements/locators.test.js +236 -0
- package/tests/unit/elements/selectors.test.js +302 -0
- package/tests/unit/elements/visibility.test.js +234 -0
- package/tests/unit/factory.test.js +174 -0
- package/tests/unit/high-level/universal-logic.test.js +299 -0
- package/tests/unit/interactions/click.test.js +340 -0
- package/tests/unit/interactions/fill.test.js +378 -0
- package/tests/unit/interactions/scroll.test.js +330 -0
- package/tests/unit/utilities/url.test.js +63 -0
- package/tests/unit/utilities/wait.test.js +207 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { isNavigationError } from '../core/navigation-safety.js';
|
|
2
|
+
import { createPlaywrightLocator } from './locators.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Query single element
|
|
6
|
+
* @param {Object} options - Configuration options
|
|
7
|
+
* @param {Object} options.page - Browser page object
|
|
8
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
9
|
+
* @param {string} options.selector - CSS selector
|
|
10
|
+
* @returns {Promise<Object|null>} - Element handle or null
|
|
11
|
+
*/
|
|
12
|
+
export async function querySelector(options = {}) {
|
|
13
|
+
const { page, engine, selector } = options;
|
|
14
|
+
|
|
15
|
+
if (!selector) {
|
|
16
|
+
throw new Error('selector is required in options');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
if (engine === 'playwright') {
|
|
21
|
+
const locator = createPlaywrightLocator({ page, selector }).first();
|
|
22
|
+
const count = await locator.count();
|
|
23
|
+
return count > 0 ? locator : null;
|
|
24
|
+
} else {
|
|
25
|
+
return await page.$(selector);
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (isNavigationError(error)) {
|
|
29
|
+
console.log(
|
|
30
|
+
'⚠️ Navigation detected during querySelector, returning null'
|
|
31
|
+
);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Query all elements
|
|
40
|
+
* @param {Object} options - Configuration options
|
|
41
|
+
* @param {Object} options.page - Browser page object
|
|
42
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
43
|
+
* @param {string} options.selector - CSS selector
|
|
44
|
+
* @returns {Promise<Array>} - Array of element handles
|
|
45
|
+
*/
|
|
46
|
+
export async function querySelectorAll(options = {}) {
|
|
47
|
+
const { page, engine, selector } = options;
|
|
48
|
+
|
|
49
|
+
if (!selector) {
|
|
50
|
+
throw new Error('selector is required in options');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
if (engine === 'playwright') {
|
|
55
|
+
const locator = createPlaywrightLocator({ page, selector });
|
|
56
|
+
const count = await locator.count();
|
|
57
|
+
const elements = [];
|
|
58
|
+
for (let i = 0; i < count; i++) {
|
|
59
|
+
elements.push(locator.nth(i));
|
|
60
|
+
}
|
|
61
|
+
return elements;
|
|
62
|
+
} else {
|
|
63
|
+
return await page.$$(selector);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (isNavigationError(error)) {
|
|
67
|
+
console.log(
|
|
68
|
+
'⚠️ Navigation detected during querySelectorAll, returning empty array'
|
|
69
|
+
);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Find elements by text content (works across both engines)
|
|
78
|
+
* @param {Object} options - Configuration options
|
|
79
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
80
|
+
* @param {string} options.text - Text to search for
|
|
81
|
+
* @param {string} options.selector - Optional base selector (e.g., 'button', 'a', 'span')
|
|
82
|
+
* @param {boolean} options.exact - Exact match vs contains (default: false)
|
|
83
|
+
* @returns {Promise<string>} - CSS selector that can be used with other commander methods
|
|
84
|
+
*/
|
|
85
|
+
export async function findByText(options = {}) {
|
|
86
|
+
const { engine, text, selector = '*', exact = false } = options;
|
|
87
|
+
|
|
88
|
+
if (!text) {
|
|
89
|
+
throw new Error('text is required in options');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (engine === 'playwright') {
|
|
93
|
+
// Playwright supports :has-text() natively
|
|
94
|
+
const textSelector = exact ? `:text-is("${text}")` : `:has-text("${text}")`;
|
|
95
|
+
return `${selector}${textSelector}`;
|
|
96
|
+
} else {
|
|
97
|
+
// For Puppeteer, we need to use XPath or evaluate
|
|
98
|
+
// Return a special selector marker that will be handled by other methods
|
|
99
|
+
return {
|
|
100
|
+
_isPuppeteerTextSelector: true,
|
|
101
|
+
baseSelector: selector,
|
|
102
|
+
text,
|
|
103
|
+
exact,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a selector is a Playwright-specific text selector
|
|
110
|
+
* @param {string} selector - The selector to check
|
|
111
|
+
* @returns {boolean} - True if selector contains Playwright text pseudo-selectors
|
|
112
|
+
*/
|
|
113
|
+
function isPlaywrightTextSelector(selector) {
|
|
114
|
+
if (typeof selector !== 'string') {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return selector.includes(':has-text(') || selector.includes(':text-is(');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse a Playwright text selector to extract base selector and text
|
|
122
|
+
* @param {string} selector - Playwright text selector like 'a:has-text("text")'
|
|
123
|
+
* @returns {Object|null} - { baseSelector, text, exact } or null if not parseable
|
|
124
|
+
*/
|
|
125
|
+
function parsePlaywrightTextSelector(selector) {
|
|
126
|
+
// Match patterns like 'a:has-text("text")' or 'button:text-is("exact text")'
|
|
127
|
+
const hasTextMatch = selector.match(/^(.+?):has-text\("(.+?)"\)$/);
|
|
128
|
+
if (hasTextMatch) {
|
|
129
|
+
return {
|
|
130
|
+
baseSelector: hasTextMatch[1],
|
|
131
|
+
text: hasTextMatch[2],
|
|
132
|
+
exact: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const textIsMatch = selector.match(/^(.+?):text-is\("(.+?)"\)$/);
|
|
137
|
+
if (textIsMatch) {
|
|
138
|
+
return {
|
|
139
|
+
baseSelector: textIsMatch[1],
|
|
140
|
+
text: textIsMatch[2],
|
|
141
|
+
exact: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Normalize selector to handle both Puppeteer and Playwright text selectors
|
|
150
|
+
* Converts engine-specific text selectors to valid CSS selectors for browser context
|
|
151
|
+
*
|
|
152
|
+
* @param {Object} options - Configuration options
|
|
153
|
+
* @param {Object} options.page - Browser page object
|
|
154
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
155
|
+
* @param {string|Object} options.selector - CSS selector or text selector object
|
|
156
|
+
* @returns {Promise<string|null>} - Valid CSS selector or null if not found
|
|
157
|
+
*/
|
|
158
|
+
export async function normalizeSelector(options = {}) {
|
|
159
|
+
const { page, engine, selector } = options;
|
|
160
|
+
|
|
161
|
+
if (!selector) {
|
|
162
|
+
throw new Error('selector is required in options');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle Playwright text selectors (strings containing :has-text or :text-is)
|
|
166
|
+
// These are valid for Playwright's locator API but NOT for document.querySelectorAll
|
|
167
|
+
if (
|
|
168
|
+
typeof selector === 'string' &&
|
|
169
|
+
engine === 'playwright' &&
|
|
170
|
+
isPlaywrightTextSelector(selector)
|
|
171
|
+
) {
|
|
172
|
+
const parsed = parsePlaywrightTextSelector(selector);
|
|
173
|
+
if (!parsed) {
|
|
174
|
+
// Could not parse, return as-is and hope for the best
|
|
175
|
+
return selector;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
// Use page.evaluate to find matching element and generate a valid CSS selector
|
|
180
|
+
const result = await page.evaluate(({ baseSelector, text, exact }) => {
|
|
181
|
+
const elements = Array.from(document.querySelectorAll(baseSelector));
|
|
182
|
+
const matchingElement = elements.find((el) => {
|
|
183
|
+
const elementText = el.textContent.trim();
|
|
184
|
+
return exact ? elementText === text : elementText.includes(text);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!matchingElement) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Generate a unique selector using data-qa or nth-of-type
|
|
192
|
+
const dataQa = matchingElement.getAttribute('data-qa');
|
|
193
|
+
if (dataQa) {
|
|
194
|
+
return `[data-qa="${dataQa}"]`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Use nth-of-type as fallback
|
|
198
|
+
const tagName = matchingElement.tagName.toLowerCase();
|
|
199
|
+
const siblings = Array.from(
|
|
200
|
+
matchingElement.parentElement.children
|
|
201
|
+
).filter((el) => el.tagName.toLowerCase() === tagName);
|
|
202
|
+
const index = siblings.indexOf(matchingElement);
|
|
203
|
+
return `${tagName}:nth-of-type(${index + 1})`;
|
|
204
|
+
}, parsed);
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (isNavigationError(error)) {
|
|
209
|
+
console.log(
|
|
210
|
+
'⚠️ Navigation detected during normalizeSelector (Playwright), returning null'
|
|
211
|
+
);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Plain string selector - return as-is
|
|
219
|
+
if (typeof selector === 'string') {
|
|
220
|
+
return selector;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle Puppeteer text selector objects
|
|
224
|
+
if (selector._isPuppeteerTextSelector) {
|
|
225
|
+
try {
|
|
226
|
+
// Find element by text and generate a unique selector
|
|
227
|
+
const result = await page.evaluate(
|
|
228
|
+
(baseSelector, text, exact) => {
|
|
229
|
+
const elements = Array.from(document.querySelectorAll(baseSelector));
|
|
230
|
+
const matchingElement = elements.find((el) => {
|
|
231
|
+
const elementText = el.textContent.trim();
|
|
232
|
+
return exact ? elementText === text : elementText.includes(text);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!matchingElement) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Generate a unique selector using data-qa or nth-of-type
|
|
240
|
+
const dataQa = matchingElement.getAttribute('data-qa');
|
|
241
|
+
if (dataQa) {
|
|
242
|
+
return `[data-qa="${dataQa}"]`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Use nth-of-type as fallback
|
|
246
|
+
const tagName = matchingElement.tagName.toLowerCase();
|
|
247
|
+
const siblings = Array.from(
|
|
248
|
+
matchingElement.parentElement.children
|
|
249
|
+
).filter((el) => el.tagName.toLowerCase() === tagName);
|
|
250
|
+
const index = siblings.indexOf(matchingElement);
|
|
251
|
+
return `${tagName}:nth-of-type(${index + 1})`;
|
|
252
|
+
},
|
|
253
|
+
selector.baseSelector,
|
|
254
|
+
selector.text,
|
|
255
|
+
selector.exact
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (isNavigationError(error)) {
|
|
261
|
+
console.log(
|
|
262
|
+
'⚠️ Navigation detected during normalizeSelector (Puppeteer), returning null'
|
|
263
|
+
);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return selector;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Enhanced wrapper for functions that need to handle text selectors
|
|
275
|
+
* @param {Function} fn - The function to wrap
|
|
276
|
+
* @param {string} engine - Engine type ('playwright' or 'puppeteer')
|
|
277
|
+
* @param {Object} page - Browser page object
|
|
278
|
+
* @returns {Function} - Wrapped function
|
|
279
|
+
*/
|
|
280
|
+
export function withTextSelectorSupport(fn, engine, page) {
|
|
281
|
+
return async (options = {}) => {
|
|
282
|
+
let { selector } = options;
|
|
283
|
+
|
|
284
|
+
// Normalize Puppeteer text selectors (object format)
|
|
285
|
+
if (
|
|
286
|
+
engine === 'puppeteer' &&
|
|
287
|
+
typeof selector === 'object' &&
|
|
288
|
+
selector._isPuppeteerTextSelector
|
|
289
|
+
) {
|
|
290
|
+
selector = await normalizeSelector({ page, engine, selector });
|
|
291
|
+
if (!selector) {
|
|
292
|
+
throw new Error('Element with specified text not found');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Normalize Playwright text selectors (string format with :has-text or :text-is)
|
|
297
|
+
if (
|
|
298
|
+
engine === 'playwright' &&
|
|
299
|
+
typeof selector === 'string' &&
|
|
300
|
+
isPlaywrightTextSelector(selector)
|
|
301
|
+
) {
|
|
302
|
+
selector = await normalizeSelector({ page, engine, selector });
|
|
303
|
+
if (!selector) {
|
|
304
|
+
throw new Error('Element with specified text not found');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return fn({ ...options, selector });
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Wait for selector to appear
|
|
314
|
+
* @param {Object} options - Configuration options
|
|
315
|
+
* @param {Object} options.page - Browser page object
|
|
316
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
317
|
+
* @param {string} options.selector - CSS selector
|
|
318
|
+
* @param {boolean} options.visible - Wait for visibility (default: true)
|
|
319
|
+
* @param {number} options.timeout - Timeout in ms (default: TIMING.DEFAULT_TIMEOUT)
|
|
320
|
+
* @param {boolean} options.throwOnNavigation - Throw on navigation error (default: true)
|
|
321
|
+
* @returns {Promise<boolean>} - True if selector found, false on navigation
|
|
322
|
+
*/
|
|
323
|
+
export async function waitForSelector(options = {}) {
|
|
324
|
+
const {
|
|
325
|
+
page,
|
|
326
|
+
engine,
|
|
327
|
+
selector,
|
|
328
|
+
visible = true,
|
|
329
|
+
timeout = 5000,
|
|
330
|
+
throwOnNavigation = true,
|
|
331
|
+
} = options;
|
|
332
|
+
|
|
333
|
+
if (!selector) {
|
|
334
|
+
throw new Error('selector is required in options');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
if (engine === 'playwright') {
|
|
339
|
+
const locator = createPlaywrightLocator({ page, selector });
|
|
340
|
+
await locator.waitFor({
|
|
341
|
+
state: visible ? 'visible' : 'attached',
|
|
342
|
+
timeout,
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
await page.waitForSelector(selector, { visible, timeout });
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (isNavigationError(error)) {
|
|
350
|
+
console.log(
|
|
351
|
+
'⚠️ Navigation detected during waitForSelector, recovering gracefully'
|
|
352
|
+
);
|
|
353
|
+
if (throwOnNavigation) {
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { TIMING } from '../core/constants.js';
|
|
2
|
+
import { isNavigationError } from '../core/navigation-safety.js';
|
|
3
|
+
import { getLocatorOrElement } from './locators.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if element is visible
|
|
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
|
+
* @returns {Promise<boolean>} - True if visible
|
|
12
|
+
*/
|
|
13
|
+
export async function isVisible(options = {}) {
|
|
14
|
+
const { page, engine, selector } = options;
|
|
15
|
+
|
|
16
|
+
if (!selector) {
|
|
17
|
+
throw new Error('selector is required in options');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
if (engine === 'playwright') {
|
|
22
|
+
const locator = await getLocatorOrElement({ page, engine, selector });
|
|
23
|
+
try {
|
|
24
|
+
await locator.waitFor({
|
|
25
|
+
state: 'visible',
|
|
26
|
+
timeout: TIMING.VISIBILITY_CHECK_TIMEOUT,
|
|
27
|
+
});
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
const element = await getLocatorOrElement({ page, engine, selector });
|
|
34
|
+
if (!element) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return await page.evaluate(
|
|
38
|
+
(el) => el.offsetWidth > 0 && el.offsetHeight > 0,
|
|
39
|
+
element
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (isNavigationError(error)) {
|
|
44
|
+
console.log(
|
|
45
|
+
'⚠️ Navigation detected during visibility check, returning false'
|
|
46
|
+
);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if element is enabled (not disabled, not loading)
|
|
55
|
+
* @param {Object} options - Configuration options
|
|
56
|
+
* @param {Object} options.page - Browser page object
|
|
57
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
58
|
+
* @param {string|Object} options.selector - CSS selector or locator
|
|
59
|
+
* @param {Array<string>} options.disabledClasses - Additional CSS classes that indicate disabled state (default: ['magritte-button_loading'])
|
|
60
|
+
* @returns {Promise<boolean>} - True if enabled
|
|
61
|
+
*/
|
|
62
|
+
export async function isEnabled(options = {}) {
|
|
63
|
+
const {
|
|
64
|
+
page,
|
|
65
|
+
engine,
|
|
66
|
+
selector,
|
|
67
|
+
disabledClasses = ['magritte-button_loading'],
|
|
68
|
+
} = options;
|
|
69
|
+
|
|
70
|
+
if (!selector) {
|
|
71
|
+
throw new Error('selector is required in options');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (engine === 'playwright') {
|
|
76
|
+
// For Playwright, use locator API
|
|
77
|
+
const locator =
|
|
78
|
+
typeof selector === 'string'
|
|
79
|
+
? page.locator(selector).first()
|
|
80
|
+
: selector;
|
|
81
|
+
return await locator.evaluate((el, classes) => {
|
|
82
|
+
const isDisabled =
|
|
83
|
+
el.hasAttribute('disabled') ||
|
|
84
|
+
el.getAttribute('aria-disabled') === 'true' ||
|
|
85
|
+
classes.some((cls) => el.classList.contains(cls));
|
|
86
|
+
return !isDisabled;
|
|
87
|
+
}, disabledClasses);
|
|
88
|
+
} else {
|
|
89
|
+
// For Puppeteer (selector should already be normalized by withTextSelectorSupport wrapper)
|
|
90
|
+
const element = await getLocatorOrElement({ page, engine, selector });
|
|
91
|
+
if (!element) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return await page.evaluate(
|
|
95
|
+
(el, classes) => {
|
|
96
|
+
const isDisabled =
|
|
97
|
+
el.hasAttribute('disabled') ||
|
|
98
|
+
el.getAttribute('aria-disabled') === 'true' ||
|
|
99
|
+
classes.some((cls) => el.classList.contains(cls));
|
|
100
|
+
return !isDisabled;
|
|
101
|
+
},
|
|
102
|
+
element,
|
|
103
|
+
disabledClasses
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (isNavigationError(error)) {
|
|
108
|
+
console.log(
|
|
109
|
+
'⚠️ Navigation detected during enabled check, returning false'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get element count
|
|
118
|
+
* @param {Object} options - Configuration options
|
|
119
|
+
* @param {Object} options.page - Browser page object
|
|
120
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
121
|
+
* @param {string|Object} options.selector - CSS selector or special text selector
|
|
122
|
+
* @returns {Promise<number>} - Number of matching elements
|
|
123
|
+
*/
|
|
124
|
+
export async function count(options = {}) {
|
|
125
|
+
const { page, engine, selector } = options;
|
|
126
|
+
|
|
127
|
+
if (!selector) {
|
|
128
|
+
throw new Error('selector is required in options');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Handle Puppeteer text selectors
|
|
133
|
+
if (
|
|
134
|
+
engine === 'puppeteer' &&
|
|
135
|
+
typeof selector === 'object' &&
|
|
136
|
+
selector._isPuppeteerTextSelector
|
|
137
|
+
) {
|
|
138
|
+
const result = await page.evaluate(
|
|
139
|
+
(baseSelector, text, exact) => {
|
|
140
|
+
const elements = Array.from(document.querySelectorAll(baseSelector));
|
|
141
|
+
return elements.filter((el) => {
|
|
142
|
+
const elementText = el.textContent.trim();
|
|
143
|
+
return exact ? elementText === text : elementText.includes(text);
|
|
144
|
+
}).length;
|
|
145
|
+
},
|
|
146
|
+
selector.baseSelector,
|
|
147
|
+
selector.text,
|
|
148
|
+
selector.exact
|
|
149
|
+
);
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (engine === 'playwright') {
|
|
154
|
+
return await page.locator(selector).count();
|
|
155
|
+
} else {
|
|
156
|
+
const elements = await page.$$(selector);
|
|
157
|
+
return elements.length;
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (isNavigationError(error)) {
|
|
161
|
+
console.log('⚠️ Navigation detected during element count, returning 0');
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/exports.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Commander - Public API exports
|
|
3
|
+
* This module centralizes all public exports from the browser-commander library.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Re-export core utilities
|
|
7
|
+
export { CHROME_ARGS, TIMING } from './core/constants.js';
|
|
8
|
+
export { isVerboseEnabled, createLogger } from './core/logger.js';
|
|
9
|
+
export { disableTranslateInPreferences } from './core/preferences.js';
|
|
10
|
+
export { detectEngine } from './core/engine-detection.js';
|
|
11
|
+
export {
|
|
12
|
+
isNavigationError,
|
|
13
|
+
safeOperation,
|
|
14
|
+
makeNavigationSafe,
|
|
15
|
+
withNavigationSafety,
|
|
16
|
+
} from './core/navigation-safety.js';
|
|
17
|
+
|
|
18
|
+
// Re-export new core components
|
|
19
|
+
export { createNetworkTracker } from './core/network-tracker.js';
|
|
20
|
+
export { createNavigationManager } from './core/navigation-manager.js';
|
|
21
|
+
export { createPageSessionFactory } from './core/page-session.js';
|
|
22
|
+
|
|
23
|
+
// Re-export engine adapter
|
|
24
|
+
export {
|
|
25
|
+
EngineAdapter,
|
|
26
|
+
PlaywrightAdapter,
|
|
27
|
+
PuppeteerAdapter,
|
|
28
|
+
createEngineAdapter,
|
|
29
|
+
} from './core/engine-adapter.js';
|
|
30
|
+
|
|
31
|
+
// Page trigger system
|
|
32
|
+
export {
|
|
33
|
+
createPageTriggerManager,
|
|
34
|
+
ActionStoppedError,
|
|
35
|
+
isActionStoppedError,
|
|
36
|
+
makeUrlCondition,
|
|
37
|
+
allConditions,
|
|
38
|
+
anyCondition,
|
|
39
|
+
notCondition,
|
|
40
|
+
} from './core/page-trigger-manager.js';
|
|
41
|
+
|
|
42
|
+
// Re-export browser management
|
|
43
|
+
export { launchBrowser } from './browser/launcher.js';
|
|
44
|
+
export {
|
|
45
|
+
waitForUrlStabilization,
|
|
46
|
+
goto,
|
|
47
|
+
waitForNavigation,
|
|
48
|
+
waitForPageReady,
|
|
49
|
+
waitAfterAction,
|
|
50
|
+
// Navigation verification
|
|
51
|
+
defaultNavigationVerification,
|
|
52
|
+
verifyNavigation,
|
|
53
|
+
} from './browser/navigation.js';
|
|
54
|
+
|
|
55
|
+
// Re-export element operations
|
|
56
|
+
export {
|
|
57
|
+
createPlaywrightLocator,
|
|
58
|
+
getLocatorOrElement,
|
|
59
|
+
waitForLocatorOrElement,
|
|
60
|
+
waitForVisible,
|
|
61
|
+
locator,
|
|
62
|
+
} from './elements/locators.js';
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
querySelector,
|
|
66
|
+
querySelectorAll,
|
|
67
|
+
findByText,
|
|
68
|
+
normalizeSelector,
|
|
69
|
+
withTextSelectorSupport,
|
|
70
|
+
waitForSelector,
|
|
71
|
+
} from './elements/selectors.js';
|
|
72
|
+
|
|
73
|
+
export { isVisible, isEnabled, count } from './elements/visibility.js';
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
textContent,
|
|
77
|
+
inputValue,
|
|
78
|
+
getAttribute,
|
|
79
|
+
getInputValue,
|
|
80
|
+
logElementInfo,
|
|
81
|
+
} from './elements/content.js';
|
|
82
|
+
|
|
83
|
+
// Re-export interactions
|
|
84
|
+
export {
|
|
85
|
+
scrollIntoView,
|
|
86
|
+
needsScrolling,
|
|
87
|
+
scrollIntoViewIfNeeded,
|
|
88
|
+
// Scroll verification
|
|
89
|
+
defaultScrollVerification,
|
|
90
|
+
verifyScroll,
|
|
91
|
+
} from './interactions/scroll.js';
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
clickElement,
|
|
95
|
+
clickButton,
|
|
96
|
+
// Click verification
|
|
97
|
+
defaultClickVerification,
|
|
98
|
+
capturePreClickState,
|
|
99
|
+
verifyClick,
|
|
100
|
+
} from './interactions/click.js';
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
checkIfElementEmpty,
|
|
104
|
+
performFill,
|
|
105
|
+
fillTextArea,
|
|
106
|
+
// Fill verification
|
|
107
|
+
defaultFillVerification,
|
|
108
|
+
verifyFill,
|
|
109
|
+
} from './interactions/fill.js';
|
|
110
|
+
|
|
111
|
+
// Re-export utilities
|
|
112
|
+
export { wait, evaluate, safeEvaluate } from './utilities/wait.js';
|
|
113
|
+
export { getUrl, unfocusAddressBar } from './utilities/url.js';
|
|
114
|
+
|
|
115
|
+
// Re-export high-level universal logic
|
|
116
|
+
export {
|
|
117
|
+
waitForUrlCondition,
|
|
118
|
+
installClickListener,
|
|
119
|
+
checkAndClearFlag,
|
|
120
|
+
findToggleButton,
|
|
121
|
+
} from './high-level/universal-logic.js';
|