import type { AppiumServer, BidiModuleMap, ExecuteMethodMap, ExternalDriver, IIpcSubscription, IpcMessage, MethodMap, } from '@appium/types'; import {BasePlugin} from 'appium/plugin'; import {sleep} from 'asyncbox'; import type {Application, Request, Response} from 'express'; /** Driver as seen by this plugin; may include plugin-specific session data */ export type DriverLike = ExternalDriver & {fakeSessionData?: unknown}; type ClockStatus = {running: boolean}; export class FakePlugin extends BasePlugin { static newMethodMap: MethodMap = { '/session/:sessionId/fake_data': { GET: {command: 'getFakeSessionData', neverProxy: true}, POST: { command: 'setFakeSessionData', payloadParams: {required: ['data']}, neverProxy: true, }, }, '/session/:sessionId/fakepluginargs': { GET: {command: 'getFakePluginArgs', neverProxy: true}, }, }; static newBidiCommands: BidiModuleMap = { 'appium:fake': { getPluginThing: { command: 'getPluginThing', }, setPluginThing: { command: 'setPluginThing', params: { required: ['thing'], }, }, doSomeMath: { command: 'doSomeMath', params: { required: ['num1', 'num2'], }, }, doSomeMath2: { command: 'doSomeMath2', params: { required: ['num1', 'num2'], }, }, }, }; static executeMethodMap: ExecuteMethodMap = { 'fake: getThing': { command: 'getFakeThing', }, 'fake: plugMeIn': { command: 'plugMeIn', params: {required: ['socket']}, }, 'fake: getFakeDriverClockStatus': { command: 'getFakeDriverClockStatus', }, }; private static _unexpectedData: string | null = null; protected _clockRunning: boolean = true; private readonly fakeThing: string; private pluginThing: unknown = null; private fakeDriverClockIsRunning: boolean = false; private ipcPluginMath?: IIpcSubscription; constructor(name: string, cliArgs: Record = {}) { super(name, cliArgs); this.fakeThing = 'PLUGIN_FAKE_THING'; void this.startClock(); } static fakeRoute(_req: Request, res: Response): void { res.send(JSON.stringify({fake: 'fakeResponse'})); } static unexpectedData(_req: Request, res: Response): void { res.send(JSON.stringify(FakePlugin._unexpectedData)); FakePlugin._unexpectedData = null; } static async updateServer( expressApp: Application, httpServer: AppiumServer, cliArgs: Record, ): Promise { expressApp.all('/fake', FakePlugin.fakeRoute); expressApp.all('/unexpected', FakePlugin.unexpectedData); expressApp.all('/cliArgs', (req, res) => { res.send(JSON.stringify(cliArgs)); }); // global middleware, exercised via httpServer.frontRouter httpServer.frontRouter.use((_req, res, next) => { res.set('x-fake-plugin-pre-server', 'true'); next(); }); } async onIpcInit() { const clockSub = this.ipcSubscribe('clockLifecycle'); clockSub.on('message', (message: IpcMessage) => { this.fakeDriverClockIsRunning = message.data.running; }); // subscribing to clockLifecycle doesn't tell us the current status if it started before this // constructor was called, so retrieve it const message = clockSub.getMessage(); if (message) { this.fakeDriverClockIsRunning = message.data.running; } this.ipcPluginMath = this.ipcSubscribe('pluginMath'); } async startClock(): Promise { while (this._clockRunning) { await sleep(250); this.eventEmitter.emit('bidiEvent', { method: 'appium:clock.currentTime', params: {time: Date.now()}, }); } } async doSomeMath(next: () => Promise, _driver: DriverLike, num1: number, num2: number): Promise { const sum = await next(); return num1 * num2 + sum; } async doSomeMath2(_next: () => Promise, _driver: DriverLike, num1: number, num2: number): Promise { await sleep(1); const result = num1 * num2; if (this.ipcPluginMath) { await this.ipcPluginMath.publish(result); } return result; } async getFakeThing(): Promise { await sleep(1); return this.fakeThing; } async getPluginThing(): Promise { this.eventEmitter.emit('bidiEvent', { method: 'appium:fake.pluginThingRetrieved', params: {}, }); return this.pluginThing; } async setPluginThing(_next: () => Promise, _driver: DriverLike, thing: unknown): Promise { this.pluginThing = thing; } async plugMeIn(_next: () => Promise, _driver: DriverLike, socket: string): Promise { await sleep(1); return `Plugged in to ${socket}`; } async getFakePluginArgs(): Promise> { await sleep(1); return this.cliArgs; } async getFakeDriverClockStatus(): Promise { return this.fakeDriverClockIsRunning; } async getPageSource(_next: () => Promise, _driver: DriverLike, ...args: unknown[]): Promise { await sleep(10); return `${JSON.stringify(args)}`; } async findElement( next: () => Promise<{fake?: boolean} & Record>, _driver: DriverLike, ...args: unknown[] ): Promise<{fake?: boolean} & Record> { this.log.info(`Before findElement is run with args ${JSON.stringify(args)}`); const originalRes = await next(); this.log.info('After findElement is run'); originalRes.fake = true; return originalRes; } async getFakeSessionData(_next: () => Promise, driver: DriverLike): Promise { await sleep(1); return driver.fakeSessionData ?? null; } async setFakeSessionData(_next: () => Promise, driver: DriverLike, ...args: unknown[]): Promise { await sleep(1); driver.fakeSessionData = args[0]; return null; } async getWindowHandle(next: () => Promise): Promise { const handle = await next(); return `<<${handle}>>`; } async onUnexpectedShutdown(_driver: DriverLike, cause: Error | string): Promise { this._clockRunning = false; FakePlugin._unexpectedData = `Session ended because ${cause}`; } async execute(next: () => Promise, driver: DriverLike, script: string, args: unknown[]): Promise { return await this.executeMethod(next, driver as any, script, args); } async deleteSession(next: () => Promise): Promise { this._clockRunning = false; return await next(); } }