diff --git a/lib/elementSearch.js b/lib/elementSearch.js index 08af0f54a..1c056bc20 100644 --- a/lib/elementSearch.js +++ b/lib/elementSearch.js @@ -1,4 +1,4 @@ -const { assertType, isString, waitUntil, isSelector, isElement } = require('./helper'); +const { assertType, isString, waitUntil, isSelector, isElement, isRegex } = require('./helper'); const { determineRetryInterval, determineRetryTimeout } = require('./config'); const runtimeHandler = require('./handlers/runtimeHandler'); const { handleRelativeSearch } = require('./proximityElementSearch'); @@ -6,11 +6,21 @@ const Element = require('./elements/element'); const { logQuery } = require('./logger'); function match(text, options = {}, ...args) { - assertType(text); + assertType(text, (obj) => isString(obj) || isRegex(obj), 'String or regex is expected'); const get = async (tagName = '*') => { let elements; + const textSearch = function (selectorElement, args) { - const searchText = args.text.toLowerCase().trim(); + const isRegex = (obj) => Object.prototype.toString.call(obj).includes('RegExp'); + + const searchText = + args.text[0] === '/' && args.text.lastIndexOf('/') > 0 + ? new RegExp( + args.text.substring(1, args.text.lastIndexOf('/')), + args.text.substring(args.text.lastIndexOf('/') + 1), + ) + : args.text.toLowerCase().trim(); + const nodeFilter = args.tagName === '*' ? { @@ -31,52 +41,66 @@ function match(text, options = {}, ...args) { : NodeFilter.FILTER_REJECT; }, }; + const iterator = document.createNodeIterator( selectorElement, NodeFilter.SHOW_ALL, nodeFilter, ); + const exactMatches = [], containsMatches = []; + function checkIfRegexMatch(text, searchText, exactMatch) { + return exactMatch + ? text && text.match(searchText) && text.match(searchText)[0] === text + : text && text.match(searchText); + } + + function normalizeText(text) { + return text + ? text + .toLowerCase() + .replace(/\s/g /* all kinds of spaces*/, ' ' /* ordinary space */) + .trim() + : ''; + } + function checkIfChildHasMatch(childNodes, exactMatch) { if (args.tagName !== '*') { return; } if (childNodes.length) { for (let childNode of childNodes) { - const nodeTextContent = childNode.textContent - ? childNode.textContent - .toLowerCase() - .replace(/\s/g /* all kinds of spaces*/, ' ' /* ordinary space */) - .trim() - : ''; - if (exactMatch && nodeTextContent === searchText) { + const nodeTextContent = normalizeText(childNode.textContent); + if ( + exactMatch && + (checkIfRegexMatch(childNode.textContent, searchText, true) || + nodeTextContent === searchText) + ) { return true; } - if (nodeTextContent.includes(searchText)) { + if ( + checkIfRegexMatch(childNode.textContent, searchText, false) || + (!isRegex(searchText) && nodeTextContent.includes(searchText)) + ) { return true; } } } return false; } + let node; while ((node = iterator.nextNode())) { - const nodeTextContent = node.textContent - ? node.textContent - .toLowerCase() - .replace(/\s/g /* all kinds of spaces*/, ' ' /* ordinary space */) - .trim() - : ''; + const nodeTextContent = normalizeText(node.textContent); + //Match values and types for Input and Button nodes if (node.nodeName === 'INPUT') { - const nodeValue = node.value - .toLowerCase() - .replace(/\s/g /* all kinds of spaces*/, ' ' /* ordinary space */) - .trim(); + const nodeValue = normalizeText(node.value); if ( // Exact match of values and types + checkIfRegexMatch(node.value, searchText, true) || nodeValue === searchText || (['submit', 'reset'].includes(node.type.toLowerCase()) && node.type.toLowerCase() === searchText) @@ -86,16 +110,20 @@ function match(text, options = {}, ...args) { } else if ( // Contains match of values and types !args.exactMatch && - (nodeValue.includes(searchText) || - (['submit', 'reset'].includes(node.type.toLowerCase()) && - node.type.toLowerCase().includes(searchText))) + (checkIfRegexMatch(node.value, searchText, false) || + (!isRegex(searchText) && + (nodeValue.includes(searchText) || + (['submit', 'reset'].includes(node.type.toLowerCase()) && + node.type.toLowerCase().includes(searchText))))) ) { containsMatches.push(node); continue; } } + + // Exact match of textContent for other nodes if ( - // Exact match of textContent for other nodes + checkIfRegexMatch(node.textContent, searchText, true) || nodeTextContent === searchText ) { const childNodesHasMatch = checkIfChildHasMatch([...node.childNodes], true); @@ -106,7 +134,8 @@ function match(text, options = {}, ...args) { } else if ( //Contains match of textContent for other nodes !args.exactMatch && - nodeTextContent.includes(searchText) + (checkIfRegexMatch(node.textContent, searchText, false) || + (!isRegex(searchText) && nodeTextContent.includes(searchText))) ) { const childNodesHasMatch = checkIfChildHasMatch([...node.childNodes], false); if (childNodesHasMatch) { @@ -115,16 +144,19 @@ function match(text, options = {}, ...args) { containsMatches.push(node); } } + return exactMatches.length ? exactMatches : containsMatches; }; elements = await $function( textSearch, - { text, tagName, exactMatch: options.exactMatch }, + { text: text.toString(), tagName, exactMatch: options.exactMatch }, options.selectHiddenElements, ); + return await handleRelativeSearch(elements, args); }; + const description = `Element matching text "${text}"`; return { get: async function (tag, retryInterval, retryTimeout) { diff --git a/lib/elementWrapper/helper.js b/lib/elementWrapper/helper.js index 8da5d7573..83239f53c 100644 --- a/lib/elementWrapper/helper.js +++ b/lib/elementWrapper/helper.js @@ -1,4 +1,4 @@ -const { isString, isElement, isSelector } = require('../helper'); +const { isString, isElement, isSelector, isRegex } = require('../helper'); const { RelativeSearchElement, handleRelativeSearch } = require('../proximityElementSearch'); const { $$ } = require('../elementSearch'); @@ -25,7 +25,12 @@ const prepareParameters = (attrValuePairs, options, ...args) => { if (attrValuePairs instanceof RelativeSearchElement) { args = [attrValuePairs].concat(args); values.selector = { args: args }; - } else if (isString(attrValuePairs) || isSelector(attrValuePairs) || isElement(attrValuePairs)) { + } else if ( + isString(attrValuePairs) || + isSelector(attrValuePairs) || + isElement(attrValuePairs) || + isRegex(attrValuePairs) + ) { values.selector = { label: attrValuePairs, args: args }; } values.options = options || {}; diff --git a/lib/taiko.js b/lib/taiko.js index c6f113823..ed6583475 100644 --- a/lib/taiko.js +++ b/lib/taiko.js @@ -2576,9 +2576,15 @@ module.exports.radioButton = (labelOrAttrValuePairs, _options, ...args) => { * @example * await text('Vehicle', { exactMatch: true }, below('text')).exists() * @example - * text('Vehicle', { selectHiddenElements: true }) + * await text('Vehicle', { selectHiddenElements: true }) + * @example + * await text('/Vehicle/').exists() //regex as string + * @example + * await text(/Vehicle/).exists() + * @example + * await text(new RegExp('Vehicle')).exists() * - * @param {string} text - Text to match. + * @param {string|RegExp} text - Text/regex to match. * @param {Object} _options * @param {boolean} [_options.selectHiddenElements=false] - Option to include hidden elements. * @param {boolean} [_options.exactMatch=false] - Option to look for exact match. diff --git a/package-lock.json b/package-lock.json index 66d770f59..f293a0571 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "taiko", - "version": "1.0.23", + "version": "1.0.24", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 25f61ee45..e7b17fa43 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/package", "name": "taiko", - "version": "1.0.23", + "version": "1.0.24", "description": "Taiko is a Node.js library for automating Chromium based browsers", "main": "bin/taiko.js", "bin": { diff --git a/test/unit-tests/textMatch.test.js b/test/unit-tests/textMatch.test.js index 3c0bd8aa0..daed4b628 100644 --- a/test/unit-tests/textMatch.test.js +++ b/test/unit-tests/textMatch.test.js @@ -141,6 +141,36 @@ describe('match', () => { }); }); + describe('regex', () => { + it('test exact match exists', async () => { + expect(await text(/User/).exists()).to.be.true; + }); + it('test contains match exists', async () => { + expect(await text(/account/).exists()).to.be.true; + }); + it('test value match', async () => { + expect(await text(/Enter password/).exists()).to.be.true; + }); + it('test regex as string', async () => { + expect(await text('/Enter password/').exists()).to.be.true; + }); + it('test with regex object', async () => { + expect(await text(new RegExp('Enter password')).exists()).to.be.true; + }); + it('test regex with flag', async () => { + expect(await text(/Enter password/g).exists()).to.be.true; + }); + it('test exact match for text', async () => { + expect(await text(/value/, { exactMatch: true }).exists()).to.be.false; + }); + it('test partial match get()', async () => { + expect(await text(/User/i).elements()).to.have.lengthOf(4); + }); + it('test partial match get()', async () => { + expect(await text(/Text/).elements()).to.have.lengthOf(3); + }); + }); + describe('text node', () => { it('test exact match exists()', async () => { expect(await text('User name:').exists()).to.be.true; diff --git a/types/taiko/index.d.ts b/types/taiko/index.d.ts index 2d719f7ef..fc5d6dbda 100644 --- a/types/taiko/index.d.ts +++ b/types/taiko/index.d.ts @@ -586,7 +586,7 @@ export function radioButton( ): RadioButtonWrapper; // https://docs.taiko.dev/api/text export function text( - selector: string, + selector: string | RegExp, options?: MatchingOptions | RelativeSearchElement, ...args: RelativeSearchElement[] ): TextWrapper; diff --git a/types/taiko/test/selectors.ts b/types/taiko/test/selectors.ts index 8cce77a4a..24453714e 100644 --- a/types/taiko/test/selectors.ts +++ b/types/taiko/test/selectors.ts @@ -173,3 +173,7 @@ text('Vehicle'); // $ExpectType TextWrapper text('Vehicle', below('text')); // $ExpectType TextWrapper text('Vehicle', { exactMatch: true }, below('text')); // $ExpectType TextWrapper text('Vehicle', { selectHiddenElements: true }); // $ExpectType TextWrapper +text('Vehicle', { selectHiddenElements: true }); // $ExpectType TextWrapper +text('/Vehicle/'); // $ExpectType TextWrapper +text(/Vehicle/); // $ExpectType TextWrapper +text(new RegExp('Vehicle')); // $ExpectType TextWrapper