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
JavaScript
;
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;