Skip to content

Commit

Permalink
Add regex support for text() API (#1535)
Browse files Browse the repository at this point in the history
* #286 Add initial regex support for text search

Signed-off-by: NivedhaSenthil <[email protected]>

* #286 add tests for regex

Signed-off-by: NivedhaSenthil <[email protected]>

* #286 Fix exact match

Signed-off-by: NivedhaSenthil <[email protected]>

* #286 add more tests

Signed-off-by: NivedhaSenthil <[email protected]>

* #286 pull out regex check

Signed-off-by: NivedhaSenthil <[email protected]>

* #286 cleanup text search

Signed-off-by: NivedhaSenthil <[email protected]>

* bump up version to 1.0.24

Signed-off-by: NivedhaSenthil <[email protected]>

* add docs for regex

Signed-off-by: NivedhaSenthil <[email protected]>

* Fix reveiw comments

Signed-off-by: NivedhaSenthil <[email protected]>

* update text typings for regex support

Signed-off-by: NivedhaSenthil <[email protected]>

Co-authored-by: Zabil Cheriya Maliackal <[email protected]>
  • Loading branch information
NivedhaSenthil and zabil committed Oct 5, 2020
1 parent 65c9157 commit 04d429b
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 34 deletions.
86 changes: 59 additions & 27 deletions lib/elementSearch.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
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');
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 === '*'
? {
Expand All @@ -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)
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
9 changes: 7 additions & 2 deletions lib/elementWrapper/helper.js
Original file line number Diff line number Diff line change
@@ -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');

Expand All @@ -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 || {};
Expand Down
10 changes: 8 additions & 2 deletions lib/taiko.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
30 changes: 30 additions & 0 deletions test/unit-tests/textMatch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion types/taiko/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions types/taiko/test/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 04d429b

Please sign in to comment.