diff --git a/src/dom.ts b/src/dom.ts index 3074b1b588..b17933f90f 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -504,7 +504,7 @@ function normalizeSelector(selector: string): string { if (selector.startsWith('//')) return 'xpath=' + selector; if (selector.startsWith('"')) - return 'zs=' + selector; + return 'text=' + selector; return 'css=' + selector; } diff --git a/src/injected/injected.ts b/src/injected/injected.ts index e773a1a5e7..1afca5191d 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -18,6 +18,7 @@ import { SelectorEngine, SelectorRoot } from './selectorEngine'; import { Utils } from './utils'; import { CSSEngine } from './cssSelectorEngine'; import { XPathEngine } from './xpathSelectorEngine'; +import { TextEngine } from './textSelectorEngine'; function createAttributeEngine(attribute: string): SelectorEngine { const engine: SelectorEngine = { @@ -53,6 +54,7 @@ class Injected { const defaultEngines = [ CSSEngine, XPathEngine, + TextEngine, createAttributeEngine('id'), createAttributeEngine('data-testid'), createAttributeEngine('data-test-id'), diff --git a/src/injected/textSelectorEngine.ts b/src/injected/textSelectorEngine.ts new file mode 100644 index 0000000000..7ca13b05f2 --- /dev/null +++ b/src/injected/textSelectorEngine.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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) + return; + for (let child = targetElement.firstChild; child; child = child.nextSibling) { + if (child.nodeType === 3 /* Node.TEXT_NODE */) { + const text = child.nodeValue; + if (!text) + continue; + if (text.match(/^\s*[a-zA-Z0-9]+\s*$/) && TextEngine.query(root, text.trim()) === targetElement) + return text.trim(); + if (TextEngine.query(root, JSON.stringify(text)) === targetElement) + return JSON.stringify(text); + } + } + }, + + query(root: SelectorRoot, selector: string): Element | undefined { + const document = root instanceof Document ? root : root.ownerDocument; + if (!document) + return; + const matcher = createMatcher(selector); + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + while (walker.nextNode()) { + const node = walker.currentNode; + const element = node.parentElement; + const text = node.nodeValue; + if (element && text && matcher(text)) + return element; + } + }, + + queryAll(root: SelectorRoot, selector: string): Element[] { + const result: Element[] = []; + const document = root instanceof Document ? root : root.ownerDocument; + if (!document) + return result; + const matcher = createMatcher(selector); + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + while (walker.nextNode()) { + const node = walker.currentNode; + const element = node.parentElement; + const text = node.nodeValue; + if (element && text && matcher(text)) + result.push(element); + } + return result; + } +}; + +type Matcher = (text: string) => boolean; +function createMatcher(selector: string): Matcher { + if (selector[0] === '"' && selector[selector.length - 1] === '"') { + const parsed = JSON.parse(selector); + return text => text === parsed; + } + if (selector[0] === '/' && selector.lastIndexOf('/') > 0) { + const lastSlash = selector.lastIndexOf('/'); + const re = new RegExp(selector.substring(1, lastSlash), selector.substring(lastSlash + 1)); + return text => re.test(text); + } + selector = selector.trim(); + return text => text.trim() === selector; +} diff --git a/test/queryselector.spec.js b/test/queryselector.spec.js index 85b37d1cfb..38866cfc2c 100644 --- a/test/queryselector.spec.js +++ b/test/queryselector.spec.js @@ -56,6 +56,11 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM, const idAttribute = await page.$eval('xpath=/html/body/section', e => e.id); expect(idAttribute).toBe('testAttribute'); }); + it('should work with text selector', async({page, server}) => { + await page.setContent('
43543
'); + const idAttribute = await page.$eval('text=43543', e => e.id); + expect(idAttribute).toBe('testAttribute'); + }); it('should auto-detect css selector', async({page, server}) => { await page.setContent('
43543
'); const idAttribute = await page.$eval('section', e => e.id); @@ -172,7 +177,7 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM, const element = await page.$('//html/body/section'); expect(element).toBeTruthy(); }); - it('should auto-detect zs selector', async({page, server}) => { + it('should auto-detect text selector', async({page, server}) => { await page.setContent('
test
'); const element = await page.$('"test"'); expect(element).toBeTruthy(); @@ -467,4 +472,45 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM, expect(await page.$$eval(`zs="ya" ~ "hey" ~ "hello"`, es => es.map(e => e.outerHTML).join('\n'))).toBe('
hello
\n
hello
'); }); }); + + describe('text selector', () => { + it('query', async ({page}) => { + await page.setContent(`
yo
ya
\nye
`); + expect(await page.$eval(`text=ya`, e => e.outerHTML)).toBe('
ya
'); + expect(await page.$eval(`text="ya"`, e => e.outerHTML)).toBe('
ya
'); + expect(await page.$eval(`text=/^[ay]+$/`, e => e.outerHTML)).toBe('
ya
'); + expect(await page.$eval(`text=/Ya/i`, e => e.outerHTML)).toBe('
ya
'); + expect(await page.$eval(`text=ye`, e => e.outerHTML)).toBe('
\nye
'); + + await page.setContent(`
ye
ye
`); + expect(await page.$eval(`text="ye"`, e => e.outerHTML)).toBe('
ye
'); + + await page.setContent(`
yo
"ya
hello world!
`); + expect(await page.$eval(`text="\\"ya"`, e => e.outerHTML)).toBe('
"ya
'); + expect(await page.$eval(`text=/hello/`, e => e.outerHTML)).toBe('
hello world!
'); + expect(await page.$eval(`text=/^\\s*heLLo/i`, e => e.outerHTML)).toBe('
hello world!
'); + + await page.setContent(`
yo
ya
hey
hey
`); + expect(await page.$eval(`text=hey`, e => e.outerHTML)).toBe('
yo
ya
hey
hey
'); + + await page.setContent(`
yo
yo
`); + expect(await page.$$eval(`text=yo`, es => es.map(e => e.outerHTML).join('\n'))).toBe('
yo
\n
yo
'); + }); + + it('create', async ({page}) => { + await page.setContent(`
yo
"ya
ye ye
`); + expect(await page._createSelector('text', await page.$('div'))).toBe('yo'); + expect(await page._createSelector('text', await page.$('div:nth-child(2)'))).toBe('"\\"ya"'); + expect(await page._createSelector('text', await page.$('div:nth-child(3)'))).toBe('"ye ye"'); + + await page.setContent(`
yo
yo
ya
hey
`); + expect(await page._createSelector('text', await page.$('div:nth-child(2)'))).toBe('hey'); + + await page.setContent(`
yo
ya
`); + expect(await page._createSelector('text', await page.$('div'))).toBe('yo'); + + await page.setContent(`
"yo
ya
`); + expect(await page._createSelector('text', await page.$('div'))).toBe('" \\"yo "'); + }); + }); };