From ac2f04f10ff2c64886a248c53369f27c0af8d1c2 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 28 Feb 2020 15:34:07 -0800 Subject: [PATCH] api(selectors): pass selector name when registering, allow file path (#1162) --- docs/api.md | 17 ++-- docs/selectors.md | 10 +-- src/chromium/crBrowser.ts | 2 +- src/dom.ts | 5 +- src/firefox/ffBrowser.ts | 2 +- src/helper.ts | 5 +- src/injected/cssSelectorEngine.ts | 2 - src/injected/injected.ts | 25 +++--- src/injected/selectorEngine.ts | 1 - src/injected/textSelectorEngine.ts | 2 - src/injected/xpathSelectorEngine.ts | 2 - src/injected/zsSelectorEngine.ts | 2 - src/page.ts | 2 +- src/selectors.ts | 17 ++-- src/webkit/wkBrowser.ts | 2 +- test/assets/sectionselectorengine.js | 10 +++ test/queryselector.spec.js | 125 ++++++++++++++++----------- utils/testrunner/TestRunner.js | 16 ++-- 18 files changed, 136 insertions(+), 111 deletions(-) create mode 100644 test/assets/sectionselectorengine.js diff --git a/docs/api.md b/docs/api.md index 18aec729ad..5096ee3142 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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.register(engineFunction[, ...args])](#selectorsregisterenginefunction-args) +- [selectors.register(name, script)](#selectorsregistername-script) -#### 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(); diff --git a/docs/selectors.md b/docs/selectors.md index 3c9fb3304b..c09c8c209e 100644 --- a/docs/selectors.md +++ b/docs/selectors.md @@ -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'); diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 93ed23ab5b..b2abd4480f 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -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); diff --git a/src/dom.ts b/src/dom.ts index acf3598dda..e23fd95f77 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -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); diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 15e8c92b17..e3265e02fc 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -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 }); } diff --git a/src/helper.ts b/src/helper.ts index fb6746ca85..e02b4be845 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -41,13 +41,14 @@ class Helper { } } - static async evaluationScript(fun: Function | string | { path?: string, content?: string }, ...args: any[]): Promise { + static async evaluationScript(fun: Function | string | { path?: string, content?: string }, args: any[] = [], addSourceUrl: boolean = true): Promise { 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'); diff --git a/src/injected/cssSelectorEngine.ts b/src/injected/cssSelectorEngine.ts index d47e9be5a4..0359b7520b 100644 --- a/src/injected/cssSelectorEngine.ts +++ b/src/injected/cssSelectorEngine.ts @@ -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[] = []; diff --git a/src/injected/injected.ts b/src/injected/injected.ts index e01fffea3d..e8c43c6477 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -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; - 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 { diff --git a/src/injected/selectorEngine.ts b/src/injected/selectorEngine.ts index e7dcf4329f..eadb8ea7a3 100644 --- a/src/injected/selectorEngine.ts +++ b/src/injected/selectorEngine.ts @@ -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[]; diff --git a/src/injected/textSelectorEngine.ts b/src/injected/textSelectorEngine.ts index 6bfb1b2cf1..4d896d3d59 100644 --- a/src/injected/textSelectorEngine.ts +++ b/src/injected/textSelectorEngine.ts @@ -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) diff --git a/src/injected/xpathSelectorEngine.ts b/src/injected/xpathSelectorEngine.ts index b41ccd82bf..edf73ea17b 100644 --- a/src/injected/xpathSelectorEngine.ts +++ b/src/injected/xpathSelectorEngine.ts @@ -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) diff --git a/src/injected/zsSelectorEngine.ts b/src/injected/zsSelectorEngine.ts index 12bf74dfcc..1ec746e229 100644 --- a/src/injected/zsSelectorEngine.ts +++ b/src/injected/zsSelectorEngine.ts @@ -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'); }, diff --git a/src/page.ts b/src/page.ts index 57617ebbfb..0abc858e5f 100644 --- a/src/page.ts +++ b/src/page.ts @@ -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) { diff --git a/src/selectors.ts b/src/selectors.ts index ac24016e58..92584ae530 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -21,7 +21,7 @@ import { helper } from './helper'; let selectors: Selectors; export class Selectors { - readonly _sources: string[]; + readonly _engines: Map; _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 { + 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; } diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 47c8c2ec8e..d115e4c924 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -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(); diff --git a/test/assets/sectionselectorengine.js b/test/assets/sectionselectorengine.js new file mode 100644 index 0000000000..e4a836452c --- /dev/null +++ b/test/assets/sectionselectorengine.js @@ -0,0 +1,10 @@ +({ + create(root, target) { + }, + query(root, selector) { + return root.querySelector('section'); + }, + queryAll(root, selector) { + return Array.from(root.querySelectorAll('section')); + } +}) \ No newline at end of file diff --git a/test/queryselector.spec.js b/test/queryselector.spec.js index 31bb506407..02df00c7e0 100644 --- a/test/queryselector.spec.js +++ b/test/queryselector.spec.js @@ -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(`
yo
ya
ye
`); - expect(await page.$eval(`zs="ya"`, e => e.outerHTML)).toBe('
ya
'); + expect(await page.$eval(`z="ya"`, e => e.outerHTML)).toBe('
ya
'); await page.setContent(`
`); - expect(await page.$eval(`zs=[foo="bar space"]`, e => e.outerHTML)).toBe('
'); + expect(await page.$eval(`z=[foo="bar space"]`, e => e.outerHTML)).toBe('
'); await page.setContent(`
yo
`); - expect(await page.$eval(`zs=span`, e => e.outerHTML)).toBe(''); - expect(await page.$eval(`zs=div > span`, e => e.outerHTML)).toBe(''); - expect(await page.$eval(`zs=div span`, e => e.outerHTML)).toBe(''); - expect(await page.$eval(`zs="yo" > span`, e => e.outerHTML)).toBe(''); - expect(await page.$eval(`zs="yo" span`, e => e.outerHTML)).toBe(''); - expect(await page.$eval(`zs=span ^`, e => e.outerHTML)).toBe('
yo
'); - expect(await page.$eval(`zs=span ~ div`, e => e.outerHTML)).toBe('
yo
'); - expect(await page.$eval(`zs=span ~ "yo"`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z=span`, e => e.outerHTML)).toBe(''); + expect(await page.$eval(`z=div > span`, e => e.outerHTML)).toBe(''); + expect(await page.$eval(`z=div span`, e => e.outerHTML)).toBe(''); + expect(await page.$eval(`z="yo" > span`, e => e.outerHTML)).toBe(''); + expect(await page.$eval(`z="yo" span`, e => e.outerHTML)).toBe(''); + expect(await page.$eval(`z=span ^`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z=span ~ div`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z=span ~ "yo"`, e => e.outerHTML)).toBe('
yo
'); await page.setContent(`
yo
yo
`); - expect(await page.$eval(`zs="yo"#0`, e => e.outerHTML)).toBe('
yo
'); - expect(await page.$eval(`zs="yo"#1`, e => e.outerHTML)).toBe('
yo
'); - expect(await page.$eval(`zs="yo" ~ DIV#1`, e => e.outerHTML)).toBe('
yo
'); - expect(await page.$eval(`zs=span ~ div#1`, e => e.outerHTML)).toBe('
yo
'); - expect(await page.$eval(`zs=span ~ div#0`, e => e.outerHTML)).toBe('
yo
'); - expect(await page.$eval(`zs=span ~ "yo"#1 ^ > div`, e => e.outerHTML)).toBe('
yo
'); - expect(await page.$eval(`zs=span ~ "yo"#1 ^ > div#1`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z="yo"#0`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z="yo"#1`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z="yo" ~ DIV#1`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z=span ~ div#1`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z=span ~ div#0`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z=span ~ "yo"#1 ^ > div`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$eval(`z=span ~ "yo"#1 ^ > div#1`, e => e.outerHTML)).toBe('
yo
'); await page.setContent(`
yo
yo
`); - expect(await page.$eval(`zs="yo"`, e => e.outerHTML)).toBe('
yo
'); - expect(await page.$$eval(`zs="yo"`, es => es.map(e => e.outerHTML).join('\n'))).toBe('
yo
\n
yo
'); - expect(await page.$$eval(`zs="yo"#1`, es => es.map(e => e.outerHTML).join('\n'))).toBe('
yo
'); - expect(await page.$$eval(`zs="yo" ~ span`, es => es.map(e => e.outerHTML).join('\n'))).toBe('\n\n'); - expect(await page.$$eval(`zs="yo"#1 ~ span`, es => es.map(e => e.outerHTML).join('\n'))).toBe('\n'); - expect(await page.$$eval(`zs="yo" ~ span#0`, es => es.map(e => e.outerHTML).join('\n'))).toBe('\n'); - expect(await page.$$eval(`zs="yo" ~ span#1`, es => es.map(e => e.outerHTML).join('\n'))).toBe('\n'); + expect(await page.$eval(`z="yo"`, e => e.outerHTML)).toBe('
yo
'); + expect(await page.$$eval(`z="yo"`, es => es.map(e => e.outerHTML).join('\n'))).toBe('
yo
\n
yo
'); + expect(await page.$$eval(`z="yo"#1`, es => es.map(e => e.outerHTML).join('\n'))).toBe('
yo
'); + expect(await page.$$eval(`z="yo" ~ span`, es => es.map(e => e.outerHTML).join('\n'))).toBe('\n\n'); + expect(await page.$$eval(`z="yo"#1 ~ span`, es => es.map(e => e.outerHTML).join('\n'))).toBe('\n'); + expect(await page.$$eval(`z="yo" ~ span#0`, es => es.map(e => e.outerHTML).join('\n'))).toBe('\n'); + expect(await page.$$eval(`z="yo" ~ span#1`, es => es.map(e => e.outerHTML).join('\n'))).toBe('\n'); }); it('create', async ({page}) => { await page.setContent(`
yo
ya
ya
`); - 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(`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(`
yo
`); - 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(`
yo
`); - expect(await selectors._createSelector('zs', await page.$('span'))).toBe('"yo"'); + expect(await selectors._createSelector('z', await page.$('span'))).toBe('"yo"'); await page.setContent(`
yo
`); - 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(`
yo
`); - 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
hello
`); - expect(await selectors._createSelector('zs', await page.$('#target'))).toBe('"ya"~"hey"~"hello"'); - expect(await page.$eval(`zs="ya"~"hey"~"hello"`, e => e.outerHTML)).toBe('
hello
'); - 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('
hello
\n
hello
'); + expect(await selectors._createSelector('z', await page.$('#target'))).toBe('"ya"~"hey"~"hello"'); + expect(await page.$eval(`z="ya"~"hey"~"hello"`, e => e.outerHTML)).toBe('
hello
'); + 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('
hello
\n
hello
'); }); it('should query existing element with zs selector', async({page, server}) => { await page.goto(server.PREFIX + '/playground.html'); await page.setContent('
A
'); - 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('
'); 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('
'); + expect(await page.$eval('foo=whatever', e => e.nodeName)).toBe('SECTION'); + }); it('should update', async ({page}) => { await page.setContent('
'); 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'); }); }); }; diff --git a/utils/testrunner/TestRunner.js b/utils/testrunner/TestRunner.js index 74793fc994..84c345f7c3 100644 --- a/utils/testrunner/TestRunner.js +++ b/utils/testrunner/TestRunner.js @@ -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) {