Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: wheresrhys/fetch-mock
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: vitest-v0.2.9
Choose a base ref
...
head repository: wheresrhys/fetch-mock
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: vitest-v0.2.10
Choose a head ref
  • 13 commits
  • 13 files changed
  • 2 contributors

Commits on Feb 23, 2025

  1. docs: deno usage with skypack

    wheresrhys committed Feb 23, 2025
    Copy the full SHA
    6c002ee View commit details
  2. docs: waitFor option

    wheresrhys committed Feb 23, 2025
    Copy the full SHA
    23ab450 View commit details
  3. test: tests for waitFor

    wheresrhys committed Feb 23, 2025
    Copy the full SHA
    a78aa19 View commit details
  4. feat: implement waitFor option

    wheresrhys committed Feb 23, 2025
    Copy the full SHA
    5500228 View commit details
  5. Copy the full SHA
    8783101 View commit details
  6. feat: add ability to wait for multiple routes

    wheresrhys committed Feb 23, 2025
    Copy the full SHA
    c3dc9c3 View commit details
  7. Merge pull request #911 from wheresrhys/rhys/waitfor

    Rhys/waitfor
    wheresrhys authored Feb 23, 2025
    Copy the full SHA
    ec5ad60 View commit details
  8. test: failing test for #908

    wheresrhys committed Feb 23, 2025
    Copy the full SHA
    f5363a7 View commit details
  9. fix: clone response before using

    wheresrhys committed Feb 23, 2025
    Copy the full SHA
    2ccf18e View commit details
  10. Update installation.md

    wheresrhys authored Feb 23, 2025
    Copy the full SHA
    256da21 View commit details
  11. Merge pull request #913 from wheresrhys/rhys/reusable-response

    Rhys/reusable response
    wheresrhys authored Feb 23, 2025
    Copy the full SHA
    d550e85 View commit details
  12. chore: release main

    github-actions[bot] authored Feb 23, 2025
    Copy the full SHA
    b12f1f3 View commit details
  13. Merge pull request #912 from wheresrhys/release-please--branches--main

    chore: release main
    wheresrhys authored Feb 23, 2025
    Copy the full SHA
    b00158d View commit details
6 changes: 3 additions & 3 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"packages/fetch-mock": "12.3.0",
"packages/vitest": "0.2.9",
"packages/jest": "0.2.11",
"packages/fetch-mock": "12.4.0",
"packages/vitest": "0.2.10",
"packages/jest": "0.2.12",
"packages/codemods": "0.1.3"
}
5 changes: 5 additions & 0 deletions docs/docs/API/route/options.md
Original file line number Diff line number Diff line change
@@ -30,6 +30,11 @@ Limits the number of times the route will be used to respond. If the route has a

Delays responding for the number of milliseconds specified.

### waitFor

`{String|[String]}`
Useful for testing race conditions. Use the name of another route (or a list of names), and this route will only respond after that one has. (Note, this does not wait for the body - only the initial response payload).

### sticky

`{Boolean}`
8 changes: 8 additions & 0 deletions docs/docs/Usage/installation.md
Original file line number Diff line number Diff line change
@@ -30,3 +30,11 @@ When using one of the following frameworks consider using the appropriate wrappe

- Jest - [@fetch-mock/jest](/fetch-mock/docs/wrappers/jest)
- Vitest - [@fetch-mock/vitest](/fetch-mock/docs/wrappers/vitest)

## Deno support

Import fetch-mock, or the wrappers above, using the `npm:` prefix, e.g.

