Skip to content

Commit 541a6bc

Browse files
committed
feat: Add baseUrl option to FetchMocker
1 parent 934ca53 commit 541a6bc

File tree

3 files changed

+127
-1
lines changed

3 files changed

+127
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
title: Mocking Browser Requests
3+
description: Learn how to mock browser requests using Mentoss
4+
---
5+
6+
import { Aside } from "@astrojs/starlight/components";
7+
8+
The global `fetch()` function is available in many JavaScript runtimes, including server-side runtimes and web browsers. While `fetch()` works similarly in both environments, there are some important differences to take into account when testing code that uses `fetch()` in a browser environment.
9+
10+
## Relative URLs with `fetch()`
11+
12+
Perhaps the most significant difference between using `fetch()` in a browser as opposed to a server environment is how relative URLs are handled. When you use `fetch()` in a browser, relative URLs are resolved relative to the current page's URL. This means that if you call `fetch("/api/data")` from a page at `https://example.com/page`, the request will be made to `https://example.com/api/data`. This happens automatically in the browser whenever you use `fetch()`, ensuring that requests go to the correct server.
13+
14+
If you try to use a relative URL with `fetch()` in a server-side environment, you'll get an error. This is because the server doesn't know what the base URL should be, so it can't resolve the relative URL. That means the same `fetch()` call that works in the browser won't work in a server environment and it's important to keep that in mind when writing tests.
15+
16+
## Mocking Browser Requests with Mentoss
17+
18+
Mentoss provides a way to mock browser requests in your tests, allowing you to test code that uses `fetch()` without making actual network requests. To mock a browser request, you can provide a `baseUrl` option to the `FetchMocker` constructor. This option specifies the base URL that relative URLs should be resolved against. You can then call a mocked `fetch()` using a relative URL. Here's an example:
19+
20+
```js {10, 26-27}
21+
import { MockServer, FetchMocker } from "mentoss";
22+
import { expect } from "chai";
23+
24+
const BASE_URL = "https://api.example.com";
25+
26+
describe("My API", () => {
27+
const server = new MockServer(BASE_URL);
28+
const mocker = new FetchMocker({
29+
servers: [server],
30+
baseUrl: BASE_URL
31+
});
32+
33+
// extract the fetch function
34+
const myFetch = mocker.fetch;
35+
36+
// reset the server after each test
37+
afterEach(() => {
38+
server.clear();
39+
});
40+
41+
it("should return a 200 status code", async () => {
42+
43+
// set up the route to test
44+
server.get("/ping", 200);
45+
46+
// make the request
47+
const response = await myFetch("/ping");
48+
49+
// check the response
50+
expect(response.status).to.equal(200);
51+
});
52+
});
53+
```
54+
55+
In this example, the `baseUrl` option is set to `"https://api.example.com"`, which means that relative URLs will be resolved against that base URL. The test then calls `myFetch("/ping")`, which resolves to `"https://api.example.com/ping"` and makes a request to the server. This allows you to test code that uses relative URLs with `fetch()` in a browser-like environment.
56+
57+
<Aside type="caution">
58+
When using the `baseUrl` option with `FetchMocker`, make sure to set it to the correct base URL for your mock server. If you don't have a mock server defined for the base URL, the request will fail.
59+
</Aside>

src/fetch-mocker.js

+45-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,35 @@ ${traces.map(trace => {
5151
}).join("\n\n") || "No partial matches found."}`;
5252
}
5353

54+
/**
55+
* Creates a base URL from a URL or string. This is also validates
56+
* the URL to ensure it's valid. Empty strings are invalid.
57+
* @param {string|URL|undefined} baseUrl The base URL to create.
58+
* @returns {URL|undefined} The created base URL.
59+
* @throws {TypeError} When the base URL is an empty string.
60+
* @throws {TypeError} When the base URL is not a string or URL.
61+
*/
62+
function createBaseUrl(baseUrl) {
63+
64+
if (baseUrl === undefined) {
65+
return undefined;
66+
}
67+
68+
if (baseUrl === "") {
69+
throw new TypeError("Base URL cannot be an empty string.");
70+
}
71+
72+
if (baseUrl instanceof URL) {
73+
return baseUrl;
74+
}
75+
76+
if (typeof baseUrl !== "string") {
77+
throw new TypeError("Base URL must be a string or URL object.");
78+
}
79+
80+
return new URL(baseUrl);
81+
}
82+
5483
//-----------------------------------------------------------------------------
5584
// Exports
5685
//-----------------------------------------------------------------------------
@@ -76,6 +105,12 @@ export class FetchMocker {
76105
* @type {typeof Request}
77106
*/
78107
#Request;
108+
109+
/**
110+
* The base URL to use for relative URLs.
111+
* @type {URL|undefined}
112+
*/
113+
#baseUrl;
79114

80115
/**
81116
* The global fetch function.
@@ -93,21 +128,30 @@ export class FetchMocker {
93128
* Creates a new instance.
94129
* @param {object} options Options for the instance.
95130
* @param {MockServer[]} options.servers The servers to use.
131+
* @param {string|URL} [options.baseUrl] The base URL to use for relative URLs.
96132
* @param {typeof Response} [options.CustomResponse] The Response constructor to use.
97133
* @param {typeof Request} [options.CustomRequest] The Request constructor to use.
98134
*/
99135
constructor({
100136
servers,
137+
baseUrl,
101138
CustomRequest = globalThis.Request,
102139
CustomResponse = globalThis.Response,
103140
}) {
104141
this.#servers = servers;
142+
this.#baseUrl = createBaseUrl(baseUrl);
105143
this.#Response = CustomResponse;
106144
this.#Request = CustomRequest;
107145

108146
// create the function here to bind to `this`
109147
this.fetch = async (input, init) => {
110-
const request = new this.#Request(input, init);
148+
149+
// adjust any relative URLs
150+
const fixedInput = typeof input === "string" && this.#baseUrl
151+
? new URL(input, this.#baseUrl).toString()
152+
: input;
153+
154+
const request = new this.#Request(fixedInput, init);
111155
const allTraces = [];
112156

113157
/*

tests/fetch-mocker.test.js

+23
Original file line numberDiff line numberDiff line change
@@ -332,4 +332,27 @@ describe("FetchMocker", () => {
332332
assert.ok(!fetchMocker.allRoutesCalled());
333333
});
334334
});
335+
336+
describe("Relative URLs", () => {
337+
338+
it("should throw an error when using a relative URL and no baseUrl", async () => {
339+
const server = new MockServer(BASE_URL);
340+
const fetchMocker = new FetchMocker({ servers: [server] });
341+
342+
await assert.rejects(fetchMocker.fetch("/hello"), {
343+
name: "TypeError",
344+
message: "Failed to parse URL from /hello",
345+
});
346+
});
347+
348+
it("should return 200 when using a relative URL and a baseUrl", async () => {
349+
const server = new MockServer(BASE_URL);
350+
const fetchMocker = new FetchMocker({ servers: [server], baseUrl: BASE_URL });
351+
352+
server.get("/hello", 200);
353+
354+
const response = await fetchMocker.fetch("/hello");
355+
assert.strictEqual(response.status, 200);
356+
});
357+
});
335358
});

0 commit comments

Comments
 (0)