UNPKG

browser-automator

Version:

Puppeteer alternative for Chrome extensions. A module for Chrome extensions that functions similarly to Puppeteer.

262 lines (261 loc) 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.selfIntegration = void 0; const selfIntegration = (global = true) => { const defaultActionOptions = { scrollToElementBeforeAction: true, scrollIntoViewOptions: { behavior: 'smooth', block: 'center' } }; const defaultPageConfigurations = { tryLimit: 30, delay: 1000, ...defaultActionOptions }; const doDelay = async (milliseconds) => { return new Promise(onDone => setTimeout(onDone, milliseconds)); }; const triggerEvent = (element, type) => { element.dispatchEvent(new Event(type, { bubbles: true, cancelable: true })); }; const setValue = (element, value) => { if (element.tagName.match(/INPUT|TEXTAREA|SELECT/i)) element.value = value; else element.innerHTML = value; triggerEvent(element, 'focus'); triggerEvent(element, 'keydown'); triggerEvent(element, 'keypress'); triggerEvent(element, 'keyup'); triggerEvent(element, 'input'); triggerEvent(element, 'change'); triggerEvent(element, 'blur'); }; const triggerPaste = (element) => { element.focus(); if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') element.select(); document.execCommand('paste'); }; const filesToFileList = (files) => { const dataTransferer = new DataTransfer(); for (const file of files) dataTransferer.items.add(file); return dataTransferer.files; }; const dataUrlToFile = (dataUrl, name) => { let [mime, data] = dataUrl.split(','); mime = mime.match(/:(.*?);/)[1]; data = atob(data); let index = data.length, dataArray = new Uint8Array(index); while (index--) dataArray[index] = data.charCodeAt(index); return new File([dataArray], name, { type: mime }); }; const getBlob = (url) => { return new Promise(resolution => { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'blob'; xhr.onload = () => resolution(xhr.response); xhr.send(); }); }; const blobToFile = (blob, name) => { return new File([blob], name, { type: blob.type }); }; /** * CSS Selectors and XPath functions. */ const isXPath = (expression) => { return expression.match(/^(\/|\.\/|\()/); }; const getElementByXPath = (expression, contextNode = document, index = -1) => { return (index === -1) ? (document.evaluate(expression, contextNode, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue) : (document.evaluate(expression, contextNode, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(index)); }; const getElementsByXPath = (expression, contextNode = document) => { let elements = []; let results = document.evaluate(expression, contextNode, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let index = 0; index < results.snapshotLength; index++) elements.push(results.snapshotItem(index)); return elements; }; const getElementBySelectors = (selectors, contextNode = document, index = -1) => { return (index === -1) ? (contextNode.querySelector(selectors)) : (contextNode.querySelectorAll(selectors)[index]); }; const getElementsBySelectors = (selectors, contextNode = document) => { return Array.from(contextNode.querySelectorAll(selectors)); }; const getElement = (selectors, contextNode = document, index = -1) => { return (isXPath(selectors) ? (getElementByXPath(selectors, contextNode, index)) : (getElementBySelectors(selectors, contextNode, index))); }; const getElements = (selectors, contextNode = document) => { return isXPath(selectors) ? (getElementsByXPath(selectors, contextNode)) : (getElementsBySelectors(selectors, contextNode)); }; /** * Page method's helper functions. */ const click = (selectors, index = -1, { scrollToElementBeforeAction, scrollIntoViewOptions } = defaultActionOptions) => { const element = getElement(selectors, document, index); if (element) { scrollToElementBeforeAction && element.scrollIntoView(scrollIntoViewOptions); element.click(); return true; } else return false; }; const elementExists = (selectors, index = -1) => { const element = getElement(selectors, document, index); return element ? true : false; }; const execPasteTo = (selectors, index = -1, { scrollToElementBeforeAction, scrollIntoViewOptions } = defaultActionOptions) => { const element = getElement(selectors, document, index); if (element) { scrollToElementBeforeAction && element.scrollIntoView(scrollIntoViewOptions); triggerPaste(element); return true; } else return false; }; const input = (selectors, value, index = -1, { scrollToElementBeforeAction, scrollIntoViewOptions } = defaultActionOptions) => { const element = getElement(selectors, document, index); if (element) { scrollToElementBeforeAction && element.scrollIntoView(scrollIntoViewOptions); setValue(element, value); return true; } else return false; }; const elementCatcher = { catch: (tagNames) => { if (!elementCatcher.current) { elementCatcher.current = { originalFunc: document.createElement, elements: [], tagNames: tagNames.map(tagName => tagName.toUpperCase()) }; document.createElement = function () { const element = elementCatcher.current?.originalFunc.apply(this, arguments); if (elementCatcher.current?.tagNames.includes(element.tagName)) elementCatcher.current.elements.push(element); return element; }; return true; } return false; }, terminate: () => { if (elementCatcher.current) { document.createElement = elementCatcher.current.originalFunc; delete elementCatcher.current; return true; } return false; } }; const manualClick = { enable: () => { if (manualClick.current) { manualClick.current.element.remove(); delete manualClick.current; } }, disable: () => { if (!manualClick.current) { manualClick.current = { element: document.createElement('div') }; manualClick.current.element.style = 'width: 100%; height: 100%; position: fixed; top: 0; cursor: not-allowed; z-index: 12500; left: 0;'; manualClick.current.element.addEventListener('contextmenu', (event) => event.preventDefault()); document.body.appendChild(manualClick.current.element); } } }; /** * Functions useable in the executed scripts/functions. */ const goto = (url) => location.href = url; const reload = () => location.reload(); const url = () => location.href; const close = () => globalThis.close(); const zoom = (zoomFactor) => document.body.style.zoom = zoomFactor; const waitFor = async (func, args, options = {}) => { let value, tryLimit = options.tryLimit || defaultPageConfigurations.tryLimit, delay = options.delay || defaultPageConfigurations.delay; while (!(value = await func(...args)) && tryLimit) { tryLimit--; await doDelay(delay); } if (value) return value; else throw new Error('Waiting timed out...'); }; const waitForNavigation = async (options = {}) => { const lastUrl = url(); await waitFor(async (lastUrl) => (url() === lastUrl ? false : true), [lastUrl], options); }; const waitForElement = async (selectors, options = {}, index = -1) => { await waitFor(isXPath(selectors) ? ((selectors, index) => getElementByXPath(selectors, document, index) ? true : false) : ((selectors, index) => getElementBySelectors(selectors, document, index) ? true : false), [selectors, index], options); }; const waitForElementMiss = async (selectors, options = {}, index = -1) => { await waitFor(isXPath(selectors) ? ((selectors, index) => getElementByXPath(selectors, document, index) ? false : true) : ((selectors, index) => getElementBySelectors(selectors, document, index) ? false : true), [selectors, index], options); }; /** * Helper class for RemoteElement. */ class ElementActions { static elements = new Map(); static getElement(elementPath) { let element = this.elements.get(elementPath); if (element) return element; for (const path of elementPath.split('→')) { let [, selectors, index] = path.match(/([\s\S]*?)⟮([0-9-]+)⟯$/); index = Number(index); element = getElement(selectors, element || document, index); } if (element) { if (this.elements.size > 50) this.elements.delete(this.elements.keys().next().value); this.elements.set(elementPath, element); return element; } } static handleCall(elementPath, key, args) { const element = this.getElement(elementPath); return args ? element?.[key](...args) : element?.[key](); } static handleGet(elementPath, key) { const element = this.getElement(elementPath); return element?.[key]; } static handleSet(elementPath, key, value) { const element = this.getElement(elementPath); element[key] = value; } } /** * Exportables. */ const Self = { exists: () => true, ElementActions, getElement, getElements, getElementBySelectors, getElementsBySelectors, getElementByXPath, getElementsByXPath, triggerEvent, triggerPaste, setValue, isXPath, filesToFileList, getBlob, dataUrlToFile, blobToFile, goto, reload, url, close, zoom, waitFor, waitForNavigation, waitForElement, waitForElementMiss, click, elementExists, execPasteTo, input, elementCatcher, manualClick }; if (global && !window.Self?.exists?.()) window.Self = Self; return Self; }; exports.selfIntegration = selfIntegration; const Self = selfIntegration(false); exports.default = Self;