```js
import fetchMock from 'npm:fetch-mock@12.3.0';
```
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions packages/fetch-mock/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [12.4.0](https://github.com/wheresrhys/fetch-mock/compare/fetch-mock-v12.3.0...fetch-mock-v12.4.0) (2025-02-23)


### Features

* add ability to wait for multiple routes ([c3dc9c3](https://github.com/wheresrhys/fetch-mock/commit/c3dc9c35da89e7ffb0ccb1bb72975acb15b13c30))
* implement waitFor option ([5500228](https://github.com/wheresrhys/fetch-mock/commit/550022826826defe1f3c844972e74bb64790596f))


### Bug Fixes

* clone response before using ([2ccf18e](https://github.com/wheresrhys/fetch-mock/commit/2ccf18e1fd2c659b55c549e3be2d009d738656d2))
* use a promise, no function, to implement waitFor ([8783101](https://github.com/wheresrhys/fetch-mock/commit/87831010c24c1de47e1b458131c07468f44e1e74))

## [12.3.0](https://github.com/wheresrhys/fetch-mock/compare/fetch-mock-v12.2.1...fetch-mock-v12.3.0) (2025-02-04)


2 changes: 1 addition & 1 deletion packages/fetch-mock/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fetch-mock",
"description": "Mock http requests made using fetch",
"version": "12.3.0",
"version": "12.4.0",
"exports": {
"browser": "./dist/esm/index.js",
"import": {
27 changes: 26 additions & 1 deletion packages/fetch-mock/src/Route.ts
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ export type UserRouteSpecificConfig = {
response?: RouteResponse | RouteResponseFunction;
repeat?: number;
delay?: number;
waitFor?: RouteName | RouteName[];
sticky?: boolean;
};
export type InternalRouteConfig = {
@@ -122,13 +123,15 @@ e.g. {"body": {"status: "registered"}}`);
class Route {
config: RouteConfig;
matcher: RouteMatcherFunction;
#responseSubscriptions: Array<() => void>;

constructor(config: RouteConfig) {
this.init(config);
}

init(config: RouteConfig | ModifyRouteConfig) {
this.config = config;
this.#responseSubscriptions = [];
this.#sanitize();
this.#validate();
this.#generateMatcher();
@@ -197,6 +200,24 @@ class Route {
}
}

waitFor(awaitedRoutes: Route[]) {
const { response } = this.config;
this.config.response = Promise.all(
awaitedRoutes.map(
(awaitedRoute) =>
new Promise((res) =>
awaitedRoute.onRespond(() => {
res(undefined);
}),
),
),
).then(() => response);
}

onRespond(func: () => void) {
this.#responseSubscriptions.push(func);
}

constructResponse(responseInput: RouteResponseConfig): {
response: Response;
responseOptions: ResponseInit;
@@ -205,11 +226,15 @@ class Route {
const responseOptions = this.constructResponseOptions(responseInput);
const body = this.constructResponseBody(responseInput, responseOptions);

return {
const responsePackage = {
response: new this.config.Response(body, responseOptions),
responseOptions,
responseInput,
};

this.#responseSubscriptions.forEach((func) => func());

return responsePackage;
}

constructResponseOptions(
26 changes: 25 additions & 1 deletion packages/fetch-mock/src/Router.ts
Original file line number Diff line number Diff line change
@@ -226,7 +226,7 @@ export default class Router {
// If the response is a pre-made Response, respond with it
if (responseInput instanceof Response) {
return {
response: responseInput,
response: responseInput.clone(),
responseOptions: {},
responseInput: {},
};
@@ -342,6 +342,30 @@ export default class Router {
'fetch-mock: Adding route with same name as existing route.',
);
}
if (route.config.waitFor) {
const routeNamesToWaitFor = Array.isArray(route.config.waitFor)
? route.config.waitFor
: [route.config.waitFor];

const routesToAwait: Route[] = [];

routeNamesToWaitFor.forEach((routeName) => {
const routeToAwait = this.routes.find(
({ config: { name: existingName } }) => routeName === existingName,
);

if (routeToAwait) {
routesToAwait.push(routeToAwait);
} else {
throw new Error(
`Cannot wait for route \`${routeName}\`: route of that name does not exist`,
);
}
});

route.waitFor(routesToAwait);
}

this.routes.push(route);
}
setFallback(response?: RouteResponse) {
249 changes: 164 additions & 85 deletions packages/fetch-mock/src/__tests__/FetchMock/response-negotiation.test.js
Original file line number Diff line number Diff line change
@@ -42,107 +42,186 @@ describe('response negotiation', () => {
expect(res.status).toEqual(200);
expect(await res.text()).toEqual('test: http://a.com/');
});
describe('delay', () => {
it('delay', async () => {
fm.route('*', 200, { delay: 20 });
const req = fm.fetchHandler('http://a.com/');
let resolved = false;
req.then(() => {
resolved = true;
});
await new Promise((res) => setTimeout(res, 10));
expect(resolved).toBe(false);
await new Promise((res) => setTimeout(res, 11));
expect(resolved).toBe(true);
const res = await req;
expect(res.status).toEqual(200);
});

it('delay', async () => {
fm.route('*', 200, { delay: 20 });
const req = fm.fetchHandler('http://a.com/');
let resolved = false;
req.then(() => {
resolved = true;
it("delay a function response's execution", async () => {
const startTimestamp = new Date().getTime();
fm.route('http://a.com/', () => ({ timestamp: new Date().getTime() }), {
delay: 20,
});
const req = fm.fetchHandler('http://a.com/');
let resolved = false;
req.then(() => {
resolved = true;
});
await new Promise((res) => setTimeout(res, 10));
expect(resolved).toBe(false);
await new Promise((res) => setTimeout(res, 11));
expect(resolved).toBe(true);
const res = await req;
expect(res.status).toEqual(200);
const responseTimestamp = (await res.json()).timestamp;
expect(responseTimestamp - startTimestamp).toBeGreaterThanOrEqual(20);
});
await new Promise((res) => setTimeout(res, 10));
expect(resolved).toBe(false);
await new Promise((res) => setTimeout(res, 11));
expect(resolved).toBe(true);
const res = await req;
expect(res.status).toEqual(200);
});

it("delay a function response's execution", async () => {
const startTimestamp = new Date().getTime();
fm.route('http://a.com/', () => ({ timestamp: new Date().getTime() }), {
delay: 20,
it('pass values to delayed function', async () => {
fm.route('*', ({ url }) => `delayed: ${url}`, {
delay: 10,
});
const req = fm.fetchHandler('http://a.com/');
await new Promise((res) => setTimeout(res, 11));
const res = await req;
expect(res.status).toEqual(200);
expect(await res.text()).toEqual('delayed: http://a.com/');
});
const req = fm.fetchHandler('http://a.com/');
let resolved = false;
req.then(() => {
resolved = true;

it('call delayed response multiple times, each with the same delay', async () => {
fm.route('*', 200, { delay: 20 });
const req1 = fm.fetchHandler('http://a.com/');
let resolved = false;
req1.then(() => {
resolved = true;
});
await new Promise((res) => setTimeout(res, 10));
expect(resolved).toBe(false);
await new Promise((res) => setTimeout(res, 11));
expect(resolved).toBe(true);
const res1 = await req1;
expect(res1.status).toEqual(200);
const req2 = fm.fetchHandler('http://a.com/');
resolved = false;
req2.then(() => {
resolved = true;
});
await new Promise((res) => setTimeout(res, 10));
expect(resolved).toBe(false);
await new Promise((res) => setTimeout(res, 11));
expect(resolved).toBe(true);
const res2 = await req2;
expect(res2.status).toEqual(200);
});
await new Promise((res) => setTimeout(res, 10));
expect(resolved).toBe(false);
await new Promise((res) => setTimeout(res, 11));
expect(resolved).toBe(true);
const res = await req;
expect(res.status).toEqual(200);
const responseTimestamp = (await res.json()).timestamp;
expect(responseTimestamp - startTimestamp).toBeGreaterThanOrEqual(20);
});

it('pass values to delayed function', async () => {
fm.route('*', ({ url }) => `delayed: ${url}`, {
delay: 10,
describe('waitFor', () => {
it('Error informatively if route to wait for does not exist', () => {
expect(() => fm.route('*', 200, { waitFor: 'huh' })).toThrow(
'Cannot wait for route `huh`: route of that name does not exist',
);
});
it('Not respond until waited for route responds', async () => {
fm.route('http://a.com', 200, 'route-a').route('http://b.com', 200, {
waitFor: 'route-a',
});
let lastRouteCalled;
await Promise.all([
fm.fetchHandler('http://b.com').then(() => (lastRouteCalled = 'b')),
fm.fetchHandler('http://a.com').then(() => (lastRouteCalled = 'a')),
]);
expect(lastRouteCalled).toEqual('b');
});
it('Can have multiple waits on the same route', async () => {
fm.route('http://a.com', 200, 'route-a')
.route('http://b.com', 200, { waitFor: 'route-a' })
.route('http://c.com', 200, { waitFor: 'route-a' });
let routesCalled = [];
await Promise.all([
fm.fetchHandler('http://c.com').then(() => routesCalled.push('c')),
fm.fetchHandler('http://b.com').then(() => routesCalled.push('b')),
fm.fetchHandler('http://a.com').then(() => routesCalled.push('a')),
]);
expect(routesCalled).toEqual(['a', 'b', 'c']);
});
it('Can chain waits', async () => {
fm.route('http://a.com', 200, 'route-a')
.route('http://b.com', 200, { name: 'route-b', waitFor: 'route-a' })
.route('http://c.com', 200, { waitFor: 'route-b' });
let routesCalled = [];
await Promise.all([
fm.fetchHandler('http://c.com').then(() => routesCalled.push('c')),
fm.fetchHandler('http://b.com').then(() => routesCalled.push('b')),
fm.fetchHandler('http://a.com').then(() => routesCalled.push('a')),
]);
expect(routesCalled).toEqual(['a', 'b', 'c']);
});
const req = fm.fetchHandler('http://a.com/');
await new Promise((res) => setTimeout(res, 11));
const res = await req;
expect(res.status).toEqual(200);
expect(await res.text()).toEqual('delayed: http://a.com/');
});

it('call delayed response multiple times, each with the same delay', async () => {
fm.route('*', 200, { delay: 20 });
const req1 = fm.fetchHandler('http://a.com/');
let resolved = false;
req1.then(() => {
resolved = true;
it('Can wait for multiple routes', async () => {
fm.route('http://a.com', 200, 'route-a')
.route('http://b.com', 200, 'route-b')
.route('http://c.com', 200, { waitFor: ['route-a', 'route-b'] });
let routesCalled = [];
await Promise.all([
fm.fetchHandler('http://c.com').then(() => routesCalled.push('c')),
fm.fetchHandler('http://b.com').then(() => routesCalled.push('b')),
fm.fetchHandler('http://a.com').then(() => routesCalled.push('a')),
]);
expect(routesCalled).toEqual(['b', 'a', 'c']);
});
await new Promise((res) => setTimeout(res, 10));
expect(resolved).toBe(false);
await new Promise((res) => setTimeout(res, 11));
expect(resolved).toBe(true);
const res1 = await req1;
expect(res1.status).toEqual(200);
const req2 = fm.fetchHandler('http://a.com/');
resolved = false;
req2.then(() => {
resolved = true;

it('Not error if waited for route responds again', async () => {
fm.route('http://a.com', 200, 'route-a').route('http://b.com', 200, {
waitFor: 'route-a',
});
await Promise.all([
fm.fetchHandler('http://b.com'),
fm.fetchHandler('http://a.com'),
]);
await expect(fm.fetchHandler('http://a.com')).resolves.toBeInstanceOf(
Response,
);
});
await new Promise((res) => setTimeout(res, 10));
expect(resolved).toBe(false);
await new Promise((res) => setTimeout(res, 11));
expect(resolved).toBe(true);
const res2 = await req2;
expect(res2.status).toEqual(200);
});
describe('Response', () => {
it('Response', async () => {
fm.route('http://a.com/', new Response('http://a.com/', { status: 200 }));
const res = await fm.fetchHandler('http://a.com/');
expect(res.status).toEqual(200);
});

it('Response', async () => {
fm.route('http://a.com/', new Response('http://a.com/', { status: 200 }));
const res = await fm.fetchHandler('http://a.com/');
expect(res.status).toEqual(200);
});
it('should work with Response.error()', async () => {
fm.route('http://a.com', Response.error());
const response = await fm.fetchHandler('http://a.com');
expect(response.status).toBe(0);
});

it('should work with Response.error()', async () => {
fm.route('http://a.com', Response.error());
const response = await fm.fetchHandler('http://a.com');
expect(response.status).toBe(0);
});
it('function that returns a Response', async () => {
fm.route(
'http://a.com/',
() => new Response('http://a.com/', { status: 200 }),
);
const res = await fm.fetchHandler('http://a.com/');
expect(res.status).toEqual(200);
});

it('function that returns a Response', async () => {
fm.route(
'http://a.com/',
() => new Response('http://a.com/', { status: 200 }),
);
const res = await fm.fetchHandler('http://a.com/');
expect(res.status).toEqual(200);
});
it('Promise that returns a Response', async () => {
fm.route(
'http://a.com/',
Promise.resolve(new Response('http://a.com/', { status: 200 })),
);
const res = await fm.fetchHandler('http://a.com/');
expect(res.status).toEqual(200);
});

it('Promise that returns a Response', async () => {
fm.route(
'http://a.com/',
Promise.resolve(new Response('http://a.com/', { status: 200 })),
);
const res = await fm.fetchHandler('http://a.com/');
expect(res.status).toEqual(200);
it('Reuse a Response', async () => {
fm.route('http://a.com/', new Response('hello', { status: 200 }));
await fm.fetchHandler('http://a.com/').then((res) => res.text());
await expect(
fm.fetchHandler('http://a.com/').then((res) => res.text()),
).resolves.toEqual('hello');
});
});

describe('rejecting', () => {
9 changes: 9 additions & 0 deletions packages/jest/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Changelog


## [0.2.12](https://github.com/wheresrhys/fetch-mock/compare/jest-v0.2.11...jest-v0.2.12) (2025-02-23)


### Dependencies

* The following workspace dependencies were updated
* dependencies
* fetch-mock bumped from ^12.3.0 to ^12.4.0

## [0.2.11](https://github.com/wheresrhys/fetch-mock/compare/jest-v0.2.10...jest-v0.2.11) (2025-02-23)


4 changes: 2 additions & 2 deletions packages/jest/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@fetch-mock/jest",
"description": "jest wrapper for fetch-mock",
"version": "0.2.11",
"version": "0.2.12",
"exports": {
"browser": "./dist/esm/index.js",
"import": {
@@ -21,7 +21,7 @@
"node": ">=18.11.0"
},
"dependencies": {
"fetch-mock": "^12.3.0"
"fetch-mock": "^12.4.0"
},
"peerDependencies": {
"jest": "*",
9 changes: 9 additions & 0 deletions packages/vitest/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [0.2.10](https://github.com/wheresrhys/fetch-mock/compare/vitest-v0.2.9...vitest-v0.2.10) (2025-02-23)


### Dependencies

* The following workspace dependencies were updated
* dependencies
* fetch-mock bumped from ^12.3.0 to ^12.4.0

## [0.2.9](https://github.com/wheresrhys/fetch-mock/compare/vitest-v0.2.8...vitest-v0.2.9) (2025-02-23)


4 changes: 2 additions & 2 deletions packages/vitest/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@fetch-mock/vitest",
"description": "Vitest wrapper for fetch-mock",
"version": "0.2.9",
"version": "0.2.10",
"exports": {
"browser": "./dist/esm/index.js",
"import": {
@@ -21,7 +21,7 @@
"node": ">=18.11.0"
},
"dependencies": {
"fetch-mock": "^12.3.0"
"fetch-mock": "^12.4.0"
},
"peerDependencies": {
"vitest": "*"