import { Page, Browser, CDPSession, BrowserContext, } from 'playwright'; import { Socket } from "socket.io"; import logger from '../../logger'; import { InterpreterSettings, RemoteBrowserOptions } from "../../types"; import { WorkflowGenerator } from "../../workflow-management/classes/Generator"; import { WorkflowInterpreter } from "../../workflow-management/classes/Interpreter"; /** * This class represents a remote browser instance. * It is used to allow a variety of interaction with the Playwright's browser instance. * Every remote browser holds an instance of a generator and interpreter classes with * the purpose of generating and interpreting workflows. * @category BrowserManagement */ export class RemoteBrowser { /** * Playwright's [browser](https://playwright.dev/docs/api/class-browser) instance. * @private */ private browser: Browser | null = null; /** * The Playwright's [CDPSession](https://playwright.dev/docs/api/class-cdpsession) instance, * used to talk raw Chrome Devtools Protocol. * @private */ private client : CDPSession | null | undefined = null; /** * Socket.io socket instance enabling communication with the client (frontend) side. * @private */ private socket : Socket; /** * The Playwright's [Page](https://playwright.dev/docs/api/class-page) instance * as current interactive remote browser's page. * @private */ private currentPage : Page | null | undefined = null; /** * Interpreter settings for any started interpretation. * @private */ private interpreterSettings: InterpreterSettings = { debug: false, maxConcurrency: 1, maxRepeats: 1, }; /** * {@link WorkflowGenerator} instance specific to the remote browser. */ public generator: WorkflowGenerator; /** * {@link WorkflowInterpreter} instance specific to the remote browser. */ public interpreter: WorkflowInterpreter; /** * Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and * assigns the socket instance everywhere. * @param socket socket.io socket instance used to communicate with the client side * @constructor */ public constructor(socket: Socket) { this.socket = socket; this.interpreter = new WorkflowInterpreter(socket); this.generator = new WorkflowGenerator(socket); } /** * An asynchronous constructor for asynchronously initialized properties. * Must be called right after creating an instance of RemoteBrowser class. * @param options remote browser options to be used when launching the browser * @returns {Promise} */ public initialize = async(options: RemoteBrowserOptions) : Promise => { this.browser = (await options.browser.launch(options.launchOptions)); const context = await this.browser.newContext(); this.currentPage = await context.newPage(); this.client = await this.currentPage.context().newCDPSession(this.currentPage); }; /** * Registers all event listeners needed for the recording editor session. * Should be called only once after the full initialization of the remote browser. * @returns void */ public registerEditorEvents = () : void => { this.socket.on('rerender', async() => await this.makeAndEmitScreenshot()); this.socket.on('settings', (settings) => this.interpreterSettings = settings); this.socket.on('changeTab', async(tabIndex) => await this.changeTab(tabIndex)); this.socket.on('addTab', async () => { await this.currentPage?.context().newPage(); const lastTabIndex = this.currentPage ? this.currentPage.context().pages().length - 1 : 0; await this.changeTab(lastTabIndex); }); this.socket.on('closeTab', async (tabInfo) => { const page = this.currentPage?.context().pages()[tabInfo.index]; if (page) { if (tabInfo.isCurrent){ if (this.currentPage?.context().pages()[tabInfo.index + 1]) { // next tab await this.changeTab(tabInfo.index + 1); } else { //previous tab await this.changeTab(tabInfo.index - 1); } } // close the page and log it await page.close(); logger.log( 'debug', `${tabInfo.index} page was closed, new length of pages: ${this.currentPage?.context().pages().length}` ) } else { logger.log('error', `${tabInfo.index} index out of range of pages`) } }); } }