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('