browser-commander 0.5.3 → 0.6.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 +18 -0
- package/README.md +61 -0
- package/package.json +1 -1
- package/src/elements/selectors.js +21 -0
- package/tests/unit/elements/selectors.test.js +32 -2
- package/tests/unit/factory.test.js +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7d83530: Document extensibility escape hatch: `commander.page` and `launchBrowser()` return values expose the raw underlying Playwright/Puppeteer page object as an official mechanism for accessing engine-specific APIs not yet supported by browser-commander (e.g. `page.pdf()`, `page.emulateMedia()`, `page.keyboard`, `page.on('dialog', ...)`). Adds tests verifying `commander.page` is the exact raw page object.
|
|
8
|
+
|
|
9
|
+
## 0.5.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- e9043cc: Fix normalizeSelector to validate input type and reject arrays
|
|
14
|
+
|
|
15
|
+
When `normalizeSelector` receives an invalid type (array, number, or non-text-selector object), it now returns `null` with a warning instead of returning the invalid value unchanged.
|
|
16
|
+
|
|
17
|
+
This prevents downstream `querySelectorAll` errors with invalid selector syntax (like trailing commas when arrays are accidentally passed).
|
|
18
|
+
|
|
19
|
+
Fixes #23
|
|
20
|
+
|
|
3
21
|
## 0.5.3
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -318,6 +318,67 @@ action: async (ctx) => {
|
|
|
318
318
|
};
|
|
319
319
|
```
|
|
320
320
|
|
|
321
|
+
## Extensibility / Escape Hatch
|
|
322
|
+
|
|
323
|
+
`browser-commander` cannot anticipate every browser API. When you need an API that is not yet supported, you can access the raw underlying engine objects directly as an **official extensibility escape hatch**.
|
|
324
|
+
|
|
325
|
+
### Using `commander.page` for engine-specific APIs
|
|
326
|
+
|
|
327
|
+
`makeBrowserCommander` exposes `commander.page` — this is the **raw Playwright or Puppeteer page object**, not a wrapper. Use it directly for APIs browser-commander doesn't yet support:
|
|
328
|
+
|
|
329
|
+
```javascript
|
|
330
|
+
const { browser, page } = await launchBrowser({ engine: 'playwright' });
|
|
331
|
+
const commander = makeBrowserCommander({ page });
|
|
332
|
+
|
|
333
|
+
// Access engine-specific API via commander.page
|
|
334
|
+
// Example: PDF generation (issue #35)
|
|
335
|
+
const pdfBuffer = await commander.page.pdf({
|
|
336
|
+
format: 'A4',
|
|
337
|
+
printBackground: true,
|
|
338
|
+
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Example: Color scheme emulation (issue #36)
|
|
342
|
+
await commander.page.emulateMedia({ colorScheme: 'dark' });
|
|
343
|
+
|
|
344
|
+
// Example: Keyboard interactions (issue #37)
|
|
345
|
+
await commander.page.keyboard.press('Escape');
|
|
346
|
+
|
|
347
|
+
// Example: Dialog handling (issue #38)
|
|
348
|
+
commander.page.on('dialog', async (dialog) => {
|
|
349
|
+
await dialog.dismiss();
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Using `launchBrowser` raw return values
|
|
354
|
+
|
|
355
|
+
`launchBrowser()` returns the raw `{ browser, page }` objects from the underlying engine. You can use these directly:
|
|
356
|
+
|
|
357
|
+
```javascript
|
|
358
|
+
const { browser, page } = await launchBrowser({ engine: 'playwright' });
|
|
359
|
+
|
|
360
|
+
// Use raw page directly for engine-specific APIs
|
|
361
|
+
await page.pdf({ format: 'A4' });
|
|
362
|
+
|
|
363
|
+
// Or create a commander for the unified API
|
|
364
|
+
const commander = makeBrowserCommander({ page });
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### No more `_page` hacks
|
|
368
|
+
|
|
369
|
+
If you previously used `page._page || page` to access the raw page, replace it with `commander.page`:
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
// BEFORE (fragile hack):
|
|
373
|
+
const rawPage = page._page || page;
|
|
374
|
+
await rawPage.pdf({ format: 'A4' });
|
|
375
|
+
|
|
376
|
+
// AFTER (official API):
|
|
377
|
+
await commander.page.pdf({ format: 'A4' });
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
This is the **official extensibility mechanism** while awaiting browser-commander to add first-class support for these APIs. Please [report missing APIs](https://github.com/link-foundation/browser-commander/issues) so they can be added.
|
|
381
|
+
|
|
321
382
|
## Debugging
|
|
322
383
|
|
|
323
384
|
Enable verbose mode for detailed logs:
|
package/package.json
CHANGED
|
@@ -162,6 +162,25 @@ export async function normalizeSelector(options = {}) {
|
|
|
162
162
|
throw new Error('selector is required in options');
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// DEFENSIVE: Validate selector type - must be string or Puppeteer text selector object
|
|
166
|
+
// Arrays and other invalid types should be rejected to prevent downstream querySelectorAll errors
|
|
167
|
+
if (Array.isArray(selector)) {
|
|
168
|
+
console.warn(
|
|
169
|
+
`normalizeSelector received invalid selector type: array. Expected string or text selector object.`
|
|
170
|
+
);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
typeof selector !== 'string' &&
|
|
176
|
+
(typeof selector !== 'object' || !selector._isPuppeteerTextSelector)
|
|
177
|
+
) {
|
|
178
|
+
console.warn(
|
|
179
|
+
`normalizeSelector received invalid selector type: ${typeof selector}. Expected string or text selector object.`
|
|
180
|
+
);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
165
184
|
// Handle Playwright text selectors (strings containing :has-text or :text-is)
|
|
166
185
|
// These are valid for Playwright's locator API but NOT for document.querySelectorAll
|
|
167
186
|
if (
|
|
@@ -267,6 +286,8 @@ export async function normalizeSelector(options = {}) {
|
|
|
267
286
|
}
|
|
268
287
|
}
|
|
269
288
|
|
|
289
|
+
// This line should be unreachable after validation, but kept as a safeguard
|
|
290
|
+
// istanbul ignore next
|
|
270
291
|
return selector;
|
|
271
292
|
}
|
|
272
293
|
|
|
@@ -207,11 +207,41 @@ describe('selectors', () => {
|
|
|
207
207
|
assert.strictEqual(result, 'button');
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
-
it('should return
|
|
210
|
+
it('should return null for invalid object selector', async () => {
|
|
211
211
|
const page = createMockPlaywrightPage();
|
|
212
212
|
const obj = { someKey: 'value' };
|
|
213
213
|
const result = await normalizeSelector({ page, selector: obj });
|
|
214
|
-
assert.strictEqual(result,
|
|
214
|
+
assert.strictEqual(result, null);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should return null for array selector', async () => {
|
|
218
|
+
const page = createMockPlaywrightPage();
|
|
219
|
+
const arr = ['[data-qa="test"]', []];
|
|
220
|
+
const result = await normalizeSelector({ page, selector: arr });
|
|
221
|
+
assert.strictEqual(result, null);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should return null for number selector', async () => {
|
|
225
|
+
const page = createMockPlaywrightPage();
|
|
226
|
+
const result = await normalizeSelector({ page, selector: 123 });
|
|
227
|
+
assert.strictEqual(result, null);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should accept valid Puppeteer text selector object', async () => {
|
|
231
|
+
const page = createMockPuppeteerPage();
|
|
232
|
+
page.evaluate = async () => '[data-qa="test"]';
|
|
233
|
+
const textSelector = {
|
|
234
|
+
_isPuppeteerTextSelector: true,
|
|
235
|
+
baseSelector: 'button',
|
|
236
|
+
text: 'Click me',
|
|
237
|
+
exact: false,
|
|
238
|
+
};
|
|
239
|
+
const result = await normalizeSelector({
|
|
240
|
+
page,
|
|
241
|
+
engine: 'puppeteer',
|
|
242
|
+
selector: textSelector,
|
|
243
|
+
});
|
|
244
|
+
assert.strictEqual(result, '[data-qa="test"]');
|
|
215
245
|
});
|
|
216
246
|
});
|
|
217
247
|
|
|
@@ -170,5 +170,62 @@ describe('factory', () => {
|
|
|
170
170
|
// Should not throw
|
|
171
171
|
await commander.destroy();
|
|
172
172
|
});
|
|
173
|
+
|
|
174
|
+
// Extensibility escape hatch tests (issue #39)
|
|
175
|
+
it('should expose the raw page object as commander.page (extensibility escape hatch)', () => {
|
|
176
|
+
const page = createMockPlaywrightPage();
|
|
177
|
+
page.locator = (sel) => ({
|
|
178
|
+
count: async () => 1,
|
|
179
|
+
waitFor: async () => {},
|
|
180
|
+
});
|
|
181
|
+
page.context = () => ({});
|
|
182
|
+
|
|
183
|
+
const commander = makeBrowserCommander({ page });
|
|
184
|
+
|
|
185
|
+
// commander.page must be the exact same object as the raw page passed in
|
|
186
|
+
// This is the official extensibility escape hatch for APIs not yet in browser-commander
|
|
187
|
+
assert.strictEqual(
|
|
188
|
+
commander.page,
|
|
189
|
+
page,
|
|
190
|
+
'commander.page must be the raw engine page (not a wrapper)'
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should allow using commander.page to call engine-specific APIs not in browser-commander', () => {
|
|
195
|
+
const page = createMockPlaywrightPage();
|
|
196
|
+
page.locator = (sel) => ({
|
|
197
|
+
count: async () => 1,
|
|
198
|
+
waitFor: async () => {},
|
|
199
|
+
});
|
|
200
|
+
page.context = () => ({});
|
|
201
|
+
|
|
202
|
+
// Add a custom method to simulate engine-specific API (e.g. page.pdf(), page.emulateMedia())
|
|
203
|
+
page.customEngineMethod = () => 'engine-specific-result';
|
|
204
|
+
|
|
205
|
+
const commander = makeBrowserCommander({ page });
|
|
206
|
+
|
|
207
|
+
// Users can call engine-specific APIs via commander.page without needing _page hacks
|
|
208
|
+
const result = commander.page.customEngineMethod();
|
|
209
|
+
assert.strictEqual(
|
|
210
|
+
result,
|
|
211
|
+
'engine-specific-result',
|
|
212
|
+
'commander.page should allow calling engine-specific APIs'
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should expose raw page with Puppeteer page too (extensibility escape hatch)', () => {
|
|
217
|
+
const page = createMockPuppeteerPage();
|
|
218
|
+
delete page.locator;
|
|
219
|
+
delete page.context;
|
|
220
|
+
page.$eval = async () => {};
|
|
221
|
+
|
|
222
|
+
const commander = makeBrowserCommander({ page });
|
|
223
|
+
|
|
224
|
+
assert.strictEqual(
|
|
225
|
+
commander.page,
|
|
226
|
+
page,
|
|
227
|
+
'commander.page must be the raw Puppeteer page (not a wrapper)'
|
|
228
|
+
);
|
|
229
|
+
});
|
|
173
230
|
});
|
|
174
231
|
});
|