@canonical/vitest-config-react 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/biome.json +6 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/reactTestConfig.js +99 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/reactTestConfig.d.ts +89 -0
- package/package.json +53 -0
- package/src/index.ts +6 -0
- package/src/reactTestConfig.ts +197 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Canonical Vitest Configuration (React)
|
|
2
|
+
|
|
3
|
+
This package provides a reusable configuration factory for the Vitest `test`
|
|
4
|
+
block of Canonical's React packages. It replaces the near-identical `test`
|
|
5
|
+
blocks that were previously copy-pasted into every package's `vite.config.ts`.
|
|
6
|
+
|
|
7
|
+
Every React vitest posture in the monorepo — jsdom component tests, the
|
|
8
|
+
client/SSR project split, coverage-gated packages, and node-only packages — is
|
|
9
|
+
a point in one option space, so this is a single factory rather than a set of
|
|
10
|
+
per-shape variants.
|
|
11
|
+
|
|
12
|
+
## Getting started
|
|
13
|
+
|
|
14
|
+
1. Add the package as a dev dependency: `bun add -d @canonical/vitest-config-react`.
|
|
15
|
+
2. In your `vite.config.ts`, import the factory and spread its result into
|
|
16
|
+
`test`:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import react from "@vitejs/plugin-react";
|
|
20
|
+
import { reactTestConfig } from "@canonical/vitest-config-react";
|
|
21
|
+
import { defineConfig } from "vitest/config";
|
|
22
|
+
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
plugins: [react()],
|
|
25
|
+
resolve: { tsconfigPaths: true },
|
|
26
|
+
build: { sourcemap: true },
|
|
27
|
+
test: reactTestConfig({
|
|
28
|
+
glob: "tests",
|
|
29
|
+
setupFiles: ["./vitest.setup.ts"],
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The factory owns only the `test` block. `plugins`, `resolve` and `build` stay
|
|
35
|
+
in each package's config, because those legitimately differ (for example, some
|
|
36
|
+
packages run in `jsdom` while framework-agnostic/SSR packages run in `node`).
|
|
37
|
+
|
|
38
|
+
## Options
|
|
39
|
+
|
|
40
|
+
| Option | Type | Default | Notes |
|
|
41
|
+
| ------------- | --------------------------------- | --------- | ---------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `glob` | `"test" \| "tests"`, or an array of those | — | **Required.** The test-file suffix convention(s). See the caveat below. |
|
|
43
|
+
| `environment` | `"jsdom" \| "node"` | `"jsdom"` | `"node"` for framework-agnostic / SSR-only packages. |
|
|
44
|
+
| `ssr` | `boolean` | `false` | Adds a second `node` project running `**/*.ssr.<glob>.tsx` (the client project excludes them). |
|
|
45
|
+
| `coverage` | `boolean \| { … }` | `false` | `true` enables v8 coverage with 100% thresholds; pass an object to override. |
|
|
46
|
+
| `setupFiles` | `string[]` | `[]` | e.g. `["./vitest.setup.ts"]`; omitted when empty. |
|
|
47
|
+
| `plugins` | `Plugin[]` | — | Attached to each project (Vitest resolves plugins per project). |
|
|
48
|
+
|
|
49
|
+
### Coverage
|
|
50
|
+
|
|
51
|
+
Coverage is **off by default**, preserving the behaviour of packages that do
|
|
52
|
+
not gate coverage today. When enabled, thresholds default to
|
|
53
|
+
`100/100/100/100`, following the storybook-config convention of "sensible
|
|
54
|
+
defaults you pass through":
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// 100% thresholds
|
|
58
|
+
reactTestConfig({ glob: "test", coverage: true });
|
|
59
|
+
|
|
60
|
+
// keep 100% for everything except lines
|
|
61
|
+
reactTestConfig({ glob: "test", coverage: { thresholds: { lines: 80 } } });
|
|
62
|
+
|
|
63
|
+
// runtime-only include (SSR adapters)
|
|
64
|
+
reactTestConfig({ glob: "tests", environment: "node", coverage: { include: ["src/lib/**/*.ts"] } });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Caveat: `glob` is load-bearing
|
|
68
|
+
|
|
69
|
+
`glob` has no default because `.test.` and `.tests.` are **not**
|
|
70
|
+
interchangeable: choosing the wrong convention silently matches zero files — a
|
|
71
|
+
green run that ran nothing. Always pass the convention(s) the package's test
|
|
72
|
+
files actually use.
|
|
73
|
+
|
|
74
|
+
### Interim: running both conventions
|
|
75
|
+
|
|
76
|
+
A package whose test files are split between `.test.` and `.tests.` (for
|
|
77
|
+
example `react-ds-global`, whose `_work_in_progress` scaffolds use `.test.`
|
|
78
|
+
while the promoted components use `.tests.`) can pass both:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
reactTestConfig({ glob: ["test", "tests"], ssr: true });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Both conventions are then matched by the client project and, with `ssr: true`,
|
|
85
|
+
by the SSR project (`**/*.ssr.test.tsx` and `**/*.ssr.tests.tsx`); coverage
|
|
86
|
+
excludes test files of both conventions. This is an **interim** escape hatch:
|
|
87
|
+
once the repo-wide test-file naming unification lands, packages should return
|
|
88
|
+
to a single convention and the array form should disappear.
|
|
89
|
+
|
|
90
|
+
## Roadmap: browser mode
|
|
91
|
+
|
|
92
|
+
`jsdom` is the interim environment. The forward path is Vitest browser mode
|
|
93
|
+
with Playwright (see the `svelte-ds-global` migration). This factory will gain
|
|
94
|
+
a `browser` option so each package can flip to browser mode without rewriting
|
|
95
|
+
its config — only its test files.
|
package/biome.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Standard coverage excludes shared by every coverage-gated package. */
|
|
2
|
+
const BASE_COVERAGE_EXCLUDE = [
|
|
3
|
+
"**/index.ts",
|
|
4
|
+
"**/*.d.ts",
|
|
5
|
+
"**/types.ts",
|
|
6
|
+
];
|
|
7
|
+
const FULL_THRESHOLDS = {
|
|
8
|
+
branches: 100,
|
|
9
|
+
functions: 100,
|
|
10
|
+
lines: 100,
|
|
11
|
+
statements: 100,
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Build the v8 coverage block. Test files are always excluded (both `.test.`
|
|
15
|
+
* and `.tests.` plus their `.ssr.` variants) so coverage measures runtime code
|
|
16
|
+
* only. Thresholds default to 100% and are overridable per the storybook-config
|
|
17
|
+
* convention of "sensible defaults you pass through".
|
|
18
|
+
*/
|
|
19
|
+
const buildCoverage = (coverage, globs) => {
|
|
20
|
+
const overrides = coverage === true ? {} : coverage;
|
|
21
|
+
const testExclude = globs.flatMap((glob) => [
|
|
22
|
+
`**/*.${glob}.ts`,
|
|
23
|
+
`**/*.${glob}.tsx`,
|
|
24
|
+
`**/*.ssr.${glob}.ts`,
|
|
25
|
+
`**/*.ssr.${glob}.tsx`,
|
|
26
|
+
]);
|
|
27
|
+
return {
|
|
28
|
+
provider: "v8",
|
|
29
|
+
include: overrides.include ?? ["src/**/*.{ts,tsx}"],
|
|
30
|
+
exclude: [
|
|
31
|
+
...BASE_COVERAGE_EXCLUDE,
|
|
32
|
+
...testExclude,
|
|
33
|
+
...(overrides.exclude ?? []),
|
|
34
|
+
],
|
|
35
|
+
thresholds: { ...FULL_THRESHOLDS, ...overrides.thresholds },
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Canonical's standard Vitest `test` configuration for React packages.
|
|
40
|
+
*
|
|
41
|
+
* Returns the `test` block to spread into `defineConfig` from `vitest/config`.
|
|
42
|
+
* Every distinct React vitest posture in the monorepo is a point in this
|
|
43
|
+
* option space — DOM-plain, DOM+SSR split, coverage-gated, and node-only — so
|
|
44
|
+
* there is a single factory rather than per-shape variants.
|
|
45
|
+
*
|
|
46
|
+
* `import { reactTestConfig } from "@canonical/vitest-config-react";`
|
|
47
|
+
*
|
|
48
|
+
* @example jsdom component package (ds-app family)
|
|
49
|
+
* ```ts
|
|
50
|
+
* test: reactTestConfig({ glob: "tests", setupFiles: ["./vitest.setup.ts"] })
|
|
51
|
+
* ```
|
|
52
|
+
* @example client + ssr split with 100% coverage (hooks, router)
|
|
53
|
+
* ```ts
|
|
54
|
+
* test: reactTestConfig({ glob: "test", ssr: true, coverage: true, plugins, setupFiles: ["./vitest.setup.ts"] })
|
|
55
|
+
* ```
|
|
56
|
+
* @example node-only adapter with overridden coverage include
|
|
57
|
+
* ```ts
|
|
58
|
+
* test: reactTestConfig({ glob: "tests", environment: "node", coverage: { include: ["src/lib/**\/*.ts"] } })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export const reactTestConfig = ({ glob, environment = "jsdom", ssr = false, coverage = false, setupFiles, plugins, }) => {
|
|
62
|
+
const hasSetup = setupFiles !== undefined && setupFiles.length > 0;
|
|
63
|
+
// Normalise to an array: a single convention is the common case, the array
|
|
64
|
+
// form covers packages whose files are split between `.test.` and `.tests.`.
|
|
65
|
+
const globs = Array.isArray(glob) ? glob : [glob];
|
|
66
|
+
// The client project: DOM (or node) tests, excluding the `.ssr.` variants
|
|
67
|
+
// when an ssr project will pick them up.
|
|
68
|
+
const clientTest = {
|
|
69
|
+
name: "client",
|
|
70
|
+
environment,
|
|
71
|
+
globals: true,
|
|
72
|
+
...(hasSetup ? { setupFiles } : {}),
|
|
73
|
+
include: globs.flatMap((g) => [`src/**/*.${g}.ts`, `src/**/*.${g}.tsx`]),
|
|
74
|
+
...(ssr ? { exclude: globs.map((g) => `src/**/*.ssr.${g}.tsx`) } : {}),
|
|
75
|
+
};
|
|
76
|
+
const coverageBlock = coverage === false ? {} : { coverage: buildCoverage(coverage, globs) };
|
|
77
|
+
// No split: return a flat single-project test block (the common case).
|
|
78
|
+
if (!ssr) {
|
|
79
|
+
// The client project's identity (`name`) is only meaningful in a split, so
|
|
80
|
+
// a flat config omits it to match the hand-written single-project shape.
|
|
81
|
+
const { name: _name, ...flat } = clientTest;
|
|
82
|
+
return { ...flat, ...coverageBlock };
|
|
83
|
+
}
|
|
84
|
+
// Split: a jsdom/node client project plus a node ssr project that runs only
|
|
85
|
+
// the `.ssr.` files.
|
|
86
|
+
const ssrTest = {
|
|
87
|
+
name: "ssr",
|
|
88
|
+
environment: "node",
|
|
89
|
+
include: globs.map((g) => `src/**/*.ssr.${g}.tsx`),
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
...coverageBlock,
|
|
93
|
+
projects: [
|
|
94
|
+
{ ...(plugins ? { plugins } : {}), test: clientTest },
|
|
95
|
+
{ ...(plugins ? { plugins } : {}), test: ssrTest },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=reactTestConfig.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { PluginOption } from "vite";
|
|
2
|
+
import type { ViteUserConfig } from "vitest/config";
|
|
3
|
+
/** The `test` block of a Vitest config, as consumed by `defineConfig({ test })`. */
|
|
4
|
+
type TestConfig = NonNullable<ViteUserConfig["test"]>;
|
|
5
|
+
/**
|
|
6
|
+
* The two test-file glob conventions in use across the React packages. This is
|
|
7
|
+
* a REQUIRED option with no default: `.test.` and `.tests.` are load-bearing,
|
|
8
|
+
* and a wrong choice silently matches zero files — a green run that ran
|
|
9
|
+
* nothing. Packages whose files are split between the two conventions pass
|
|
10
|
+
* both as an array until the repo-wide naming unification lands. See
|
|
11
|
+
* pragma-adrs (Track: unified config).
|
|
12
|
+
*/
|
|
13
|
+
export type TestGlob = "test" | "tests";
|
|
14
|
+
/** Coverage options. `true` enables v8 coverage with 100% thresholds. */
|
|
15
|
+
export type CoverageOption = boolean | {
|
|
16
|
+
/** Threshold overrides; each defaults to 100 when coverage is enabled. */
|
|
17
|
+
thresholds?: Partial<Record<"branches" | "functions" | "lines" | "statements", number>>;
|
|
18
|
+
/** Extra `coverage.include` globs (defaults to `src/**` for the runtime). */
|
|
19
|
+
include?: string[];
|
|
20
|
+
/** Extra `coverage.exclude` globs, merged after the standard excludes. */
|
|
21
|
+
exclude?: string[];
|
|
22
|
+
};
|
|
23
|
+
export interface ReactTestConfigOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Test-file glob convention. REQUIRED — no default; see {@link TestGlob}.
|
|
26
|
+
*
|
|
27
|
+
* Pass an array to run BOTH conventions (e.g. `["test", "tests"]`) in a
|
|
28
|
+
* package whose files are mid-migration between the two. This is an interim
|
|
29
|
+
* escape hatch until the repo-wide naming unification lands — see the README.
|
|
30
|
+
*/
|
|
31
|
+
glob: TestGlob | TestGlob[];
|
|
32
|
+
/**
|
|
33
|
+
* Test environment. `"jsdom"` (default) for DOM component tests, `"node"`
|
|
34
|
+
* for framework-agnostic / SSR-only packages.
|
|
35
|
+
*
|
|
36
|
+
* @note jsdom is the interim environment. The forward path is Vitest browser
|
|
37
|
+
* mode with Playwright (see the svelte-ds-global migration); this factory
|
|
38
|
+
* will gain a `browser` option to drive that per-package flip without a
|
|
39
|
+
* config rewrite.
|
|
40
|
+
*/
|
|
41
|
+
environment?: "jsdom" | "node";
|
|
42
|
+
/**
|
|
43
|
+
* When true, adds a second `node`-environment project that runs
|
|
44
|
+
* `**\/*.ssr.<glob>.tsx` files (client project excludes them). Mirrors the
|
|
45
|
+
* client/ssr split used by ds-global, hooks, head, router and tokens.
|
|
46
|
+
*/
|
|
47
|
+
ssr?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* v8 coverage. `false` (default) attaches no coverage block — preserving the
|
|
50
|
+
* behaviour of packages that do not currently gate coverage. `true` enables
|
|
51
|
+
* coverage with 100/100/100/100 thresholds. Pass an object to override
|
|
52
|
+
* thresholds/include/exclude while keeping the 100% defaults for any omitted
|
|
53
|
+
* threshold.
|
|
54
|
+
*/
|
|
55
|
+
coverage?: CoverageOption;
|
|
56
|
+
/** Setup files (e.g. `["./vitest.setup.ts"]`); omitted when empty. */
|
|
57
|
+
setupFiles?: string[];
|
|
58
|
+
/**
|
|
59
|
+
* Vite plugins to attach to each project. Vitest resolves plugins per
|
|
60
|
+
* project, so packages with a split pass their `[react()]` here.
|
|
61
|
+
*/
|
|
62
|
+
plugins?: any[] | PluginOption[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Canonical's standard Vitest `test` configuration for React packages.
|
|
66
|
+
*
|
|
67
|
+
* Returns the `test` block to spread into `defineConfig` from `vitest/config`.
|
|
68
|
+
* Every distinct React vitest posture in the monorepo is a point in this
|
|
69
|
+
* option space — DOM-plain, DOM+SSR split, coverage-gated, and node-only — so
|
|
70
|
+
* there is a single factory rather than per-shape variants.
|
|
71
|
+
*
|
|
72
|
+
* `import { reactTestConfig } from "@canonical/vitest-config-react";`
|
|
73
|
+
*
|
|
74
|
+
* @example jsdom component package (ds-app family)
|
|
75
|
+
* ```ts
|
|
76
|
+
* test: reactTestConfig({ glob: "tests", setupFiles: ["./vitest.setup.ts"] })
|
|
77
|
+
* ```
|
|
78
|
+
* @example client + ssr split with 100% coverage (hooks, router)
|
|
79
|
+
* ```ts
|
|
80
|
+
* test: reactTestConfig({ glob: "test", ssr: true, coverage: true, plugins, setupFiles: ["./vitest.setup.ts"] })
|
|
81
|
+
* ```
|
|
82
|
+
* @example node-only adapter with overridden coverage include
|
|
83
|
+
* ```ts
|
|
84
|
+
* test: reactTestConfig({ glob: "tests", environment: "node", coverage: { include: ["src/lib/**\/*.ts"] } })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare const reactTestConfig: ({ glob, environment, ssr, coverage, setupFiles, plugins, }: ReactTestConfigOptions) => TestConfig;
|
|
88
|
+
export {};
|
|
89
|
+
//# sourceMappingURL=reactTestConfig.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@canonical/vitest-config-react",
|
|
3
|
+
"version": "0.29.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Canonical's standard Vitest configuration factory for React packages",
|
|
6
|
+
"main": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/types/index.d.ts",
|
|
11
|
+
"default": "./dist/esm/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"author": {
|
|
15
|
+
"email": "webteam@canonical.com",
|
|
16
|
+
"name": "Canonical Webteam"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/canonical/pragma"
|
|
21
|
+
},
|
|
22
|
+
"license": "LGPL-3.0",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/canonical/pragma/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/canonical/pragma#readme",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.build.json",
|
|
32
|
+
"build:all": "tsc -p tsconfig.build.json",
|
|
33
|
+
"check": "bun run check:biome",
|
|
34
|
+
"check:fix": "bun run check:biome:fix && bun run check:ts",
|
|
35
|
+
"check:biome": "biome check",
|
|
36
|
+
"check:biome:fix": "biome check --write",
|
|
37
|
+
"check:ts": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"vite": "^8.0.1",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "2.4.9",
|
|
46
|
+
"@canonical/biome-config": "^0.29.0",
|
|
47
|
+
"@canonical/typescript-config": "^0.29.0",
|
|
48
|
+
"@types/node": "^24.12.0",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"vite": "^8.0.1",
|
|
51
|
+
"vitest": "^4.0.18"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { PluginOption } from "vite";
|
|
2
|
+
import type { ViteUserConfig } from "vitest/config";
|
|
3
|
+
|
|
4
|
+
/** The `test` block of a Vitest config, as consumed by `defineConfig({ test })`. */
|
|
5
|
+
type TestConfig = NonNullable<ViteUserConfig["test"]>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vitest's coverage options. Derived from the public `test` type rather than
|
|
9
|
+
* importing the internal `CoverageV8Options` name, which `vitest/config` does
|
|
10
|
+
* not re-export.
|
|
11
|
+
*/
|
|
12
|
+
type CoverageConfig = NonNullable<TestConfig["coverage"]>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The two test-file glob conventions in use across the React packages. This is
|
|
16
|
+
* a REQUIRED option with no default: `.test.` and `.tests.` are load-bearing,
|
|
17
|
+
* and a wrong choice silently matches zero files — a green run that ran
|
|
18
|
+
* nothing. Packages whose files are split between the two conventions pass
|
|
19
|
+
* both as an array until the repo-wide naming unification lands. See
|
|
20
|
+
* pragma-adrs (Track: unified config).
|
|
21
|
+
*/
|
|
22
|
+
export type TestGlob = "test" | "tests";
|
|
23
|
+
|
|
24
|
+
/** Coverage options. `true` enables v8 coverage with 100% thresholds. */
|
|
25
|
+
export type CoverageOption =
|
|
26
|
+
| boolean
|
|
27
|
+
| {
|
|
28
|
+
/** Threshold overrides; each defaults to 100 when coverage is enabled. */
|
|
29
|
+
thresholds?: Partial<
|
|
30
|
+
Record<"branches" | "functions" | "lines" | "statements", number>
|
|
31
|
+
>;
|
|
32
|
+
/** Extra `coverage.include` globs (defaults to `src/**` for the runtime). */
|
|
33
|
+
include?: string[];
|
|
34
|
+
/** Extra `coverage.exclude` globs, merged after the standard excludes. */
|
|
35
|
+
exclude?: string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface ReactTestConfigOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Test-file glob convention. REQUIRED — no default; see {@link TestGlob}.
|
|
41
|
+
*
|
|
42
|
+
* Pass an array to run BOTH conventions (e.g. `["test", "tests"]`) in a
|
|
43
|
+
* package whose files are mid-migration between the two. This is an interim
|
|
44
|
+
* escape hatch until the repo-wide naming unification lands — see the README.
|
|
45
|
+
*/
|
|
46
|
+
glob: TestGlob | TestGlob[];
|
|
47
|
+
/**
|
|
48
|
+
* Test environment. `"jsdom"` (default) for DOM component tests, `"node"`
|
|
49
|
+
* for framework-agnostic / SSR-only packages.
|
|
50
|
+
*
|
|
51
|
+
* @note jsdom is the interim environment. The forward path is Vitest browser
|
|
52
|
+
* mode with Playwright (see the svelte-ds-global migration); this factory
|
|
53
|
+
* will gain a `browser` option to drive that per-package flip without a
|
|
54
|
+
* config rewrite.
|
|
55
|
+
*/
|
|
56
|
+
environment?: "jsdom" | "node";
|
|
57
|
+
/**
|
|
58
|
+
* When true, adds a second `node`-environment project that runs
|
|
59
|
+
* `**\/*.ssr.<glob>.tsx` files (client project excludes them). Mirrors the
|
|
60
|
+
* client/ssr split used by ds-global, hooks, head, router and tokens.
|
|
61
|
+
*/
|
|
62
|
+
ssr?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* v8 coverage. `false` (default) attaches no coverage block — preserving the
|
|
65
|
+
* behaviour of packages that do not currently gate coverage. `true` enables
|
|
66
|
+
* coverage with 100/100/100/100 thresholds. Pass an object to override
|
|
67
|
+
* thresholds/include/exclude while keeping the 100% defaults for any omitted
|
|
68
|
+
* threshold.
|
|
69
|
+
*/
|
|
70
|
+
coverage?: CoverageOption;
|
|
71
|
+
/** Setup files (e.g. `["./vitest.setup.ts"]`); omitted when empty. */
|
|
72
|
+
setupFiles?: string[];
|
|
73
|
+
/**
|
|
74
|
+
* Vite plugins to attach to each project. Vitest resolves plugins per
|
|
75
|
+
* project, so packages with a split pass their `[react()]` here.
|
|
76
|
+
*/
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: Vite 8 plugin types are incompatible with vitest's Vite 7 re-exports (matches per-package configs).
|
|
78
|
+
plugins?: any[] | PluginOption[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Standard coverage excludes shared by every coverage-gated package. */
|
|
82
|
+
const BASE_COVERAGE_EXCLUDE = [
|
|
83
|
+
"**/index.ts",
|
|
84
|
+
"**/*.d.ts",
|
|
85
|
+
"**/types.ts",
|
|
86
|
+
] as const;
|
|
87
|
+
|
|
88
|
+
const FULL_THRESHOLDS = {
|
|
89
|
+
branches: 100,
|
|
90
|
+
functions: 100,
|
|
91
|
+
lines: 100,
|
|
92
|
+
statements: 100,
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build the v8 coverage block. Test files are always excluded (both `.test.`
|
|
97
|
+
* and `.tests.` plus their `.ssr.` variants) so coverage measures runtime code
|
|
98
|
+
* only. Thresholds default to 100% and are overridable per the storybook-config
|
|
99
|
+
* convention of "sensible defaults you pass through".
|
|
100
|
+
*/
|
|
101
|
+
const buildCoverage = (
|
|
102
|
+
coverage: Exclude<CoverageOption, false>,
|
|
103
|
+
globs: readonly TestGlob[],
|
|
104
|
+
): CoverageConfig => {
|
|
105
|
+
const overrides = coverage === true ? {} : coverage;
|
|
106
|
+
const testExclude = globs.flatMap((glob) => [
|
|
107
|
+
`**/*.${glob}.ts`,
|
|
108
|
+
`**/*.${glob}.tsx`,
|
|
109
|
+
`**/*.ssr.${glob}.ts`,
|
|
110
|
+
`**/*.ssr.${glob}.tsx`,
|
|
111
|
+
]);
|
|
112
|
+
return {
|
|
113
|
+
provider: "v8",
|
|
114
|
+
include: overrides.include ?? ["src/**/*.{ts,tsx}"],
|
|
115
|
+
exclude: [
|
|
116
|
+
...BASE_COVERAGE_EXCLUDE,
|
|
117
|
+
...testExclude,
|
|
118
|
+
...(overrides.exclude ?? []),
|
|
119
|
+
],
|
|
120
|
+
thresholds: { ...FULL_THRESHOLDS, ...overrides.thresholds },
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Canonical's standard Vitest `test` configuration for React packages.
|
|
126
|
+
*
|
|
127
|
+
* Returns the `test` block to spread into `defineConfig` from `vitest/config`.
|
|
128
|
+
* Every distinct React vitest posture in the monorepo is a point in this
|
|
129
|
+
* option space — DOM-plain, DOM+SSR split, coverage-gated, and node-only — so
|
|
130
|
+
* there is a single factory rather than per-shape variants.
|
|
131
|
+
*
|
|
132
|
+
* `import { reactTestConfig } from "@canonical/vitest-config-react";`
|
|
133
|
+
*
|
|
134
|
+
* @example jsdom component package (ds-app family)
|
|
135
|
+
* ```ts
|
|
136
|
+
* test: reactTestConfig({ glob: "tests", setupFiles: ["./vitest.setup.ts"] })
|
|
137
|
+
* ```
|
|
138
|
+
* @example client + ssr split with 100% coverage (hooks, router)
|
|
139
|
+
* ```ts
|
|
140
|
+
* test: reactTestConfig({ glob: "test", ssr: true, coverage: true, plugins, setupFiles: ["./vitest.setup.ts"] })
|
|
141
|
+
* ```
|
|
142
|
+
* @example node-only adapter with overridden coverage include
|
|
143
|
+
* ```ts
|
|
144
|
+
* test: reactTestConfig({ glob: "tests", environment: "node", coverage: { include: ["src/lib/**\/*.ts"] } })
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export const reactTestConfig = ({
|
|
148
|
+
glob,
|
|
149
|
+
environment = "jsdom",
|
|
150
|
+
ssr = false,
|
|
151
|
+
coverage = false,
|
|
152
|
+
setupFiles,
|
|
153
|
+
plugins,
|
|
154
|
+
}: ReactTestConfigOptions): TestConfig => {
|
|
155
|
+
const hasSetup = setupFiles !== undefined && setupFiles.length > 0;
|
|
156
|
+
// Normalise to an array: a single convention is the common case, the array
|
|
157
|
+
// form covers packages whose files are split between `.test.` and `.tests.`.
|
|
158
|
+
const globs = Array.isArray(glob) ? glob : [glob];
|
|
159
|
+
|
|
160
|
+
// The client project: DOM (or node) tests, excluding the `.ssr.` variants
|
|
161
|
+
// when an ssr project will pick them up.
|
|
162
|
+
const clientTest: TestConfig = {
|
|
163
|
+
name: "client",
|
|
164
|
+
environment,
|
|
165
|
+
globals: true,
|
|
166
|
+
...(hasSetup ? { setupFiles } : {}),
|
|
167
|
+
include: globs.flatMap((g) => [`src/**/*.${g}.ts`, `src/**/*.${g}.tsx`]),
|
|
168
|
+
...(ssr ? { exclude: globs.map((g) => `src/**/*.ssr.${g}.tsx`) } : {}),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const coverageBlock =
|
|
172
|
+
coverage === false ? {} : { coverage: buildCoverage(coverage, globs) };
|
|
173
|
+
|
|
174
|
+
// No split: return a flat single-project test block (the common case).
|
|
175
|
+
if (!ssr) {
|
|
176
|
+
// The client project's identity (`name`) is only meaningful in a split, so
|
|
177
|
+
// a flat config omits it to match the hand-written single-project shape.
|
|
178
|
+
const { name: _name, ...flat } = clientTest;
|
|
179
|
+
return { ...flat, ...coverageBlock };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Split: a jsdom/node client project plus a node ssr project that runs only
|
|
183
|
+
// the `.ssr.` files.
|
|
184
|
+
const ssrTest: TestConfig = {
|
|
185
|
+
name: "ssr",
|
|
186
|
+
environment: "node",
|
|
187
|
+
include: globs.map((g) => `src/**/*.ssr.${g}.tsx`),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
...coverageBlock,
|
|
192
|
+
projects: [
|
|
193
|
+
{ ...(plugins ? { plugins } : {}), test: clientTest },
|
|
194
|
+
{ ...(plugins ? { plugins } : {}), test: ssrTest },
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "dist/esm",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationDir": "dist/types",
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"types": ["node"]
|
|
12
|
+
}
|
|
13
|
+
}
|