Skip to content

Commit b0295c8

Browse files
authored
Support relative request URLs (#34)
Fixes #31 This takes an approach similar to MSW, whereby we will add the absolute base url to a request if we're able to. See https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#resource for which absolute base url is used in window vs worker contexts.
1 parent 66aeefd commit b0295c8

File tree

5 files changed

+87
-48
lines changed

5 files changed

+87
-48
lines changed

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
"type-check": "tsc",
1414
"test": "vitest run",
1515
"test:watch": "vitest run --watch",
16-
"lint": "eslint src",
17-
"lint:fix": "eslint src --fix",
18-
"format": "prettier src --write",
19-
"format:check": "prettier src --check",
16+
"lint": "eslint src tests",
17+
"lint:fix": "eslint src tests --fix",
18+
"format": "prettier src tests --write",
19+
"format:check": "prettier src tests --check",
2020
"build": "tsc -p tsconfig.build.json"
2121
},
2222
"repository": {

src/index.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,33 @@ function requestNotMatches(request: Request, urlOrPredicate: UrlOrPredicate): bo
309309
return !requestMatches(request, urlOrPredicate);
310310
}
311311

312+
// Node 18 does not support URL.canParse()
313+
export function canParseURL(url: string): boolean {
314+
try {
315+
new URL(url);
316+
return true;
317+
} catch (err) {
318+
return false;
319+
}
320+
}
321+
322+
// Node Requests cannot be relative
323+
function resolveInput(input: string): string {
324+
if (canParseURL(input)) return input;
325+
326+
// Window context
327+
if (typeof window.document !== 'undefined') {
328+
return new URL(input, window.document.baseURI).toString();
329+
}
330+
331+
// Worker context
332+
if (typeof location !== 'undefined') {
333+
return new URL(input, location.origin).toString();
334+
}
335+
336+
return input;
337+
}
338+
312339
function normalizeRequest(input: RequestInput, requestInit?: RequestInit): Request {
313340
if (input instanceof Request) {
314341
if (input.signal && input.signal.aborted) {
@@ -319,12 +346,12 @@ function normalizeRequest(input: RequestInput, requestInit?: RequestInit): Reque
319346
if (requestInit && requestInit.signal && requestInit.signal.aborted) {
320347
abort();
321348
}
322-
return new Request(input, requestInit);
349+
return new Request(resolveInput(input), requestInit);
323350
} else {
324351
if (requestInit && requestInit.signal && requestInit.signal.aborted) {
325352
abort();
326353
}
327-
return new Request(input.toString(), requestInit);
354+
return new Request(resolveInput(input.toString()), requestInit);
328355
}
329356
}
330357

tests/api.test.ts

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -100,41 +100,49 @@ describe('testing mockResponse', () => {
100100
expect(fetch.mock.calls[0]![0]).toEqual(new URL('https://instagram.com'));
101101
});
102102

103-
it('should allow empty response bodies', async () => {
104-
fetch.mockResponseOnce(null, { status: 204 });
105-
fetch.mockResponseOnce(undefined, { status: 204 });
106-
fetch.mockResponseOnce(() => null, { status: 204 });
107-
fetch.mockResponseOnce(() => undefined, { status: 204 });
108-
fetch.mockResponseOnce(() => Promise.resolve(null), { status: 204 });
109-
fetch.mockResponseOnce(() => Promise.resolve(undefined), { status: 204 });
110-
fetch.mockResponseOnce({ status: 204 });
111-
fetch.mockResponseOnce(() => ({ status: 204 }));
112-
fetch.mockResponseOnce(() => Promise.resolve({ status: 204 }));
113-
fetch.mockResponseOnce(new Response(null, { status: 204 }));
114-
fetch.mockResponseOnce(new Response(undefined, { status: 204 }));
115-
fetch.mockResponseOnce(() => new Response(null, { status: 204 }));
116-
fetch.mockResponseOnce(() => new Response(undefined, { status: 204 }));
117-
fetch.mockResponseOnce(() => Promise.resolve(new Response(null, { status: 204 })));
118-
fetch.mockResponseOnce(() => Promise.resolve(new Response(undefined, { status: 204 })));
119-
fetch.mockResponseOnce('done');
120-
121-
expect(await request()).toBe('');
122-
expect(await request()).toBe('');
123-
expect(await request()).toBe('');
124-
expect(await request()).toBe('');
125-
expect(await request()).toBe('');
126-
expect(await request()).toBe('');
127-
expect(await request()).toBe('');
128-
expect(await request()).toBe('');
129-
expect(await request()).toBe('');
130-
expect(await request()).toBe('');
131-
expect(await request()).toBe('');
132-
expect(await request()).toBe('');
133-
expect(await request()).toBe('');
134-
expect(await request()).toBe('');
135-
expect(await request()).toBe('');
136-
expect(await request()).toBe('done');
137-
});
103+
it('should support relative request urls', async () => {
104+
fetch.mockResponseOnce(JSON.stringify({ data: 'abcde' }), { status: 200 });
105+
106+
const response = await fetch('folder/file.json').then((res) => res.json());
107+
108+
expect(response).toEqual({ data: 'abcde' });
109+
});
110+
111+
it('should allow empty response bodies', async () => {
112+
fetch.mockResponseOnce(null, { status: 204 });
113+
fetch.mockResponseOnce(undefined, { status: 204 });
114+
fetch.mockResponseOnce(() => null, { status: 204 });
115+
fetch.mockResponseOnce(() => undefined, { status: 204 });
116+
fetch.mockResponseOnce(() => Promise.resolve(null), { status: 204 });
117+
fetch.mockResponseOnce(() => Promise.resolve(undefined), { status: 204 });
118+
fetch.mockResponseOnce({ status: 204 });
119+
fetch.mockResponseOnce(() => ({ status: 204 }));
120+
fetch.mockResponseOnce(() => Promise.resolve({ status: 204 }));
121+
fetch.mockResponseOnce(new Response(null, { status: 204 }));
122+
fetch.mockResponseOnce(new Response(undefined, { status: 204 }));
123+
fetch.mockResponseOnce(() => new Response(null, { status: 204 }));
124+
fetch.mockResponseOnce(() => new Response(undefined, { status: 204 }));
125+
fetch.mockResponseOnce(() => Promise.resolve(new Response(null, { status: 204 })));
126+
fetch.mockResponseOnce(() => Promise.resolve(new Response(undefined, { status: 204 })));
127+
fetch.mockResponseOnce('done');
128+
129+
expect(await request()).toBe('');
130+
expect(await request()).toBe('');
131+
expect(await request()).toBe('');
132+
expect(await request()).toBe('');
133+
expect(await request()).toBe('');
134+
expect(await request()).toBe('');
135+
expect(await request()).toBe('');
136+
expect(await request()).toBe('');
137+
expect(await request()).toBe('');
138+
expect(await request()).toBe('');
139+
expect(await request()).toBe('');
140+
expect(await request()).toBe('');
141+
expect(await request()).toBe('');
142+
expect(await request()).toBe('');
143+
expect(await request()).toBe('');
144+
expect(await request()).toBe('done');
145+
});
138146
});
139147

140148
describe('testing mockResponses', () => {
@@ -822,15 +830,15 @@ describe('overloads', () => {
822830
expect(await request()).toBe('i');
823831
});
824832
});
825-
833+
826834
it('works globally', async () => {
827-
const fm = createFetchMock(vi);
828-
fm.enableMocks();
835+
const fm = createFetchMock(vi);
836+
fm.enableMocks();
829837

830-
fetchMock.mockResponseOnce('foo');
831-
expect(await request()).toBe('foo');
838+
fetchMock.mockResponseOnce('foo');
839+
expect(await request()).toBe('foo');
832840

833-
fm.disableMocks();
841+
fm.disableMocks();
834842
});
835843

836844
it('enable/disable', async () => {

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"module": "NodeNext",
44
"moduleResolution": "NodeNext",
55
"lib": [
6-
"es2022"
6+
"es2022",
7+
"dom",
78
],
89
"baseUrl": "./",
910
"paths": {

vitest.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ export default defineConfig({
66
'vitest-fetch-mock': './src/index',
77
},
88
},
9+
test: {
10+
environment: 'jsdom',
11+
},
912
});

0 commit comments

Comments
 (0)