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.
<!-- GEN:toc -->
- [selectors.register(engineFunction[, ...args])](#selectorsregisterenginefunction-args)
- [selectors.register(name, script)](#selectorsregistername-script)
<!-- GEN:stop -->
#### selectors.register(engineFunction[, ...args])
- `engineFunction` <[function]|[string]> Function that evaluates to a selector engine instance.
- `...args` <...[Serializable]> Arguments to pass to `engineFunction`.
#### selectors.register(name, script)
- `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.
- `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]>
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 () => {
// Must be a function that evaluates to a selector engine instance.
const createTagNameEngine = () => ({
// Selectors will be prefixed with "tag=".
name: 'tag',
// Creates a selector that matches given target when queried at the root.
// Can return undefined if unable to create one.
create(root, target) {
@ -3184,8 +3183,8 @@ const { selectors, firefox } = require('playwright'); // Or 'chromium' or 'webk
}
});
// Register the engine.
await selectors.register(createTagNameEngine);
// Register the engine. Selectors will be prefixed with "tag=".
await selectors.register('tag', createTagNameEngine);
const browser = await firefox.launch();
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
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:
- `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.
- `query` Function to query first element 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
// Must be a function that evaluates to a selector engine instance.
const createTagNameEngine = () => ({
// Selectors will be prefixed with "tag=".
name: 'tag',
// Creates a selector that matches given target when queried at the root.
// Can return undefined if unable to create one.
create(root, target) {
@ -117,8 +113,8 @@ const createTagNameEngine = () => ({
}
});
// Register the engine.
await selectors.register(createTagNameEngine);
// Register the engine. Selectors will be prefixed with "tag=".
await selectors.register('tag', createTagNameEngine);
// Now we can use 'tag=' selectors.
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[]) {
const source = await helper.evaluationScript(script, ...args);
const source = await helper.evaluationScript(script, args);
this._evaluateOnNewDocumentSources.push(source);
for (const page of this._existingPages())
await (page._delegate as CRPage).evaluateOnNewDocument(source);

View file

@ -85,9 +85,12 @@ export class FrameExecutionContext extends js.ExecutionContext {
this._injectedPromise = undefined;
}
if (!this._injectedPromise) {
const custom: string[] = [];
for (const [name, source] of selectors._engines)
custom.push(`{ name: '${name}', engine: (${source}) }`);
const source = `
new (${injectedSource.source})([
${selectors._sources.join(',\n')}
${custom.join(',\n')}
])
`;
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[]) {
const source = await helper.evaluationScript(script, ...args);
const source = await helper.evaluationScript(script, args);
this._evaluateOnNewDocumentSources.push(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 (fun.content !== undefined) {
fun = fun.content;
} else if (fun.path !== undefined) {
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;
} else {
throw new Error('Either path or content property must be present');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -751,8 +751,6 @@ class Engine {
}
const ZSSelectorEngine: SelectorEngine = {
name: 'zs',
create(root: SelectorRoot, element: Element, type?: SelectorType): string {
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[]) {
await this._delegate.evaluateOnNewDocument(await helper.evaluationScript(script, ...args));
await this._delegate.evaluateOnNewDocument(await helper.evaluationScript(script, args));
}
async setCacheEnabled(enabled: boolean = true) {

View file

@ -21,7 +21,7 @@ import { helper } from './helper';
let selectors: Selectors;
export class Selectors {
readonly _sources: string[];
readonly _engines: Map<string, string>;
_generation = 0;
static _instance() {
@ -31,12 +31,19 @@ export class Selectors {
}
constructor() {
this._sources = [];
this._engines = new Map();
}
async register(engineFunction: string | Function, ...args: any[]) {
const source = helper.evaluationString(engineFunction, ...args);
this._sources.push(source);
async register(name: string, script: string | Function | { path?: string, content?: string }): Promise<void> {
if (!name.match(/^[a-zA-Z_0-9-]+$/))
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;
}

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[]) {
const source = await helper.evaluationScript(script, ...args);
const source = await helper.evaluationScript(script, args);
this._evaluateOnNewDocumentSources.push(source);
for (const page of this._existingPages())
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.
*/
const path = require('path');
const zsSelectorEngineSource = require('../lib/generated/zsSelectorEngineSource');
/**
@ -359,69 +360,74 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU
describe('zselector', () => {
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}) => {
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>`);
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>`);
expect(await page.$eval(`zs=span`, e => e.outerHTML)).toBe('<span></span>');
expect(await page.$eval(`zs=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(`zs="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(`zs=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(`zs=span ~ "yo"`, e => e.outerHTML)).toBe('<div>yo<span></span></div>');
expect(await page.$eval(`z=span`, e => e.outerHTML)).toBe('<span></span>');
expect(await page.$eval(`z=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(`z="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(`z=span ^`, 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(`z=span ~ "yo"`, e => e.outerHTML)).toBe('<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(`zs="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(`zs=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(`zs=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="yo"#0`, e => e.outerHTML)).toBe('<div>yo</div>');
expect(await page.$eval(`z="yo"#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(`z=span ~ div#1`, 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(`z=span ~ "yo"#1 ^ > div`, e => e.outerHTML)).toBe('<div>yo</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>`);
expect(await page.$eval(`zs="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(`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(`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(`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(`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(`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"`, e => e.outerHTML)).toBe('<div>yo<span id="s1"></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(`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(`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(`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(`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(`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}) => {
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('zs', 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'))).toBe('"yo"');
expect(await selectors._createSelector('z', await page.$('div:nth-child(2)'))).toBe('"ya"');
expect(await selectors._createSelector('z', await page.$('div:nth-child(3)'))).toBe('"ya"#1');
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>`);
expect(await selectors._createSelector('zs', 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'))).toBe('"yo"~SPAN');
expect(await selectors._createSelector('z', await page.$('span:nth-child(2)'))).toBe('SPAN#1');
});
it('children of various display parents', async ({page}) => {
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>`);
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.
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}) => {
@ -472,18 +478,18 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU
<div id=target2>hello</div>
</div>
</div>`);
expect(await selectors._createSelector('zs', 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(`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(`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 selectors._createSelector('z', await page.$('#target'))).toBe('"ya"~"hey"~"hello"');
expect(await page.$eval(`z="ya"~"hey"~"hello"`, e => e.outerHTML)).toBe('<div id="target">hello</div>');
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(`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}) => {
await page.goto(server.PREFIX + '/playground.html');
await page.setContent('<html><body><div class="second"><div class="inner">A</div></div></body></html>');
const html = await page.$('zs=html');
const second = await html.$('zs=.second');
const inner = await second.$('zs=.inner');
const html = await page.$('z=html');
const second = await html.$('z=.second');
const inner = await second.$('z=.inner');
const content = await page.evaluate(e => e.textContent, inner);
expect(content).toBe('A');
});
@ -538,7 +544,6 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU
describe('selectors.register', () => {
it('should work', async ({page}) => {
const createTagSelector = () => ({
name: 'tag',
create(root, target) {
return target.nodeName;
},
@ -549,33 +554,49 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU
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>');
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=SPAN', e => e.nodeName)).toBe('SPAN');
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}) => {
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');
const error = await page.$('dummy=foo').catch(e => e);
expect(error.message).toContain('Unknown engine dummy while parsing selector dummy=foo');
const createDummySelector = (name) => ({
name,
let error = await page.$('dummy=ignored').catch(e => e);
expect(error.message).toContain('Unknown engine dummy while parsing selector dummy=ignored');
const createDummySelector = () => ({
create(root, target) {
return target.nodeName;
},
query(root, selector) {
return root.querySelector(name);
return root.querySelector('dummy');
},
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');
expect(await page.$eval('css=span >> dummy=foo', e => e.id)).toBe('d2');
error = await selectors.register('$', createDummySelector).catch(e => e);
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) {
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}"`;
this._runner._didFailHook(suite, hook, hookName);
this._runner._didFailHook(suite, hook, hookName, workerId);
return await this._terminate(TestResult.Crashed, message, null);
}
if (error) {
const location = `${hook.location.fileName}:${hook.location.lineNumber}:${hook.location.columnNumber}`;
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);
}
this._runner._didCompleteHook(suite, hook, hookName);
this._runner._didCompleteHook(suite, hook, hookName, workerId);
return false;
}
@ -495,23 +495,23 @@ class TestRunner extends EventEmitter {
}
_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) {
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) {
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) {
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) {
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) {