ulixee/secret-agent

it doesn't seem possible to use IPuppetFrame.evaluate with css-tricks.com domain

andynuss opened this issue · 0 comments

I always get an exception with the css-tricks.com domain when I use my plugin for injecting javascript into a page/frame, even though the plugin works with the vast majority of urls I have tested.

It appears that on the line of code where I try to access the window global, I get 'window is undefined'. Likewise if I try to access thru the document global.


This is my test snippet:

import { Agent, ConnectionFactory, ConnectionToCore, LocationStatus } from 'secret-agent';
import Path from 'path';

let sharedConnection: ConnectionToCore = null;

function getConnection (maxAgents: number): ConnectionToCore {
  if (sharedConnection !== null) return sharedConnection;
  sharedConnection = ConnectionFactory.createConnection({ maxConcurrency: maxAgents });
  return sharedConnection;
}

function sleep(millis: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, millis));
}

async function test(): Promise<void> {
  const agent: Agent = new Agent({
    connectionToCore: getConnection(1),
  });

  try {
    agent.use(Path.resolve(__dirname, 'simple-plugin'));
    await agent.goto('https://css-tricks.com/a-complete-guide-to-dark-mode-on-the-web/');
    await agent.activeTab.waitForLoad(LocationStatus.DomContentLoaded);

    // a small variable delay has no impact
    sleep(2000);

    // @ts-ignore
    const result: [string, number] = await agent.evaluate(0, (arg: any) => {
      // @ts-ignore
      const win: any = window;
      return [win.document.documentElement.outerHTML, arg];
    }, 999);

    console.log('success', result);

  } catch (e) {
    console.log('failure', e);
  } finally {
    await agent.close();
  }
}

(async () => {
  await test();
})();

This is my simple-plugin.ts file in the same directory as the above test:

import ClientPlugin from '@secret-agent/plugin-utils/lib/ClientPlugin';
import CorePlugin from '@secret-agent/plugin-utils/lib/CorePlugin';
import { ISendToCoreFn } from '@secret-agent/interfaces/IClientPlugin';
import { IOnClientCommandMeta } from '@secret-agent/interfaces/ICorePlugin';
import { IPuppetPage } from '@secret-agent/interfaces/IPuppetPage';
import { IPuppetFrame, ILifecycleEvents } from '@secret-agent/interfaces/IPuppetFrame';

export class MyClientPlugin extends ClientPlugin {

  static readonly id = 'my-evaluate-plugin';
  static coreDependencyIds = [MyClientPlugin.id];

  public onAgent(agent, sendToCore: ISendToCoreFn): void {

    agent.numFrames = (): Promise<number> => {
      return this.execFunc(sendToCore, 'numFrames');
    };

    agent.evaluate = (frameIndex: number, callback: Function, ...args: any[]): Promise<any> => {
      return this.execFunc(sendToCore, 'evaluate', frameIndex, callback, args);
    };
  }

  public onTab(tab, sendToCore: ISendToCoreFn): void {

    tab.numFrames = (): Promise<number> => {
      return this.execFunc(sendToCore, 'numFrames');
    };

    tab.evaluate = (frameIndex: number, callback: Function, ...args: any[]): Promise<any> => {
      return this.execFunc(sendToCore, 'evaluate', frameIndex, callback, args);
    };
  }

  private async execFunc(
    sendToCore: ISendToCoreFn,
    funcName: string,
    frameIndex?: number,
    callback?: Function,
    args?: any[]
  ): Promise<any> {
    const callbackStr: string = callback ? callback.toString() : undefined;
    return await sendToCore(MyClientPlugin.id, funcName, frameIndex, callbackStr, args);
  }
}

export class MyCorePlugin extends CorePlugin {

  static readonly id = 'my-evaluate-plugin';

  private hasWaited: boolean[] = [];

  private async getAwaitedFrame(page: IPuppetPage, frameIndex: number): Promise<IPuppetFrame | null> {
    if (frameIndex < 0) throw new Error('frameIndex is negative');
    const frames: IPuppetFrame[] = page.frames;
    if (frameIndex >= frames.length) return null;
    const frame: IPuppetFrame = frames[frameIndex];
    if (!frame) throw new Error('frameIndex in bounds but frame is ' + frame);
    if (!this.hasWaited[frameIndex]) {
      const key: keyof ILifecycleEvents = 'load';
      try {
        await frame.waitForLoad(key);
      } catch (e) {
        console.log('getAwaitedFrame error', e);
        throw e;
      }
      this.hasWaited[frameIndex] = true;
    }
    return frame;
  }

  public async onClientCommand(
    { puppetPage }: IOnClientCommandMeta,
    funcName: string,
    frameIndex: number,
    callback: string,         // this is the stringified value of the callback given to execFunc above
    args: any[]
  ): Promise<any> {
    const page: IPuppetPage = puppetPage;

    if (funcName === 'numFrames') {
      return page.frames.length;
    }

    if (funcName === 'evaluate') {
      const frame: IPuppetFrame = await this.getAwaitedFrame(page, frameIndex);
      if (!frame) return null;
      const expression: string = `((args) => {
        const fn = ${callback};
        const result = fn.apply(null, args);
        return result;
      })(${JSON.stringify(args)})`;
      try {
        return await frame.evaluate(expression, true);
      } catch (e) {
        console.log(`evaluate error: ${e.toString()}, frameIndex: ${frameIndex}`)
        throw e;
      }
    }

    throw new Error('invalid funcName: ' + funcName);
  }
}