Skip to content

chore(openapi-fetch): Split apart tests #1895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
"version": "pnpm run build && changeset version && pnpm i"
},
"devDependencies": {
"@biomejs/biome": "^1.8.1",
"@biomejs/biome": "^1.8.3",
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.5",
"@playwright/test": "^1.44.1",
"@changesets/cli": "^2.27.7",
"@playwright/test": "^1.46.1",
"@size-limit/preset-small-lib": "^11.1.4",
"@types/node": "^20.14.7",
"@types/node": "^22.5.2",
"del-cli": "^5.1.0",
"prettier": "^3.3.2",
"prettier": "^3.3.3",
"size-limit": "^11.1.4",
"typescript": "^5.4.5",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
},
"size-limit": [
Expand Down
19 changes: 11 additions & 8 deletions packages/openapi-fetch/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,36 @@ This library uses [Vitest](https://vitest.dev/) for testing. There’s a great [

To run the entire test suite, run:

```bash
```sh
pnpm test
```

To run an individual test:

```bash
```sh
pnpm test -- [partial filename]
```

To start the entire test suite in watch mode:

```bash
```sh
npx vitest
```

#### TypeScript tests
#### Important test-writing tips

**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).
All tests in this project should adhere to the following rules:

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.
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.
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.
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!
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.

### Running linting
### Linting

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

```bash
```sh
pnpm run lint
```

Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-fetch/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"extends": ["../../biome.json"],
"files": {
"include": ["./src/", "./test/"],
"ignore": ["**/fixtures/**/*"]
"ignore": ["**/test/**/schemas/**"]
},
"linter": {
"rules": {
Expand Down
6 changes: 3 additions & 3 deletions packages/openapi-fetch/examples/react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "vite dev"
},
"dependencies": {
"@tanstack/react-query": "^5.51.18",
"@tanstack/react-query": "^5.53.3",
"openapi-fetch": "workspace:^",
"openapi-typescript": "workspace:^",
"react": "^18.3.1",
Expand All @@ -16,7 +16,7 @@
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"typescript": "^5.4.5",
"vite": "^5.3.5"
"typescript": "^5.5.4",
"vite": "^5.4.3"
}
}
16 changes: 8 additions & 8 deletions packages/openapi-fetch/examples/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
"openapi-fetch": "workspace:^"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.2.2",
"@sveltejs/kit": "^2.5.19",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@sveltejs/adapter-auto": "^3.2.4",
"@sveltejs/kit": "^2.5.25",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"openapi-typescript": "workspace:^",
"svelte": "^4.2.18",
"svelte-check": "^3.8.5",
"tslib": "^2.6.3",
"typescript": "^5.4.5",
"vite": "^5.3.5"
"svelte": "^4.2.19",
"svelte-check": "^3.8.6",
"tslib": "^2.7.0",
"typescript": "^5.5.4",
"vite": "^5.4.3"
}
}
10 changes: 5 additions & 5 deletions packages/openapi-fetch/examples/vue-3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
},
"dependencies": {
"openapi-fetch": "workspace:^",
"vue": "^3.4.35"
"vue": "^3.5.0"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue": "^5.1.3",
"@vue/tsconfig": "^0.5.1",
"openapi-typescript": "workspace:^",
"typescript": "^5.4.5",
"vite": "^5.3.5",
"vue-tsc": "^2.0.29"
"typescript": "^5.5.4",
"vite": "^5.4.3",
"vue-tsc": "^2.1.4"
}
}
18 changes: 9 additions & 9 deletions packages/openapi-fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,25 +62,25 @@
"test:ts-no-strict": "tsc --noEmit -p test/no-strict-null-checks/tsconfig.json",
"test-e2e": "playwright test",
"bench:js": "vitest bench",
"e2e-vite-build": "vite build test/fixtures/e2e",
"e2e-vite-start": "vite preview test/fixtures/e2e",
"e2e-vite-build": "vite build test/e2e/app",
"e2e-vite-start": "vite preview test/e2e/app",
"version": "pnpm run prepare && pnpm run build"
},
"dependencies": {
"openapi-typescript-helpers": "workspace:^"
"openapi-typescript-helpers": "workspace:^",
"vitest-fetch-mock": "^0.3.0"
},
"devDependencies": {
"axios": "^1.7.4",
"axios": "^1.7.7",
"del-cli": "^5.1.0",
"esbuild": "^0.23.0",
"esbuild": "^0.23.1",
"execa": "^8.0.1",
"feature-fetch": "^0.0.15",
"msw": "^2.3.1",
"openapi-typescript": "workspace:^",
"openapi-typescript-codegen": "^0.25.0",
"openapi-typescript-fetch": "^2.0.0",
"superagent": "^10.0.1",
"typescript": "^5.4.5",
"vite": "^5.3.5"
"superagent": "^10.1.0",
"typescript": "^5.5.4",
"vite": "^5.4.2"
}
}
153 changes: 153 additions & 0 deletions packages/openapi-fetch/test/common/create-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { describe, expect, test } from "vitest";
import { createObservedClient } from "../helpers.js";
import type { FetchOptions, HeadersOptions } from "../../src/index.js";
import type { paths } from "./schemas/common.js";

