Skip to content

Commit c778254

Browse files
committed
Split apart tests
1 parent e40fe06 commit c778254

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+186918
-5882
lines changed

package.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@
1717
"version": "pnpm run build && changeset version && pnpm i"
1818
},
1919
"devDependencies": {
20-
"@biomejs/biome": "^1.8.1",
20+
"@biomejs/biome": "^1.8.3",
2121
"@changesets/changelog-github": "^0.5.0",
22-
"@changesets/cli": "^2.27.5",
23-
"@playwright/test": "^1.44.1",
22+
"@changesets/cli": "^2.27.7",
23+
"@playwright/test": "^1.46.1",
2424
"@size-limit/preset-small-lib": "^11.1.4",
25-
"@types/node": "^20.14.7",
25+
"@types/node": "^20.16.3",
2626
"del-cli": "^5.1.0",
27-
"prettier": "^3.3.2",
27+
"prettier": "^3.3.3",
2828
"size-limit": "^11.1.4",
29-
"typescript": "^5.4.5",
29+
"typescript": "^5.5.4",
3030
"vitest": "^2.0.5"
3131
},
3232
"size-limit": [

packages/openapi-fetch/CONTRIBUTING.md

+11-8
Original file line numberDiff line numberDiff line change
@@ -32,33 +32,36 @@ This library uses [Vitest](https://vitest.dev/) for testing. There’s a great [
3232

3333
To run the entire test suite, run:
3434

35-
```bash
35+
```sh
3636
pnpm test
3737
```
3838

3939
To run an individual test:
4040

41-
```bash
41+
```sh
4242
pnpm test -- [partial filename]
4343
```
4444

4545
To start the entire test suite in watch mode:
4646

47-
```bash
47+
```sh
4848
npx vitest
4949
```
5050

51-
#### TypeScript tests
51+
#### Important test-writing tips
5252

53-
**Don’t neglect writing TS tests!** In the test suite, you’ll see `// @ts-expect-error` comments. These are critical tests in and of themselves—they are asserting that TypeScript throws an error when it should be throwing an error (the test suite will actually fail in places if a TS error is _not_ raised).
53+
All tests in this project should adhere to the following rules:
5454

55-
As this is just a minimal fetch wrapper meant to provide deep type inference for API schemas, **testing TS types** is arguably more important than testing the runtime. So please make liberal use of `// @ts-expect-error`, and as a general rule of thumb, write more **unwanted** output tests than _wanted_ output tests.
55+
1. **Use `assertType<T>(…)`** ([docs](https://vitest.dev/guide/testing-types)). Don’t just check the _actual_ runtime value; check the _perceived_ type as well.
56+
2. **Testing TS errors is just as important as testing for expected types.** Use `// @ts-expected-error` liberally. this is discouraged because it’s hiding an error. But in our tests, we **want** to test that a TS error is thrown for invalid input.
57+
3. **Scope `// @ts-expect-error` as closely as possible.** Remember that JS largely ignores whitespace. When using `// @ts-expect-error`, try and break up an expression into as many lines as possible, so that the `// @ts-expect-error` is scoped to the right line. Otherwise it is possible a _different part of the expression_ is throwing the error!
58+
4. **Manually type out type tests.** Avoid using [test.each](https://vitest.dev/api/#test-each) for type tests, as it’s likely hiding errors.
5659

57-
### Running linting
60+
### Linting
5861

5962
Linting is handled via [Biome](https://biomejs.dev), a faster ESLint replacement. It was installed with `pnpm i` and can be run with:
6063

61-
```bash
64+
```sh
6265
pnpm run lint
6366
```
6467

packages/openapi-fetch/biome.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"extends": ["../../biome.json"],
44
"files": {
55
"include": ["./src/", "./test/"],
6-
"ignore": ["**/fixtures/**/*"]
6+
"ignore": ["**/test/**/schemas/**"]
77
},
88
"linter": {
99
"rules": {

packages/openapi-fetch/examples/react-query/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@types/react": "18.3.1",
1717
"@types/react-dom": "18.3.0",
1818
"@vitejs/plugin-react-swc": "^3.7.0",
19-
"typescript": "^5.4.5",
19+
"typescript": "^5.5.4",
2020
"vite": "^5.3.5"
2121
}
2222
}

packages/openapi-fetch/examples/sveltekit/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"svelte": "^4.2.18",
1919
"svelte-check": "^3.8.5",
2020
"tslib": "^2.6.3",
21-
"typescript": "^5.4.5",
21+
"typescript": "^5.5.4",
2222
"vite": "^5.3.5"
2323
}
2424
}

packages/openapi-fetch/examples/vue-3/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@vitejs/plugin-vue": "^5.1.2",
2020
"@vue/tsconfig": "^0.5.1",
2121
"openapi-typescript": "workspace:^",
22-
"typescript": "^5.4.5",
22+
"typescript": "^5.5.4",
2323
"vite": "^5.3.5",
2424
"vue-tsc": "^2.0.29"
2525
}

packages/openapi-fetch/package.json

+9-9
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,25 @@
6262
"test:ts-no-strict": "tsc --noEmit -p test/no-strict-null-checks/tsconfig.json",
6363
"test-e2e": "playwright test",
6464
"bench:js": "vitest bench",
65-
"e2e-vite-build": "vite build test/fixtures/e2e",
66-
"e2e-vite-start": "vite preview test/fixtures/e2e",
65+
"e2e-vite-build": "vite build test/e2e/app",
66+
"e2e-vite-start": "vite preview test/e2e/app",
6767
"version": "pnpm run prepare && pnpm run build"
6868
},
6969
"dependencies": {
70-
"openapi-typescript-helpers": "workspace:^"
70+
"openapi-typescript-helpers": "workspace:^",
71+
"vitest-fetch-mock": "^0.3.0"
7172
},
7273
"devDependencies": {
73-
"axios": "^1.7.4",
74+
"axios": "^1.7.7",
7475
"del-cli": "^5.1.0",
75-
"esbuild": "^0.23.0",
76+
"esbuild": "^0.23.1",
7677
"execa": "^8.0.1",
7778
"feature-fetch": "^0.0.15",
78-
"msw": "^2.3.1",
7979
"openapi-typescript": "workspace:^",
8080
"openapi-typescript-codegen": "^0.25.0",
8181
"openapi-typescript-fetch": "^2.0.0",
82-
"superagent": "^10.0.1",
83-
"typescript": "^5.4.5",
84-
"vite": "^5.3.5"
82+
"superagent": "^10.1.0",
83+
"typescript": "^5.5.4",
84+
"vite": "^5.4.2"
8585
}
8686
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { describe, expect, test } from "vitest";
2+
import { createObservedClient } from "../helpers.js";
3+
import type { FetchOptions, HeadersOptions } from "../../src/index.js";
4+
import type { paths } from "./schemas/common.js";
5+
6+
describe("createClient options", () => {
7+
test("baseUrl", async () => {
8+
let actualURL = new URL("https://fakeurl.example");
9+
const client = createObservedClient<paths>({ baseUrl: "https://api.foo.bar/v2" }, async (req) => {
10+
actualURL = new URL(req.url);
11+
return Response.json([]);
12+
});
13+
await client.GET("/resources");
14+
expect(actualURL.href).toBe("https://api.foo.bar/v2/resources");
15+
});
16+
17+
test("baseUrl removes trailing slash", async () => {
18+
let actualURL = new URL("https://fakeurl.example");
19+
const client = createObservedClient<paths>({ baseUrl: "https://api.foo.bar/v3/" }, async (req) => {
20+
actualURL = new URL(req.url);
21+
return Response.json([]);
22+
});
23+
await client.GET("/resources");
24+
expect(actualURL.href).toBe("https://api.foo.bar/v3/resources");
25+
});
26+
27+
test("baseUrl per request", async () => {
28+
let actualURL = new URL("https://fakeurl.example");
29+
const client = createObservedClient<paths>({ baseUrl: "https://fakeurl.example" }, async (req) => {
30+
actualURL = new URL(req.url);
31+
return Response.json([]);
32+
});
33+
34+
const localBaseUrl = "https://api.foo.bar/v3";
35+
await client.GET("/resources", { baseUrl: localBaseUrl });
36+
37+
// assert baseUrl and path mesh as expected
38+
expect(actualURL.href).toBe("https://api.foo.bar/v3/resources");
39+
});
40+
41+
describe("content-type", () => {
42+
const BODY_ACCEPTING_METHODS = [["PUT"], ["POST"], ["DELETE"], ["OPTIONS"], ["PATCH"]] as const;
43+
const ALL_METHODS = [...BODY_ACCEPTING_METHODS, ["GET"], ["HEAD"]] as const;
44+
45+
async function fireRequestAndGetContentType(options: {
46+
defaultHeaders?: HeadersOptions;
47+
method: (typeof ALL_METHODS)[number][number];
48+
fetchOptions: FetchOptions<any>;
49+
}) {
50+
let headers = new Headers();
51+
const client = createObservedClient<any>({ headers: options.defaultHeaders }, async (req) => {
52+
headers = req.headers;
53+
return Response.json([]);
54+
});
55+
await client[options.method]("/resources", options.fetchOptions as any);
56+
return headers.get("content-type");
57+
}
58+
59+
test.each(ALL_METHODS)("no content-type for body-less requests - %s", async (method) => {
60+
const contentType = await fireRequestAndGetContentType({
61+
method,
62+
fetchOptions: {},
63+
});
64+
65+
expect(contentType).toBe(null);
66+
});
67+
68+
test.each(ALL_METHODS)("no content-type for `undefined` body requests - %s", async (method) => {
69+
const contentType = await fireRequestAndGetContentType({
70+
method,
71+
fetchOptions: { body: undefined },
72+
});
73+
74+
expect(contentType).toBe(null);
75+
});
76+
77+
const BODIES = [{ prop: "a" }, {}, "", "str", null, false, 0, 1, new Date("2024-08-07T09:52:00.836Z")] as const;
78+
const METHOD_BODY_COMBINATIONS = BODY_ACCEPTING_METHODS.flatMap(([method]) =>
79+
BODIES.map((body) => [method, body] as const),
80+
);
81+
82+
test.each(METHOD_BODY_COMBINATIONS)(
83+
"implicit default content-type for body-full requests - %s, %j",
84+
async (method, body) => {
85+
const contentType = await fireRequestAndGetContentType({
86+
method,
87+
fetchOptions: { body },
88+
});
89+
90+
expect(contentType).toBe("application/json");
91+
},
92+
);
93+
94+
test.each(METHOD_BODY_COMBINATIONS)(
95+
"provided default content-type for body-full requests - %s, %j",
96+
async (method, body) => {
97+
const contentType = await fireRequestAndGetContentType({
98+
defaultHeaders: { "content-type": "application/my-json" },
99+
method,
100+
fetchOptions: { body },
101+
});
102+
103+
expect(contentType).toBe("application/my-json");
104+
},
105+
);
106+
107+
test.each(METHOD_BODY_COMBINATIONS)(
108+
"native-fetch default content-type for body-full requests, when default is suppressed - %s, %j",
109+
async (method, body) => {
110+
const contentType = await fireRequestAndGetContentType({
111+
defaultHeaders: { "content-type": null },
112+
method,
113+
fetchOptions: { body },
114+
});
115+
// the fetch implementation won't allow sending a body without content-type,
116+
// and it defaults to `text/plain;charset=UTF-8`, however the actual default value
117+
// is irrelevant and might be flaky across different fetch implementations
118+
// for us, it's important that it's not `application/json`
119+
expect(contentType).not.toBe("application/json");
120+
},
121+
);
122+
123+
test.each(METHOD_BODY_COMBINATIONS)(
124+
"specified content-type for body-full requests - %s, %j",
125+
async (method, body) => {
126+
const contentType = await fireRequestAndGetContentType({
127+
method,
128+
fetchOptions: {
129+
body,
130+
headers: { "content-type": "application/my-json" },
131+
},
132+
});
133+
134+
expect(contentType).toBe("application/my-json");
135+
},
136+
);
137+
138+
test.each(METHOD_BODY_COMBINATIONS)(
139+
"specified content-type for body-full requests, even when default is suppressed - %s, %j",
140+
async (method, body) => {
141+
const contentType = await fireRequestAndGetContentType({
142+
method,
143+
fetchOptions: {
144+
body,
145+
headers: { "content-type": "application/my-json" },
146+
},
147+
});
148+
149+
expect(contentType).toBe("application/my-json");
150+
},
151+
);
152+
});
153+
});

0 commit comments

Comments
 (0)