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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8ef86eb: Add page HTML, rendered text, and positional page evaluation APIs to commander instances.
8
+
3
9
  ## 0.10.0
4
10
 
5
11
  ### Minor Changes
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Universal browser automation library that supports both Playwright and Puppeteer with a unified API",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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 = (opts) => evaluate({ ...opts, page, engine });
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
  }
@@ -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
@@ -78,6 +78,8 @@ export {
78
78
  export { isVisible, isEnabled, count } from './elements/visibility.js';
79
79
 
80
80
  export {
81
+ content,
82
+ innerText,
81
83
  textContent,
82
84
  inputValue,
83
85
  getAttribute,
@@ -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();