Skip to content

Commit

Permalink
feat: expose runner APIs for running individual steps (#215)
Browse files Browse the repository at this point in the history
The idea behind this PR is to make the code we generate more composable: instead of providing the entire recording one can run individual steps using .runStep method and, for example, easily integrate it into control flow statements. 

BREAKING CHANGE: the user flow parameter in the runner extensions is now optional
  • Loading branch information
OrKoN committed Oct 25, 2022
1 parent 50aae4a commit 4ef380d
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 26 deletions.
6 changes: 3 additions & 3 deletions src/PuppeteerRunnerExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ export class PuppeteerRunnerExtension extends RunnerExtension {
}
}

#getTimeoutForStep(step: Step, flow: UserFlow): number {
return step.timeout || flow.timeout || this.timeout;
#getTimeoutForStep(step: Step, flow?: UserFlow): number {
return step.timeout || flow?.timeout || this.timeout;
}

override async runStep(step: Step, flow: UserFlow): Promise<void> {
override async runStep(step: Step, flow?: UserFlow): Promise<void> {
const timeout = this.#getTimeoutForStep(step, flow);
const page = this.page;
const browser = this.browser;
Expand Down
58 changes: 40 additions & 18 deletions src/Runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@

import { PuppeteerRunnerOwningBrowserExtension } from './PuppeteerRunnerExtension.js';
import { RunnerExtension } from './RunnerExtension.js';
import { UserFlow } from './Schema.js';
import { UserFlow, Step } from './Schema.js';

async function _runStepWithHooks(
extension: RunnerExtension,
step: Step,
flow?: UserFlow
) {
await extension.beforeEachStep?.(step, flow);
await extension.runStep(step, flow);
await extension.afterEachStep?.(step, flow);
}

export class Runner {
#flow: UserFlow;
Expand All @@ -35,40 +45,52 @@ export class Runner {
this.#aborted = true;
}

async runStep(step: Step): Promise<void> {
await _runStepWithHooks(this.#extension, step);
}

/**
* Run all the steps in the flow
* @returns whether all the steps are run or the execution is aborted
*/
async run(): Promise<boolean> {
this.#aborted = false;

await this.#extension.beforeAllSteps?.(this.#flow);

let nextStepIndex = 0;
while (nextStepIndex < this.#flow.steps.length && !this.#aborted) {
const nextStep = this.#flow.steps[nextStepIndex]!;
await this.#extension.beforeEachStep?.(nextStep, this.#flow);
await this.#extension.runStep(nextStep, this.#flow);
await this.#extension.afterEachStep?.(nextStep, this.#flow);
nextStepIndex++;
if (this.#aborted) {
return false;
}

for (const step of this.#flow.steps) {
if (this.#aborted) {
await this.#extension.afterAllSteps?.(this.#flow);
return false;
}
await _runStepWithHooks(this.#extension, step, this.#flow);
}

await this.#extension.afterAllSteps?.(this.#flow);

return nextStepIndex >= this.#flow.steps.length;
return true;
}
}

export async function createRunner(
flow: UserFlow,
extension?: RunnerExtension
) {
if (!extension) {
const { default: puppeteer } = await import('puppeteer');
const browser = await puppeteer.launch({
headless: true,
});
const page = await browser.newPage();
extension = new PuppeteerRunnerOwningBrowserExtension(browser, page);
}
return new Runner(flow, extension);
return new Runner(
flow,
extension ?? (await createPuppeteerRunnerOwningBrowserExtension())
);
}

async function createPuppeteerRunnerOwningBrowserExtension() {
const { default: puppeteer } = await import('puppeteer');
const browser = await puppeteer.launch({
headless: true,
});
const page = await browser.newPage();
return new PuppeteerRunnerOwningBrowserExtension(browser, page);
}
10 changes: 5 additions & 5 deletions src/RunnerExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
import { UserFlow, Step } from './Schema.js';

export class RunnerExtension {
async beforeAllSteps?(flow: UserFlow): Promise<void> {}
async afterAllSteps?(flow: UserFlow): Promise<void> {}
async beforeEachStep?(step: Step, flow: UserFlow): Promise<void> {}
async runStep(step: Step, flow: UserFlow): Promise<void> {}
async afterEachStep?(step: Step, flow: UserFlow): Promise<void> {}
async beforeAllSteps?(flow?: UserFlow): Promise<void> {}
async afterAllSteps?(flow?: UserFlow): Promise<void> {}
async beforeEachStep?(step: Step, flow?: UserFlow): Promise<void> {}
async runStep(step: Step, flow?: UserFlow): Promise<void> {}
async afterEachStep?(step: Step, flow?: UserFlow): Promise<void> {}
}
32 changes: 32 additions & 0 deletions test/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,38 @@ describe('Runner', () => {
);
});

it('should replay individual steps', async () => {
const runner = await createRunner(
{
title: 'Test Recording',
timeout: 3000,
steps: [],
},
new PuppeteerRunnerExtension(browser, page)
);
await runner.runStep({
type: StepType.Navigate as const,
url: `${HTTP_PREFIX}/main.html`,
assertedEvents: [
{
title: '',
type: AssertedEventType.Navigation,
url: `${HTTP_PREFIX}/main.html`,
},
],
});
await runner.runStep({
type: StepType.Hover as const,
selectors: [['#hover-button']],
});
assert.ok(
await page.evaluate(
() => document.getElementById('hover-button')?.textContent
),
'Hovered'
);
});

describe('abort', () => {
it('should abort execution of remaining steps', async () => {
class AbortAfterFirstStepExtension extends RunnerExtension {
Expand Down

0 comments on commit 4ef380d

Please sign in to comment.