Skip to content

Commit 87eb23a

Browse files
authored
Fix/mock bridge (#22)
* test: improve missing coverage * refactor: remove code duplication * fix: improve coverage * fix: intercept path error * feat: run coverage report only on main
1 parent f8e6ab5 commit 87eb23a

15 files changed

Lines changed: 197 additions & 108 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737

3838
coverage:
3939
runs-on: ubuntu-latest
40+
if: github.ref == 'refs/heads/main'
4041
steps:
4142
- name: Checkout repo
4243
uses: actions/checkout@v5

src/cli/mock-sw.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,8 @@ self.addEventListener("message", (event) => {
4444
rules.push(rule);
4545
console.log("Rule added:", rule);
4646
}
47+
if (type === "CLEAR_RULES") {
48+
rules = [];
49+
console.log("All rules cleared");
50+
}
4751
});

src/cli/utils/findRule.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
export function findRule(method, url, rules) {
55
return rules.find(
6-
(r) =>
7-
r.method === method &&
8-
(typeof r.url === 'string' ? r.url === url : new RegExp(r.url).test(url))
9-
);
6+
(r) => {
7+
const isMethodMatch = r.method.toLowerCase() === method.toLowerCase();
8+
const isUrlMatch = typeof url === 'string'
9+
? r.url === url || r.url.includes(url) : new RegExp(url).test(r.url);
10+
return isMethodMatch && isUrlMatch;
11+
});
1012
}

src/commands/mockBridge.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export interface Options {
1919
}
2020

2121
const rules: Rule[] = [];
22-
const waiters: Record<string, (rule: Rule) => void> = {};
2322
const SW_DELAY = 100;
2423

2524
/**
@@ -36,10 +35,6 @@ export const initRequestMocking = async () => {
3635
if (rule) {
3736
rule.executed = true;
3837
rule.request = request;
39-
if (waiters[alias]) {
40-
waiters[alias](rule);
41-
delete waiters[alias];
42-
}
4338
}
4439
}
4540
});
@@ -94,9 +89,7 @@ export const waitForRequest = async (alias: string): Promise<Rule> => {
9489
await sleep(SW_DELAY);
9590
const rule = rules.find((r) => r.alias === alias && r.executed);
9691
if (rule) return Promise.resolve(rule);
97-
return new Promise((resolve) => {
98-
waiters[alias] = resolve;
99-
});
92+
throw new Error("Rule not found or not executed");
10093
};
10194

10295
/**
@@ -109,5 +102,9 @@ export const getRequestMockRules = () => rules;
109102
* Clear all request mock rules.
110103
*/
111104
export const clearRequestMockRules = () => {
105+
// Also tell the SW
106+
navigator.serviceWorker.controller?.postMessage({
107+
type: "CLEAR_RULES",
108+
});
112109
rules.length = 0;
113110
};

