Skip to content

Commit b39b723

Browse files
authored
Feat/improve networkintercept (#17)
* feat: remove log statements * feat: add mock bridge code to use sw feature * test: add mock bridge tests * docs: update examples with service mock * feat: update twd api * feat: remove old fething monkey patch code * feat: improve coverage
1 parent 915b12c commit b39b723

11 files changed

Lines changed: 383 additions & 196 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// mock-sw.js
2+
3+
// Storage for rules inside the SW
4+
let rules = [];
5+
6+
/**
7+
* Match a request against rules
8+
*/
9+
const findRule = (method, url) => {
10+
return rules.find(
11+
(r) =>
12+
r.method === method &&
13+
(typeof r.url === "string" ? r.url === url : new RegExp(r.url).test(url))
14+
);
15+
};
16+
17+
// Intercept fetches
18+
self.addEventListener("fetch", (event) => {
19+
const { method } = event.request;
20+
const url = event.request.url;
21+
22+
const rule = findRule(method, url);
23+
24+
if (rule) {
25+
console.log("Mock hit:", rule.alias, method, url);
26+
27+
event.respondWith(
28+
(async () => {
29+
// Capture body if needed
30+
let body = null;
31+
try {
32+
body = await event.request.clone().text();
33+
} catch {}
34+
35+
// Mark executed and notify page
36+
self.clients.matchAll().then((clients) => {
37+
clients.forEach((client) =>
38+
client.postMessage({
39+
type: "EXECUTED",
40+
alias: rule.alias,
41+
request: body,
42+
})
43+
);
44+
});
45+
46+
return new Response(JSON.stringify(rule.response), {
47+
status: rule.status || 200,
48+
headers: rule.headers || { "Content-Type": "application/json" },
49+
});
50+
})()
51+
);
52+
}
53+
});
54+
55+
// Listen for messages from the app
56+
self.addEventListener("message", (event) => {
57+
const { type, rule } = event.data || {};
58+
if (type === "ADD_RULE") {
59+
rules = rules.filter((r) => r.alias !== rule.alias);
60+
rules.push(rule);
61+
console.log("Rule added:", rule);
62+
}
63+
});

examples/my-twd-app/src/twd-tests/app.twd.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ describe("App interactions", () => {
2828
});
2929

3030
it("fetches a joke", async () => {
31-
twd.mockRequest("joke", {
31+
await twd.initRequestMocking();
32+
await twd.mockRequest("joke", {
3233
method: "GET",
3334
url: "https://api.chucknorris.io/jokes/random",
3435
response: {
@@ -38,12 +39,12 @@ describe("App interactions", () => {
3839
let btn = await twd.get("button[data-twd='joke-button']");
3940
btn.click();
4041
// Wait for the mock fetch to fire
41-
await twd.waitFor("joke");
42+
await twd.waitForRequest("joke");
4243
let jokeText = await twd.get("p[data-twd='joke-text']");
4344
// console.log(`Joke text: ${jokeText.el.textContent}`);
4445
jokeText.should("have.text", "Mocked joke!");
4546
// overwrite mid-test
46-
twd.mockRequest("joke", {
47+
await twd.mockRequest("joke", {
4748
method: "GET",
4849
url: "https://api.chucknorris.io/jokes/random",
4950
response: {
@@ -52,10 +53,11 @@ describe("App interactions", () => {
5253
});
5354
btn = await twd.get("button[data-twd='joke-button']");
5455
btn.click();
55-
await twd.waitFor("joke");
56+
await twd.waitForRequest("joke");
5657
jokeText = await twd.get("p[data-twd='joke-text']");
58+
jokeText.should("have.text", "Mocked second joke!");
5759
// console.log(`Joke text: ${jokeText.el.textContent}`);
58-
jokeText.should('be.disabled');
60+
// jokeText.should('be.disabled');
5961
});
6062

6163
it("visit contact page", async () => {
@@ -71,7 +73,7 @@ describe("App interactions", () => {
7173
messageInput.type("Hello, this is a test message.");
7274
const submitBtn = await twd.get("button[type='submit']");
7375
submitBtn.click();
74-
const rule = await twd.waitFor("contactSubmit");
76+
const rule = await twd.waitForRequest("contactSubmit");
7577
console.log(`Submitted body: ${rule.request}`);
7678
});
7779
});

src/commands/mockBridge.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// mockBridge.ts
2+
export type Rule = {
3+
method: string;
4+
url: string | RegExp;
5+
response: unknown;
6+
alias: string;
7+
executed?: boolean;
8+
request?: unknown;
9+
status?: number;
10+
headers?: Record<string, string>;
11+
};
12+
13+
export interface Options {
14+
method: string;
15+
url: string | RegExp;
16+
response: unknown;
17+
status?: number;
18+
headers?: Record<string, string>;
19+
}
20+
21+
const rules: Rule[] = [];
22+
const waiters: Record<string, (rule: Rule) => void> = {};
23+
const SW_DELAY = 100;
24+
25+
/**
26+
* Initialize the mocking service worker.
27+
* Call this once before using `mockRequest` or `waitFor`.
28+
*/
29+
export const initRequestMocking = async () => {
30+
if ("serviceWorker" in navigator) {
31+
await navigator.serviceWorker.register("/mock-sw.js?v=1");
32+
navigator.serviceWorker.addEventListener("message", (event) => {
33+
if (event.data?.type === "EXECUTED") {
34+
const { alias, request } = event.data;
35+
const rule = rules.find((r) => r.alias === alias);
36+
if (rule) {
37+
rule.executed = true;
38+
rule.request = request;
39+
if (waiters[alias]) {
40+
waiters[alias](rule);
41+
delete waiters[alias];
42+
}
43+
}
44+
}
45+
});
46+
}
47+
};
48+
49+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
50+
51+
/**
52+
* Mock a network request.
53+
*
54+
* @param alias Identifier for the mock rule. Useful for `waitFor()`.
55+
* @param options Options to configure the mock:
56+
* - `method`: HTTP method ("GET", "POST", …)
57+
* - `url`: URL string or RegExp to match
58+
* - `response`: Body of the mocked response
59+
* - `status`: (optional) HTTP status code (default: 200)
60+
* - `headers`: (optional) Response headers
61+
*
62+
* @example
63+
* ```ts
64+
* mockRequest("getUser", {
65+
* method: "GET",
66+
* url: /\/api\/user\/\d+/,
67+
* response: { id: 1, name: "Kevin" },
68+
* status: 200,
69+
* headers: { "x-mock": "true" }
70+
* });
71+
* ```
72+
*/
73+
export const mockRequest = async (alias: string, options: Options) => {
74+
const rule: Rule = { alias, ...options, executed: false };
75+
const idx = rules.findIndex((r) => r.alias === alias);
76+
if (idx !== -1) rules[idx] = rule;
77+
else rules.push(rule);
78+
79+
// Push to SW
80+
navigator.serviceWorker.controller?.postMessage({
81+
type: "ADD_RULE",
82+
rule,
83+
});
84+
await sleep(SW_DELAY);
85+
await Promise.resolve();
86+
};
87+
88+
/**
89+
* Wait for a mocked request to be made.
90+
* @param alias The alias of the mock rule to wait for
91+
* @returns The matched rule (with body if applicable)
92+
*/
93+
export const waitForRequest = async (alias: string): Promise<Rule> => {
94+
await sleep(SW_DELAY);
95+
const rule = rules.find((r) => r.alias === alias && r.executed);
96+
if (rule) return Promise.resolve(rule);
97+
return new Promise((resolve) => {
98+
waiters[alias] = resolve;
99+
});
100+
};
101+
102+
/**
103+
* Get the current list of request mock rules.
104+
* @returns The current list of request mock rules.
105+
*/
106+
export const getRequestMockRules = () => rules;
107+
108+
/**
109+
* Clear all request mock rules.
110+
*/
111+
export const clearRequestMockRules = () => {
112+
rules.length = 0;
113+
};

src/commands/mockResponses.ts

Lines changed: 0 additions & 148 deletions
This file was deleted.

0 commit comments

Comments
 (0)