import { driverProvider } from './driverProvider'; import { Browser, BrowserContext, BrowserContextOptions, ElectronApplication, Page, expect } from '@playwright/test'; import timeout from './defaultTimeouts'; import type { ResolvedDriverConfig } from './IQavajsPlaywrightConfig'; type BrowserDict = { [key: string]: Browser; } interface NamedContext extends BrowserContext { name?: string; } type LaunchDriverConfig = Partial>; function isElectronApp(driver: unknown): driver is ElectronApplication { return typeof (driver as ElectronApplication)?.firstWindow === 'function'; } export class Playwright { public drivers: BrowserDict = {}; public driver!: Browser; public browser!: Browser; public context!: NamedContext; public page!: Page; public expect = expect; private readonly driverProvider: (config: any) => Promise; constructor(driverProvider: (config: any) => Promise) { this.driverProvider = driverProvider; } async launchDriver(key: string, driverConfig: LaunchDriverConfig) { const driverInstance = (key === 'default' && this.drivers.default) ? this.drivers.default : await this.driverProvider(driverConfig); this.drivers[key] = driverInstance; this.setDriver(driverInstance); if (driverConfig.isElectron && isElectronApp(driverInstance)) { this.setContext(driverInstance.context()); this.setPage(await driverInstance.firstWindow()); } else { const firstContext = driverInstance.contexts()[0]; const context = driverConfig.reuseSession && firstContext ? firstContext : await driverInstance.newContext(driverConfig.capabilities); this.setContext(context); const firstPage = this.context?.pages()[0]; const page = driverConfig.reuseSession && firstPage ? firstPage : await this.context.newPage(); this.setPage(page); } (this.context as NamedContext).name = 'default'; this.context.setDefaultTimeout(driverConfig?.timeout?.action ?? timeout.action); } async launchContext(key: string, contextConfig?: BrowserContextOptions) { if (!this.driver) throw new Error('No active drivers launched'); const newContext: NamedContext = await this.driver.newContext(contextConfig); newContext.name = key; const newPage = await newContext.newPage(); this.setContext(newContext); this.setPage(newPage); } async switchDriver(key: string) { const currentDriver = this.drivers[key]; if (!currentDriver) throw new Error(`Driver '${key}' was not found`); this.setDriver(currentDriver); this.setContext(this.getDefaultContext()); this.setPage(await this.getFirstPage()); } async switchContext(key: string) { const currentContext = this.findContext(key); this.setContext(currentContext); this.setPage(await this.getFirstPage()); } async closeContext(key: string) { const currentContext = this.findContext(key); await currentContext.close(); if (this.context === currentContext) { this.setContext(this.getDefaultContext()); this.setPage(await this.getFirstPage()); } } findContext(key: string) { const currentContext = this.driver?.contexts().find((ctx: NamedContext) => ctx.name === key); if (!currentContext) throw new Error(`Context '${key}' was not found`); return currentContext; } async closeDriver(driverKey: string) { const currentDriver = this.drivers[driverKey]; if (!currentDriver) throw new Error(`Driver '${driverKey}' was not found`); await currentDriver.close(); delete this.drivers[driverKey]; if (this.driver === currentDriver) { this.setDriver(this.drivers['default']); this.setContext(this.getDefaultContext()); this.setPage(await this.getFirstPage()); } } /** * return to default state (1 browser, no contexts) */ async teardown({ reuseSession, restartBrowser }: { reuseSession?: boolean, restartBrowser?: boolean } = { reuseSession: false, restartBrowser: false }) { this.setDriver(this.drivers['default']); if (reuseSession) return; for (const driverKey in this.drivers) { const driverInstance: unknown = this.drivers[driverKey]; if (isElectronApp(driverInstance)) { await this.electronTeardown(driverInstance, driverKey); } else { await this.browserTeardown(driverInstance as Browser, driverKey, restartBrowser); } } } async browserTeardown(driverInstance: Browser, driverKey: string, restartBrowser?: boolean) { if (driverKey !== 'default' || restartBrowser) { await driverInstance.close(); delete this.drivers[driverKey]; } else { const contexts = driverInstance.contexts() as BrowserContext[]; for (const ctx of contexts) { await ctx.close(); } } } async electronTeardown(driverInstance: ElectronApplication, driverKey: string) { await driverInstance.evaluate((main) => { main.app.exit(0) }); delete this.drivers[driverKey]; } async close() { for (const driverKey in this.drivers) { const driverInstance: unknown = this.drivers[driverKey]; if (isElectronApp(driverInstance)) { await this.electronTeardown(driverInstance, driverKey); } else { await (driverInstance as Browser).close(); } delete this.drivers[driverKey]; } }; getDefaultContext(): BrowserContext { const currentDriver: unknown = this.driver; return isElectronApp(currentDriver) ? currentDriver.context() : this.driver.contexts().at(0) as BrowserContext; } async getFirstPage(): Promise { const currentDriver: unknown = this.driver; return isElectronApp(currentDriver) ? await currentDriver.firstWindow() : this.context.pages().at(0) as Page; } setDriver(driver: Browser) { this.driver = this.browser = driver; } setContext(context: BrowserContext) { this.context = context; } setPage(page: Page) { this.page = page; } async getElementTree() { const frames = this.page.frames().filter(frame => frame.parentFrame() === this.page.mainFrame()); let tree = `Page URL: ${this.page.url()}\n`; tree += await this.page.locator('body').ariaSnapshot() + '\n'; for (const frame of frames) { tree += `Frame URL: ${frame.url()}\n`; tree += await frame.locator('body').ariaSnapshot() + '\n'; } return tree; } } export default new Playwright(driverProvider);