From d44fbde65337569c548666260da7db4c9d725b46 Mon Sep 17 00:00:00 2001 From: Sparticuz Date: Mon, 26 Sep 2022 15:10:14 -0400 Subject: [PATCH] Remove embedded puppeteer-core --- Makefile | 2 +- package.json | 7 +- source/hooks/adblock.ts | 48 --- source/hooks/agent.ts | 16 - source/hooks/chrome.ts | 189 --------- source/hooks/languages.ts | 23 - source/hooks/permissions.ts | 28 -- source/hooks/timezone.ts | 10 - source/hooks/webdriver.ts | 19 - source/hooks/window.ts | 47 --- source/index.ts | 55 ++- source/puppeteer/lib/Browser.ts | 43 -- source/puppeteer/lib/BrowserContext.ts | 43 -- source/puppeteer/lib/ElementHandle.ts | 557 ------------------------- source/puppeteer/lib/Frame.ts | 148 ------- source/puppeteer/lib/Page.ts | 184 -------- tsconfig.json | 1 - 17 files changed, 35 insertions(+), 1385 deletions(-) delete mode 100644 source/hooks/adblock.ts delete mode 100644 source/hooks/agent.ts delete mode 100644 source/hooks/chrome.ts delete mode 100644 source/hooks/languages.ts delete mode 100644 source/hooks/permissions.ts delete mode 100644 source/hooks/timezone.ts delete mode 100644 source/hooks/webdriver.ts delete mode 100644 source/hooks/window.ts delete mode 100644 source/puppeteer/lib/Browser.ts delete mode 100644 source/puppeteer/lib/BrowserContext.ts delete mode 100644 source/puppeteer/lib/ElementHandle.ts delete mode 100644 source/puppeteer/lib/Frame.ts delete mode 100644 source/puppeteer/lib/Page.ts diff --git a/Makefile b/Makefile index 61f0f1c..c2bc3ed 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ test: %.zip: npm install --fund=false --package-lock=false mkdir -p nodejs - npm install --prefix nodejs/ tar-fs@2.1.1 puppeteer-core@17.1.3 --bin-links=false --fund=false --omit=optional --omit=dev --package-lock=false --save=false + npm install --prefix nodejs/ tar-fs@2.1.1 --bin-links=false --fund=false --omit=optional --omit=dev --package-lock=false --save=false npm pack mkdir -p nodejs/node_modules/@sparticuz/chrome-aws-lambda/ tar --directory nodejs/node_modules/@sparticuz/chrome-aws-lambda/ --extract --file sparticuz-chrome-aws-lambda-*.tgz --strip-components=1 diff --git a/package.json b/package.json index c0b0593..2fd9cca 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "types": "build/index.d.ts", "files": [ "bin", - "build", - "typings" + "build" ], "engines": { "node": ">= 14" @@ -27,12 +26,8 @@ "@types/node": "^16.11.49", "@types/tar-fs": "^2.0.1", "clean-modules": "^2.0.6", - "puppeteer-core": "17.1.3", "typescript": "^4.6.4" }, - "peerDependencies": { - "puppeteer-core": "17.1.3" - }, "bugs": { "url": "https://github.com/Sparticuz/chrome-aws-lambda/issues" }, diff --git a/source/hooks/adblock.ts b/source/hooks/adblock.ts deleted file mode 100644 index b4f32d1..0000000 --- a/source/hooks/adblock.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { promises } from 'fs'; -import { get } from 'https'; -import { Page } from 'puppeteer-core'; - -let adblocker: any = null; - -/** - * Enables ad blocking in page. - * Requires `@cliqz/adblocker-puppeteer` package. - * - * @param page - Page to hook to. - */ -export = async function (page: Page): Promise { - if (adblocker == null) { - const { fullLists, PuppeteerBlocker } = require('@cliqz/adblocker-puppeteer'); - - adblocker = await PuppeteerBlocker.fromLists( - (url: string) => { - return new Promise((resolve, reject) => { - return get(url, (response) => { - if (response.statusCode !== 200) { - return reject(`Unexpected status code: ${response.statusCode}.`); - } - - let result = ''; - - response.on('data', (chunk) => { - result += chunk; - }); - - response.on('end', () => { - return resolve({ text: () => result }); - }); - }); - }); - }, - fullLists, - { enableCompression: false }, - { - path: '/tmp/adblock.bin', - read: promises.readFile, - write: promises.writeFile, - } - ); - } - - return await adblocker.enableBlockingInPage(page).then(() => page); -} diff --git a/source/hooks/agent.ts b/source/hooks/agent.ts deleted file mode 100644 index c21fbcd..0000000 --- a/source/hooks/agent.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Page } from 'puppeteer-core'; - -/** - * Removes `Headless` from the User Agent string, if present. - * - * @param page - Page to hook to. - */ -export = async function (page: Page): Promise { - let result = await page.browser().userAgent(); - - if (result.includes('Headless') === true) { - await page.setUserAgent(result.replace('Headless', '')); - } - - return page; -}; diff --git a/source/hooks/chrome.ts b/source/hooks/chrome.ts deleted file mode 100644 index 337c12d..0000000 --- a/source/hooks/chrome.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { Page } from 'puppeteer-core'; -import { Writeable } from '../../typings/chrome-aws-lambda'; - -/** - * Mocks the global `chrome` property to mimic headful Chrome. - * - * @param page - Page to hook to. - */ -export = async function (page: Page): Promise { - const handler = () => { - let alpha = Date.now(); - let delta = Math.floor(500 * Math.random()); - - if ((window as any).chrome === undefined) { - Object.defineProperty(window, 'chrome', { - configurable: false, - enumerable: true, - value: {}, - writable: true, - }); - } - - /** - * https://github.com/berstend/puppeteer-extra/blob/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.app/index.js - */ - if ((window as any).chrome.app === undefined) { - const InvocationError = (callback: string) => { - /** - * Truncates every line of the stack trace (with the exception of the first), until `search` is found. - */ - const truncateStackTrace = (error: Error, search: string) => { - const stack = error.stack.split('\n'); - const index = stack.findIndex((value: string) => value.trim().startsWith(search)); - - if (index > 0) { - error.stack = [stack[0], ...stack.slice(index + 1)].join('\n'); - } - - return error; - }; - - return truncateStackTrace(new TypeError(`Error in invocation of app.${callback}()`), `at ${callback} (eval at `); - }; - - Object.defineProperty((window as any).chrome, 'app', { - value: { - InstallState: { - DISABLED: 'disabled', - INSTALLED: 'installed', - NOT_INSTALLED: 'not_installed', - }, - RunningState: { - CANNOT_RUN: 'cannot_run', - READY_TO_RUN: 'ready_to_run', - RUNNING: 'running', - }, - get isInstalled() { - return false; - }, - getDetails: function getDetails(): null { - if (arguments.length > 0) { - throw InvocationError('getDetails'); - } - - return null; - }, - getIsInstalled: function getIsInstalled() { - if (arguments.length > 0) { - throw InvocationError('getIsInstalled'); - } - - return false; - }, - runningState: function runningState() { - if (arguments.length > 0) { - throw InvocationError('runningState'); - } - - return 'cannot_run'; - }, - }, - }); - } - - let timing: Partial = { - navigationStart: alpha + 1 * delta, - domContentLoadedEventEnd: alpha + 4 * delta, - responseStart: alpha + 2 * delta, - loadEventEnd: alpha + 5 * delta, - }; - - if (window.performance?.timing !== undefined) { - timing = window.performance.timing; - } - - /** - * https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.csi - */ - if ((window as any).chrome.csi === undefined) { - Object.defineProperty((window as any).chrome, 'csi', { - value: function csi() { - return { - startE: timing.navigationStart, - onloadT: timing.domContentLoadedEventEnd, - pageT: Date.now() - timing.navigationStart + Math.random().toFixed(3), - tran: 15, - }; - }, - }); - } - - /** - * https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions/chrome.loadTimes - */ - if ((window as any).chrome.loadTimes === undefined) { - let navigation: Writeable> = { - nextHopProtocol: 'h2', - startTime: 3 * delta, - type: 'other' as any, - }; - - if (typeof window.performance?.getEntriesByType === 'function') { - let entries = { - navigation: window.performance.getEntriesByType('navigation') as PerformanceNavigationTiming[], - paint: window.performance.getEntriesByType('paint') as PerformanceNavigationTiming[], - }; - - if (entries.navigation.length > 0) { - navigation = entries.navigation.shift(); - } - - if (entries.paint.length > 0) { - navigation.startTime = entries.paint.shift().startTime; - } - } - - Object.defineProperty((window as any).chrome, 'loadTimes', { - value: function loadTimes() { - return { - get commitLoadTime() { - return timing.responseStart / 1000; - }, - get connectionInfo() { - return navigation.nextHopProtocol; - }, - get finishDocumentLoadTime() { - return timing.domContentLoadedEventEnd / 1000; - }, - get finishLoadTime() { - return timing.loadEventEnd / 1000; - }, - get firstPaintAfterLoadTime() { - return 0; - }, - get firstPaintTime() { - return parseFloat(((navigation.startTime + (window.performance?.timeOrigin ?? timing.navigationStart)) / 1000).toFixed(3)); - }, - get navigationType() { - return navigation.type; - }, - get npnNegotiatedProtocol() { - return ['h2', 'hq'].includes(navigation.nextHopProtocol) ? navigation.nextHopProtocol : 'unknown'; - }, - get requestTime() { - return timing.navigationStart / 1000; - }, - get startLoadTime() { - return timing.navigationStart / 1000; - }, - get wasAlternateProtocolAvailable() { - return false; - }, - get wasFetchedViaSpdy() { - return ['h2', 'hq'].includes(navigation.nextHopProtocol); - }, - get wasNpnNegotiated() { - return ['h2', 'hq'].includes(navigation.nextHopProtocol); - }, - }; - }, - }); - }; - } - - await page.evaluate(handler); - await page.evaluateOnNewDocument(handler); - - return page; -} diff --git a/source/hooks/languages.ts b/source/hooks/languages.ts deleted file mode 100644 index 3e5fb30..0000000 --- a/source/hooks/languages.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Page } from 'puppeteer-core'; - -/** - * Emulates `en-US` language. - * - * @param page - Page to hook to. - */ -export = async function (page: Page): Promise { - const handler = () => { - Object.defineProperty(Object.getPrototypeOf(navigator), 'language', { - get: () => 'en-US', - }); - - Object.defineProperty(Object.getPrototypeOf(navigator), 'languages', { - get: () => ['en-US', 'en'], - }); - }; - - await page.evaluate(handler); - await page.evaluateOnNewDocument(handler); - - return page; -} diff --git a/source/hooks/permissions.ts b/source/hooks/permissions.ts deleted file mode 100644 index 28101cf..0000000 --- a/source/hooks/permissions.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Page } from 'puppeteer-core'; - -/** - * Emulates `denied` state for all permission queries. - * - * @param page - Page to hook to. - */ -export = async function (page: Page): Promise { - const handler = () => { - let query = window.navigator.permissions.query; - - (Permissions as any).prototype.query = function (parameters: PermissionDescriptor) { - if (parameters?.name?.length > 0) { - return Promise.resolve({ - onchange: null, - state: 'denied', - }); - } - - return query(parameters); - }; - }; - - await page.evaluate(handler); - await page.evaluateOnNewDocument(handler); - - return page; -} diff --git a/source/hooks/timezone.ts b/source/hooks/timezone.ts deleted file mode 100644 index 760f371..0000000 --- a/source/hooks/timezone.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Page } from 'puppeteer-core'; - -/** - * Emulates UTC timezone. - * - * @param page - Page to hook to. - */ -export = function (page: Page): Promise { - return page.emulateTimezone('UTC').then(() => page); -} diff --git a/source/hooks/webdriver.ts b/source/hooks/webdriver.ts deleted file mode 100644 index a2b2c7b..0000000 --- a/source/hooks/webdriver.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Page } from 'puppeteer-core'; - -/** - * Removes global `webdriver` property to mimic headful Chrome. - * - * @param page - Page to hook to. - */ -export = async function (page: Page): Promise { - const handler = () => { - Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', { - get: () => false, - }); - }; - - await page.evaluate(handler); - await page.evaluateOnNewDocument(handler); - - return page; -} diff --git a/source/hooks/window.ts b/source/hooks/window.ts deleted file mode 100644 index 37a3401..0000000 --- a/source/hooks/window.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Page } from 'puppeteer-core'; - -/** - * Patches window outer dimentions to mimic headful Chrome. - * - * @param page - Page to hook to. - */ -export = async function (page: Page): Promise { - const handler = () => { - if (window.outerWidth === 0) { - Object.defineProperty(window, 'outerWidth', { - get: () => screen.availWidth, - }); - } - - if (window.outerHeight === 0) { - Object.defineProperty(window, 'outerHeight', { - get: () => screen.availHeight, - }); - } - - if (window.screenX === 0) { - Object.defineProperty(window, 'screenX', { - get: () => screen.width - screen.availWidth, - }); - - Object.defineProperty(window, 'screenLeft', { - get: () => screenX, - }); - } - - if (window.screenY === 0) { - Object.defineProperty(window, 'screenY', { - get: () => screen.height - screen.availHeight, - }); - - Object.defineProperty(window, 'screenTop', { - get: () => screenY, - }); - } - }; - - await page.evaluate(handler); - await page.evaluateOnNewDocument(handler); - - return page; -} diff --git a/source/index.ts b/source/index.ts index e222c69..ca5192e 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,12 +1,42 @@ -/// - import { access, createWriteStream, existsSync, mkdirSync, readdirSync, symlink, unlinkSync } from 'fs'; import { IncomingMessage } from 'http'; import LambdaFS from './lambdafs'; import { join } from 'path'; -import { PuppeteerNode, Viewport } from 'puppeteer-core'; import { URL } from 'url'; +/** Viewport taken from https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.viewport.md */ +interface Viewport { + /** + * The page width in pixels. + */ + width: number; + /** + * The page height in pixels. + */ + height: number; + /** + * Specify device scale factor. + * See {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio | devicePixelRatio} for more info. + * @defaultValue 1 + */ + deviceScaleFactor?: number; + /** + * Whether the `meta viewport` tag is taken into account. + * @defaultValue false + */ + isMobile?: boolean; + /** + * Specifies if the viewport is in landscape mode. + * @defaultValue false + */ + isLandscape?: boolean; + /** + * Specify if the viewport supports touch events. + * @defaultValue false + */ + hasTouch?: boolean; +} + if (/^AWS_Lambda_nodejs(?:10|12|14|16)[.]x$/.test(process.env.AWS_EXECUTION_ENV) === true) { if (process.env.FONTCONFIG_PATH === undefined) { process.env.FONTCONFIG_PATH = '/tmp/aws'; @@ -195,25 +225,6 @@ class Chromium { return environments.some((key) => process.env[key] !== undefined); } - - /** - * Overloads puppeteer with useful methods and returns the resolved package. - */ - static get puppeteer(): PuppeteerNode { - for (const overload of ['Browser', 'BrowserContext', 'ElementHandle', 'Frame', 'Page']) { - require(`${__dirname}/puppeteer/lib/${overload}`); - } - - try { - return require('puppeteer'); - } catch (error: any) { - if (error.code !== 'MODULE_NOT_FOUND') { - throw error; - } - - return require('puppeteer-core'); - } - } } export = Chromium; diff --git a/source/puppeteer/lib/Browser.ts b/source/puppeteer/lib/Browser.ts deleted file mode 100644 index 6ed3eff..0000000 --- a/source/puppeteer/lib/Browser.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Browser, Page } from 'puppeteer-core'; -import { Hook, Prototype } from '../../../typings/chrome-aws-lambda'; - -let Super: Prototype = null; - -try { - Super = require('puppeteer/lib/cjs/puppeteer/common/Browser.js').Browser; -} catch (error) { - Super = require('puppeteer-core/lib/cjs/puppeteer/common/Browser.js').Browser; -} - -Super.prototype.defaultPage = async function (...hooks: Hook[]) { - let page: Page = null; - let pages: Page[] = await this.pages(); - - if (pages.length === 0) { - pages = [await this.newPage()]; - } - - page = pages.shift(); - - if (hooks != null && Array.isArray(hooks) === true) { - for (let hook of hooks) { - page = await hook(page); - } - } - - return page; -}; - -let newPage: any = Super.prototype.newPage; - -Super.prototype.newPage = async function (...hooks: Hook[]) { - let page: Page = await newPage.apply(this, arguments); - - if (hooks != null && Array.isArray(hooks) === true) { - for (let hook of hooks) { - page = await hook(page); - } - } - - return page; -}; diff --git a/source/puppeteer/lib/BrowserContext.ts b/source/puppeteer/lib/BrowserContext.ts deleted file mode 100644 index 774ae4a..0000000 --- a/source/puppeteer/lib/BrowserContext.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BrowserContext, Page } from 'puppeteer-core'; -import { Hook, Prototype } from '../../../typings/chrome-aws-lambda'; - -let Super: Prototype = null; - -try { - Super = require('puppeteer/lib/cjs/puppeteer/common/Browser.js').BrowserContext; -} catch (error) { - Super = require('puppeteer-core/lib/cjs/puppeteer/common/Browser.js').BrowserContext; -} - -Super.prototype.defaultPage = async function (...hooks: Hook[]) { - let page: Page = null; - let pages: Page[] = await this.pages(); - - if (pages.length === 0) { - pages = [await this.newPage()]; - } - - page = pages.shift(); - - if (hooks != null && Array.isArray(hooks) === true) { - for (let hook of hooks) { - page = await hook(page); - } - } - - return page; -}; - -let newPage: any = Super.prototype.newPage; - -Super.prototype.newPage = async function (...hooks: Hook[]) { - let page: Page = await newPage.apply(this, arguments); - - if (hooks != null && Array.isArray(hooks) === true) { - for (let hook of hooks) { - page = await hook(page); - } - } - - return page; -}; diff --git a/source/puppeteer/lib/ElementHandle.ts b/source/puppeteer/lib/ElementHandle.ts deleted file mode 100644 index aa75fcf..0000000 --- a/source/puppeteer/lib/ElementHandle.ts +++ /dev/null @@ -1,557 +0,0 @@ -import { ElementHandle, EvaluateFunc, HTTPRequest, HTTPResponse, Page, WaitForOptions, WaitTimeoutOptions } from 'puppeteer-core'; -import { Prototype } from '../../../typings/chrome-aws-lambda'; - -let Super: Prototype = null; - -try { - Super = require('puppeteer/lib/cjs/puppeteer/common/ElementHandle.js').ElementHandle; -} catch (error) { - Super = require('puppeteer-core/lib/cjs/puppeteer/common/ElementHandle.js').ElementHandle; -} - -Super.prototype.clear = function () { - return this.click({ clickCount: 3 }).then(() => this.press('Backspace')); -}; - -Super.prototype.clickAndWaitForNavigation = function (options?: WaitForOptions) { - options = options ?? { - waitUntil: [ - 'load', - ], - }; - - let promises: [Promise, Promise] = [ - ((this as any)._page as Page).waitForNavigation(options), - this.click(), - ]; - - return Promise.all(promises).then((value) => value.shift() as HTTPResponse); -}; - -Super.prototype.clickAndWaitForRequest = function (predicate: string | RegExp | ((request: HTTPRequest) => boolean | Promise), options?: WaitTimeoutOptions) { - let callback = (request: HTTPRequest) => { - let url = request.url(); - - if (typeof predicate === 'string' && predicate.includes('*') === true) { - predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g'); - } - - if (predicate instanceof RegExp) { - return predicate.test(url); - } - - return predicate === url; - }; - - let promises: [Promise, Promise] = [ - ((this as any)._page as Page).waitForRequest((typeof predicate === 'function') ? predicate : callback, options), - this.click(), - ]; - - return Promise.all(promises).then((value) => value.shift() as HTTPRequest); -}; - -Super.prototype.clickAndWaitForResponse = function (predicate: string | RegExp | ((request: HTTPResponse) => boolean | Promise), options?: WaitTimeoutOptions) { - let callback = (request: HTTPResponse) => { - let url = request.url(); - - if (typeof predicate === 'string' && predicate.includes('*') === true) { - predicate = new RegExp(predicate.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]+/g, '.*?'), 'g'); - } - - if (predicate instanceof RegExp) { - return predicate.test(url); - } - - return predicate === url; - }; - - let promises: [Promise, Promise] = [ - ((this as any)._page as Page).waitForResponse((typeof predicate === 'function') ? predicate : callback, options), - this.click(), - ]; - - return Promise.all(promises).then((value) => value.shift() as HTTPResponse); -}; - -Super.prototype.fillFormByLabel = function >(data: T) { - let callback = (node: HTMLFormElement, data: T) => { - if (node.nodeName.toLowerCase() !== 'form') { - throw new Error('Element is not a
element.'); - } - - let result: Record = {}; - - for (let [key, value] of Object.entries(data)) { - let selector = [ - `id(string(//label[normalize-space(.) = "${key}"]/@for))`, - `//label[normalize-space(.) = "${key}"]//*[self::input or self::select or self::textarea]`, - ].join(' | '); - - if (result.hasOwnProperty(key) !== true) { - result[key] = []; - } - - let element: Node = null; - let elements: HTMLInputElement[] = []; - let iterator = document.evaluate(selector, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); - - while ((element = iterator.iterateNext()) != null) { - elements.push(element as HTMLInputElement); - } - - if (elements.length === 0) { - throw new Error(`No elements match the selector '${selector}' for '${key}'.`); - } - - let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); - let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; - - if (type === 'file') { - throw new Error(`Input element of type 'file' is not supported.`); - } - - for (let element of elements) { - try { - element.focus(); - element.dispatchEvent(new Event('focus')); - } catch (error) { - } - - if (type === 'select') { - element.value = undefined; - - for (let index of ['value', 'label'] as ['value', 'label']) { - if (result[key].length > 0) { - break; - } - - for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { - option.selected = values.includes(option[index]); - - if (option.selected === true) { - result[key].push(option.value); - - if (element.multiple !== true) { - break; - } - } - } - } - } else if (type === 'checkbox' || type === 'radio') { - element.checked = (value === true) || values.includes(element.value); - - if (element.checked === true) { - result[key].push(element.value); - } - } else if (typeof value === 'string') { - if (element.isContentEditable === true) { - result[key].push(element.textContent = value); - } else { - result[key].push(element.value = value); - } - } - - for (let trigger of ['input', 'change']) { - element.dispatchEvent(new Event(trigger, { 'bubbles': true })); - } - - try { - element.blur(); - element.dispatchEvent(new Event('blur')); - } catch (error) { - } - - if (type === 'checkbox' || type === 'radio') { - break; - } - } - } - - return result; - }; - - return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle, T]>, data) as any; -}; - -Super.prototype.fillFormByName = function >(data: T) { - let callback = (node: HTMLFormElement, data: T, heuristic: 'css' | 'label' | 'name' | 'xpath' = 'css') => { - if (node.nodeName.toLowerCase() !== 'form') { - throw new Error('Element is not a element.'); - } - - let result: Record = {}; - - for (let [key, value] of Object.entries(data)) { - let selector = `[name="${key}"]`; - - if (result.hasOwnProperty(key) !== true) { - result[key] = []; - } - - let elements: HTMLInputElement[] = Array.from(node.querySelectorAll(selector)); - - if (elements.length === 0) { - throw new Error(`No elements match the selector '${selector}' for '${key}'.`); - } - - let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); - let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; - - if (type === 'file') { - throw new Error(`Input element of type 'file' is not supported.`); - } - - for (let element of elements) { - try { - element.focus(); - element.dispatchEvent(new Event('focus')); - } catch (error) { - } - - if (type === 'select') { - element.value = undefined; - - for (let index of ['value', 'label'] as ['value', 'label']) { - if (result[key].length > 0) { - break; - } - - for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { - option.selected = values.includes(option[index]); - - if (option.selected === true) { - result[key].push(option.value); - - if (element.multiple !== true) { - break; - } - } - } - } - } else if (type === 'checkbox' || type === 'radio') { - element.checked = (value === true) || values.includes(element.value); - - if (element.checked === true) { - result[key].push(element.value); - } - } else if (typeof value === 'string') { - if (element.isContentEditable === true) { - result[key].push(element.textContent = value); - } else { - result[key].push(element.value = value); - } - } - - for (let trigger of ['input', 'change']) { - element.dispatchEvent(new Event(trigger, { 'bubbles': true })); - } - - try { - element.blur(); - element.dispatchEvent(new Event('blur')); - } catch (error) { - } - - if (type === 'checkbox' || type === 'radio') { - break; - } - } - } - - return result; - }; - - return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle, T]>, data) as any; -}; - -Super.prototype.fillFormBySelector = function >(data: T) { - let callback = (node: HTMLFormElement, data: T, heuristic: 'css' | 'label' | 'name' | 'xpath' = 'css') => { - if (node.nodeName.toLowerCase() !== 'form') { - throw new Error('Element is not a element.'); - } - - let result: Record = {}; - - for (let [key, value] of Object.entries(data)) { - let selector = key; - - if (result.hasOwnProperty(key) !== true) { - result[key] = []; - } - - let elements: HTMLInputElement[] = Array.from(node.querySelectorAll(selector)); - - if (elements.length === 0) { - throw new Error(`No elements match the selector '${selector}' for '${key}'.`); - } - - let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); - let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; - - if (type === 'file') { - throw new Error(`Input element of type 'file' is not supported.`); - } - - for (let element of elements) { - try { - element.focus(); - element.dispatchEvent(new Event('focus')); - } catch (error) { - } - - if (type === 'select') { - element.value = undefined; - - for (let index of ['value', 'label'] as ['value', 'label']) { - if (result[key].length > 0) { - break; - } - - for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { - option.selected = values.includes(option[index]); - - if (option.selected === true) { - result[key].push(option.value); - - if (element.multiple !== true) { - break; - } - } - } - } - } else if (type === 'checkbox' || type === 'radio') { - element.checked = (value === true) || values.includes(element.value); - - if (element.checked === true) { - result[key].push(element.value); - } - } else if (typeof value === 'string') { - if (element.isContentEditable === true) { - result[key].push(element.textContent = value); - } else { - result[key].push(element.value = value); - } - } - - for (let trigger of ['input', 'change']) { - element.dispatchEvent(new Event(trigger, { 'bubbles': true })); - } - - try { - element.blur(); - element.dispatchEvent(new Event('blur')); - } catch (error) { - } - - if (type === 'checkbox' || type === 'radio') { - break; - } - } - } - - return result; - }; - - return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle, T]>, data) as any; -}; - -Super.prototype.fillFormByXPath = function >(data: T) { - let callback = (node: HTMLFormElement, data: T) => { - if (node.nodeName.toLowerCase() !== 'form') { - throw new Error('Element is not a element.'); - } - - let result: Record = {}; - - for (let [key, value] of Object.entries(data)) { - let selector = key; - - if (result.hasOwnProperty(key) !== true) { - result[key] = []; - } - - let element: Node = null; - let elements: HTMLInputElement[] = []; - let iterator = document.evaluate(selector, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); - - while ((element = iterator.iterateNext()) != null) { - elements.push(element as HTMLInputElement); - } - - if (elements.length === 0) { - throw new Error(`No elements match the selector '${selector}' for '${key}'.`); - } - - let type = (elements[0].getAttribute('type') || elements[0].nodeName).toLowerCase(); - let values: (boolean | string)[] = (Array.isArray(value) === true) ? value as (boolean | string)[] : [value] as (boolean | string)[]; - - if (type === 'file') { - throw new Error(`Input element of type 'file' is not supported.`); - } - - for (let element of elements) { - try { - element.focus(); - element.dispatchEvent(new Event('focus')); - } catch (error) { - } - - if (type === 'select') { - element.value = undefined; - - for (let index of ['value', 'label'] as ['value', 'label']) { - if (result[key].length > 0) { - break; - } - - for (let option of Array.from((element as unknown as HTMLSelectElement).options)) { - option.selected = values.includes(option[index]); - - if (option.selected === true) { - result[key].push(option.value); - - if (element.multiple !== true) { - break; - } - } - } - } - } else if (type === 'checkbox' || type === 'radio') { - element.checked = (value === true) || values.includes(element.value); - - if (element.checked === true) { - result[key].push(element.value); - } - } else if (typeof value === 'string') { - if (element.isContentEditable === true) { - result[key].push(element.textContent = value); - } else { - result[key].push(element.value = value); - } - } - - for (let trigger of ['input', 'change']) { - element.dispatchEvent(new Event(trigger, { 'bubbles': true })); - } - - try { - element.blur(); - element.dispatchEvent(new Event('blur')); - } catch (error) { - } - - if (type === 'checkbox' || type === 'radio') { - break; - } - } - } - - return result; - }; - - return this.evaluate(callback as unknown as EvaluateFunc<[ElementHandle, T]>, data) as any; -}; - -Super.prototype.getInnerHTML = function () { - return this.evaluate((node: Element) => { - return (node as HTMLElement).innerHTML; - }); -}; - -Super.prototype.getInnerText = function () { - return this.evaluate((node: Element) => { - return (node as HTMLElement).innerText; - }); -}; - -Super.prototype.number = function (decimal: string = '.', property: any) { - let callback = (node: any, decimal: string, property: any) => { - let data = (node[property] as unknown) as string; - - if (typeof data === 'string') { - decimal = decimal ?? '.'; - - if (typeof decimal === 'string') { - decimal = decimal.replace(/[.]/g, '\\$&'); - } - - let matches = data.match(/((?:[-+]|\b)[0-9]+(?:[ ,.'`ยด]*[0-9]+)*)\b/g); - - if (matches != null) { - return matches.map((value) => parseFloat(value.replace(new RegExp(`[^-+0-9${decimal}]+`, 'g'), '').replace(decimal, '.'))); - } - } - - return null; - }; - - return this.evaluate(callback, decimal, property as any); -}; - -Super.prototype.selectByLabel = function (...values: string[]) { - for (let value of values) { - console.assert(typeof value === 'string', `Values must be strings. Found value '${value}' of type '${typeof value}'.`); - } - - let callback = (node: HTMLSelectElement, values: string[]) => { - if (node.nodeName.toLowerCase() !== 'select') { - throw new Error('Element is not a