describe("createClient options", () => {
test("baseUrl", async () => {
let actualURL = new URL("https://fakeurl.example");
const client = createObservedClient<paths>({ baseUrl: "https://api.foo.bar/v2" }, async (req) => {
actualURL = new URL(req.url);
return Response.json([]);
});
await client.GET("/resources");
expect(actualURL.href).toBe("https://api.foo.bar/v2/resources");
});

test("baseUrl removes trailing slash", async () => {
let actualURL = new URL("https://fakeurl.example");
const client = createObservedClient<paths>({ baseUrl: "https://api.foo.bar/v3/" }, async (req) => {
actualURL = new URL(req.url);
return Response.json([]);
});
await client.GET("/resources");
expect(actualURL.href).toBe("https://api.foo.bar/v3/resources");
});

test("baseUrl per request", async () => {
let actualURL = new URL("https://fakeurl.example");
const client = createObservedClient<paths>({ baseUrl: "https://fakeurl.example" }, async (req) => {
actualURL = new URL(req.url);
return Response.json([]);
});

const localBaseUrl = "https://api.foo.bar/v3";
await client.GET("/resources", { baseUrl: localBaseUrl });

// assert baseUrl and path mesh as expected
expect(actualURL.href).toBe("https://api.foo.bar/v3/resources");
});

describe("content-type", () => {
const BODY_ACCEPTING_METHODS = [["PUT"], ["POST"], ["DELETE"], ["OPTIONS"], ["PATCH"]] as const;
const ALL_METHODS = [...BODY_ACCEPTING_METHODS, ["GET"], ["HEAD"]] as const;

async function fireRequestAndGetContentType(options: {
defaultHeaders?: HeadersOptions;
method: (typeof ALL_METHODS)[number][number];
fetchOptions: FetchOptions<any>;
}) {
let headers = new Headers();
const client = createObservedClient<any>({ headers: options.defaultHeaders }, async (req) => {
headers = req.headers;
return Response.json([]);
});
await client[options.method]("/resources", options.fetchOptions as any);
return headers.get("content-type");
}

test.each(ALL_METHODS)("no content-type for body-less requests - %s", async (method) => {
const contentType = await fireRequestAndGetContentType({
method,
fetchOptions: {},
});

expect(contentType).toBe(null);
});

test.each(ALL_METHODS)("no content-type for `undefined` body requests - %s", async (method) => {
const contentType = await fireRequestAndGetContentType({
method,
fetchOptions: { body: undefined },
});

expect(contentType).toBe(null);
});

const BODIES = [{ prop: "a" }, {}, "", "str", null, false, 0, 1, new Date("2024-08-07T09:52:00.836Z")] as const;
const METHOD_BODY_COMBINATIONS = BODY_ACCEPTING_METHODS.flatMap(([method]) =>
BODIES.map((body) => [method, body] as const),
);

test.each(METHOD_BODY_COMBINATIONS)(
"implicit default content-type for body-full requests - %s, %j",
async (method, body) => {
const contentType = await fireRequestAndGetContentType({
method,
fetchOptions: { body },
});

expect(contentType).toBe("application/json");
},
);

test.each(METHOD_BODY_COMBINATIONS)(
"provided default content-type for body-full requests - %s, %j",
async (method, body) => {
const contentType = await fireRequestAndGetContentType({
defaultHeaders: { "content-type": "application/my-json" },
method,
fetchOptions: { body },
});

expect(contentType).toBe("application/my-json");
},
);

test.each(METHOD_BODY_COMBINATIONS)(
"native-fetch default content-type for body-full requests, when default is suppressed - %s, %j",
async (method, body) => {
const contentType = await fireRequestAndGetContentType({
defaultHeaders: { "content-type": null },
method,
fetchOptions: { body },
});
// the fetch implementation won't allow sending a body without content-type,
// and it defaults to `text/plain;charset=UTF-8`, however the actual default value
// is irrelevant and might be flaky across different fetch implementations
// for us, it's important that it's not `application/json`
expect(contentType).not.toBe("application/json");
},
);

test.each(METHOD_BODY_COMBINATIONS)(
"specified content-type for body-full requests - %s, %j",
async (method, body) => {
const contentType = await fireRequestAndGetContentType({
method,
fetchOptions: {
body,
headers: { "content-type": "application/my-json" },
},
});

expect(contentType).toBe("application/my-json");
},
);

test.each(METHOD_BODY_COMBINATIONS)(
"specified content-type for body-full requests, even when default is suppressed - %s, %j",
async (method, body) => {
const contentType = await fireRequestAndGetContentType({
method,
fetchOptions: {
body,
headers: { "content-type": "application/my-json" },
},
});

expect(contentType).toBe("application/my-json");
},
);
});
});
Loading