UNPKG

taiko

Version:

Taiko is a Node.js library for automating Chromium based browsers

386 lines (361 loc) 11.5 kB
const { assertType, isString, waitUntil, isSelector, isElement, isRegex, } = require("./helper"); const { determineRetryInterval, determineRetryTimeout, defaultConfig, } = 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, (obj) => isString(obj) || isRegex(obj), "String or regex is expected", ); const get = async (tagName = "*") => { const textSearch = (selectorElement, args) => { 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() .replace( /\s+/g /* all kinds of spaces*/, " " /* ordinary space */, ) .trim(); const nodeFilter = args.tagName === "*" ? { acceptNode(node) { //Filter nodes that need not be searched for text return [ "head", "script", "style", "html", "body", "#comment", ].includes(node.nodeName.toLowerCase()) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }, } : { acceptNode(node) { //Filter nodes that match tagName return args.tagName.toLowerCase() === node.nodeName.toLowerCase() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, }; const iterator = document.createNodeIterator( selectorElement, NodeFilter.SHOW_ALL, nodeFilter, ); const exactMatches = []; const containsMatches = []; function checkIfRegexMatch(text, searchText, exactMatch) { return exactMatch ? isRegex(searchText) && text && text.match(searchText) && text.match(searchText)[0] === text : isRegex(searchText) && 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 (const childNode of childNodes) { const nodeTextContent = normalizeText(childNode.textContent); if ( exactMatch && (checkIfRegexMatch(childNode.textContent, searchText, true) || nodeTextContent === searchText) ) { return true; } if ( checkIfRegexMatch(childNode.textContent, searchText, false) || (!isRegex(searchText) && nodeTextContent.includes(searchText)) ) { return true; } } } return false; } let node; // biome-ignore lint/suspicious/noAssignInExpressions: No other way to do this while ((node = iterator.nextNode())) { const nodeTextContent = normalizeText(node.textContent); //Match values and types for Input and Button nodes if (node.nodeName === "INPUT") { 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) ) { exactMatches.push(node); continue; // biome-ignore lint/style/noUselessElse: Does not work without this logic } else if ( // Contains match of values and types !args.exactMatch && (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 ( checkIfRegexMatch(node.textContent, searchText, true) || nodeTextContent === searchText ) { const childNodesHasMatch = checkIfChildHasMatch( [...node.childNodes], true, ); if (childNodesHasMatch) { continue; } exactMatches.push(node); } else if ( //Contains match of textContent for other nodes !args.exactMatch && (checkIfRegexMatch(node.textContent, searchText, false) || (!isRegex(searchText) && nodeTextContent.includes(searchText))) ) { const childNodesHasMatch = checkIfChildHasMatch( [...node.childNodes], false, ); if (childNodesHasMatch) { continue; } containsMatches.push(node); } } return exactMatches.length ? exactMatches : containsMatches; }; const elements = await $function(textSearch, { text: text.toString(), tagName, exactMatch: options.exactMatch, }); return await handleRelativeSearch(elements, args); }; const description = `Element matching text "${text}"`; return { get: async function (tag, retryInterval, retryTimeout) { console.warn("DEPRECATED use .elements()"); return this.elements(tag, retryInterval, retryTimeout); }, description, elements: getIfExists(get, description), }; } const $$ = async (selector) => { logQuery(`document.querySelectorAll('${selector}')`); function customCssQuerySelector(selectorElement, args) { return selectorElement.querySelectorAll(args); } return await $function(customCssQuerySelector, selector); }; const $$xpath = async (selector) => { logQuery(`xpath - ${selector}`); const xpathFunc = (selector) => { const result = []; const nodesSnapshot = document.evaluate( selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null, ); for (let i = 0; i < nodesSnapshot.snapshotLength; i++) { result.push(nodesSnapshot.snapshotItem(i)); } return result; }; const elements = Element.create( await runtimeHandler.findElements(xpathFunc, selector), runtimeHandler, ); return elements; }; const $function = async (callBack, args) => { function searchThroughShadowDom(argument) { const isString = (obj) => Object.prototype.toString.call(obj).includes("String"); let { searchElement, querySelector, args, elements } = argument; if (isString(querySelector)) { if (typeof querySelector === "string") { querySelector = new Function(`return ${querySelector}`)(); } } if (searchElement === null) { searchElement = document; } elements = elements.concat(Array.from(querySelector(searchElement, args))); const searchElements = searchElement.querySelectorAll("*"); for (const element of searchElements) { if (element.shadowRoot) { elements = searchThroughShadowDom({ searchElement: element.shadowRoot, querySelector: querySelector, args: args, elements: elements, }); } } return elements; } const elements = Element.create( await runtimeHandler.findElements(searchThroughShadowDom, { querySelector: callBack.toString(), args: args, searchElement: null, elements: [], }), runtimeHandler, ); return elements; }; const findFirstElement = async (selector, tag) => { return (await findElements(selector, tag))[0]; }; const findElements = async (selector, tag) => { const elements = await (async () => { if (isString(selector)) { return match(selector).elements(tag); // biome-ignore lint/style/noUselessElse: Does not work without this logic } else if (isSelector(selector)) { return selector.elements(); // biome-ignore lint/style/noUselessElse: Does not work without this logic } else if (isElement(selector)) { return [selector]; } return null; })(); if (!elements || !elements.length) { const error = isString(selector) ? `Element with text ${selector} not found` : `${selector.description} not found`; throw new Error(error); } return elements; }; const findActiveElement = async () => { const getActiveElement = () => { let activeElement = document.activeElement; const parsedShadowRoots = []; while ( activeElement.shadowRoot && !parsedShadowRoots.includes(activeElement.shadowRoot) ) { parsedShadowRoots.push(activeElement.shadowRoot); activeElement = activeElement.shadowRoot.activeElement; } return activeElement; }; const activeElementObjectIds = await runtimeHandler.findElements(getActiveElement); const elements = Element.create(activeElementObjectIds, runtimeHandler); return elements; }; const waitAndGetFocusedElement = async () => { let activeElement; await waitUntil( async () => { try { activeElement = await findActiveElement(); return activeElement.length; } catch (e) { if (e.message.match(/Browser process with pid \d+ exited with/)) { throw e; } } }, defaultConfig.retryInterval, defaultConfig.retryTimeout, ).catch(() => { throw new Error( "There is no element focused, provide an appropriate selector.", ); }); return activeElement; }; const getIfExists = (findElements, description, customFuncs = {}) => { return async (tag, retryInterval, retryTimeout) => { const _retryInterval = determineRetryInterval(retryInterval); const _retryTimeout = determineRetryTimeout(retryTimeout); try { let elements = []; await waitUntil( async () => { elements = await findElements(tag); return elements.length > 0; }, _retryInterval, _retryTimeout, ); elements = elements.length ? elements : await findElements(tag); return elements.map((element) => Object.assign(element, { description }, customFuncs), ); } catch (e) { if (e.message.includes("waiting failed: retryTimeout")) { return []; } throw e; } }; }; module.exports = { match, $$xpath, $$, $function, findFirstElement, findElements, waitAndGetFocusedElement, getIfExists, };