Skip to content

Commit 6a9b44e

Browse files
committed
Mock with har using editorial config
1 parent 259d21c commit 6a9b44e

File tree

93 files changed

+162
-591
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+162
-591
lines changed

e2e/apiMock.ts

+73-105
Original file line numberDiff line numberDiff line change
@@ -6,129 +6,97 @@
66
*
77
*/
88

9-
import { readFile, writeFile, mkdir } from "fs/promises";
10-
import isEqual from "lodash/isEqual.js";
11-
import { Page } from "@playwright/test";
12-
const mockDir = "e2e/apiMocks/";
13-
14-
interface MockRoute {
15-
page: Page;
16-
path: string | RegExp;
17-
fixture: string;
18-
overrideValue?: string | ((value: string) => string);
19-
status?: number;
20-
overrideRoute?: boolean;
21-
}
22-
23-
const skipHttpMethods = ["POST", "PATCH", "PUT", "DELETE"];
9+
import { readFile, writeFile } from "fs/promises";
10+
import { Page, test as Ptest, TestInfo } from "@playwright/test";
2411

25-
/**
26-
* Method for capturing and mocking calls that are not graphql calls.
27-
* We only per now capture get methods.
28-
*
29-
*/
30-
export const mockRoute = async ({ page, path, fixture, overrideValue, overrideRoute, status = 200 }: MockRoute) => {
31-
if (overrideRoute) {
32-
await page.unroute(path);
33-
}
12+
const mockDir = "e2e/apiMocks/";
3413

35-
return await page.route(path, async (route) => {
36-
if (process.env.RECORD_FIXTURES === "true") {
37-
const text = skipHttpMethods.includes(route.request().method()) ? "" : await (await route.fetch()).text();
38-
const override = overrideValue
39-
? typeof overrideValue === "string"
40-
? overrideValue
41-
: overrideValue(text)
42-
: undefined;
43-
await mkdir(mockDir, { recursive: true });
44-
await writeFile(`${mockDir}${fixture}.json`, override ?? text, {
45-
flag: "w",
46-
});
47-
return route.fulfill({ body: text, status });
48-
} else {
49-
try {
50-
const res = await readFile(`${mockDir}${fixture}.json`, "utf8");
51-
return route.fulfill({ body: res, status });
52-
} catch (e) {
53-
route.abort();
54-
}
55-
}
56-
});
57-
};
14+
const apiTestRegex = "https://api.test.ndla.no/.*";
5815

