feat(selectors): text=foo selector engine (#475)

This commit is contained in:
Dmitry Gozman 2020-01-13 17:39:43 -08:00 committed by Pavel Feldman
parent b388722777
commit 74b208cae5
4 changed files with 135 additions and 2 deletions

View File

@ -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;
}

View File

@ -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'),

View File

@ -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;
}

View File

@ -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('<section id="testAttribute">43543</section>');
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('<section id="testAttribute">43543</section>');
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('<section>test</section>');
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('<div id="target">hello</div>\n<div id="target2">hello</div>');
});
});
describe('text selector', () => {
it('query', async ({page}) => {
await page.setContent(`<div>yo</div><div>ya</div><div>\nye </div>`);
expect(await page.$eval(`text=ya`, e => e.outerHTML)).toBe('<div>ya</div>');
expect(await page.$eval(`text="ya"`, e => e.outerHTML)).toBe('<div>ya</div>');
expect(await page.$eval(`text=/^[ay]+$/`, e => e.outerHTML)).toBe('<div>ya</div>');
expect(await page.$eval(`text=/Ya/i`, e => e.outerHTML)).toBe('<div>ya</div>');
expect(await page.$eval(`text=ye`, e => e.outerHTML)).toBe('<div>\nye </div>');
await page.setContent(`<div> ye </div><div>ye</div>`);
expect(await page.$eval(`text="ye"`, e => e.outerHTML)).toBe('<div>ye</div>');
await page.setContent(`<div>yo</div><div>"ya</div><div> hello world! </div>`);
expect(await page.$eval(`text="\\"ya"`, e => e.outerHTML)).toBe('<div>"ya</div>');
expect(await page.$eval(`text=/hello/`, e => e.outerHTML)).toBe('<div> hello world! </div>');
expect(await page.$eval(`text=/^\\s*heLLo/i`, e => e.outerHTML)).toBe('<div> hello world! </div>');
await page.setContent(`<div>yo<div>ya</div>hey<div>hey</div></div>`);
expect(await page.$eval(`text=hey`, e => e.outerHTML)).toBe('<div>yo<div>ya</div>hey<div>hey</div></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(`text=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>');
});
it('create', async ({page}) => {
await page.setContent(`<div>yo</div><div>"ya</div><div>ye ye</div>`);
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(`<div>yo</div><div>yo<div>ya</div>hey</div>`);
expect(await page._createSelector('text', await page.$('div:nth-child(2)'))).toBe('hey');
await page.setContent(`<div> yo <div></div>ya</div>`);
expect(await page._createSelector('text', await page.$('div'))).toBe('yo');
await page.setContent(`<div> "yo <div></div>ya</div>`);
expect(await page._createSelector('text', await page.$('div'))).toBe('" \\"yo "');
});
});
};