feat(core): perform workflow steps
This commit is contained in:
@@ -225,5 +225,124 @@ export default class Interpreter extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a Playwright's page object and a "declarative" list of actions, this function
|
||||||
|
* calls all mentioned functions on the Page object.\
|
||||||
|
* \
|
||||||
|
* Manipulates the iterator indexes (experimental feature, likely to be removed in
|
||||||
|
* the following versions of waw-interpreter)
|
||||||
|
* @param page Playwright Page object
|
||||||
|
* @param steps Array of actions.
|
||||||
|
*/
|
||||||
|
private async carryOutSteps(page: Page, steps: What[]) : Promise<void> {
|
||||||
|
/**
|
||||||
|
* Defines overloaded (or added) methods/actions usable in the workflow.
|
||||||
|
* If a method overloads any existing method of the Page class, it accepts the same set
|
||||||
|
* of parameters *(but can override some!)*\
|
||||||
|
* \
|
||||||
|
* Also, following piece of code defines functions to be run in the browser's context.
|
||||||
|
* Beware of false linter errors - here, we know better!
|
||||||
|
*/
|
||||||
|
const wawActions : Record<CustomFunctions, (...args: any[]) => void> = {
|
||||||
|
screenshot: async (params: PageScreenshotOptions) => {
|
||||||
|
const screenshotBuffer = await page.screenshot({
|
||||||
|
...params, path: undefined,
|
||||||
|
});
|
||||||
|
await this.options.binaryCallback(screenshotBuffer, 'image/png');
|
||||||
|
},
|
||||||
|
enqueueLinks: async (selector : string) => {
|
||||||
|
const links : string[] = await page.locator(selector)
|
||||||
|
.evaluateAll(
|
||||||
|
// @ts-ignore
|
||||||
|
(elements) => elements.map((a) => a.href).filter((x) => x),
|
||||||
|
);
|
||||||
|
const context = page.context();
|
||||||
|
|
||||||
|
for (const link of links) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.concurrency.addJob(async () => {
|
||||||
|
try {
|
||||||
|
const newPage = await context.newPage();
|
||||||
|
await newPage.goto(link);
|
||||||
|
await newPage.waitForLoadState('networkidle');
|
||||||
|
await this.runLoop(newPage, this.initializedWorkflow!);
|
||||||
|
} catch (e) {
|
||||||
|
// `runLoop` uses soft mode, so it recovers from it's own exceptions
|
||||||
|
// but newPage(), goto() and waitForLoadState() don't (and will kill
|
||||||
|
// the interpreter by throwing).
|
||||||
|
this.log(<Error>e, Level.ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await page.close();
|
||||||
|
},
|
||||||
|
scrape: async (selector?: string) => {
|
||||||
|
const scrapeResults : Record<string, string>[] = <any> await page
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore
|
||||||
|
.evaluate((s) => scrape(s ?? null), selector);
|
||||||
|
await this.options.serializableCallback(scrapeResults);
|
||||||
|
},
|
||||||
|
scrapeSchema: async (schema: Record<string, string>) => {
|
||||||
|
const handleLists = await Promise.all(
|
||||||
|
Object.values(schema).map((selector) => page.$$(selector)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const namedHandleLists = Object.fromEntries(
|
||||||
|
Object.keys(schema).map((key, i) => [key, handleLists[i]]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrapeResult = await page.evaluate((n) => scrapeSchema(n), namedHandleLists);
|
||||||
|
|
||||||
|
this.options.serializableCallback(scrapeResult);
|
||||||
|
},
|
||||||
|
scroll: async (pages? : number) => {
|
||||||
|
await page.evaluate(async (pagesInternal) => {
|
||||||
|
for (let i = 1; i <= (pagesInternal ?? 1); i += 1) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.scrollTo(0, window.scrollY + window.innerHeight);
|
||||||
|
}
|
||||||
|
}, pages ?? 1);
|
||||||
|
},
|
||||||
|
script: async (code : string) => {
|
||||||
|
const AsyncFunction : FunctionConstructor = Object.getPrototypeOf(
|
||||||
|
async () => {},
|
||||||
|
).constructor;
|
||||||
|
const x = new AsyncFunction('page', 'log', code);
|
||||||
|
await x(page, this.log);
|
||||||
|
},
|
||||||
|
flag: async () => new Promise((res) => {
|
||||||
|
this.emit('flag', page, res);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const step of steps) {
|
||||||
|
this.log(`Launching ${step.action}`, Level.LOG);
|
||||||
|
|
||||||
|
if (step.action in wawActions) {
|
||||||
|
// "Arrayifying" here should not be needed (TS + syntax checker - only arrays; but why not)
|
||||||
|
const params = !step.args || Array.isArray(step.args) ? step.args : [step.args];
|
||||||
|
await wawActions[step.action as CustomFunctions](...(params ?? []));
|
||||||
|
} else {
|
||||||
|
// Implements the dot notation for the "method name" in the workflow
|
||||||
|
const levels = step.action.split('.');
|
||||||
|
const methodName = levels[levels.length - 1];
|
||||||
|
|
||||||
|
let invokee : any = page;
|
||||||
|
for (const level of levels.splice(0, levels.length - 1)) {
|
||||||
|
invokee = invokee[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!step.args || Array.isArray(step.args)) {
|
||||||
|
await (<any>invokee[methodName])(...(step.args ?? []));
|
||||||
|
} else {
|
||||||
|
await (<any>invokee[methodName])(step.args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((res) => { setTimeout(res, 500); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user