59-
interface GraphqlMockRoute {
60-
page: Page;
61-
operation: {
62-
names: string[];
63-
fixture: string;
64-
}[];
65-
overrideRoute?: boolean;
16+
interface ExtendParams {
17+
harCheckpoint: () => Promise<void>;
6618
}
19+
const regex = new RegExp(`^(${apiTestRegex})$`);
6720

68-
interface GQLBody {
69-
operationName: string;
70-
variables: Record<string, string>;
71-
query: string;
72-
}
21+
const mockFile = ({ titlePath, title: test_name }: TestInfo) => {
22+
const [_dir, SPEC_GROUP, SPEC_NAME] = titlePath[0].split("/");
23+
return `${mockDir}${SPEC_GROUP}_${SPEC_NAME}_${test_name.replace(/\s/g, "_")}.har`;
24+
};
7325

7426
/**
75-
* Method for capturing multiple graphql calls.
76-
* Graphql calls comes in batches of operations
77-
* and this method accepts an object with an array of
78-
* batches GQL operations and fixture name. The call is
79-
* only recorded/mocked if the batched operation names
80-
* match the gql body operation names
81-
*
27+
* Extending the playwright test object with a checkpoint function.
28+
* The checkpoint function helps us differentiate between subsequent
29+
* requests, and allows us to more easily mock recurring calls.
8230
*/
83-
export const mockGraphqlRoute = async ({ page, operation }: GraphqlMockRoute) => {
84-
return await page.route("**/graphql-api/graphql", async (route) => {
85-
if (process.env.RECORD_FIXTURES === "true") {
86-
const body: GQLBody[] | GQLBody = await route.request().postDataJSON();
87-
const resp = await route.fetch();
88-
const text = await resp.text();
31+
export const test = Ptest.extend<ExtendParams>({
32+
harCheckpoint: [
33+
async ({ context, page }, use) => {
34+
let checkpointIndex = 0;
8935

90-
const bodyOperationNames = Array.isArray(body) ? body.map((b) => b.operationName) : [body.operationName];
36+
// Appending the checkpoint index to the request headers
37+
// Only appended for the stored headers in the HAR file
38+
await context.route(
39+
regex,
40+
async (route, request) =>
41+
await route.fallback({
42+
headers: {
43+
...request.headers(),
44+
"X-Playwright-Checkpoint": `${checkpointIndex}`,
45+
},
46+
}),
47+
);
9148

92-
const match = operation.filter((op) => isEqual(bodyOperationNames.sort(), op.names.sort())).pop();
93-
94-
if (match) {
95-
await mkdir(mockDir, { recursive: true });
96-
await writeFile(`${mockDir}${match.fixture}.json`, text, {
97-
flag: "w",
98-
});
99-
return route.fulfill({
100-
body: text,
49+
// Appending the checkpoint index to the request headers
50+
if (process.env.RECORD_FIXTURES === "true") {
51+
await page.setExtraHTTPHeaders({
52+
"X-Playwright-Checkpoint": `${checkpointIndex}`,
10153
});
10254
}
103-
} else {
104-
const body = await route.request().postDataJSON();
105-
const bodyOperationNames = Array.isArray(body) ? body.map((b) => b.operationName) : [body.operationName];
10655

107-
const match = operation.filter((op) => isEqual(bodyOperationNames.sort(), op.names.sort())).pop();
56+
// Appending the new checkpoint index to the request headers
57+
await use(async () => {
58+
checkpointIndex += 1;
59+
process.env.RECORD_FIXTURES !== "true" &&
60+
(await page.setExtraHTTPHeaders({
61+
"X-Playwright-Checkpoint": `${checkpointIndex}`,
62+
}));
63+
});
64+
},
65+
{ auto: true, scope: "test" },
66+
],
67+
page: async ({ page }, use, testInfo) => {
68+
// Creating the API mocking for the wanted API's
69+
await page.routeFromHAR(mockFile(testInfo), {
70+
update: process.env.RECORD_FIXTURES === "true",
71+
updateMode: "minimal",
72+
url: regex,
73+
updateContent: "embed",
74+
});
75+
76+
await use(page);
10877

109-
if (match) {
110-
try {
111-
const res = await readFile(`${mockDir}${match.fixture}.json`, "utf-8");
112-
return route.fulfill({
113-
contentType: "application/json",
114-
body: res,
115-
});
116-
} catch (e) {
117-
route.abort();
118-
}
119-
} else {
120-
const bodyOpNames = `[${bodyOperationNames.sort()}]`;
121-
const availableOpNames = `[${operation.map((op) => `[${op.names}]`)}]`;
122-
console.error(
123-
`[ERROR] Operationname array does not match any results. Update mock array and rerecord test. Operationname: ${bodyOpNames}. Available values: ${availableOpNames}`,
124-
);
125-
}
78+
await page.close();
79+
},
80+
context: async ({ context }, use, testInfo) => {
81+
await use(context);
82+
await context.close();
83+
84+
// Removing sensitive data from the HAR file after saving. Har files are saved on close.
85+
if (process.env.RECORD_FIXTURES === "true") {
86+
await removeSensitiveData(mockFile(testInfo));
12687
}
127-
});
128-
};
88+
},
89+
});
12990

13091
export const mockWaitResponse = async (page: Page, url: string) => {
13192
if (process.env.RECORD_FIXTURES === "true") {
13293
await page.waitForResponse(url);
13394
}
13495
};
96+
97+
// Method to remove sensitive data from the HAR file
98+
// Currently only working to write har file as a single line
99+
const removeSensitiveData = async (fileName: string) => {
100+
const data = JSON.parse(await readFile(fileName, "utf8"));
101+
await writeFile(fileName, JSON.stringify(data), "utf8");
102+
};

e2e/apiMocks/arena_notifications.json

-2
This file was deleted.

0 commit comments

Comments
 (0)