Skip to content

Commit ac2f04f

Browse files
authored
api(selectors): pass selector name when registering, allow file path (#1162)
1 parent d511d7d commit ac2f04f

18 files changed

+136
-111
lines changed

docs/api.md

+8-9
Original file line numberDiff line numberDiff line change
@@ -3149,12 +3149,14 @@ Contains the URL of the response.
31493149
Selectors can be used to install custom selector engines. See [Working with selectors](#working-with-selectors) for more information.
31503150

31513151
<!-- GEN:toc -->
3152-
- [selectors.register(engineFunction[, ...args])](#selectorsregisterenginefunction-args)
3152+
- [selectors.register(name, script)](#selectorsregistername-script)
31533153
<!-- GEN:stop -->
31543154

3155-
#### selectors.register(engineFunction[, ...args])
3156-
- `engineFunction` <[function]|[string]> Function that evaluates to a selector engine instance.
3157-
- `...args` <...[Serializable]> Arguments to pass to `engineFunction`.
3155+
#### selectors.register(name, script)
3156+
- `name` <[string]> Name that is used in selectors as a prefix, e.g. `{name: 'foo'}` enables `foo=myselectorbody` selectors. May only contain `[a-zA-Z0-9_]` characters.
3157+
- `script` <[function]|[string]|[Object]> Script that evaluates to a selector engine instance.
3158+
- `path` <[string]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd).
3159+
- `content` <[string]> Raw script content.
31583160
- returns: <[Promise]>
31593161

31603162
An example of registering selector engine that queries elements based on a tag name:
@@ -3164,9 +3166,6 @@ const { selectors, firefox } = require('playwright'); // Or 'chromium' or 'webk
31643166
(async () => {
31653167
// Must be a function that evaluates to a selector engine instance.
31663168
const createTagNameEngine = () => ({
3167-
// Selectors will be prefixed with "tag=".
3168-
name: 'tag',
3169-
31703169
// Creates a selector that matches given target when queried at the root.
31713170
// Can return undefined if unable to create one.
31723171
create(root, target) {
@@ -3184,8 +3183,8 @@ const { selectors, firefox } = require('playwright'); // Or 'chromium' or 'webk
31843183
}
31853184
});
31863185

3187-
// Register the engine.
3188-
await selectors.register(createTagNameEngine);
3186+
// Register the engine. Selectors will be prefixed with "tag=".
3187+
await selectors.register('tag', createTagNameEngine);
31893188

31903189
const browser = await firefox.launch();
31913190
const page = await browser.newPage();

docs/selectors.md

+3-7
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,10 @@ Id engines are selecting based on the corresponding atrribute value. For example
8484

8585
## Custom selector engines
8686

87-
Playwright supports custom selector engines, registered with [selectors.register(engineFunction[, ...args])](api.md#selectorsregisterenginefunction-args).
87+
Playwright supports custom selector engines, registered with [selectors.register(name, script)](api.md#selectorsregistername-script).
8888

8989
Selector engine should have the following properties:
9090

91-
- `name` Selector name used in selector strings.
9291
- `create` Function to create a relative selector from `root` (root is either a `Document`, `ShadowRoot` or `Element`) to a `target` element.
9392
- `query` Function to query first element matching `selector` relative to the `root`.
9493
- `queryAll` Function to query all elements matching `selector` relative to the `root`.
@@ -97,9 +96,6 @@ An example of registering selector engine that queries elements based on a tag n
9796
```js
9897
// Must be a function that evaluates to a selector engine instance.
9998
const createTagNameEngine = () => ({
100-
// Selectors will be prefixed with "tag=".
101-
name: 'tag',
102-
10399
// Creates a selector that matches given target when queried at the root.
104100
// Can return undefined if unable to create one.
105101
create(root, target) {
@@ -117,8 +113,8 @@ const createTagNameEngine = () => ({
117113
}
118114
});
119115

120-
// Register the engine.
121-
await selectors.register(createTagNameEngine);
116+
// Register the engine. Selectors will be prefixed with "tag=".
117+
await selectors.register('tag', createTagNameEngine);
122118

123119
// Now we can use 'tag=' selectors.
124120
const button = await page.$('tag=button');

src/chromium/crBrowser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
303303
}
304304

305305
async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) {
306-
const source = await helper.evaluationScript(script, ...args);
306+
const source = await helper.evaluationScript(script, args);
307307
this._evaluateOnNewDocumentSources.push(source);
308308
for (const page of this._existingPages())
309309
await (page._delegate as CRPage).evaluateOnNewDocument(source);

src/dom.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,12 @@ export class FrameExecutionContext extends js.ExecutionContext {
8585
this._injectedPromise = undefined;
8686
}
8787
if (!this._injectedPromise) {
88+
const custom: string[] = [];
89+
for (const [name, source] of selectors._engines)
90+
custom.push(`{ name: '${name}', engine: (${source}) }`);
8891
const source = `
8992
new (${injectedSource.source})([
90-
${selectors._sources.join(',\n')}
93+
${custom.join(',\n')}
9194
])
9295
`;
9396
this._injectedPromise = this.evaluateHandle(source);

src/firefox/ffBrowser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
360360
}
361361

362362
async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) {
363-
const source = await helper.evaluationScript(script, ...args);
363+
const source = await helper.evaluationScript(script, args);
364364
this._evaluateOnNewDocumentSources.push(source);
365365
await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source });
366366
}

src/helper.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@ class Helper {
4141
}
4242
}
4343

44-
static async evaluationScript(fun: Function | string | { path?: string, content?: string }, ...args: any[]): Promise<string> {
44+
static async evaluationScript(fun: Function | string | { path?: string, content?: string }, args: any[] = [], addSourceUrl: boolean = true): Promise<string> {
4545
if (!helper.isString(fun) && typeof fun !== 'function') {
4646
if (fun.content !== undefined) {
4747
fun = fun.content;
4848
} else if (fun.path !== undefined) {
4949
let contents = await platform.readFileAsync(fun.path, 'utf8');
50-
contents += '//# sourceURL=' + fun.path.replace(/\n/g, '');
50+
if (addSourceUrl)
51+
contents += '//# sourceURL=' + fun.path.replace(/\n/g, '');
5152
fun = contents;
5253
} else {
5354
throw new Error('Either path or content property must be present');

src/injected/cssSelectorEngine.ts

-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
import { SelectorEngine, SelectorRoot } from './selectorEngine';
1818

1919
export const CSSEngine: SelectorEngine = {
20-
name: 'css',
21-
2220
create(root: SelectorRoot, targetElement: Element): string | undefined {
2321
const tokens: string[] = [];
2422

src/injected/injected.ts

+11-14
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import * as types from '../types';
2323

2424
function createAttributeEngine(attribute: string): SelectorEngine {
2525
const engine: SelectorEngine = {
26-
name: attribute,
27-
2826
create(root: SelectorRoot, target: Element): string | undefined {
2927
const value = target.getAttribute(attribute);
3028
if (!value)
@@ -51,20 +49,19 @@ class Injected {
5149
readonly utils: Utils;
5250
readonly engines: Map<string, SelectorEngine>;
5351

54-
constructor(customEngines: SelectorEngine[]) {
55-
const defaultEngines = [
56-
CSSEngine,
57-
XPathEngine,
58-
TextEngine,
59-
createAttributeEngine('id'),
60-
createAttributeEngine('data-testid'),
61-
createAttributeEngine('data-test-id'),
62-
createAttributeEngine('data-test'),
63-
];
52+
constructor(customEngines: { name: string, engine: SelectorEngine}[]) {
6453
this.utils = new Utils();
6554
this.engines = new Map();
66-
for (const engine of [...defaultEngines, ...customEngines])
67-
this.engines.set(engine.name, engine);
55+
// Note: keep predefined names in sync with Selectors class.
56+
this.engines.set('css', CSSEngine);
57+
this.engines.set('xpath', XPathEngine);
58+
this.engines.set('text', TextEngine);
59+
this.engines.set('id', createAttributeEngine('id'));
60+
this.engines.set('data-testid', createAttributeEngine('data-testid'));
61+
this.engines.set('data-test-id', createAttributeEngine('data-test-id'));
62+
this.engines.set('data-test', createAttributeEngine('data-test'));
63+
for (const {name, engine} of customEngines)
64+
this.engines.set(name, engine);
6865
}
6966

7067
querySelector(selector: string, root: Node): Element | undefined {

src/injected/selectorEngine.ts

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export type SelectorType = 'default' | 'notext';
1818
export type SelectorRoot = Element | ShadowRoot | Document;
1919

2020
export interface SelectorEngine {
21-
name: string;
2221
create(root: SelectorRoot, target: Element, type?: SelectorType): string | undefined;
2322
query(root: SelectorRoot, selector: string): Element | undefined;
2423
queryAll(root: SelectorRoot, selector: string): Element[];

src/injected/textSelectorEngine.ts

-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
import { SelectorEngine, SelectorType, SelectorRoot } from './selectorEngine';
1818

1919
export const TextEngine: SelectorEngine = {
20-
name: 'text',
21-
2220
create(root: SelectorRoot, targetElement: Element, type: SelectorType): string | undefined {
2321
const document = root instanceof Document ? root : root.ownerDocument;
2422
if (!document)

src/injected/xpathSelectorEngine.ts

-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ const maxTextLength = 80;
2020
const minMeaningfulSelectorLegth = 100;
2121

2222
export const XPathEngine: SelectorEngine = {
23-
name: 'xpath',
24-
2523
create(root: SelectorRoot, targetElement: Element, type: SelectorType): string | undefined {
2624
const maybeDocument = root instanceof Document ? root : root.ownerDocument;
2725
if (!maybeDocument)

src/injected/zsSelectorEngine.ts

-2
Original file line numberDiff line numberDiff line change
@@ -751,8 +751,6 @@ class Engine {
751751
}
752752

753753
const ZSSelectorEngine: SelectorEngine = {
754-
name: 'zs',
755-
756754
create(root: SelectorRoot, element: Element, type?: SelectorType): string {
757755
return new Engine().create(root, element, type || 'default');
758756
},

src/page.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ export class Page extends platform.EventEmitter {
412412
}
413413

414414
async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) {
415-
await this._delegate.evaluateOnNewDocument(await helper.evaluationScript(script, ...args));
415+
await this._delegate.evaluateOnNewDocument(await helper.evaluationScript(script, args));
416416
}
417417

418418
async setCacheEnabled(enabled: boolean = true) {

src/selectors.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { helper } from './helper';
2121
let selectors: Selectors;
2222

2323
export class Selectors {
24-
readonly _sources: string[];
24+
readonly _engines: Map<string, string>;
2525
_generation = 0;
2626

2727
static _instance() {
@@ -31,12 +31,19 @@ export class Selectors {
3131
}
3232

3333
constructor() {
34-
this._sources = [];
34+
this._engines = new Map();
3535
}
3636

37-
async register(engineFunction: string | Function, ...args: any[]) {
38-
const source = helper.evaluationString(engineFunction, ...args);
39-
this._sources.push(source);
37+
async register(name: string, script: string | Function | { path?: string, content?: string }): Promise<void> {
38+
if (!name.match(/^[a-zA-Z_0-9-]+$/))
39+
throw new Error('Selector engine name may only contain [a-zA-Z0-9_] characters');
40+
// Note: keep in sync with Injected class, and also keep 'zs' for future.
41+
if (['css', 'xpath', 'text', 'id', 'zs', 'data-testid', 'data-test-id', 'data-test'].includes(name))
42+
throw new Error(`"${name}" is a predefined selector engine`);
43+
const source = await helper.evaluationScript(script, [], false);
44+
if (this._engines.has(name))
45+
throw new Error(`"${name}" selector engine has been already registered`);
46+
this._engines.set(name, source);
4047
++this._generation;
4148
}
4249

src/webkit/wkBrowser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
279279
}
280280

281281
async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) {
282-
const source = await helper.evaluationScript(script, ...args);
282+
const source = await helper.evaluationScript(script, args);
283283
this._evaluateOnNewDocumentSources.push(source);
284284
for (const page of this._existingPages())
285285
await (page._delegate as WKPage)._updateBootstrapScript();

test/assets/sectionselectorengine.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
({
2+
create(root, target) {
3+
},
4+
query(root, selector) {
5+
return root.querySelector('section');
6+
},
7+
queryAll(root, selector) {
8+
return Array.from(root.querySelectorAll('section'));
9+
}
10+
})

0 commit comments

Comments
 (0)