it doesn't seem possible to use IPuppetFrame.evaluate with css-tricks.com domain
andynuss opened this issue · 0 comments
andynuss commented
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);
}
}