Skip to content

Commit

Permalink
fix: postprocess function will work with update: true (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
NoamGaash committed Feb 20, 2024
1 parent 7016d51 commit b5d1079
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 19 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Expand Up @@ -14,6 +14,7 @@
"rules": {
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"]
"semi": ["error", "always"],
"no-empty-pattern": "off"
}
}
12 changes: 12 additions & 0 deletions playwright.config.ts
Expand Up @@ -20,9 +20,21 @@ export default defineConfig({
trace: "on-first-retry",
},
projects: [
{
name: "setup",
use: { ...devices["Desktop Chrome"] },
testMatch: /setup.ts/,
},
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
dependencies: ["setup"],
teardown: "delete temp har files",
},
{
name: "delete temp har files",
use: { ...devices["Desktop Chrome"] },
testMatch: /teardown.ts/,
}
],
});
17 changes: 15 additions & 2 deletions src/index.ts
@@ -1,7 +1,7 @@
import { test as base } from "@playwright/test";
import * as fs from "fs";
import { AdvancedRouteFromHAR } from "./utils/types";
import { serveFromHar } from "./utils/serveFromHar";
import { AdvancedRouteFromHAR, requestResponseToEntry } from "./utils/types";
import { parseContent, serveFromHar } from "./utils/serveFromHar";
import { defaultMatcher } from "./utils/matchers/defaultMatcher";
export { Matcher, AdvancedRouteFromHAR } from "./utils/types";
export { defaultMatcher } from "./utils/matchers/defaultMatcher";
Expand All @@ -15,6 +15,19 @@ export const test = base.extend<{
const originalRouteFromHAR = page.routeFromHAR.bind(page);
const advancedRouteFromHAR: AdvancedRouteFromHAR = async (filename, options) => {
if (options?.update) {
const {matcher} = options;
if (matcher && "postProcess" in matcher) {
await page.route(options.url || /.*/, async (route, request) => {
const resp = await route.fetch();
const response = matcher.postProcess?.(await requestResponseToEntry(request, resp, await page.context().cookies())).response;
if(response)
route.fulfill({
status: response.status,
headers: Object.fromEntries(response.headers.map((header) => [header.name, header.value])),
body: await parseContent(response.content, path.dirname(filename)),
});
});
}
// on update, we want to record the HAR just like the original playwright method
return originalRouteFromHAR(filename, {
update: true,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/serveFromHar.ts
Expand Up @@ -66,7 +66,7 @@ export function findEntry(
return bestEntry?.entry ?? null;
}

async function parseContent(
export async function parseContent(
content: Omit<Content & { _file?: string }, "text"> & { text?: Buffer | string },
dirName: string = ".",
) {
Expand Down
53 changes: 52 additions & 1 deletion src/utils/types.ts
@@ -1,4 +1,4 @@
import type { Request, Route } from "@playwright/test";
import type { APIResponse, Request, Route, Cookie } from "@playwright/test";
import type { Entry } from "har-format";
import type { findEntry } from "./serveFromHar";

Expand Down Expand Up @@ -56,3 +56,54 @@ export type RouteFromHAROptions = {
};

export type AdvancedRouteFromHAR = (filename: string, options?: RouteFromHAROptions) => Promise<void>;

export async function requestResponseToEntry(request: Request, resp: APIResponse, requestCookies: Cookie[]): Promise<Entry> {
const postData = request.postData();
const body = await resp.body();
const respHeaders = resp.headers();
return {
startedDateTime: new Date().toISOString(),
time: 0, // TODO: get the actual time
cache: {},
timings: {
send: 0, // TODO: get the actual time
wait: 0, // TODO: get the actual time
receive: 0, // TODO: get the actual time
},
request: {
httpVersion: "HTTP/2.0", // TODO: is it possible to get this from playwright?
bodySize: postData ? postData.length : 0,
cookies: requestCookies.map((cookie) => ({
...cookie,
expires: cookie.expires?.toString(),
})),
headersSize: -1, // TODO: get the actual size
queryString: Object.entries(request.url().split("?")[1] ?? {}).map(([name, value]) => ({ name, value })),
url: request.url(),
method: request.method(),
headers: Object.entries(request.headers()).map(([name, value]) => ({ name, value })),
postData: postData ? {
text: postData,
mimeType: request.headers()["content-type"],
} : undefined,
},
response: {
httpVersion: "HTTP/2.0", // TODO: is it possible to get this from playwright?
bodySize: body.length,
cookies: respHeaders["set-cookie"]?.split(";").map((cookie) => {
const [name, value] = cookie.split("=");
return { name, value };
}),
headersSize: -1, // TODO: get the actual size
content: {
text: body.toString(),
size: body.length,
mimeType: respHeaders["content-type"],
},
headers: Object.entries(resp.headers()).map(([name, value]) => ({ name, value })),
redirectURL: resp.url(),
status: resp.status(),
statusText: resp.statusText(),
},
};
}
35 changes: 35 additions & 0 deletions tests/setup.ts
@@ -0,0 +1,35 @@
import { test } from "../lib/index";

test("record", async ({ page, advancedRouteFromHAR }) => {
await advancedRouteFromHAR("tests/har/temp/demo.playwright.dev.har", {
update: true,
updateContent: "embed",
});
await page.goto("https://demo.playwright.dev/todomvc");
await page.close();
});

test("record test with a joke", async ({ page, advancedRouteFromHAR }) => {
await advancedRouteFromHAR("tests/har/temp/joke.har", {
update: true,
updateContent: "embed",
});
await page.goto("https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit");
await page.close();
});

test("record test with a joke and postprocess", async ({ page, advancedRouteFromHAR }) => {
await advancedRouteFromHAR("tests/har/temp/joke-postprocess.har", {
update: true,
updateContent: "embed",
matcher: {
postProcess(entry) {
entry.response.content.text = "This is a joke";
return entry;
}
}
});
await page.goto("https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit");
await page.waitForSelector("text=This is a joke");
await page.close();
});
7 changes: 7 additions & 0 deletions tests/teardown.ts
@@ -0,0 +1,7 @@
import { test } from "../lib/index";
import fs from "fs";

test("teardown", async ({}) => {
// clean up
await fs.promises.rmdir("tests/har/temp", { recursive: true });
});
74 changes: 60 additions & 14 deletions tests/test.spec.ts
Expand Up @@ -24,25 +24,13 @@ test("sanity with content attached", async ({ page, advancedRouteFromHAR }) => {
await page.getByRole("heading", { name: "Example Domain" }).waitFor();
});

test.skip("record", async ({ page, advancedRouteFromHAR }) => {
// todo: see what Playwright did with the contextFactory https://github.com/microsoft/playwright/blob/c3b533d8341d72d45b7296c7a895ff9fe7d8ff3b/tests/library/browsercontext-har.spec.ts#L342
await advancedRouteFromHAR("tests/har/temp-record.har", {
update: true,
updateContent: "embed",
});
await page.goto("https://demo.playwright.dev/todomvc");
await page.close();

const data = await waitForFile("tests/har/temp-record.har");
test("validate recorded har", async ({}) => {
const data = await waitForFile("tests/har/temp/demo.playwright.dev.har");
const har = JSON.parse(data);
expect(har.log.entries.length).toBeGreaterThan(0);
expect(har.log.entries[0].request.url).toBe("https://demo.playwright.dev/todomvc");
expect(har.log.entries[0].response.status).toBeGreaterThanOrEqual(200);
expect(har.log.entries[0].response.status).toBeLessThan(400);

await new Promise((resolve) => setTimeout(resolve, 1000));
// clean up
await fs.promises.rm("tests/har/temp-record.har");
});

test("sanity with matcher", async ({ page, advancedRouteFromHAR }) => {
Expand Down Expand Up @@ -182,3 +170,61 @@ async function waitForFile(path: string) {
}
throw "can't read file";
}

test("test a joke recording with postprocess", async ({ page, advancedRouteFromHAR }) => {
await advancedRouteFromHAR("tests/har/temp/joke-postprocess.har", {
update: true,
updateContent: "embed",
matcher: {
postProcess(entry) {
entry.response.content.text = "This is a joke";
return entry;
}
}
});
await page.goto("https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit");
await page.waitForSelector("text=This is a joke");
await page.close();
});

test("test a joke recording with different postprocess that was not recorded", async ({ page, advancedRouteFromHAR }) => {
await advancedRouteFromHAR("tests/har/temp/joke-postprocess.har", {
update: true,
updateContent: "embed",
matcher: {
postProcess(entry) {
entry.response.content.text = "This is not a joke";
return entry;
}
}
});
await page.goto("https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit");
await page.waitForSelector("text=This is not a joke");
await page.close();
});

test("test a postprocess that change only part of the output", async ({ page, advancedRouteFromHAR }) => {
await advancedRouteFromHAR("tests/har/temp/joke-postprocess.har", {
update: true,
updateContent: "embed",
matcher: {
postProcess(entry) {
const json = JSON.parse(entry.response.content.text ?? "{}");
console.log(json);
console.log(entry.response.content.text);
json.flags.custom = true;
entry.response.content.text = JSON.stringify(json);
return entry;
}
}
});
await page.goto("https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit");
const flags = await page.evaluate(() => {
return JSON.parse(document.body.textContent ?? "{}").flags;
});
expect(flags.custom).toBe(true);
expect(flags.nsfw).toBe(false);
await page.close();
});


0 comments on commit b5d1079

Please sign in to comment.