api(selectors): pass selector name when registering, allow file path (#1162)

This commit is contained in:
Dmitry Gozman 2020-02-28 15:34:07 -08:00 committed by GitHub
parent d511d7dd99
commit ac2f04f10f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 136 additions and 111 deletions

View file

@ -3149,12 +3149,14 @@ Contains the URL of the response.
Selectors can be used to install custom selector engines. See [Working with selectors](#working-with-selectors) for more information. Selectors can be used to install custom selector engines. See [Working with selectors](#working-with-selectors) for more information.
<!-- GEN:toc --> <!-- GEN:toc -->
- [selectors.register(engineFunction[, ...args])](#selectorsregisterenginefunction-args) - [selectors.register(name, script)](#selectorsregistername-script)
<!-- GEN:stop --> <!-- GEN:stop -->
#### selectors.register(engineFunction[, ...args]) #### selectors.register(name, script)
- `engineFunction` <[function]|[string]> Function that evaluates to a selector engine instance. - `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.
- `...args` <...[Serializable]> Arguments to pass to `engineFunction`. - `script` <[function]|[string]|[Object]> Script that evaluates to a selector engine instance.
- `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).
- `content` <[string]> Raw script content.
- returns: <[Promise]> - returns: <[Promise]>
An example of registering selector engine that queries elements based on a tag name: 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
(async () => { (async () => {
// Must be a function that evaluates to a selector engine instance. // Must be a function that evaluates to a selector engine instance.
const createTagNameEngine = () => ({ const createTagNameEngine = () => ({
// Selectors will be prefixed with "tag=".
name: 'tag',
// Creates a selector that matches given target when queried at the root. // Creates a selector that matches given target when queried at the root.
// Can return undefined if unable to create one. // Can return undefined if unable to create one.
create(root, target) { create(root, target) {
@ -3184,8 +3183,8 @@ const { selectors, firefox } = require('playwright'); // Or 'chromium' or 'webk
} }
}); });
// Register the engine. // Register the engine. Selectors will be prefixed with "tag=".
await selectors.register(createTagNameEngine); await selectors.register('tag', createTagNameEngine);
const browser = await firefox.launch(); const browser = await firefox.launch();
const page = await browser.newPage(); const page = await browser.newPage();

View file

@ -84,11 +84,10 @@ Id engines are selecting based on the corresponding atrribute value. For example
## Custom selector engines ## Custom selector engines
Playwright supports custom selector engines, registered with [selectors.register(engineFunction[, ...args])](api.md#selectorsregisterenginefunction-args). Playwright supports custom selector engines, registered with [selectors.register(name, script)](api.md#selectorsregistername-script).
Selector engine should have the following properties: Selector engine should have the following properties:
- `name` Selector name used in selector strings.
- `create` Function to create a relative selector from `root` (root is either a `Document`, `ShadowRoot` or `Element`) to a `target` element. - `create` Function to create a relative selector from `root` (root is either a `Document`, `ShadowRoot` or `Element`) to a `target` element.
- `query` Function to query first element matching `selector` relative to the `root`. - `query` Function to query first element matching `selector` relative to the `root`.
- `queryAll` Function to query all elements matching `selector` relative to the `root`. - `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
```js ```js
// Must be a function that evaluates to a selector engine instance. // Must be a function that evaluates to a selector engine instance.
const createTagNameEngine = () => ({ const createTagNameEngine = () => ({
// Selectors will be prefixed with "tag=".
name: 'tag',
// Creates a selector that matches given target when queried at the root. // Creates a selector that matches given target when queried at the root.
// Can return undefined if unable to create one. // Can return undefined if unable to create one.
create(root, target) { create(root, target) {
@ -117,8 +113,8 @@ const createTagNameEngine = () => ({
} }
}); });
// Register the engine. // Register the engine. Selectors will be prefixed with "tag=".
await selectors.register(createTagNameEngine); await selectors.register('tag', createTagNameEngine);
// Now we can use 'tag=' selectors. // Now we can use 'tag=' selectors.
const button = await page.$('tag=button'); const button = await page.$('tag=button');

View file

@ -303,7 +303,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) {
const source = await helper.evaluationScript(script, ...args); const source = await helper.evaluationScript(script, args);
this._evaluateOnNewDocumentSources.push(source); this._evaluateOnNewDocumentSources.push(source);
for (const page of this._existingPages()) for (const page of this._existingPages())
await (page._delegate as CRPage).evaluateOnNewDocument(source); await (page._delegate as CRPage).evaluateOnNewDocument(source);

View file

@ -85,9 +85,12 @@ export class FrameExecutionContext extends js.ExecutionContext {
this._injectedPromise = undefined; this._injectedPromise = undefined;
} }
if (!this._injectedPromise) { if (!this._injectedPromise) {
const custom: string[] = [];
for (const [name, source] of selectors._engines)
custom.push(`{ name: '${name}', engine: (${source}) }`);
const source = ` const source = `
new (${injectedSource.source})([ new (${injectedSource.source})([
${selectors._sources.join(',\n')} ${custom.join(',\n')}
]) ])
`; `;
this._injectedPromise = this.evaluateHandle(source); this._injectedPromise = this.evaluateHandle(source);

View file

@ -360,7 +360,7 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) {
const source = await helper.evaluationScript(script, ...args); const source = await helper.evaluationScript(script, args);
this._evaluateOnNewDocumentSources.push(source); this._evaluateOnNewDocumentSources.push(source);
await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source }); await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source });
} }

View file

@ -41,13 +41,14 @@ class Helper {
} }
} }
static async evaluationScript(fun: Function | string | { path?: string, content?: string }, ...args: any[]): Promise<string> { static async evaluationScript(fun: Function | string | { path?: string, content?: string }, args: any[] = [], addSourceUrl: boolean = true): Promise<string> {
if (!helper.isString(fun) && typeof fun !== 'function') { if (!helper.isString(fun) && typeof fun !== 'function') {
if (fun.content !== undefined) { if (fun.content !== undefined) {
fun = fun.content; fun = fun.content;
} else if (fun.path !== undefined) { } else if (fun.path !== undefined) {
let contents = await platform.readFileAsync(fun.path, 'utf8'); let contents = await platform.readFileAsync(fun.path, 'utf8');
contents += '//# sourceURL=' + fun.path.replace(/\n/g, ''); if (addSourceUrl)
contents += '//# sourceURL=' + fun.path.replace(/\n/g, '');
fun = contents; fun = contents;
} else { } else {
throw new Error('Either path or content property must be present'); throw new Error('Either path or content property must be present');

View file

@ -17,8 +17,6 @@
import { SelectorEngine, SelectorRoot } from './selectorEngine'; import { SelectorEngine, SelectorRoot } from './selectorEngine';
export const CSSEngine: SelectorEngine = { export const CSSEngine: SelectorEngine = {
name: 'css',
create(root: SelectorRoot, targetElement: Element): string | undefined { create(root: SelectorRoot, targetElement: Element): string | undefined {
const tokens: string[] = []; const tokens: string[] = [];

View file

@ -23,8 +23,6 @@ import * as types from '../types';
function createAttributeEngine(attribute: string): SelectorEngine { function createAttributeEngine(attribute: string): SelectorEngine {
const engine: SelectorEngine = { const engine: SelectorEngine = {
name: attribute,
create(root: SelectorRoot, target: Element): string | undefined { create(root: SelectorRoot, target: Element): string | undefined {
const value = target.getAttribute(attribute); const value = target.getAttribute(attribute);
if (!value) if (!value)
@ -51,20 +49,19 @@ class Injected {
readonly utils: Utils; readonly utils: Utils;
readonly engines: Map<string, SelectorEngine>; readonly engines: Map<string, SelectorEngine>;
constructor(customEngines: SelectorEngine[]) { constructor(customEngines: { name: string, engine: SelectorEngine}[]) {
const defaultEngines = [
CSSEngine,
XPathEngine,
TextEngine,
createAttributeEngine('id'),
createAttributeEngine('data-testid'),
createAttributeEngine('data-test-id'),
createAttributeEngine('data-test'),
];
this.utils = new Utils(); this.utils = new Utils();
this.engines = new Map(); this.engines = new Map();
for (const engine of [...defaultEngines, ...customEngines]) // Note: keep predefined names in sync with Selectors class.
this.engines.set(engine.name, engine); this.engines.set('css', CSSEngine);
this.engines.set('xpath', XPathEngine);
this.engines.set('text', TextEngine);
this.engines.set('id', createAttributeEngine('id'));
this.engines.set('data-testid', createAttributeEngine('data-testid'));
this.engines.set('data-test-id', createAttributeEngine('data-test-id'));
this.engines.set('data-test', createAttributeEngine('data-test'));
for (const {name, engine} of customEngines)
this.engines.set(name, engine);
} }
querySelector(selector: string, root: Node): Element | undefined { querySelector(selector: string, root: Node): Element | undefined {

View file

@ -18,7 +18,6 @@ export type SelectorType = 'default' | 'notext';
export type SelectorRoot = Element | ShadowRoot | Document; export type SelectorRoot = Element | ShadowRoot | Document;
export interface SelectorEngine { export interface SelectorEngine {
name: string;
create(root: SelectorRoot, target: Element, type?: SelectorType): string | undefined; create(root: SelectorRoot, target: Element, type?: SelectorType): string | undefined;
query(root: SelectorRoot, selector: string): Element | undefined; query(root: SelectorRoot, selector: string): Element | undefined;
queryAll(root: SelectorRoot, selector: string): Element[]; queryAll(root: SelectorRoot, selector: string): Element[];

View file

@ -17,8 +17,6 @@
import { SelectorEngine, SelectorType, SelectorRoot } from './selectorEngine'; import { SelectorEngine, SelectorType, SelectorRoot } from './selectorEngine';
export const TextEngine: SelectorEngine = { export const TextEngine: SelectorEngine = {
name: 'text',
create(root: SelectorRoot, targetElement: Element, type: SelectorType): string | undefined { create(root: SelectorRoot, targetElement: Element, type: SelectorType): string | undefined {
const document = root instanceof Document ? root : root.ownerDocument; const document = root instanceof Document ? root : root.ownerDocument;
if (!document) if (!document)

View file

@ -20,8 +20,6 @@ const maxTextLength = 80;
const minMeaningfulSelectorLegth = 100; const minMeaningfulSelectorLegth = 100;
export const XPathEngine: SelectorEngine = { export const XPathEngine: SelectorEngine = {
name: 'xpath',
create(root: SelectorRoot, targetElement: Element, type: SelectorType): string | undefined { create(root: SelectorRoot, targetElement: Element, type: SelectorType): string | undefined {
const maybeDocument = root instanceof Document ? root : root.ownerDocument; const maybeDocument = root instanceof Document ? root : root.ownerDocument;
if (!maybeDocument) if (!maybeDocument)

View file

@ -751,8 +751,6 @@ class Engine {
} }
const ZSSelectorEngine: SelectorEngine = { const ZSSelectorEngine: SelectorEngine = {
name: 'zs',
create(root: SelectorRoot, element: Element, type?: SelectorType): string { create(root: SelectorRoot, element: Element, type?: SelectorType): string {
return new Engine().create(root, element, type || 'default'); return new Engine().create(root, element, type || 'default');
}, },

View file

@ -412,7 +412,7 @@ export class Page extends platform.EventEmitter {
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) {
await this._delegate.evaluateOnNewDocument(await helper.evaluationScript(script, ...args)); await this._delegate.evaluateOnNewDocument(await helper.evaluationScript(script, args));
} }
async setCacheEnabled(enabled: boolean = true) { async setCacheEnabled(enabled: boolean = true) {

View file

@ -21,7 +21,7 @@ import { helper } from './helper';
let selectors: Selectors; let selectors: Selectors;
export class Selectors { export class Selectors {
readonly _sources: string[]; readonly _engines: Map<string, string>;
_generation = 0; _generation = 0;
static _instance() { static _instance() {
@ -31,12 +31,19 @@ export class Selectors {
} }
constructor() { constructor() {
this._sources = []; this._engines = new Map();
} }
async register(engineFunction: string | Function, ...args: any[]) { async register(name: string, script: string | Function | { path?: string, content?: string }): Promise<void> {
const source = helper.evaluationString(engineFunction, ...args); if (!name.match(/^[a-zA-Z_0-9-]+$/))
this._sources.push(source); throw new Error('Selector engine name may only contain [a-zA-Z0-9_] characters');
// Note: keep in sync with Injected class, and also keep 'zs' for future.
if (['css', 'xpath', 'text', 'id', 'zs', 'data-testid', 'data-test-id', 'data-test'].includes(name))
throw new Error(`"${name}" is a predefined selector engine`);
const source = await helper.evaluationScript(script, [], false);
if (this._engines.has(name))
throw new Error(`"${name}" selector engine has been already registered`);
this._engines.set(name, source);
++this._generation; ++this._generation;
} }

View file

@ -279,7 +279,7 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) {
const source = await helper.evaluationScript(script, ...args); const source = await helper.evaluationScript(script, args);
this._evaluateOnNewDocumentSources.push(source); this._evaluateOnNewDocumentSources.push(source);
for (const page of this._existingPages()) for (const page of this._existingPages())
await (page._delegate as WKPage)._updateBootstrapScript(); await (page._delegate as WKPage)._updateBootstrapScript();

View file

@ -0,0 +1,10 @@
({
create(root, target) {
},
query(root, selector) {
return root.querySelector('section');
},
queryAll(root, selector) {
return Array.from(root.querySelectorAll('section'));
}
})

View file

@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
const path = require('path');
const zsSelectorEngineSource = require('../lib/generated/zsSelectorEngineSource'); const zsSelectorEngineSource = require('../lib/generated/zsSelectorEngineSource');
/** /**
@ -359,69 +360,74 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU
describe('zselector', () => { describe('zselector', () => {
beforeAll(async () => { beforeAll(async () => {
await selectors.register(zsSelectorEngineSource.source); try {
await selectors.register('z', zsSelectorEngineSource.source);
} catch (e) {
if (!e.message.includes('has been already registered'))
throw e;
}
}); });
it('query', async ({page}) => { it('query', async ({page}) => {
await page.setContent(`<div>yo</div><div>ya</div><div>ye</div>`); await page.setContent(`<div>yo</div><div>ya</div><div>ye</div>`);
expect(await page.$eval(`zs="ya"`, e => e.outerHTML)).toBe('<div>ya</div>'); expect(await page.$eval(`z="ya"`, e => e.outerHTML)).toBe('<div>ya</div>');
await page.setContent(`<div foo="baz"></div><div foo="bar space"></div>`); await page.setContent(`<div foo="baz"></div><div foo="bar space"></div>`);
expect(await page.$eval(`zs=[foo="bar space"]`, e => e.outerHTML)).toBe('<div foo="bar space"></div>'); expect(await page.$eval(`z=[foo="bar space"]`, e => e.outerHTML)).toBe('<div foo="bar space"></div>');
await page.setContent(`<div>yo<span></span></div>`); await page.setContent(`<div>yo<span></span></div>`);
expect(await page.$eval(`zs=span`, e => e.outerHTML)).toBe('<span></span>'); expect(await page.$eval(`z=span`, e => e.outerHTML)).toBe('<span></span>');
expect(await page.$eval(`zs=div > span`, e => e.outerHTML)).toBe('<span></span>'); expect(await page.$eval(`z=div > span`, e => e.outerHTML)).toBe('<span></span>');
expect(await page.$eval(`zs=div span`, e => e.outerHTML)).toBe('<span></span>'); expect(await page.$eval(`z=div span`, e => e.outerHTML)).toBe('<span></span>');
expect(await page.$eval(`zs="yo" > span`, e => e.outerHTML)).toBe('<span></span>'); expect(await page.$eval(`z="yo" > span`, e => e.outerHTML)).toBe('<span></span>');
expect(await page.$eval(`zs="yo" span`, e => e.outerHTML)).toBe('<span></span>'); expect(await page.$eval(`z="yo" span`, e => e.outerHTML)).toBe('<span></span>');
expect(await page.$eval(`zs=span ^`, e => e.outerHTML)).toBe('<div>yo<span></span></div>'); expect(await page.$eval(`z=span ^`, e => e.outerHTML)).toBe('<div>yo<span></span></div>');
expect(await page.$eval(`zs=span ~ div`, e => e.outerHTML)).toBe('<div>yo<span></span></div>'); expect(await page.$eval(`z=span ~ div`, e => e.outerHTML)).toBe('<div>yo<span></span></div>');
expect(await page.$eval(`zs=span ~ "yo"`, e => e.outerHTML)).toBe('<div>yo<span></span></div>'); expect(await page.$eval(`z=span ~ "yo"`, e => e.outerHTML)).toBe('<div>yo<span></span></div>');
await page.setContent(`<div>yo</div><div>yo<span></span></div>`); await page.setContent(`<div>yo</div><div>yo<span></span></div>`);
expect(await page.$eval(`zs="yo"#0`, e => e.outerHTML)).toBe('<div>yo</div>'); expect(await page.$eval(`z="yo"#0`, e => e.outerHTML)).toBe('<div>yo</div>');
expect(await page.$eval(`zs="yo"#1`, e => e.outerHTML)).toBe('<div>yo<span></span></div>'); expect(await page.$eval(`z="yo"#1`, e => e.outerHTML)).toBe('<div>yo<span></span></div>');
expect(await page.$eval(`zs="yo" ~ DIV#1`, e => e.outerHTML)).toBe('<div>yo<span></span></div>'); expect(await page.$eval(`z="yo" ~ DIV#1`, e => e.outerHTML)).toBe('<div>yo<span></span></div>');
expect(await page.$eval(`zs=span ~ div#1`, e => e.outerHTML)).toBe('<div>yo<span></span></div>'); expect(await page.$eval(`z=span ~ div#1`, e => e.outerHTML)).toBe('<div>yo<span></span></div>');
expect(await page.$eval(`zs=span ~ div#0`, e => e.outerHTML)).toBe('<div>yo<span></span></div>'); expect(await page.$eval(`z=span ~ div#0`, e => e.outerHTML)).toBe('<div>yo<span></span></div>');
expect(await page.$eval(`zs=span ~ "yo"#1 ^ > div`, e => e.outerHTML)).toBe('<div>yo</div>'); expect(await page.$eval(`z=span ~ "yo"#1 ^ > div`, e => e.outerHTML)).toBe('<div>yo</div>');
expect(await page.$eval(`zs=span ~ "yo"#1 ^ > div#1`, e => e.outerHTML)).toBe('<div>yo<span></span></div>'); expect(await page.$eval(`z=span ~ "yo"#1 ^ > div#1`, e => e.outerHTML)).toBe('<div>yo<span></span></div>');
await page.setContent(`<div>yo<span id="s1"></span></div><div>yo<span id="s2"></span><span id="s3"></span></div>`); await page.setContent(`<div>yo<span id="s1"></span></div><div>yo<span id="s2"></span><span id="s3"></span></div>`);
expect(await page.$eval(`zs="yo"`, e => e.outerHTML)).toBe('<div>yo<span id="s1"></span></div>'); expect(await page.$eval(`z="yo"`, e => e.outerHTML)).toBe('<div>yo<span id="s1"></span></div>');
expect(await page.$$eval(`zs="yo"`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<div>yo<span id="s1"></span></div>\n<div>yo<span id="s2"></span><span id="s3"></span></div>'); expect(await page.$$eval(`z="yo"`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<div>yo<span id="s1"></span></div>\n<div>yo<span id="s2"></span><span id="s3"></span></div>');
expect(await page.$$eval(`zs="yo"#1`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<div>yo<span id="s2"></span><span id="s3"></span></div>'); expect(await page.$$eval(`z="yo"#1`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<div>yo<span id="s2"></span><span id="s3"></span></div>');
expect(await page.$$eval(`zs="yo" ~ span`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<span id="s1"></span>\n<span id="s2"></span>\n<span id="s3"></span>'); expect(await page.$$eval(`z="yo" ~ span`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<span id="s1"></span>\n<span id="s2"></span>\n<span id="s3"></span>');
expect(await page.$$eval(`zs="yo"#1 ~ span`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<span id="s2"></span>\n<span id="s3"></span>'); expect(await page.$$eval(`z="yo"#1 ~ span`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<span id="s2"></span>\n<span id="s3"></span>');
expect(await page.$$eval(`zs="yo" ~ span#0`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<span id="s1"></span>\n<span id="s2"></span>'); expect(await page.$$eval(`z="yo" ~ span#0`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<span id="s1"></span>\n<span id="s2"></span>');
expect(await page.$$eval(`zs="yo" ~ span#1`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<span id="s2"></span>\n<span id="s3"></span>'); expect(await page.$$eval(`z="yo" ~ span#1`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<span id="s2"></span>\n<span id="s3"></span>');
}); });
it('create', async ({page}) => { it('create', async ({page}) => {
await page.setContent(`<div>yo</div><div>ya</div><div>ya</div>`); await page.setContent(`<div>yo</div><div>ya</div><div>ya</div>`);
expect(await selectors._createSelector('zs', await page.$('div'))).toBe('"yo"'); expect(await selectors._createSelector('z', await page.$('div'))).toBe('"yo"');
expect(await selectors._createSelector('zs', await page.$('div:nth-child(2)'))).toBe('"ya"'); expect(await selectors._createSelector('z', await page.$('div:nth-child(2)'))).toBe('"ya"');
expect(await selectors._createSelector('zs', await page.$('div:nth-child(3)'))).toBe('"ya"#1'); expect(await selectors._createSelector('z', await page.$('div:nth-child(3)'))).toBe('"ya"#1');
await page.setContent(`<img alt="foo bar">`); await page.setContent(`<img alt="foo bar">`);
expect(await selectors._createSelector('zs', await page.$('img'))).toBe('img[alt="foo bar"]'); expect(await selectors._createSelector('z', await page.$('img'))).toBe('img[alt="foo bar"]');
await page.setContent(`<div>yo<span></span></div><span></span>`); await page.setContent(`<div>yo<span></span></div><span></span>`);
expect(await selectors._createSelector('zs', await page.$('span'))).toBe('"yo"~SPAN'); expect(await selectors._createSelector('z', await page.$('span'))).toBe('"yo"~SPAN');
expect(await selectors._createSelector('zs', await page.$('span:nth-child(2)'))).toBe('SPAN#1'); expect(await selectors._createSelector('z', await page.$('span:nth-child(2)'))).toBe('SPAN#1');
}); });
it('children of various display parents', async ({page}) => { it('children of various display parents', async ({page}) => {
await page.setContent(`<body><div style='position: fixed;'><span>yo</span></div></body>`); await page.setContent(`<body><div style='position: fixed;'><span>yo</span></div></body>`);
expect(await selectors._createSelector('zs', await page.$('span'))).toBe('"yo"'); expect(await selectors._createSelector('z', await page.$('span'))).toBe('"yo"');
await page.setContent(`<div style='position: relative;'><span>yo</span></div>`); await page.setContent(`<div style='position: relative;'><span>yo</span></div>`);
expect(await selectors._createSelector('zs', await page.$('span'))).toBe('"yo"'); expect(await selectors._createSelector('z', await page.$('span'))).toBe('"yo"');
// "display: none" makes all children text invisible - fallback to tag name. // "display: none" makes all children text invisible - fallback to tag name.
await page.setContent(`<div style='display: none;'><span>yo</span></div>`); await page.setContent(`<div style='display: none;'><span>yo</span></div>`);
expect(await selectors._createSelector('zs', await page.$('span'))).toBe('SPAN'); expect(await selectors._createSelector('z', await page.$('span'))).toBe('SPAN');
}); });
it('boundary', async ({page}) => { it('boundary', async ({page}) => {
@ -472,18 +478,18 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU
<div id=target2>hello</div> <div id=target2>hello</div>
</div> </div>
</div>`); </div>`);
expect(await selectors._createSelector('zs', await page.$('#target'))).toBe('"ya"~"hey"~"hello"'); expect(await selectors._createSelector('z', await page.$('#target'))).toBe('"ya"~"hey"~"hello"');
expect(await page.$eval(`zs="ya"~"hey"~"hello"`, e => e.outerHTML)).toBe('<div id="target">hello</div>'); expect(await page.$eval(`z="ya"~"hey"~"hello"`, e => e.outerHTML)).toBe('<div id="target">hello</div>');
expect(await page.$eval(`zs="ya"~"hey"~"unique"`, e => e.outerHTML).catch(e => e.message)).toBe('Error: failed to find element matching selector "zs="ya"~"hey"~"unique""'); expect(await page.$eval(`z="ya"~"hey"~"unique"`, e => e.outerHTML).catch(e => e.message)).toBe('Error: failed to find element matching selector "z="ya"~"hey"~"unique""');
expect(await page.$$eval(`zs="ya" ~ "hey" ~ "hello"`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<div id="target">hello</div>\n<div id="target2">hello</div>'); expect(await page.$$eval(`z="ya" ~ "hey" ~ "hello"`, es => es.map(e => e.outerHTML).join('\n'))).toBe('<div id="target">hello</div>\n<div id="target2">hello</div>');
}); });
it('should query existing element with zs selector', async({page, server}) => { it('should query existing element with zs selector', async({page, server}) => {
await page.goto(server.PREFIX + '/playground.html'); await page.goto(server.PREFIX + '/playground.html');
await page.setContent('<html><body><div class="second"><div class="inner">A</div></div></body></html>'); await page.setContent('<html><body><div class="second"><div class="inner">A</div></div></body></html>');
const html = await page.$('zs=html'); const html = await page.$('z=html');
const second = await html.$('zs=.second'); const second = await html.$('z=.second');
const inner = await second.$('zs=.inner'); const inner = await second.$('z=.inner');
const content = await page.evaluate(e => e.textContent, inner); const content = await page.evaluate(e => e.textContent, inner);
expect(content).toBe('A'); expect(content).toBe('A');
}); });
@ -538,7 +544,6 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU
describe('selectors.register', () => { describe('selectors.register', () => {
it('should work', async ({page}) => { it('should work', async ({page}) => {
const createTagSelector = () => ({ const createTagSelector = () => ({
name: 'tag',
create(root, target) { create(root, target) {
return target.nodeName; return target.nodeName;
}, },
@ -549,33 +554,49 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU
return Array.from(root.querySelectorAll(selector)); return Array.from(root.querySelectorAll(selector));
} }
}); });
await selectors.register(`(${createTagSelector.toString()})()`); await selectors.register('tag', `(${createTagSelector.toString()})()`);
await page.setContent('<div><span></span></div><div></div>'); await page.setContent('<div><span></span></div><div></div>');
expect(await selectors._createSelector('tag', await page.$('div'))).toBe('DIV'); expect(await selectors._createSelector('tag', await page.$('div'))).toBe('DIV');
expect(await page.$eval('tag=DIV', e => e.nodeName)).toBe('DIV'); expect(await page.$eval('tag=DIV', e => e.nodeName)).toBe('DIV');
expect(await page.$eval('tag=SPAN', e => e.nodeName)).toBe('SPAN'); expect(await page.$eval('tag=SPAN', e => e.nodeName)).toBe('SPAN');
expect(await page.$$eval('tag=DIV', es => es.length)).toBe(2); expect(await page.$$eval('tag=DIV', es => es.length)).toBe(2);
}); });
it('should work with path', async ({page}) => {
await selectors.register('foo', { path: path.join(__dirname, 'assets/sectionselectorengine.js') });
await page.setContent('<section></section>');
expect(await page.$eval('foo=whatever', e => e.nodeName)).toBe('SECTION');
});
it('should update', async ({page}) => { it('should update', async ({page}) => {
await page.setContent('<div><dummy id=d1></dummy></div><span><dummy id=d2></dummy></span>'); await page.setContent('<div><dummy id=d1></dummy></div><span><dummy id=d2></dummy></span>');
expect(await page.$eval('div', e => e.nodeName)).toBe('DIV'); expect(await page.$eval('div', e => e.nodeName)).toBe('DIV');
const error = await page.$('dummy=foo').catch(e => e);
expect(error.message).toContain('Unknown engine dummy while parsing selector dummy=foo'); let error = await page.$('dummy=ignored').catch(e => e);
const createDummySelector = (name) => ({ expect(error.message).toContain('Unknown engine dummy while parsing selector dummy=ignored');
name,
const createDummySelector = () => ({
create(root, target) { create(root, target) {
return target.nodeName; return target.nodeName;
}, },
query(root, selector) { query(root, selector) {
return root.querySelector(name); return root.querySelector('dummy');
}, },
queryAll(root, selector) { queryAll(root, selector) {
return Array.from(root.querySelectorAll(name)); return Array.from(root.querySelectorAll('dummy'));
} }
}); });
await selectors.register(createDummySelector, 'dummy');
expect(await page.$eval('dummy=foo', e => e.id)).toBe('d1'); error = await selectors.register('$', createDummySelector).catch(e => e);
expect(await page.$eval('css=span >> dummy=foo', e => e.id)).toBe('d2'); expect(error.message).toBe('Selector engine name may only contain [a-zA-Z0-9_] characters');
await selectors.register('dummy', createDummySelector);
expect(await page.$eval('dummy=ignored', e => e.id)).toBe('d1');
expect(await page.$eval('css=span >> dummy=ignored', e => e.id)).toBe('d2');
error = await selectors.register('dummy', createDummySelector).catch(e => e);
expect(error.message).toBe('"dummy" selector engine has been already registered');
error = await selectors.register('css', createDummySelector).catch(e => e);
expect(error.message).toBe('"css" is a predefined selector engine');
}); });
}); });
}; };

View file

@ -250,16 +250,16 @@ class TestPass {
if (error === TimeoutError) { if (error === TimeoutError) {
const location = `${hook.location.fileName}:${hook.location.lineNumber}:${hook.location.columnNumber}`; const location = `${hook.location.fileName}:${hook.location.lineNumber}:${hook.location.columnNumber}`;
const message = `${location} - Timeout Exceeded ${hook.timeout}ms while running "${hookName}" in suite "${suite.fullName}"`; const message = `${location} - Timeout Exceeded ${hook.timeout}ms while running "${hookName}" in suite "${suite.fullName}"`;
this._runner._didFailHook(suite, hook, hookName); this._runner._didFailHook(suite, hook, hookName, workerId);
return await this._terminate(TestResult.Crashed, message, null); return await this._terminate(TestResult.Crashed, message, null);
} }
if (error) { if (error) {
const location = `${hook.location.fileName}:${hook.location.lineNumber}:${hook.location.columnNumber}`; const location = `${hook.location.fileName}:${hook.location.lineNumber}:${hook.location.columnNumber}`;
const message = `${location} - FAILED while running "${hookName}" in suite "${suite.fullName}"`; const message = `${location} - FAILED while running "${hookName}" in suite "${suite.fullName}"`;
this._runner._didFailHook(suite, hook, hookName); this._runner._didFailHook(suite, hook, hookName, workerId);
return await this._terminate(TestResult.Crashed, message, error); return await this._terminate(TestResult.Crashed, message, error);
} }
this._runner._didCompleteHook(suite, hook, hookName); this._runner._didCompleteHook(suite, hook, hookName, workerId);
return false; return false;
} }
@ -495,23 +495,23 @@ class TestRunner extends EventEmitter {
} }
_willStartTestBody(test, workerId) { _willStartTestBody(test, workerId) {
debug('testrunner:test')(`starting "${test.fullName}" (${test.location.fileName + ':' + test.location.lineNumber})`); debug('testrunner:test')(`[${workerId}] starting "${test.fullName}" (${test.location.fileName + ':' + test.location.lineNumber})`);
} }
_didFinishTestBody(test, workerId) { _didFinishTestBody(test, workerId) {
debug('testrunner:test')(`${test.result.toUpperCase()} "${test.fullName}" (${test.location.fileName + ':' + test.location.lineNumber})`); debug('testrunner:test')(`[${workerId}] ${test.result.toUpperCase()} "${test.fullName}" (${test.location.fileName + ':' + test.location.lineNumber})`);
} }
_willStartHook(suite, hook, hookName, workerId) { _willStartHook(suite, hook, hookName, workerId) {
debug('testrunner:hook')(`"${hookName}" started for "${suite.fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`); debug('testrunner:hook')(`[${workerId}] "${hookName}" started for "${suite.fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`);
} }
_didFailHook(suite, hook, hookName, workerId) { _didFailHook(suite, hook, hookName, workerId) {
debug('testrunner:hook')(`"${hookName}" FAILED for "${suite.fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`); debug('testrunner:hook')(`[${workerId}] "${hookName}" FAILED for "${suite.fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`);
} }
_didCompleteHook(suite, hook, hookName, workerId) { _didCompleteHook(suite, hook, hookName, workerId) {
debug('testrunner:hook')(`"${hookName}" OK for "${suite.fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`); debug('testrunner:hook')(`[${workerId}] "${hookName}" OK for "${suite.fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`);
} }
_willTerminate(termination) { _willTerminate(termination) {