src/tests/cli/findRule.spec.js

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,51 @@ describe('findRule', () => {
1414

1515
it('matches by regex url', () => {
1616
const rules = [
17-
{ method: 'GET', url: /^https:\/\/foo\..+/, alias: 'a' },
17+
{ method: 'GET', url: 'https://foo.com', alias: 'a' },
1818
];
19-
expect(findRule('GET', 'https://foo.com', rules)).toEqual(rules[0]);
20-
expect(findRule('GET', 'https://foo.org', rules)).toEqual(rules[0]);
19+
expect(findRule('GET', /^https:\/\/foo\..+/, rules)).toEqual(rules[0]);
2120
expect(findRule('GET', 'https://bar.com', rules)).toBeUndefined();
2221
});
22+
23+
it('returns the first matching rule', () => {
24+
const rules = [
25+
{ method: 'GET', url: 'https://foo.com', alias: 'first' },
26+
{ method: 'GET', url: 'https://foo.com', alias: 'second' },
27+
];
28+
expect(findRule('GET', 'https://foo.com', rules)).toEqual(rules[0]);
29+
});
30+
31+
it('should match by contains url', () => {
32+
const rules = [
33+
{ method: 'GET', url: 'https://foo.com/api', alias: 'a' },
34+
];
35+
expect(findRule('GET', 'https://foo.com/api', rules)).toEqual(rules[0]);
36+
expect(findRule('GET', 'https://foo.com/api/users', rules)).toBeUndefined();
37+
expect(findRule('GET', 'https://foo.com/home', rules)).toBeUndefined();
38+
});
39+
40+
it('should match by regex', () => {
41+
const rules = [
42+
{ method: 'GET', url: 'https://foo.com/api/users/preferences', alias: 'a' },
43+
];
44+
expect(findRule('GET', /\/users\/*/, rules)).toEqual(rules[0]);
45+
});
46+
47+
it('should match by contains some path of the url', () => {
48+
const rules = [
49+
{ method: 'GET', url: 'https://foo.com/api', alias: 'a' },
50+
];
51+
expect(findRule('GET', '/api', rules)).toEqual(rules[0]);
52+
expect(findRule('GET', '/api/users', rules)).toBeUndefined();
53+
expect(findRule('GET', 'https://foo.com/home', rules)).toBeUndefined();
54+
});
55+
56+
it('is case-insensitive for method', () => {
57+
const rules = [
58+
{ method: 'get', url: 'https://foo.com', alias: 'a' },
59+
];
60+
expect(findRule('GET', 'https://foo.com', rules)).toEqual(rules[0]);
61+
expect(findRule('get', 'https://foo.com', rules)).toEqual(rules[0]);
62+
expect(findRule('Get', 'https://foo.com', rules)).toEqual(rules[0]);
63+
});
2364
});

src/tests/commands/mockBridge/initMocking.spec.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
1-
import { describe, it, expect, vi } from 'vitest';
2-
import { initRequestMocking } from '../../../commands/mockBridge';
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { clearRequestMockRules, getRequestMockRules, initRequestMocking, mockRequest, waitForRequest } from '../../../commands/mockBridge';
3+
4+
// Fake service worker API
5+
class FakeServiceWorker {
6+
listeners: Record<string, Function[]> = {};
7+
controller = { postMessage: vi.fn() };
8+
9+
addEventListener(type: string, cb: Function) {
10+
this.listeners[type] ??= [];
11+
this.listeners[type].push(cb);
12+
}
13+
14+
dispatchMessage(data: any) {
15+
this.listeners["message"]?.forEach((cb) => cb({ data }));
16+
}
17+
18+
async register() {
19+
return Promise.resolve();
20+
}
21+
}
322

423
describe('initRequestMocking', () => {
24+
let fakeSW: FakeServiceWorker;
25+
26+
beforeEach(() => {
27+
fakeSW = new FakeServiceWorker();
28+
// @ts-ignore
29+
navigator.serviceWorker = fakeSW;
30+
clearRequestMockRules();
31+
});
32+
533
it('registers the service worker and sets up message listener', async () => {
634
// Mock navigator.serviceWorker
735
const registerMock = vi.fn().mockResolvedValue({});
@@ -25,4 +53,52 @@ describe('initRequestMocking', () => {
2553
value: originalSW,
2654
});
2755
});
56+
57+
it("should mark rule as executed and update request when EXECUTED message is received", async () => {
58+
await initRequestMocking();
59+
60+
await mockRequest("getUser", {
61+
method: "GET",
62+
url: "/api/user/1",
63+
response: { id: 1, name: "Kevin" },
64+
});
65+
66+
// Initially not executed
67+
let rule = getRequestMockRules()[0];
68+
expect(rule.executed).toBe(false);
69+
70+
// Fire EXECUTED message
71+
fakeSW.dispatchMessage({
72+
type: "EXECUTED",
73+
alias: "getUser",
74+
request: { headers: { foo: "bar" } },
75+
});
76+
77+
rule = getRequestMockRules()[0];
78+
expect(rule.executed).toBe(true);
79+
expect(rule.request).toEqual({ headers: { foo: "bar" } });
80+
});
81+
82+
it("should resolve waitForRequest when EXECUTED arrives", async () => {
83+
await initRequestMocking();
84+
85+
await mockRequest("createUser", {
86+
method: "POST",
87+
url: "/api/user",
88+
response: { ok: true },
89+
});
90+
91+
// Trigger EXECUTED asynchronously
92+
setTimeout(() => {
93+
fakeSW.dispatchMessage({
94+
type: "EXECUTED",
95+
alias: "createUser",
96+
request: { body: { name: "Alice" } },
97+
});
98+
}, 50);
99+
100+
const rule = await waitForRequest("createUser");
101+
expect(rule.executed).toBe(true);
102+
expect(rule.request).toEqual({ body: { name: "Alice" } });
103+
});
28104
});

src/tests/commands/mockBridge/mockRequest.spec.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { describe, expect, it, vi } from 'vitest';
1+
import { describe, expect, it, vi, beforeEach } from 'vitest';
22
import { clearRequestMockRules, getRequestMockRules, mockRequest } from '../../../commands/mockBridge';
33

44
describe('mockBridge mock request methods', () => {
5+
beforeEach(() => {
6+
vi.clearAllMocks();
7+
});
8+
59
it('should send mock rules to the service worker', async () => {
610
const mockUrl = 'https://api.example.com/data';
711
const alias = 'getData';
@@ -41,7 +45,7 @@ describe('mockBridge mock request methods', () => {
4145
headers: { 'Content-Type': 'application/json' },
4246
});
4347

44-
expect(postMessageMock).toHaveBeenCalledWith({
48+
expect(postMessageMock).toHaveBeenNthCalledWith(1, {
4549
type: 'ADD_RULE',
4650
rule: expect.objectContaining({
4751
alias,
@@ -54,17 +58,19 @@ describe('mockBridge mock request methods', () => {
5458
}),
5559
});
5660

57-
// Restore original controller
58-
Object.defineProperty(navigator.serviceWorker, 'controller', {
59-
configurable: true,
60-
get: () => originalController,
61-
});
62-
6361
expect(getRequestMockRules().length).toBe(1);
6462
expect(getRequestMockRules()[0].alias).toBe(alias);
6563

6664
// clear rules
6765
clearRequestMockRules();
6866
expect(getRequestMockRules().length).toBe(0);
67+
expect(postMessageMock).toHaveBeenCalledTimes(3);
68+
expect(postMessageMock).toHaveBeenNthCalledWith(3, { type: "CLEAR_RULES" });
69+
70+
// Restore original controller
71+
Object.defineProperty(navigator.serviceWorker, 'controller', {
72+
configurable: true,
73+
get: () => originalController,
74+
});
6975
});
7076
});

src/tests/commands/mockBridge/waitFor.spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { waitForRequest, mockRequest, clearRequestMockRules, getRequestMockRules
33

44
describe('waitForRequest', () => {
55
beforeEach(() => {
6-
clearRequestMockRules();
76
// Ensure navigator.serviceWorker exists
87
if (!('serviceWorker' in navigator)) {
98
Object.defineProperty(navigator, 'serviceWorker', {
@@ -20,6 +19,7 @@ describe('waitForRequest', () => {
2019
configurable: true,
2120
get: () => ({ postMessage: postMessageMock }),
2221
});
22+
clearRequestMockRules();
2323
});
2424

2525
it('resolves when the rule is executed', async () => {
@@ -46,12 +46,17 @@ describe('waitForRequest', () => {
4646
const rules = getRequestMockRules();
4747
rules[0].executed = true;
4848
rules[0].request = { delayed: true };
49-
// Simulate waiter callback (directly resolve the promise by marking executed)
50-
// The waitFor promise will resolve on next tick after executed is set
49+
// Simulate waiter callback (directly resolve the promise by marking executed)
50+
// The waitFor promise will resolve on next tick after executed is set
5151
}, 50);
5252
const result = await rulePromise;
5353
expect(result.alias).toBe(alias);
5454
expect(result.executed).toBe(true);
5555
expect(result.request).toEqual({ delayed: true });
5656
});
57+
58+
it('throws if the rule is not found or not executed', async () => {
59+
const alias = 'nonExistentAlias';
60+
await expect(waitForRequest(alias)).rejects.toThrow('Rule not found or not executed');
61+
});
5762
});

src/tests/e2e/api/text.spec.ts

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

src/twd-types.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,20 +139,6 @@ export interface TWDElemAPI {
139139
*
140140
*/
141141
type: (text: string) => HTMLInputElement | HTMLTextAreaElement;
142-
/**
143-
* Gets the text content of the element.
144-
* @returns The text content.
145-
*
146-
* @example
147-
* ```ts
148-
* const para = await twd.get("p");
149-
* const content = para.text();
150-
* console.log(content);
151-
*
152-
* ```
153-
*
154-
*/
155-
text: () => string;
156142
/**
157143
* Asserts something about the element.
158144
* @param name The name of the assertion.

0 commit comments

Comments
 (0)