browser-commander 0.10.0 → 0.11.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/CHANGELOG.md +6 -0
- package/README.md +21 -0
- package/package.json +1 -1
- package/src/bindings.js +21 -1
- package/src/core/engine-adapter.js +33 -0
- package/src/elements/content.js +55 -0
- package/src/exports.js +2 -0
- package/tests/helpers/mocks.js +7 -0
- package/tests/unit/bindings.test.js +64 -0
- package/tests/unit/elements/content.test.js +102 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -360,6 +360,27 @@ await commander.fillTextArea({
|
|
|
360
360
|
});
|
|
361
361
|
```
|
|
362
362
|
|
|
363
|
+
### Page Content Extraction
|
|
364
|
+
|
|
365
|
+
Read the current page without reaching through to the engine-specific page API:
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
const html = await commander.content();
|
|
369
|
+
const pageText = await commander.innerText(); // document.body
|
|
370
|
+
const mainText = await commander.innerText('main');
|
|
371
|
+
const title = await commander.evaluate(() => document.title);
|
|
372
|
+
const total = await commander.evaluate((a, b) => a + b, 2, 3);
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
The existing options-object form of `evaluate` remains supported:
|
|
376
|
+
|
|
377
|
+
```javascript
|
|
378
|
+
const total = await commander.evaluate({
|
|
379
|
+
fn: (a, b) => a + b,
|
|
380
|
+
args: [2, 3],
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
363
384
|
### Keyboard Interactions
|
|
364
385
|
|
|
365
386
|
```javascript
|
package/package.json
CHANGED
package/src/bindings.js
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
} from './elements/selectors.js';
|
|
31
31
|
import { isVisible, isEnabled, count } from './elements/visibility.js';
|
|
32
32
|
import {
|
|
33
|
+
content,
|
|
34
|
+
innerText,
|
|
33
35
|
textContent,
|
|
34
36
|
inputValue,
|
|
35
37
|
getAttribute,
|
|
@@ -85,7 +87,13 @@ export function createBoundFunctions(options = {}) {
|
|
|
85
87
|
: null;
|
|
86
88
|
return wait({ ...opts, log, abortSignal: opts.abortSignal || abortSignal });
|
|
87
89
|
};
|
|
88
|
-
const evaluateBound = (
|
|
90
|
+
const evaluateBound = (fnOrOptions, ...args) => {
|
|
91
|
+
const opts =
|
|
92
|
+
typeof fnOrOptions === 'function'
|
|
93
|
+
? { fn: fnOrOptions, args }
|
|
94
|
+
: fnOrOptions;
|
|
95
|
+
return evaluate({ ...opts, page, engine });
|
|
96
|
+
};
|
|
89
97
|
const safeEvaluateBound = (opts) => safeEvaluate({ ...opts, page, engine });
|
|
90
98
|
const getUrlBound = () => getUrl({ page });
|
|
91
99
|
const unfocusAddressBarBound = (opts = {}) =>
|
|
@@ -158,6 +166,16 @@ export function createBoundFunctions(options = {}) {
|
|
|
158
166
|
const countBound = (opts) => count({ ...opts, page, engine });
|
|
159
167
|
|
|
160
168
|
// Bound content
|
|
169
|
+
const contentBound = (opts = {}) => content({ ...opts, page, engine });
|
|
170
|
+
const innerTextBound = (selectorOrOptions = 'body') => {
|
|
171
|
+
const opts =
|
|
172
|
+
typeof selectorOrOptions === 'object' &&
|
|
173
|
+
selectorOrOptions !== null &&
|
|
174
|
+
Object.hasOwn(selectorOrOptions, 'selector')
|
|
175
|
+
? selectorOrOptions
|
|
176
|
+
: { selector: selectorOrOptions };
|
|
177
|
+
return innerText({ ...opts, page, engine });
|
|
178
|
+
};
|
|
161
179
|
const textContentBound = (opts) => textContent({ ...opts, page, engine });
|
|
162
180
|
const inputValueBound = (opts) => inputValue({ ...opts, page, engine });
|
|
163
181
|
const getAttributeBound = (opts) => getAttribute({ ...opts, page, engine });
|
|
@@ -302,6 +320,8 @@ export function createBoundFunctions(options = {}) {
|
|
|
302
320
|
isVisible: isVisibleWrapped,
|
|
303
321
|
isEnabled: isEnabledWrapped,
|
|
304
322
|
count: countBound,
|
|
323
|
+
content: contentBound,
|
|
324
|
+
innerText: innerTextBound,
|
|
305
325
|
textContent: textContentWrapped,
|
|
306
326
|
inputValue: inputValueWrapped,
|
|
307
327
|
locator: locatorBound,
|
|
@@ -120,6 +120,15 @@ export class EngineAdapter {
|
|
|
120
120
|
return notImplemented('getTextContent');
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Get rendered text from an element
|
|
125
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
126
|
+
* @returns {Promise<string>} - Rendered text
|
|
127
|
+
*/
|
|
128
|
+
getInnerText(_locatorOrElement) {
|
|
129
|
+
return notImplemented('getInnerText');
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
/**
|
|
124
133
|
* Get input value
|
|
125
134
|
* @param {Object} locatorOrElement - Locator or element
|
|
@@ -236,6 +245,14 @@ export class EngineAdapter {
|
|
|
236
245
|
return notImplemented('evaluateOnPage');
|
|
237
246
|
}
|
|
238
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Get the full HTML of the current page
|
|
250
|
+
* @returns {Promise<string>} - Serialized page HTML
|
|
251
|
+
*/
|
|
252
|
+
getPageContent() {
|
|
253
|
+
return notImplemented('getPageContent');
|
|
254
|
+
}
|
|
255
|
+
|
|
239
256
|
/**
|
|
240
257
|
* Get the main frame
|
|
241
258
|
* @returns {Object} - Main frame
|
|
@@ -325,6 +342,10 @@ export class PlaywrightAdapter extends EngineAdapter {
|
|
|
325
342
|
return await locatorOrElement.textContent();
|
|
326
343
|
}
|
|
327
344
|
|
|
345
|
+
async getInnerText(locatorOrElement) {
|
|
346
|
+
return await locatorOrElement.innerText();
|
|
347
|
+
}
|
|
348
|
+
|
|
328
349
|
async getInputValue(locatorOrElement) {
|
|
329
350
|
return await locatorOrElement.inputValue();
|
|
330
351
|
}
|
|
@@ -401,6 +422,10 @@ export class PlaywrightAdapter extends EngineAdapter {
|
|
|
401
422
|
}
|
|
402
423
|
}
|
|
403
424
|
|
|
425
|
+
async getPageContent() {
|
|
426
|
+
return await this.page.content();
|
|
427
|
+
}
|
|
428
|
+
|
|
404
429
|
getMainFrame() {
|
|
405
430
|
return this.page.mainFrame();
|
|
406
431
|
}
|
|
@@ -469,6 +494,10 @@ export class PuppeteerAdapter extends EngineAdapter {
|
|
|
469
494
|
return await this.page.evaluate((el) => el.textContent, locatorOrElement);
|
|
470
495
|
}
|
|
471
496
|
|
|
497
|
+
async getInnerText(locatorOrElement) {
|
|
498
|
+
return await this.page.evaluate((el) => el.innerText, locatorOrElement);
|
|
499
|
+
}
|
|
500
|
+
|
|
472
501
|
async getInputValue(locatorOrElement) {
|
|
473
502
|
return await this.page.evaluate((el) => el.value, locatorOrElement);
|
|
474
503
|
}
|
|
@@ -541,6 +570,10 @@ export class PuppeteerAdapter extends EngineAdapter {
|
|
|
541
570
|
return await this.page.evaluate(fn, ...args);
|
|
542
571
|
}
|
|
543
572
|
|
|
573
|
+
async getPageContent() {
|
|
574
|
+
return await this.page.content();
|
|
575
|
+
}
|
|
576
|
+
|
|
544
577
|
getMainFrame() {
|
|
545
578
|
return this.page.mainFrame();
|
|
546
579
|
}
|
package/src/elements/content.js
CHANGED
|
@@ -2,6 +2,61 @@ import { isNavigationError } from '../core/navigation-safety.js';
|
|
|
2
2
|
import { getLocatorOrElement } from './locators.js';
|
|
3
3
|
import { createEngineAdapter } from '../core/engine-adapter.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Get the full HTML of the current page
|
|
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 {Object} options.adapter - Engine adapter (optional, will be created if not provided)
|
|
11
|
+
* @returns {Promise<string|null>} - Serialized page HTML, or null during navigation
|
|
12
|
+
*/
|
|
13
|
+
export async function content(options = {}) {
|
|
14
|
+
const { page, engine, adapter: providedAdapter } = options;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const adapter = providedAdapter || createEngineAdapter(page, engine);
|
|
18
|
+
return await adapter.getPageContent();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (isNavigationError(error)) {
|
|
21
|
+
console.log('⚠️ Navigation detected during content, returning null');
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get rendered text from an element, defaulting to the document body
|
|
30
|
+
* @param {Object} options - Configuration options
|
|
31
|
+
* @param {Object} options.page - Browser page object
|
|
32
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
33
|
+
* @param {string|Object} [options.selector='body'] - CSS selector or element
|
|
34
|
+
* @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
|
|
35
|
+
* @returns {Promise<string|null>} - Rendered text, or null if unavailable
|
|
36
|
+
*/
|
|
37
|
+
export async function innerText(options = {}) {
|
|
38
|
+
const { page, engine, selector = 'body', adapter: providedAdapter } = options;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const adapter = providedAdapter || createEngineAdapter(page, engine);
|
|
42
|
+
const locatorOrElement = await getLocatorOrElement({
|
|
43
|
+
page,
|
|
44
|
+
engine,
|
|
45
|
+
selector,
|
|
46
|
+
});
|
|
47
|
+
if (!locatorOrElement) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return await adapter.getInnerText(locatorOrElement);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isNavigationError(error)) {
|
|
53
|
+
console.log('⚠️ Navigation detected during innerText, returning null');
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
5
60
|
/**
|
|
6
61
|
* Get text content
|
|
7
62
|
* @param {Object} options - Configuration options
|
package/src/exports.js
CHANGED
package/tests/helpers/mocks.js
CHANGED
|
@@ -44,6 +44,7 @@ export function createMockPlaywrightPage(options = {}) {
|
|
|
44
44
|
},
|
|
45
45
|
focus: async () => {},
|
|
46
46
|
textContent: async () => elementData.textContent,
|
|
47
|
+
innerText: async () => elementData.innerText ?? elementData.textContent,
|
|
47
48
|
inputValue: async () => elementData.value,
|
|
48
49
|
getAttribute: async (name) => elementData[name] || null,
|
|
49
50
|
isVisible: async () => elementData.visible,
|
|
@@ -56,6 +57,7 @@ export function createMockPlaywrightPage(options = {}) {
|
|
|
56
57
|
const mockEl = {
|
|
57
58
|
tagName: 'DIV',
|
|
58
59
|
textContent: elementData.textContent,
|
|
60
|
+
innerText: elementData.innerText ?? elementData.textContent,
|
|
59
61
|
value: elementData.value,
|
|
60
62
|
className: elementData.className,
|
|
61
63
|
disabled: !elementData.enabled,
|
|
@@ -105,6 +107,7 @@ export function createMockPlaywrightPage(options = {}) {
|
|
|
105
107
|
return locs.map((l) => l.evaluate(fn, ...args));
|
|
106
108
|
},
|
|
107
109
|
locator: locatorMock,
|
|
110
|
+
content: async () => '<html><body>Mock page</body></html>',
|
|
108
111
|
// Playwright's page.evaluate() only accepts a single argument (not spread)
|
|
109
112
|
// This is the key difference from Puppeteer
|
|
110
113
|
evaluate: async (fn, arg) => {
|
|
@@ -213,6 +216,8 @@ export function createMockPuppeteerPage(options = {}) {
|
|
|
213
216
|
const mockEl = {
|
|
214
217
|
tagName: 'DIV',
|
|
215
218
|
textContent: elementData.textContent || 'Mock text',
|
|
219
|
+
innerText:
|
|
220
|
+
elementData.innerText ?? elementData.textContent ?? 'Mock text',
|
|
216
221
|
value: elementData.value || '',
|
|
217
222
|
className: elementData.className || 'mock-class',
|
|
218
223
|
disabled: !elementData.enabled,
|
|
@@ -252,6 +257,7 @@ export function createMockPuppeteerPage(options = {}) {
|
|
|
252
257
|
const mockEl = {
|
|
253
258
|
tagName: 'DIV',
|
|
254
259
|
textContent: 'Mock text',
|
|
260
|
+
innerText: 'Mock text',
|
|
255
261
|
value: '',
|
|
256
262
|
className: 'mock-class',
|
|
257
263
|
disabled: false,
|
|
@@ -280,6 +286,7 @@ export function createMockPuppeteerPage(options = {}) {
|
|
|
280
286
|
return fn;
|
|
281
287
|
}
|
|
282
288
|
},
|
|
289
|
+
content: async () => '<html><body>Mock page</body></html>',
|
|
283
290
|
mainFrame: () => ({
|
|
284
291
|
url: () => url,
|
|
285
292
|
}),
|
|
@@ -129,6 +129,14 @@ describe('bindings', () => {
|
|
|
129
129
|
typeof bindings.textContent === 'function',
|
|
130
130
|
'textContent should be a function'
|
|
131
131
|
);
|
|
132
|
+
assert.ok(
|
|
133
|
+
typeof bindings.content === 'function',
|
|
134
|
+
'content should be a function'
|
|
135
|
+
);
|
|
136
|
+
assert.ok(
|
|
137
|
+
typeof bindings.innerText === 'function',
|
|
138
|
+
'innerText should be a function'
|
|
139
|
+
);
|
|
132
140
|
assert.ok(
|
|
133
141
|
typeof bindings.inputValue === 'function',
|
|
134
142
|
'inputValue should be a function'
|
|
@@ -204,6 +212,62 @@ describe('bindings', () => {
|
|
|
204
212
|
assert.strictEqual(url, 'https://example.com');
|
|
205
213
|
});
|
|
206
214
|
|
|
215
|
+
it('should expose page content with no arguments', async () => {
|
|
216
|
+
const page = createMockPlaywrightPage();
|
|
217
|
+
const bindings = createBoundFunctions({
|
|
218
|
+
page,
|
|
219
|
+
engine: 'playwright',
|
|
220
|
+
log: createMockLogger(),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
assert.strictEqual(
|
|
224
|
+
await bindings.content(),
|
|
225
|
+
'<html><body>Mock page</body></html>'
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should expose innerText with a selector or the body by default', async () => {
|
|
230
|
+
const page = createMockPlaywrightPage({
|
|
231
|
+
elements: {
|
|
232
|
+
body: { innerText: 'Body text', count: 1 },
|
|
233
|
+
main: { innerText: 'Main text', count: 1 },
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
const bindings = createBoundFunctions({
|
|
237
|
+
page,
|
|
238
|
+
engine: 'playwright',
|
|
239
|
+
log: createMockLogger(),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
assert.strictEqual(await bindings.innerText(), 'Body text');
|
|
243
|
+
assert.strictEqual(await bindings.innerText('main'), 'Main text');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should evaluate a function with positional arguments', async () => {
|
|
247
|
+
const page = createMockPlaywrightPage();
|
|
248
|
+
const bindings = createBoundFunctions({
|
|
249
|
+
page,
|
|
250
|
+
engine: 'playwright',
|
|
251
|
+
log: createMockLogger(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
assert.strictEqual(await bindings.evaluate((a, b) => a + b, 2, 3), 5);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should preserve the evaluate options-object API', async () => {
|
|
258
|
+
const page = createMockPlaywrightPage();
|
|
259
|
+
const bindings = createBoundFunctions({
|
|
260
|
+
page,
|
|
261
|
+
engine: 'playwright',
|
|
262
|
+
log: createMockLogger(),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
assert.strictEqual(
|
|
266
|
+
await bindings.evaluate({ fn: (a, b) => a + b, args: [2, 3] }),
|
|
267
|
+
5
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
207
271
|
it('should pre-bind log to functions', async () => {
|
|
208
272
|
const page = createMockPlaywrightPage();
|
|
209
273
|
const log = createMockLogger({ collectLogs: true });
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import {
|
|
4
|
+
content,
|
|
5
|
+
innerText,
|
|
4
6
|
textContent,
|
|
5
7
|
inputValue,
|
|
6
8
|
getAttribute,
|
|
@@ -14,6 +16,106 @@ import {
|
|
|
14
16
|
} from '../../helpers/mocks.js';
|
|
15
17
|
|
|
16
18
|
describe('content', () => {
|
|
19
|
+
describe('content', () => {
|
|
20
|
+
for (const [engine, createPage] of [
|
|
21
|
+
['playwright', createMockPlaywrightPage],
|
|
22
|
+
['puppeteer', createMockPuppeteerPage],
|
|
23
|
+
]) {
|
|
24
|
+
it(`should return the full page HTML for ${engine}`, async () => {
|
|
25
|
+
const page = createPage();
|
|
26
|
+
page.content = async () =>
|
|
27
|
+
'<!doctype html><html><body>Hello</body></html>';
|
|
28
|
+
|
|
29
|
+
const html = await content({ page, engine });
|
|
30
|
+
|
|
31
|
+
assert.strictEqual(
|
|
32
|
+
html,
|
|
33
|
+
'<!doctype html><html><body>Hello</body></html>'
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
it('should return null on navigation errors', async () => {
|
|
39
|
+
const page = createMockPlaywrightPage();
|
|
40
|
+
page.content = async () => {
|
|
41
|
+
throw new Error('Execution context was destroyed');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
assert.strictEqual(await content({ page, engine: 'playwright' }), null);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('innerText', () => {
|
|
49
|
+
it('should return rendered text for Playwright', async () => {
|
|
50
|
+
const page = createMockPlaywrightPage({
|
|
51
|
+
elements: { main: { innerText: 'Rendered text', count: 1 } },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const text = await innerText({
|
|
55
|
+
page,
|
|
56
|
+
engine: 'playwright',
|
|
57
|
+
selector: 'main',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
assert.strictEqual(text, 'Rendered text');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return rendered text for Puppeteer', async () => {
|
|
64
|
+
const page = createMockPuppeteerPage({
|
|
65
|
+
elements: { main: { innerText: 'Rendered text', count: 1 } },
|
|
66
|
+
});
|
|
67
|
+
page.evaluate = async () => 'Rendered text';
|
|
68
|
+
|
|
69
|
+
const text = await innerText({
|
|
70
|
+
page,
|
|
71
|
+
engine: 'puppeteer',
|
|
72
|
+
selector: 'main',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
assert.strictEqual(text, 'Rendered text');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should default to the document body', async () => {
|
|
79
|
+
const page = createMockPlaywrightPage({
|
|
80
|
+
elements: { body: { innerText: 'Page text', count: 1 } },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
assert.strictEqual(
|
|
84
|
+
await innerText({ page, engine: 'playwright' }),
|
|
85
|
+
'Page text'
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return null when the element is not found', async () => {
|
|
90
|
+
const page = createMockPuppeteerPage({
|
|
91
|
+
elements: { main: { count: 0 } },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
assert.strictEqual(
|
|
95
|
+
await innerText({ page, engine: 'puppeteer', selector: 'main' }),
|
|
96
|
+
null
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return null on navigation errors', async () => {
|
|
101
|
+
const page = createMockPlaywrightPage();
|
|
102
|
+
page.locator = () => ({
|
|
103
|
+
count: async () => 1,
|
|
104
|
+
first() {
|
|
105
|
+
return this;
|
|
106
|
+
},
|
|
107
|
+
innerText: async () => {
|
|
108
|
+
throw new Error('Execution context was destroyed');
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(
|
|
113
|
+
await innerText({ page, engine: 'playwright', selector: 'main' }),
|
|
114
|
+
null
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
17
119
|
describe('textContent', () => {
|
|
18
120
|
it('should throw when selector is not provided', async () => {
|
|
19
121
|
const page = createMockPlaywrightPage();
|