Skip to content

Commit

Permalink
Add read alerts option (#101)
Browse files Browse the repository at this point in the history
* Add option to read and return popped alerts
  • Loading branch information
Miłosz Skaza authored Apr 8, 2023
1 parent 884f40c commit 013094a
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 5 deletions.
2 changes: 2 additions & 0 deletions src/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum JobOptions {
RECORD = "RECORD",
SCREENSHOT = "SCREENSHOT",
PDF = "PDF",
READ_ALERTS = "READ_ALERTS",
}

export enum CookieSameSite {
Expand Down Expand Up @@ -46,6 +47,7 @@ export const JobResult = Type.Object({
screenshot: Type.Optional(Type.String()),
video: Type.Optional(Type.String()),
pdf: Type.Optional(Type.String()),
messages: Type.Optional(Type.Array(Type.String())),
});

export type JobResultType = Static<typeof JobResult>;
Expand Down
63 changes: 58 additions & 5 deletions src/utils/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ export class PlaywrightRunner {
public context?: BrowserContext;
public page?: Page;

public options: JobOptions[] = [];
public readonly browserKind: JobBrowser;
public readonly steps: JobStepType[];
public readonly cookies: JobCookieType[];
public options: JobOptions[] = [];

private _dialogMessages: string[] = [];
private readonly _debug: boolean;

constructor(data: PlaywrightRunnerData, debug: boolean = false) {
Expand All @@ -43,7 +45,14 @@ export class PlaywrightRunner {
// init() initializes playwright: browser, context and page
// it is intentionally separated from exec to allow for modifying playwright
public async init() {
const launchOptions: LaunchOptions = {};
const launchOptions: LaunchOptions = {
// adding minimal slowMo resolves all sorts of issues regarding waiting for js
// execution or load state - without requiring to always wait for networkidle.
// it's an intentional trade-off - as it's better to always wait 50ms longer
// than to have to wait for networkidle state which may lead to stalling.
slowMo: 50,
};

if (this._debug) {
launchOptions.slowMo = 1000;
launchOptions.headless = false;
Expand Down Expand Up @@ -104,7 +113,7 @@ export class PlaywrightRunner {
);
}

// provide the page object to the isolated context
// provide the page & context objects to the isolated context
const vm = new VM({
eval: false,
wasm: false,
Expand All @@ -131,8 +140,48 @@ export class PlaywrightRunner {
}
}

await this.page.goto(step.url);
await this.page.waitForLoadState();
// register handlers required for reading alerts
// they will overwrite any user-provided context.on("page") as well as
// page.on("dialog") handlers, which is intended
if (this.options.includes(JobOptions.READ_ALERTS)) {
// handler for reading alerts
const onDialog = async (dialog: playwright.Dialog) => {
// only read text from alerts - skip confirms, prompts and beforeunload
if (dialog.type() === "alert") {
try {
const msg = dialog.message();
await dialog.dismiss();

// only push the value if dialog hasn't been handled (dismiss doesn't throw an error)
this._dialogMessages.push(msg);
} catch (e: any) {
// playwright will throw an error if dialog has already been handled
}
}
};

// add listener for current page
this.page.on("dialog", onDialog);

// add listener for possible new pages / popups
this.context.on("page", async (page: playwright.Page) => {
page.on("dialog", onDialog);

// auto-close new pages after they reach load state.
// control over this can't be provided to the user because
// READ_ALERTS option needs to overwrite any handlers for new pages.
await page.waitForLoadState();
await page.close();
});
}

try {
await this.page.goto(step.url);
await this.page.waitForLoadState();
} catch (e) {
await this.teardown();
throw new Error(`[runtime] failed navigating to URL: '${step.url}'`);
}

// run all post-open actions for this step
if (step.actions && actions.postOpen) {
Expand Down Expand Up @@ -160,6 +209,10 @@ export class PlaywrightRunner {

const result: JobResultType = {};

if (this.options.includes(JobOptions.READ_ALERTS)) {
result.messages = [...this._dialogMessages];
}

if (this.options.includes(JobOptions.SCREENSHOT)) {
const screenshotBuffer = await this.page!.screenshot({ fullPage: true });
result.screenshot = screenshotBuffer.toString("base64");
Expand Down
121 changes: 121 additions & 0 deletions tests/async-job.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,127 @@ test("POST '/api/v1/async-job' creates screenshot", async (t) => {
t.assert(result.result.hasOwnProperty("screenshot"));
t.is(base64regex.test(result.result.screenshot), true);
});

test("POST '/api/v1/async-job' reads alerts", async (t) => {
const { app, testAppURL } = t.context;

const response = await app.inject({
method: "POST",
url: "/api/v1/async-job",
payload: {
steps: [{ url: `${testAppURL}/alert` }],
options: [JobOptions.READ_ALERTS],
},
});

t.is(response.statusCode, 200);
const data = response.json();
t.assert(data.hasOwnProperty("status"));
t.assert(data.hasOwnProperty("id"));
t.is(data.status, "scheduled");
t.assert(typeof data.id === "number");

const result = await asyncJobResult(app, data.id);
t.assert(result.hasOwnProperty("status"));
t.is(result.status, "success");

t.assert(result.hasOwnProperty("result"));
t.assert(result.result.hasOwnProperty("messages"));
t.deepEqual(result.result.messages, ["1"]);
});

test("POST '/api/v1/async-job' reads multiple alerts", async (t) => {
const { app, testAppURL } = t.context;

const response = await app.inject({
method: "POST",
url: "/api/v1/async-job",
payload: {
steps: [
{ url: `${testAppURL}/alert` },
{ url: `${testAppURL}/alert` },
{ url: `${testAppURL}/alert` },
],
options: [JobOptions.READ_ALERTS],
},
});

t.is(response.statusCode, 200);
const data = response.json();
t.assert(data.hasOwnProperty("status"));
t.assert(data.hasOwnProperty("id"));
t.is(data.status, "scheduled");
t.assert(typeof data.id === "number");

const result = await asyncJobResult(app, data.id);
t.assert(result.hasOwnProperty("status"));
t.is(result.status, "success");

t.assert(result.hasOwnProperty("result"));
t.assert(result.result.hasOwnProperty("messages"));
t.deepEqual(result.result.messages, ["1", "1", "1"]);
});

test("POST '/api/v1/async-job' reads alerts from popup windows", async (t) => {
const { app, testAppURL } = t.context;

const response = await app.inject({
method: "POST",
url: "/api/v1/async-job",
payload: {
steps: [{ url: `${testAppURL}/popup?to=/alert` }],
options: [JobOptions.READ_ALERTS],
},
});

t.is(response.statusCode, 200);
const data = response.json();
t.assert(data.hasOwnProperty("status"));
t.assert(data.hasOwnProperty("id"));
t.is(data.status, "scheduled");
t.assert(typeof data.id === "number");

const result = await asyncJobResult(app, data.id);
t.assert(result.hasOwnProperty("status"));
t.is(result.status, "success");

t.assert(result.hasOwnProperty("result"));
t.assert(result.result.hasOwnProperty("messages"));
t.deepEqual(result.result.messages, ["1"]);
});

test("POST '/api/v1/async-job' reads multiple alerts from popup windows", async (t) => {
const { app, testAppURL } = t.context;

const response = await app.inject({
method: "POST",
url: "/api/v1/async-job",
payload: {
steps: [
{ url: `${testAppURL}/popup?to=/alert` },
{ url: `${testAppURL}/popup?to=/alert` },
{ url: `${testAppURL}/popup?to=/alert` },
],
options: [JobOptions.READ_ALERTS],
},
});

t.is(response.statusCode, 200);
const data = response.json();
t.assert(data.hasOwnProperty("status"));
t.assert(data.hasOwnProperty("id"));
t.is(data.status, "scheduled");
t.assert(typeof data.id === "number");

const result = await asyncJobResult(app, data.id);
t.assert(result.hasOwnProperty("status"));
t.is(result.status, "success");

t.assert(result.hasOwnProperty("result"));
t.assert(result.result.hasOwnProperty("messages"));
t.deepEqual(result.result.messages, ["1", "1", "1"]);
});

test("POST '/api/v1/async-job' creates all of pdf, screenshot, video", async (t) => {
const { app, testAppURL } = t.context;

Expand Down
26 changes: 26 additions & 0 deletions tests/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -838,3 +838,29 @@ test("PlaywrightRunner closes the browser if an error occurs", async (t) => {

t.assert(runner_2.browser === undefined);
});

test("PlaywrightRunner closes the browser if page fails to connect", async (t) => {
const runner = new PlaywrightRunner({
browser: JobBrowser.CHROMIUM,
steps: [
{
url: `http://nonexistent/`,
},
],
cookies: [],
options: [],
});

await t.throwsAsync(
async () => {
await runner.init();
await runner.exec();
await runner.finish();
},
{
message: `[runtime] failed navigating to URL: 'http://nonexistent/'`,
},
);

t.assert(runner.browser === undefined);
});
48 changes: 48 additions & 0 deletions tests/snapshots/swagger.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ Generated by [AVA](https://avajs.dev).
],
type: 'string',
},
{
enum: [
'READ_ALERTS',
],
type: 'string',
},
],
},
type: 'array',
Expand Down Expand Up @@ -388,6 +394,12 @@ Generated by [AVA](https://avajs.dev).
properties: {
result: {
properties: {
messages: {
items: {
type: 'string',
},
type: 'array',
},
pdf: {
type: 'string',
},
Expand Down Expand Up @@ -633,6 +645,12 @@ Generated by [AVA](https://avajs.dev).
],
type: 'string',
},
{
enum: [
'READ_ALERTS',
],
type: 'string',
},
],
},
type: 'array',
Expand Down Expand Up @@ -679,6 +697,12 @@ Generated by [AVA](https://avajs.dev).
properties: {
result: {
properties: {
messages: {
items: {
type: 'string',
},
type: 'array',
},
pdf: {
type: 'string',
},
Expand Down Expand Up @@ -946,6 +970,12 @@ Generated by [AVA](https://avajs.dev).
],
type: 'string',
},
{
enum: [
'READ_ALERTS',
],
type: 'string',
},
],
},
type: 'array',
Expand Down Expand Up @@ -1194,6 +1224,12 @@ Generated by [AVA](https://avajs.dev).
properties: {
result: {
properties: {
messages: {
items: {
type: 'string',
},
type: 'array',
},
pdf: {
type: 'string',
},
Expand Down Expand Up @@ -1439,6 +1475,12 @@ Generated by [AVA](https://avajs.dev).
],
type: 'string',
},
{
enum: [
'READ_ALERTS',
],
type: 'string',
},
],
},
type: 'array',
Expand Down Expand Up @@ -1485,6 +1527,12 @@ Generated by [AVA](https://avajs.dev).
properties: {
result: {
properties: {
messages: {
items: {
type: 'string',
},
type: 'array',
},
pdf: {
type: 'string',
},
Expand Down
Binary file modified tests/snapshots/swagger.test.ts.snap
Binary file not shown.
Loading

0 comments on commit 013094a

Please sign in to comment.