/* eslint-disable no-await-in-loop, no-restricted-syntax */ import { Page, PageScreenshotOptions } from 'playwright'; import path from 'path'; import { EventEmitter } from 'events'; import { Where, What, PageState, Workflow, WorkflowFile, ParamType, SelectorArray, CustomFunctions, } from './types/workflow'; import { operators, meta } from './types/logic'; import { arrayToObject } from './utils/utils'; import Concurrency from './utils/concurrency'; import Preprocessor from './preprocessor'; import log, { Level } from './utils/logger'; /** * Defines optional intepreter options (passed in constructor) */ interface InterpreterOptions { maxRepeats: number; maxConcurrency: number; serializableCallback: (output: any) => (void | Promise); binaryCallback: (output: any, mimeType: string) => (void | Promise); debug: boolean; debugChannel: Partial<{ activeId: Function, debugMessage: Function, }> } /** * Class for running the Smart Workflows. */ export default class Interpreter extends EventEmitter { private workflow: Workflow; private initializedWorkflow: Workflow | null; private options: InterpreterOptions; private concurrency : Concurrency; private stopper : Function | null = null; private log : typeof log; constructor(workflow: WorkflowFile, options?: Partial) { super(); this.workflow = workflow.workflow; this.initializedWorkflow = null; this.options = { maxRepeats: 5, maxConcurrency: 5, serializableCallback: (data) => { log(JSON.stringify(data), Level.WARN); }, binaryCallback: () => { log('Received binary data, thrashing them.', Level.WARN); }, debug: false, debugChannel: {}, ...options, }; this.concurrency = new Concurrency(this.options.maxConcurrency); this.log = (...args) => log(...args); const error = Preprocessor.validateWorkflow(workflow); if (error) { throw (error); } if (this.options.debugChannel?.debugMessage) { const oldLog = this.log; // @ts-ignore this.log = (...args: Parameters) => { if (args[1] !== Level.LOG) { this.options.debugChannel.debugMessage!(typeof args[0] === 'string' ? args[0] : args[0].message); } oldLog(...args); }; } } /** * Returns the context object from given Page and the current workflow.\ * \ * `workflow` is used for selector extraction - function searches for used selectors to * look for later in the page's context. * @param page Playwright Page object * @param workflow Current **initialized** workflow (array of where-what pairs). * @returns {PageState} State of the current page. */ private async getState(page: Page, workflow: Workflow) : Promise { /** * All the selectors present in the current Workflow */ const selectors = Preprocessor.extractSelectors(workflow); /** * Determines whether the element targetted by the selector is [actionable](https://playwright.dev/docs/actionability). * @param selector Selector to be queried * @returns True if the targetted element is actionable, false otherwise. */ const actionable = async (selector: string) : Promise => { try { const proms = [ page.isEnabled(selector, { timeout: 500 }), page.isVisible(selector, { timeout: 500 }), ]; return await Promise.all(proms).then((bools) => bools.every((x) => x)); } catch (e) { // log(e, Level.ERROR); return false; } }; /** * Object of selectors present in the current page. */ const presentSelectors : SelectorArray = await Promise.all( selectors.map(async (selector) => { if (await actionable(selector)) { return [selector]; } return []; }), ).then((x) => x.flat()); return { url: page.url(), cookies: (await page.context().cookies([page.url()])) .reduce((p, cookie) => ( { ...p, [cookie.name]: cookie.value, }), {}), selectors: presentSelectors, }; } }