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,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine Adapter - Abstract away Playwright/Puppeteer differences
|
|
3
|
+
*
|
|
4
|
+
* This module implements the Adapter pattern to encapsulate engine-specific
|
|
5
|
+
* logic in a single place, following the "Protected Variations" principle.
|
|
6
|
+
*
|
|
7
|
+
* Benefits:
|
|
8
|
+
* - Eliminates scattered `if (engine === 'playwright')` checks
|
|
9
|
+
* - Easier to add new engines (e.g., Selenium)
|
|
10
|
+
* - Easier to test with mock adapters
|
|
11
|
+
* - Clearer separation of concerns
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { TIMING } from './constants.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base class defining the engine adapter interface
|
|
18
|
+
* All engine-specific operations should be defined here
|
|
19
|
+
*/
|
|
20
|
+
export class EngineAdapter {
|
|
21
|
+
constructor(page) {
|
|
22
|
+
this.page = page;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get engine name
|
|
27
|
+
* @returns {string} - 'playwright' or 'puppeteer'
|
|
28
|
+
*/
|
|
29
|
+
getEngineName() {
|
|
30
|
+
throw new Error('getEngineName() must be implemented by subclass');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Element Selection and Locators
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a locator/element handle from a selector
|
|
39
|
+
* @param {string} selector - CSS selector
|
|
40
|
+
* @returns {Object} - Locator (Playwright) or ElementHandle (Puppeteer)
|
|
41
|
+
*/
|
|
42
|
+
createLocator(selector) {
|
|
43
|
+
throw new Error('createLocator() must be implemented by subclass');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Query single element
|
|
48
|
+
* @param {string} selector - CSS selector
|
|
49
|
+
* @returns {Promise<Object|null>} - Locator/Element or null
|
|
50
|
+
*/
|
|
51
|
+
async querySelector(selector) {
|
|
52
|
+
throw new Error('querySelector() must be implemented by subclass');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Query all elements
|
|
57
|
+
* @param {string} selector - CSS selector
|
|
58
|
+
* @returns {Promise<Array>} - Array of locators/elements
|
|
59
|
+
*/
|
|
60
|
+
async querySelectorAll(selector) {
|
|
61
|
+
throw new Error('querySelectorAll() must be implemented by subclass');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Wait for selector to appear
|
|
66
|
+
* @param {string} selector - CSS selector
|
|
67
|
+
* @param {Object} options - Wait options {visible, timeout}
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
async waitForSelector(selector, options = {}) {
|
|
71
|
+
throw new Error('waitForSelector() must be implemented by subclass');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Wait for element to be visible
|
|
76
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
77
|
+
* @param {number} timeout - Timeout in ms
|
|
78
|
+
* @returns {Promise<Object>} - The locator/element
|
|
79
|
+
*/
|
|
80
|
+
async waitForVisible(locatorOrElement, timeout = TIMING.DEFAULT_TIMEOUT) {
|
|
81
|
+
throw new Error('waitForVisible() must be implemented by subclass');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Count matching elements
|
|
86
|
+
* @param {string} selector - CSS selector
|
|
87
|
+
* @returns {Promise<number>} - Number of matching elements
|
|
88
|
+
*/
|
|
89
|
+
async count(selector) {
|
|
90
|
+
throw new Error('count() must be implemented by subclass');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Element Evaluation and Properties
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Evaluate function on element
|
|
99
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
100
|
+
* @param {Function} fn - Function to evaluate
|
|
101
|
+
* @param {any} args - Arguments to pass (optional)
|
|
102
|
+
* @returns {Promise<any>} - Result of evaluation
|
|
103
|
+
*/
|
|
104
|
+
async evaluateOnElement(locatorOrElement, fn, args) {
|
|
105
|
+
throw new Error('evaluateOnElement() must be implemented by subclass');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get element text content
|
|
110
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
111
|
+
* @returns {Promise<string|null>} - Text content
|
|
112
|
+
*/
|
|
113
|
+
async getTextContent(locatorOrElement) {
|
|
114
|
+
throw new Error('getTextContent() must be implemented by subclass');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get input value
|
|
119
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
120
|
+
* @returns {Promise<string>} - Input value
|
|
121
|
+
*/
|
|
122
|
+
async getInputValue(locatorOrElement) {
|
|
123
|
+
throw new Error('getInputValue() must be implemented by subclass');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get element attribute
|
|
128
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
129
|
+
* @param {string} attribute - Attribute name
|
|
130
|
+
* @returns {Promise<string|null>} - Attribute value
|
|
131
|
+
*/
|
|
132
|
+
async getAttribute(locatorOrElement, attribute) {
|
|
133
|
+
throw new Error('getAttribute() must be implemented by subclass');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Element Interactions
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Click element
|
|
142
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
143
|
+
* @param {Object} options - Click options {force, etc.}
|
|
144
|
+
* @returns {Promise<void>}
|
|
145
|
+
*/
|
|
146
|
+
async click(locatorOrElement, options = {}) {
|
|
147
|
+
throw new Error('click() must be implemented by subclass');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Type text into element (simulates typing)
|
|
152
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
153
|
+
* @param {string} text - Text to type
|
|
154
|
+
* @returns {Promise<void>}
|
|
155
|
+
*/
|
|
156
|
+
async type(locatorOrElement, text) {
|
|
157
|
+
throw new Error('type() must be implemented by subclass');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Fill element with text (direct value assignment)
|
|
162
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
163
|
+
* @param {string} text - Text to fill
|
|
164
|
+
* @returns {Promise<void>}
|
|
165
|
+
*/
|
|
166
|
+
async fill(locatorOrElement, text) {
|
|
167
|
+
throw new Error('fill() must be implemented by subclass');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Focus element
|
|
172
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
173
|
+
* @returns {Promise<void>}
|
|
174
|
+
*/
|
|
175
|
+
async focus(locatorOrElement) {
|
|
176
|
+
throw new Error('focus() must be implemented by subclass');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Page-level Operations
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Evaluate JavaScript in page context
|
|
185
|
+
* @param {Function} fn - Function to evaluate
|
|
186
|
+
* @param {Array} args - Arguments to pass
|
|
187
|
+
* @returns {Promise<any>} - Result of evaluation
|
|
188
|
+
*/
|
|
189
|
+
async evaluateOnPage(fn, args = []) {
|
|
190
|
+
throw new Error('evaluateOnPage() must be implemented by subclass');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get the main frame
|
|
195
|
+
* @returns {Object} - Main frame
|
|
196
|
+
*/
|
|
197
|
+
getMainFrame() {
|
|
198
|
+
throw new Error('getMainFrame() must be implemented by subclass');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Playwright adapter implementation
|
|
204
|
+
*/
|
|
205
|
+
export class PlaywrightAdapter extends EngineAdapter {
|
|
206
|
+
getEngineName() {
|
|
207
|
+
return 'playwright';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Element Selection and Locators
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
createLocator(selector) {
|
|
215
|
+
// Handle :nth-of-type() pseudo-selectors which don't work in Playwright locators
|
|
216
|
+
const nthOfTypeMatch = selector.match(/^(.+):nth-of-type\((\d+)\)$/);
|
|
217
|
+
if (nthOfTypeMatch) {
|
|
218
|
+
const baseSelector = nthOfTypeMatch[1];
|
|
219
|
+
const index = parseInt(nthOfTypeMatch[2], 10) - 1; // Convert to 0-based
|
|
220
|
+
return this.page.locator(baseSelector).nth(index);
|
|
221
|
+
}
|
|
222
|
+
return this.page.locator(selector);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async querySelector(selector) {
|
|
226
|
+
const locator = this.createLocator(selector).first();
|
|
227
|
+
const count = await locator.count();
|
|
228
|
+
return count > 0 ? locator : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async querySelectorAll(selector) {
|
|
232
|
+
const locator = this.createLocator(selector);
|
|
233
|
+
const count = await locator.count();
|
|
234
|
+
const elements = [];
|
|
235
|
+
for (let i = 0; i < count; i++) {
|
|
236
|
+
elements.push(locator.nth(i));
|
|
237
|
+
}
|
|
238
|
+
return elements;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async waitForSelector(selector, options = {}) {
|
|
242
|
+
const { visible = true, timeout = 5000 } = options;
|
|
243
|
+
const locator = this.createLocator(selector);
|
|
244
|
+
await locator.waitFor({ state: visible ? 'visible' : 'attached', timeout });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async waitForVisible(locatorOrElement, timeout = TIMING.DEFAULT_TIMEOUT) {
|
|
248
|
+
const firstLocator = locatorOrElement.first();
|
|
249
|
+
await firstLocator.waitFor({ state: 'visible', timeout });
|
|
250
|
+
return firstLocator;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async count(selector) {
|
|
254
|
+
return await this.page.locator(selector).count();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// Element Evaluation and Properties
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
async evaluateOnElement(locatorOrElement, fn, args) {
|
|
262
|
+
// Playwright only accepts a single argument
|
|
263
|
+
if (args === undefined) {
|
|
264
|
+
return await locatorOrElement.evaluate(fn);
|
|
265
|
+
}
|
|
266
|
+
return await locatorOrElement.evaluate(fn, args);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async getTextContent(locatorOrElement) {
|
|
270
|
+
return await locatorOrElement.textContent();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async getInputValue(locatorOrElement) {
|
|
274
|
+
return await locatorOrElement.inputValue();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async getAttribute(locatorOrElement, attribute) {
|
|
278
|
+
return await locatorOrElement.getAttribute(attribute);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// Element Interactions
|
|
283
|
+
// ============================================================================
|
|
284
|
+
|
|
285
|
+
async click(locatorOrElement, options = {}) {
|
|
286
|
+
await locatorOrElement.click(options);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async type(locatorOrElement, text) {
|
|
290
|
+
await locatorOrElement.type(text);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async fill(locatorOrElement, text) {
|
|
294
|
+
await locatorOrElement.fill(text);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async focus(locatorOrElement) {
|
|
298
|
+
await locatorOrElement.focus();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ============================================================================
|
|
302
|
+
// Page-level Operations
|
|
303
|
+
// ============================================================================
|
|
304
|
+
|
|
305
|
+
async evaluateOnPage(fn, args = []) {
|
|
306
|
+
// Playwright only accepts a single argument (can be array/object)
|
|
307
|
+
if (args.length === 0) {
|
|
308
|
+
return await this.page.evaluate(fn);
|
|
309
|
+
} else if (args.length === 1) {
|
|
310
|
+
return await this.page.evaluate(fn, args[0]);
|
|
311
|
+
} else {
|
|
312
|
+
// Multiple args - pass as array
|
|
313
|
+
return await this.page.evaluate(fn, args);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
getMainFrame() {
|
|
318
|
+
return this.page.mainFrame();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Puppeteer adapter implementation
|
|
324
|
+
*/
|
|
325
|
+
export class PuppeteerAdapter extends EngineAdapter {
|
|
326
|
+
getEngineName() {
|
|
327
|
+
return 'puppeteer';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// Element Selection and Locators
|
|
332
|
+
// ============================================================================
|
|
333
|
+
|
|
334
|
+
createLocator(selector) {
|
|
335
|
+
// Puppeteer doesn't have locators - just returns selector
|
|
336
|
+
// The actual element will be queried when needed
|
|
337
|
+
return selector;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async querySelector(selector) {
|
|
341
|
+
return await this.page.$(selector);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async querySelectorAll(selector) {
|
|
345
|
+
return await this.page.$$(selector);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async waitForSelector(selector, options = {}) {
|
|
349
|
+
const { visible = true, timeout = 5000 } = options;
|
|
350
|
+
await this.page.waitForSelector(selector, { visible, timeout });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async waitForVisible(locatorOrElement, timeout = TIMING.DEFAULT_TIMEOUT) {
|
|
354
|
+
// For Puppeteer, locatorOrElement is already an ElementHandle
|
|
355
|
+
// We can't wait on it directly, so we just return it
|
|
356
|
+
// The caller should have already used waitForSelector
|
|
357
|
+
return locatorOrElement;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async count(selector) {
|
|
361
|
+
const elements = await this.page.$$(selector);
|
|
362
|
+
return elements.length;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ============================================================================
|
|
366
|
+
// Element Evaluation and Properties
|
|
367
|
+
// ============================================================================
|
|
368
|
+
|
|
369
|
+
async evaluateOnElement(locatorOrElement, fn, args) {
|
|
370
|
+
// Puppeteer accepts the element as first arg, then spread args
|
|
371
|
+
if (args === undefined) {
|
|
372
|
+
return await this.page.evaluate(fn, locatorOrElement);
|
|
373
|
+
}
|
|
374
|
+
return await this.page.evaluate(fn, locatorOrElement, args);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async getTextContent(locatorOrElement) {
|
|
378
|
+
return await this.page.evaluate((el) => el.textContent, locatorOrElement);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async getInputValue(locatorOrElement) {
|
|
382
|
+
return await this.page.evaluate((el) => el.value, locatorOrElement);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async getAttribute(locatorOrElement, attribute) {
|
|
386
|
+
return await this.page.evaluate(
|
|
387
|
+
(el, attr) => el.getAttribute(attr),
|
|
388
|
+
locatorOrElement,
|
|
389
|
+
attribute
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ============================================================================
|
|
394
|
+
// Element Interactions
|
|
395
|
+
// ============================================================================
|
|
396
|
+
|
|
397
|
+
async click(locatorOrElement, options = {}) {
|
|
398
|
+
await locatorOrElement.click(options);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async type(locatorOrElement, text) {
|
|
402
|
+
// Puppeteer requires focus before typing
|
|
403
|
+
await locatorOrElement.focus();
|
|
404
|
+
await this.page.keyboard.type(text);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async fill(locatorOrElement, text) {
|
|
408
|
+
// Puppeteer doesn't have fill() - use evaluate to set value
|
|
409
|
+
await this.page.evaluate(
|
|
410
|
+
(el, value) => {
|
|
411
|
+
el.value = value;
|
|
412
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
413
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
414
|
+
},
|
|
415
|
+
locatorOrElement,
|
|
416
|
+
text
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async focus(locatorOrElement) {
|
|
421
|
+
await locatorOrElement.focus();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// Page-level Operations
|
|
426
|
+
// ============================================================================
|
|
427
|
+
|
|
428
|
+
async evaluateOnPage(fn, args = []) {
|
|
429
|
+
// Puppeteer accepts spread arguments
|
|
430
|
+
return await this.page.evaluate(fn, ...args);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
getMainFrame() {
|
|
434
|
+
return this.page.mainFrame();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Factory function to create appropriate adapter
|
|
440
|
+
* @param {Object} page - Playwright or Puppeteer page object
|
|
441
|
+
* @param {string} engine - Engine type ('playwright' or 'puppeteer')
|
|
442
|
+
* @returns {EngineAdapter} - Appropriate adapter instance
|
|
443
|
+
*/
|
|
444
|
+
export function createEngineAdapter(page, engine) {
|
|
445
|
+
if (!page) {
|
|
446
|
+
const errorDetails = {
|
|
447
|
+
page,
|
|
448
|
+
pageType: typeof page,
|
|
449
|
+
engine,
|
|
450
|
+
stackTrace: new Error().stack,
|
|
451
|
+
};
|
|
452
|
+
throw new Error(
|
|
453
|
+
`page is required in createEngineAdapter. Received: page=${page} (type: ${typeof page}), engine=${engine}. This may indicate that the page object was not properly passed through the function call chain. Stack trace: ${errorDetails.stackTrace}`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (engine === 'playwright') {
|
|
458
|
+
return new PlaywrightAdapter(page);
|
|
459
|
+
} else if (engine === 'puppeteer') {
|
|
460
|
+
return new PuppeteerAdapter(page);
|
|
461
|
+
} else {
|
|
462
|
+
throw new Error(
|
|
463
|
+
`Unsupported engine: ${engine}. Expected 'playwright' or 'puppeteer'`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isVerboseEnabled } from './logger.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect which browser automation engine is being used
|
|
5
|
+
* @param {Object} pageOrContext - Page or context object from Playwright or Puppeteer
|
|
6
|
+
* @returns {string} - 'playwright' or 'puppeteer'
|
|
7
|
+
*/
|
|
8
|
+
export function detectEngine(pageOrContext) {
|
|
9
|
+
const hasEval = !!pageOrContext.$eval;
|
|
10
|
+
const hasEvalAll = !!pageOrContext.$$eval;
|
|
11
|
+
const locatorType = typeof pageOrContext.locator;
|
|
12
|
+
const contextType = typeof pageOrContext.context;
|
|
13
|
+
const hasContext = contextType === 'function' || contextType === 'object';
|
|
14
|
+
|
|
15
|
+
// Debug logging
|
|
16
|
+
if (isVerboseEnabled()) {
|
|
17
|
+
console.log('🔍 [ENGINE DETECTION]', {
|
|
18
|
+
hasEval,
|
|
19
|
+
hasEvalAll,
|
|
20
|
+
locatorType,
|
|
21
|
+
contextType,
|
|
22
|
+
hasContext,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for Playwright-specific methods first
|
|
27
|
+
// Playwright has locator as a function and context() method
|
|
28
|
+
// Both engines have $eval and $$eval, so we check for unique Playwright features first
|
|
29
|
+
if (locatorType === 'function' && hasContext) {
|
|
30
|
+
if (isVerboseEnabled()) {
|
|
31
|
+
console.log('🔍 [ENGINE DETECTION] Detected: playwright');
|
|
32
|
+
}
|
|
33
|
+
return 'playwright';
|
|
34
|
+
}
|
|
35
|
+
// Check for Puppeteer-specific methods
|
|
36
|
+
// Puppeteer has $eval, $$eval but no context() method
|
|
37
|
+
if (hasEval && hasEvalAll && !hasContext) {
|
|
38
|
+
if (isVerboseEnabled()) {
|
|
39
|
+
console.log('🔍 [ENGINE DETECTION] Detected: puppeteer');
|
|
40
|
+
}
|
|
41
|
+
return 'puppeteer';
|
|
42
|
+
}
|
|
43
|
+
if (isVerboseEnabled()) {
|
|
44
|
+
console.log('🔍 [ENGINE DETECTION] Could not detect engine!');
|
|
45
|
+
}
|
|
46
|
+
throw new Error(
|
|
47
|
+
'Unknown browser automation engine. Expected Playwright or Puppeteer page object.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import makeLog from 'log-lazy';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if verbose logging is enabled via environment or CLI args
|
|
5
|
+
* @returns {boolean} - True if verbose mode is enabled
|
|
6
|
+
*/
|
|
7
|
+
export function isVerboseEnabled() {
|
|
8
|
+
return !!(process.env.VERBOSE || process.argv.includes('--verbose'));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a logger instance with verbose level control
|
|
13
|
+
* @param {Object} options - Configuration options
|
|
14
|
+
* @param {boolean} options.verbose - Enable verbose logging
|
|
15
|
+
* @returns {Object} - Logger instance
|
|
16
|
+
*/
|
|
17
|
+
export function createLogger(options = {}) {
|
|
18
|
+
const { verbose = false } = options;
|
|
19
|
+
const log = makeLog({ level: verbose ? 'debug' : 'error' });
|
|
20
|
+
return log;
|
|
21
|
+
}
|