diff --git a/.changeset/gentle-chicken-push.md b/.changeset/gentle-chicken-push.md new file mode 100644 index 00000000..5808da3f --- /dev/null +++ b/.changeset/gentle-chicken-push.md @@ -0,0 +1,5 @@ +--- +'@ice/pkg': major +--- + +feat: remove vitest/jest define method and support vitest in-source test diff --git a/examples/react-component/jest.config.mjs b/examples/react-component/jest.config.mjs index 7af928f4..8f747e61 100644 --- a/examples/react-component/jest.config.mjs +++ b/examples/react-component/jest.config.mjs @@ -1,11 +1,10 @@ -import pkgService, { defineJestConfig } from '@ice/pkg'; - -export default defineJestConfig(pkgService, { +export default { preset: 'ts-jest', setupFilesAfterEnv: ['/jest-setup.ts'], testEnvironment: 'jest-environment-jsdom', moduleNameMapper: { + '^@/(.*)$': '/src/$1', '\\.module\\.(css|scss|less)$': 'identity-obj-proxy', '\\.(css|less|scss)$': '/__mocks__/styleMock.js', }, -}); +}; diff --git a/examples/react-component/vitest.config.mts b/examples/react-component/vitest.config.mts index 6026e88c..166681a4 100644 --- a/examples/react-component/vitest.config.mts +++ b/examples/react-component/vitest.config.mts @@ -1,6 +1,6 @@ -import pkgService, { defineVitestConfig } from '@ice/pkg'; +import { defineConfig } from 'vitest/config'; -export default defineVitestConfig(pkgService, () => ({ +export default defineConfig(() => ({ test: { environment: 'jsdom', setupFiles: ['./vitest-setup.ts'], diff --git a/packages/pkg/src/engine/shared/define.ts b/packages/pkg/src/engine/shared/define.ts index 3b20b907..86eb569e 100644 --- a/packages/pkg/src/engine/shared/define.ts +++ b/packages/pkg/src/engine/shared/define.ts @@ -4,5 +4,6 @@ export default function getDefaultDefineValues(mode: NodeEnvMode) { return { __DEV__: JSON.stringify(mode !== 'production'), 'process.env.NODE_ENV': JSON.stringify(mode), + 'import.meta.vitest': 'undefined', }; } diff --git a/packages/pkg/src/index.ts b/packages/pkg/src/index.ts index 9bebf6a4..911d8ba4 100644 --- a/packages/pkg/src/index.ts +++ b/packages/pkg/src/index.ts @@ -1,5 +1,3 @@ -export * from './test/index.js'; - export * from './types.js'; export { getBuiltInPlugins } from './utils.js'; diff --git a/packages/pkg/src/tasks/transform.ts b/packages/pkg/src/tasks/transform.ts index 60254135..1e81a0df 100644 --- a/packages/pkg/src/tasks/transform.ts +++ b/packages/pkg/src/tasks/transform.ts @@ -36,7 +36,7 @@ class TransformRunner extends Runner { const entry = config.entry as Record; const allEntryDirs = getTransformEntryDirs(rootDir, entry); const entryRoot = config.entryRoot ?? getTransformEntryRoot(rootDir, entry); - const excludes = config.excludes ?? []; + const excludes = config.excludes ?? ['**/__tests__/**']; this.cache = { allEntryDirs, diff --git a/packages/pkg/src/test/defineJestConfig.ts b/packages/pkg/src/test/defineJestConfig.ts deleted file mode 100644 index 352ea183..00000000 --- a/packages/pkg/src/test/defineJestConfig.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as path from 'path'; -import fse from 'fs-extra'; -import getTaskConfig from './getTaskConfig.js'; -import { merge } from 'es-toolkit/object'; -import type { Config as JestConfig } from 'jest'; -import type { Service } from 'build-scripts'; -import type { TaskConfig, UserConfig } from '../types.js'; - -export default function defineJestConfig( - service: Service, - userJestConfig: JestConfig | (() => Promise), -): () => Promise { - return async () => { - // Support jest configuration (object or function) Ref: https://jestjs.io/docs/configuration - let customJestConfig: JestConfig; - if (typeof userJestConfig === 'function') { - customJestConfig = await userJestConfig(); - } else { - customJestConfig = userJestConfig; - } - - const defaultConfig = await getDefaultConfig(service); - - return merge(defaultConfig, customJestConfig); - }; -} - -async function getDefaultConfig(service: Service): Promise { - const { - taskConfig, - context: { rootDir }, - } = await getTaskConfig(service); - const { alias = {}, define = {} } = taskConfig; - - const moduleNameMapper = generateModuleNameMapper(rootDir, alias); - - return { - moduleNameMapper, - globals: define, - }; -} - -function generateModuleNameMapper(rootDir: string, alias: TaskConfig['alias']) { - const moduleNameMapper: Record = {}; - for (const key in alias) { - const aliasPath = alias[key]; - const absoluteAliasPath = path.isAbsolute(aliasPath) ? aliasPath : path.join(rootDir, aliasPath); - const isDir = fse.lstatSync(absoluteAliasPath).isDirectory(); - moduleNameMapper[`^${key}${isDir ? '/(.*)' : ''}`] = `${absoluteAliasPath}${isDir ? '/$1' : ''}`; - } - - return moduleNameMapper; -} diff --git a/packages/pkg/src/test/defineVitestConfig.ts b/packages/pkg/src/test/defineVitestConfig.ts deleted file mode 100644 index d5e4cdf3..00000000 --- a/packages/pkg/src/test/defineVitestConfig.ts +++ /dev/null @@ -1,41 +0,0 @@ -import getTaskConfig from './getTaskConfig.js'; -import { merge } from 'es-toolkit/object'; -import type { Service } from 'build-scripts'; -import type { - ViteUserConfigExport as UserConfigExport, - ConfigEnv, - ViteUserConfig as VitestUserConfig, - ViteUserConfigFn as VitestUserConfigFn, -} from 'vitest/config'; -import type { TaskConfig, UserConfig } from '../types.js'; - -export default function defineVitestConfig( - service: Service, - userConfig: UserConfigExport, -): VitestUserConfigFn { - return async (env: ConfigEnv) => { - // Support vitest configuration (object or function) Ref: https://github.com/vitest-dev/vitest/blob/e5c40cff0925c3c12d8cdfa59f5649d3562668ce/packages/vitest/src/config.ts#L3 - let customConfig: VitestUserConfig; - if (typeof userConfig === 'function') { - customConfig = await userConfig(env); - } else { - customConfig = await userConfig; - } - - const defaultConfig = await getDefaultConfig(service); - - return merge(defaultConfig, customConfig); - }; -} - -async function getDefaultConfig(service: Service): Promise { - const { taskConfig } = await getTaskConfig(service); - const { alias = {}, define = {} } = taskConfig; - return { - resolve: { - alias, - }, - // FIXME: pass the custom define config - define, - }; -} diff --git a/packages/pkg/src/test/getTaskConfig.ts b/packages/pkg/src/test/getTaskConfig.ts deleted file mode 100644 index abf99d3a..00000000 --- a/packages/pkg/src/test/getTaskConfig.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Service } from 'build-scripts'; -import type { TaskConfig, UserConfig } from '../types.js'; -import { getBuiltInPlugins } from '../utils.js'; - -export default async function getTaskConfig(service: Service) { - const { taskConfigs, context } = (await service.run({ - command: 'test', - commandArgs: {}, - getBuiltInPlugins, - })) as any; - - if (taskConfigs.length === 0) { - throw new Error('No task config was found.'); - } - - return { - taskConfig: taskConfigs[0] as TaskConfig, - context, - }; -} diff --git a/packages/pkg/src/test/index.ts b/packages/pkg/src/test/index.ts deleted file mode 100644 index 4f0c72b7..00000000 --- a/packages/pkg/src/test/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as defineJestConfig } from './defineJestConfig.js'; -export { default as defineVitestConfig } from './defineVitestConfig.js'; diff --git a/packages/pkg/src/types.ts b/packages/pkg/src/types.ts index 331c1e38..e611141e 100644 --- a/packages/pkg/src/types.ts +++ b/packages/pkg/src/types.ts @@ -73,6 +73,7 @@ export interface TransformUserConfig { * Exclude all files matching any of those conditions. * - `string` to match any paths by `minimatch` glob patterns * - An `array` to match at least one of the conditions + * Default: files in __tests__ directories are excluded by default. * @see https://github.com/isaacs/minimatch */ excludes?: string | string[]; diff --git a/packages/pkg/tests/defaultDefine.test.ts b/packages/pkg/tests/defaultDefine.test.ts new file mode 100644 index 00000000..43138a46 --- /dev/null +++ b/packages/pkg/tests/defaultDefine.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import getDefaultDefineValues from '../src/engine/shared/define'; + +describe('getDefaultDefineValues', () => { + it('should include vitest in-source test guard by default', () => { + expect(getDefaultDefineValues('development')).toEqual({ + __DEV__: 'true', + 'process.env.NODE_ENV': '"development"', + 'import.meta.vitest': 'undefined', + }); + }); + + it('should preserve production defaults', () => { + expect(getDefaultDefineValues('production')).toEqual({ + __DEV__: 'false', + 'process.env.NODE_ENV': '"production"', + 'import.meta.vitest': 'undefined', + }); + }); +}); diff --git a/website/docs/guide/test.md b/website/docs/guide/test.md index f41abbc6..aa4f2836 100644 --- a/website/docs/guide/test.md +++ b/website/docs/guide/test.md @@ -1,58 +1,58 @@ # 测试 -
- 示例 - -
- -ICE PKG 不耦合任意一个测试框架,开发者可自由选择。目前提供开箱即用 [Jest](https://jestjs.io/) 和 [Vitest](https://vitest.dev/) 配置,以便快速开始单元测试。 +ICE PKG 不耦合任意一个测试框架,开发者可自由选择 [Jest](https://jestjs.io/) 或 [Vitest](https://vitest.dev/) 开展单元测试。 + +## 相关链接 + +- [Jest 官方文档](https://jestjs.io/docs/getting-started) +- [Vitest 官方文档](https://vitest.dev/guide/) +- [ts-jest 配置指南](https://kulshekhar.github.io/ts-jest/docs/getting-started/installation) +- [@swc/jest](https://www.npmjs.com/package/@swc/jest) +- [Testing Library React](https://testing-library.com/docs/react-testing-library/intro/) +- [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) ## Jest ### 安装依赖 ```bash -$ npm i jest ts-jest -D +$ npm i jest ts-jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom -D ``` ### 配置 -首先需要在项目的根目录下新建 `jest.config.mjs` 文件,并加入以下内容: - -```js -import pkgService, { defineJestConfig } from '@ice/pkg'; +快速开始时,可以先在项目根目录创建 `jest.config.mjs`: -export default defineJestConfig(pkgService, { - // 你也可以使用 @swc/jest 编译 TS 代码 +```js title="jest.config.mjs" +export default { preset: 'ts-jest', -}); + testEnvironment: 'jest-environment-jsdom', + setupFilesAfterEnv: ['/jest-setup.ts'], +}; ``` -`defineJestConfig()` 方法返回的是 ice.js 默认配置好的 Jest 配置,支持在第二个参数中传入自定义的 [Jest 配置](https://jestjs.io/docs/configuration),第二个参数的类型是: +再创建 `jest-setup.ts`: -```ts -type UserJestConfig = jest.Config | () => Promise +```ts title="jest-setup.ts" +import '@testing-library/jest-dom'; ``` -以添加 `@swc/jest` 为例: +并在 `package.json` 中添加脚本: -```js title="jest.config.mjs" -import pkgService, { defineJestConfig } from '@ice/pkg'; +```diff title="package.json" +{ + "scripts": { ++ "test": "jest" + } +} +``` -export default defineJestConfig(pkgService, { +如果你希望改用 `@swc/jest` 编译 TS/TSX,可以改成: + +```js title="jest.config.mjs" +export default { transform: { - '^.+\\.(t|j)sx?$': [ + '^.+\\.(t|j)sx?$': [ '@swc/jest', { jsc: { @@ -64,18 +64,8 @@ export default defineJestConfig(pkgService, { }, }, ], - } -}); -``` - -然后在 `package.json` 中加入 `test` 脚本: - -```diff -{ - "scripts": { -+ "test": "jest" - } -} + }, +}; ``` ### 编写测试用例 @@ -99,40 +89,23 @@ test('add function', () => { expect(add(1, 2)).toBe(3); }); ``` + 这时,运行 `npm run test` 查看测试结果了。 #### UI 测试 -组件 UI 测试推荐使用 [@testing-library/react](https://www.npmjs.com/package/@testing-library/react) 和 [@testing-library/jest-dom](https://www.npmjs.com/package/@testing-library/jest-dom)。 +组件 UI 测试推荐使用 [@testing-library/react](https://www.npmjs.com/package/@testing-library/react) 和 [@testing-library/jest-dom](https://www.npmjs.com/package/@testing-library/jest-dom)。上面的快速开始配置已经包含这两个库常用的 jsdom 环境和 matcher 设置。 -首先安装依赖: -```bash -$ npm i @testing-library/react jest-environment-jsdom @testing-library/jest-dom -D -``` -然后在项目根目录下新建 `jest-setup.ts` 并写入以下内容,以扩展匹配器(matchers): -```ts title="jest-setup.ts" -import '@testing-library/jest-dom'; -``` -最后在 `jest.config.mjs` 中加入以下内容: - -```diff title="jest.config.mjs" -import pkgService, { defineJestConfig } from '@ice/pkg'; - -export default defineJestConfig(pkgService, { -+ setupFilesAfterEnv: ['/jest-setup.ts'], -+ testEnvironment: 'jest-environment-jsdom', -}); -``` 假设现在要测试一个 Header 组件: + ```tsx title="src/components/Header.tsx" export default function Header() { - return ( -

Jest Test

- ); + return

Jest Test

; } ``` 编写组件的测试用例: + ```tsx title="tests/Header.spec.tsx" import { render, screen } from '@testing-library/react'; import Header from '../src/components/Header'; @@ -150,37 +123,37 @@ test('test Header component', () => { ### 安装依赖 ```bash -$ npm i vitest -D +$ npm i vitest jsdom @testing-library/react @testing-library/jest-dom -D ``` ### 配置 -首先需要在项目的根目录下新建 `vitest.config.mts` 文件,并加入以下内容: -```js title="vitest.config.mts" -import pkgService, { defineVitestConfig } from '@ice/pkg'; +快速开始时,可以先在项目根目录创建 `vitest.config.mts`: -export default defineVitestConfig(pkgService, {}); +```js title="vitest.config.mts" +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + setupFiles: ['./vitest-setup.ts'], + globals: true, + }, +}); ``` -`defineVitestConfig()` 方法返回的是 ice.js 默认配置好的 vitest 配置,支持传入自定义的 [vitest 配置](https://vitest.dev/config/)。 -defineVitestConfig 第二个入参支持以下三种类型: +默认情况下,ICE PKG 构建时会将 `import.meta.vitest` 替换为 `undefined`,因此可以直接使用 Vitest 的 in-source test 写法,而不会把测试分支带入最终产物。 -- `vitest.UserConfig` -- `Promise` -- `(env) => Promise` - -以修改 `include` 参数为例: +再创建 `vitest-setup.ts`: -```diff title="vitest.config.mts" -import pkgService, { defineVitestConfig } from '@ice/pkg'; +```ts title="vitest-setup.ts" +import matchers from '@testing-library/jest-dom/matchers'; +import { expect } from 'vitest'; -export default defineVitestConfig(pkgService, { -+ test: { -+ include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] -+ } -}); +expect.extend(matchers); ``` -然后在 `package.json` 中加入 `test` 脚本: + +并在 `package.json` 中添加脚本: ```diff title="package.json" { @@ -190,47 +163,36 @@ export default defineVitestConfig(pkgService, { } ``` -### 编写测试用例 - -#### 非 UI 测试 - -请见 [Jest 非 UI 测试章节](#非-ui-测试)。 - -首先安装依赖: - -```bash -$ npm i @testing-library/react jsdom @testing-library/jest-dom -D -``` - -然后在项目根目录下新建 `vitest-setup.ts` 并写入以下内容,以扩展匹配器(matchers): - -```ts title="vitest-setup.ts" -import matchers from '@testing-library/jest-dom/matchers'; -import { expect } from 'vitest'; +可直接传入 [vitest 配置](https://vitest.dev/config/)。 -expect.extend(matchers); -``` - -最后在 `vitest.config.mts` 中加入以下内容: +以修改 `include` 参数为例: ```diff title="vitest.config.mts" -import pkgService, { defineVitestConfig } from '@ice/pkg'; +import { defineConfig } from 'vitest/config'; -export default defineVitestConfig(pkgService, { +export default defineConfig({ + test: { + environment: 'jsdom', + setupFiles: ['./vitest-setup.ts'], ++ globals: true, ++ include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + }, }); ``` +### 编写测试用例 + +#### 非 UI 测试 + +请见 [Jest 非 UI 测试章节](#非-ui-测试)。 + +组件 UI 测试时,上面的快速开始配置已经包含 jsdom 环境、全局 API 和 matcher 扩展。 + 假设现在测试一个 Header 组件: ```tsx title="src/components/Header.tsx" export default function Header() { - return ( -

Vitest Test

- ); + return

Vitest Test

; } ``` diff --git a/website/docs/reference/config.md b/website/docs/reference/config.md index 5d95b630..97795171 100644 --- a/website/docs/reference/config.md +++ b/website/docs/reference/config.md @@ -78,7 +78,7 @@ export default defineConfig({ ### define - 类型:`Record` -- 默认值:`{ __DEV__: 'true' | 'false', 'process.env.NODE_ENV': '"development"' | '"production"' }` +- 默认值:`{ __DEV__: 'true' | 'false', 'process.env.NODE_ENV': '"development"' | '"production"', 'import.meta.vitest': 'undefined' }` 定义编译时环境变量,会在编译时被替换。注意:属性值会经过一次 `JSON.stringify()` 转换。 @@ -119,6 +119,8 @@ if (__DEV__) { 实际上,在编译时,`__DEV__` 会被替换为 `process.env.NODE_ENV !== 'production'`。 ::: +另外,ICE PKG 默认会将 `import.meta.vitest` 替换为 `undefined`。这意味着在源码里使用 Vitest 的 [in-source test](https://vitest.dev/guide/in-source.html) 写法时,非测试构建默认不会把对应测试逻辑保留到产物中。 + ### sourceMaps - 类型:`boolean | 'inline'` @@ -308,15 +310,15 @@ export default defineConfig({ #### excludes - 类型:`string | string[]` -- 默认值:`undefined` +- 默认值:`['**/__tests__/**']` -排除无需编译的文件。比如,我们不想编译 `src` 下的所有测试文件,其中测试文件包含在 `__tests__` 目录下,或以 `*.test.[j|t]s` 结尾。 +排除无需编译的文件。默认会排除 `__tests__` 目录下文件。比如,我们还不想编译 `src` 下以 `*.test.[j|t]s` 结尾的测试文件。 ```ts title="build.config.mts" import { defineConfig } from '@ice/pkg'; export default defineConfig({ - transfrom: { + transform: { excludes: ['**/__tests__/**', '*.test.[j|t]s'], }, });