Skip to content

Commit b9e6a59

Browse files
committed
Split apart tests
1 parent e40fe06 commit b9e6a59

37 files changed

+182996
-100
lines changed

packages/openapi-fetch/CONTRIBUTING.md

Lines changed: 11 additions & 8 deletions
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/examples/react-query/package.json

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,17 @@
7070
"openapi-typescript-helpers": "workspace:^"
7171
},
7272
"devDependencies": {
73-
"axios": "^1.7.4",
73+
"axios": "^1.7.7",
7474
"del-cli": "^5.1.0",
75-
"esbuild": "^0.23.0",
75+
"esbuild": "^0.23.1",
7676
"execa": "^8.0.1",
7777
"feature-fetch": "^0.0.15",
78-
"msw": "^2.3.1",
78+
"msw": "^2.4.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
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/**
2+
* Core tests
3+
* General functionality that is not specific to any HTTP method
4+
* (so don’t add tests for request body, form data, etc.)
5+
*/
6+
import { http, HttpResponse } from "msw";
7+
import { setupServer, type SetupServerApi } from "msw/node";
8+
import createClient from "../../src/index.js";
9+
import type { components, paths } from "./schema.js";
10+
11+
type Resource = components["schemas"]["Resource"];
12+
type Error = components["schemas"]["Error"];
13+
14+
const resource: Resource = {
15+
id: 123,
16+
name: "Resource",
17+
created_at: "2024-06-01T12:00:00Z",
18+
updated_at: "2024-06-01T12:00:00Z",
19+
};
20+
21+
const baseUrl = "https://openapi-ts.dev/core-test";
22+
23+
// generic msw setup
24+
let server: SetupServerApi;
25+
26+
beforeAll(() => {
27+
server = setupServer(
28+
http.get(`${baseUrl}/error-404`, () =>
29+
HttpResponse.json({ code: 404, message: "Internal server error" }, { status: 404 }),
30+
),
31+
http.get(`${baseUrl}/error-500`, () =>
32+
HttpResponse.json({ code: 500, message: "Internal server error" }, { status: 500 }),
33+
),
34+
// default
35+
http.get(`${baseUrl}/*`, () => HttpResponse.json([resource, resource, resource])),
36+
);
37+
server.listen();
38+
});
39+
40+
afterAll(() => {
41+
server.close();
42+
});
43+
44+
describe("request", () => {
45+
describe("headers", () => {
46+
test("default headers are preserved", async () => {
47+
const baseUrl = "https://openapi-ts.dev/core-test/header-test-default";
48+
let headers = "";
49+
server.use(
50+
http.get(`${baseUrl}/resources`, (req) => {
51+
headers = req.request.headers.toString();
52+
return HttpResponse.json([resource, resource, resource]);
53+
}),
54+
);
55+
const client = createClient<paths>({ baseUrl, headers: { foo: "bar" } });
56+
await client.GET("/resources");
57+
expect(headers).toEqual('{foo:"bar"}');
58+
});
59+
60+
test("default headers can be overridden", async () => {
61+
const baseUrl = "https://openapi-ts.dev/core-test/header-test-override";
62+
let headers = "";
63+
server.use(
64+
http.get(`${baseUrl}/resources`, (req) => {
65+
headers = req.request.headers.toString();
66+
return HttpResponse.json([resource, resource, resource]);
67+
}),
68+
);
69+
const client = createClient<paths>({ baseUrl, headers: { foo: "bar" } });
70+
await client.GET("/resources", {
71+
headers: { foo: "baz" },
72+
});
73+
expect(headers).toEqual({ foo: "baz" });
74+
});
75+
76+
test('default headers removed by "null"', async () => {
77+
const baseUrl = "https://openapi-ts.dev/core-test/header-test-null";
78+
let headers = "";
79+
server.use(
80+
http.get(`${baseUrl}/resources`, (req) => {
81+
headers = req.request.headers.toString();
82+
return HttpResponse.json([resource, resource, resource]);
83+
}),
84+
);
85+
const client = createClient<paths>({ baseUrl, headers: { foo: "bar" } });
86+
await client.GET("/resources", { headers: { foo: null } });
87+
expect(headers).toEqual({ foo: "baz" });
88+
});
89+
90+
test('default headers aren’t overridden by "undefined"', async () => {
91+
const baseUrl = "https://openapi-ts.dev/core-test/header-test-undefined";
92+
let headers = "";
93+
server.use(
94+
http.get(`${baseUrl}/resources`, (req) => {
95+
headers = req.request.headers.toString();
96+
return HttpResponse.json([resource, resource, resource]);
97+
}),
98+
);
99+
const client = createClient<paths>({ baseUrl, headers: { foo: "bar" } });
100+
await client.GET("/resources", { headers: { foo: undefined } });
101+
expect(headers).toEqual({ foo: "bar" });
102+
});
103+
104+
test("arbitrary headers are allowed on any request", async () => {
105+
const baseUrl = "https://openapi-ts.dev/core-test/header-test-arbitrary";
106+
let headers = "";
107+
server.use(
108+
http.get(`${baseUrl}/resources`, (req) => {
109+
headers = req.request.headers.toString();
110+
return HttpResponse.json([resource, resource, resource]);
111+
}),
112+
);
113+
const client = createClient<paths>({ baseUrl });
114+
client.GET("/resources", {
115+
headers: {
116+
foo: "bar",
117+
bar: 123,
118+
baz: true,
119+
},
120+
});
121+
expect(headers).toEqual({});
122+
});
123+
});
124+
});
125+
126+
describe("response", () => {
127+
describe("data/error", () => {
128+
test("valid path", async () => {
129+
const client = createClient<paths>({ baseUrl });
130+
131+
const result = await client.GET("/resources");
132+
133+
// 1. assert data & error may be undefined initially
134+
assertType<Resource[] | undefined>(result.data);
135+
assertType<Error | undefined>(result.error);
136+
137+
// 2. assert data is not undefined inside condition block
138+
if (result.data) {
139+
assertType<NonNullable<Resource[]>>(result.data);
140+
assertType<undefined>(result.error);
141+
}
142+
// 2b. inverse should work, too
143+
if (!result.error) {
144+
assertType<NonNullable<Resource[]>>(result.data);
145+
assertType<undefined>(result.error);
146+
}
147+
148+
// 3. assert error is not undefined inside condition block
149+
if (result.error) {
150+
assertType<undefined>(result.data);
151+
assertType<NonNullable<Error>>(result.error);
152+
}
153+
// 3b. inverse should work, too
154+
if (!result.data) {
155+
assertType<undefined>(result.data);
156+
assertType<NonNullable<Error>>(result.error);
157+
}
158+
});
159+
160+
test("invalid path", async () => {
161+
const client = createClient<paths>({ baseUrl });
162+
163+
const result = await client.GET(
164+
// @ts-expect-error this should throw an error
165+
"/not-a-real-path",
166+
);
167+
168+
assertType<undefined>(result.data);
169+
// @ts-expect-error: FIXME when #1723 is resolved; this shouldn’t throw an error
170+
assertType<undefined>(result.error);
171+
});
172+
173+
describe("media union", () => {
174+
const client = createClient<paths>({ baseUrl });
175+
176+
// ⚠️ Warning: DO NOT iterate over type tests! Deduplicating runtime tests
177+
// is good. But these do not test runtime.
178+
test("application/json", async () => {
179+
const { data } = await client.GET("/media-json");
180+
assertType<Resource[] | undefined>(data);
181+
});
182+
183+
test("application/vnd.api+json", async () => {
184+
const { data } = await client.GET("/media-vnd-json");
185+
assertType<Resource[] | undefined>(data);
186+
});
187+
188+
test("text/html", async () => {
189+
const { data } = await client.GET("/media-text");
190+
assertType<string | undefined>(data);
191+
});
192+
193+
test("multiple", async () => {
194+
const { data } = await client.GET("/media-multiple");
195+
assertType<Resource[] | string | undefined>(data);
196+
});
197+
198+
test("invalid", async () => {
199+
const { data } = await client.GET(
200+
// @ts-expect-error not a real path
201+
"/invalid",
202+
);
203+
assertType<unknown>(data);
204+
});
205+
});
206+
});
207+
208+
describe("response object", () => {
209+
test.each([200, 404, 500] as const)("%s", async (status) => {
210+
const client = createClient<paths>({ baseUrl });
211+
const result = await client.GET(status === 200 ? "/resources" : `/error-${status}`);
212+
expect(result.response.status).toBe(status);
213+
});
214+
});
215+
});

0 commit comments

Comments
 (0)