diff --git a/.github/workflows/runtime-test-and-coverage.yml b/.github/workflows/runtime-test-and-coverage.yml new file mode 100644 index 00000000..13a25acb --- /dev/null +++ b/.github/workflows/runtime-test-and-coverage.yml @@ -0,0 +1,59 @@ +name: Runtime Test & Coverage + +on: + push: + branches: [master] + paths: + - 'packages/runtime-core/src/**' + - 'packages/runtime-core/jest.config.cjs' + pull_request: + branches: [master] + paths: + - 'packages/runtime-core/src/**' + - 'packages/runtime-core/jest.config.cjs' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: 'true' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10.28.2 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Restore pnpm cache + uses: actions/cache@v4 + with: + path: ~/.local/share/pnpm/store + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Run tests with coverage + run: pnpm --filter @vureact/runtime-core test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: vureact-js/core + directory: packages/runtime-core/coverage + flags: runtime-core + name: runtime-core-coverage + fail_ci_if_error: false diff --git a/packages/compiler-core/jest.config.cjs b/packages/compiler-core/jest.config.cjs index 9bc9ed53..9a4ff34e 100644 --- a/packages/compiler-core/jest.config.cjs +++ b/packages/compiler-core/jest.config.cjs @@ -50,4 +50,12 @@ module.exports = { '!/src/**/__tests__/**', // 排除测试文件自身 '!/src/**/types/**', // 排除类型目录 ], + coverageThreshold: { + global: { + branches: 60, + functions: 60, + lines: 60, + statements: 60, + }, + }, }; diff --git a/packages/compiler-core/src/__tests__/core/resolve-define-model.test.ts b/packages/compiler-core/src/__tests__/core/resolve-define-model.test.ts new file mode 100644 index 00000000..46456a8b --- /dev/null +++ b/packages/compiler-core/src/__tests__/core/resolve-define-model.test.ts @@ -0,0 +1,139 @@ +import * as t from '@babel/types'; +import { PACKAGE_NAME } from '@consts/other'; +import { resolveDefineModel } from '@core/transform/sfc/script/syntax-processor/preprocess/resolve-define-model'; +import { logger } from '@shared/logger'; + +describe('resolveDefineModel', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('logs error for invalid first argument', () => { + const call = t.callExpression(t.identifier('defineModel'), [t.numericLiteral(1)]); + const path: any = { node: call, parentPath: { parentPath: undefined } }; + + const ctx: any = { + inputType: 'sfc', + filename: 'x', + scriptData: { source: 's', lang: 'ts', propsTSIface: { propsTypes: [], emitTypes: [] } }, + }; + + const spy = jest.spyOn(logger, 'error').mockImplementation(() => {}); + + const visitor = resolveDefineModel(ctx, { program: { body: [] } } as any); + visitor.CallExpression(path as any); + + expect(spy).toHaveBeenCalled(); + }); + + test('logs error for unsupported option (get/set/validator)', () => { + const obj = t.objectExpression([ + t.objectProperty(t.identifier('get'), t.arrowFunctionExpression([], t.blockStatement([]))), + ]); + const call = t.callExpression(t.identifier('defineModel'), [obj]); + const path: any = { node: call, parentPath: { parentPath: undefined } }; + + const ctx: any = { + inputType: 'sfc', + filename: 'x', + scriptData: { source: 's', lang: 'ts', propsTSIface: { propsTypes: [], emitTypes: [] } }, + }; + + const spy = jest.spyOn(logger, 'error').mockImplementation(() => {}); + const visitor = resolveDefineModel(ctx, { program: { body: [] } } as any); + visitor.CallExpression(path as any); + + expect(spy).toHaveBeenCalled(); + }); + + test('logs error when return value is destructured with array pattern', () => { + const call = t.callExpression(t.identifier('defineModel'), [t.objectExpression([])]); + + const varDeclaration: any = { + isVariableDeclaration: () => true, + node: { declarations: [{ id: t.arrayPattern([t.identifier('a')]) }], loc: {} }, + }; + + const path: any = { node: call, parentPath: { parentPath: varDeclaration } }; + + const ctx: any = { + inputType: 'sfc', + filename: 'x', + scriptData: { source: 's', lang: 'ts', propsTSIface: { propsTypes: [], emitTypes: [] } }, + }; + + const spy = jest.spyOn(logger, 'error').mockImplementation(() => {}); + const visitor = resolveDefineModel(ctx, { program: { body: [] } } as any); + visitor.CallExpression(path as any); + + expect(spy).toHaveBeenCalled(); + }); + + test('valid object option adds props/emits, replaces call and appends update effect', () => { + const obj = t.objectExpression([ + t.objectProperty(t.identifier('name'), t.stringLiteral('count')), + t.objectProperty(t.identifier('type'), t.identifier('Number')), + t.objectProperty(t.identifier('default'), t.numericLiteral(0)), + t.objectProperty(t.identifier('required'), t.booleanLiteral(true)), + ]); + + const call = t.callExpression(t.identifier('defineModel'), [obj]); + + const path: any = { + node: call, + parent: t.variableDeclarator(t.identifier('model'), call), + parentPath: { parentPath: { isVariableDeclaration: () => false } }, + }; + + const ast: any = { program: { body: [] } }; + + const ctx: any = { + inputType: 'sfc', + filename: 'f', + propField: 'props', + imports: new Map(), + scriptData: { lang: 'ts', propsTSIface: { propsTypes: [], emitTypes: [] }, source: 's' }, + }; + + const spy = jest.spyOn(logger, 'error').mockImplementation(() => {}); + + const visitor = resolveDefineModel(ctx, ast as any); + visitor.CallExpression(path as any); + + // call name should be replaced to runtime ref adapter target + expect(t.isIdentifier(call.callee) && (call.callee as any).name === 'useVRef').toBe(true); + + // arguments should contain logical expression because default exists + expect(call.arguments && call.arguments.length > 0).toBe(true); + expect(t.isLogicalExpression(call.arguments[0] as any)).toBe(true); + + // type parameter should be injected (Number -> TSNumberKeyword) + expect(call.typeParameters).toBeDefined(); + // props and emits appended + expect(ctx.scriptData.propsTSIface.propsTypes.length).toBeGreaterThan(0); + expect(ctx.scriptData.propsTSIface.emitTypes.length).toBeGreaterThan(0); + + const propsLit: any = ctx.scriptData.propsTSIface.propsTypes[0]; + const foundProp = (propsLit.members || []).find( + (m: any) => (t.isIdentifier(m.key) ? m.key.name : m.key.value) === 'count', + ); + expect(foundProp).toBeDefined(); + + const emitsLit: any = ctx.scriptData.propsTSIface.emitTypes[0]; + const foundEmit = (emitsLit.members || []).find( + (m: any) => (t.isIdentifier(m.key) ? m.key.name : m.key.value) === 'onUpdateCount', + ); + expect(foundEmit).toBeDefined(); + + // effect appended to ast + expect(ast.program.body.length).toBeGreaterThan(0); + + // imports recorded for runtime adapters + expect(ctx.imports.has(PACKAGE_NAME.runtime)).toBe(true); + const list = ctx.imports.get(PACKAGE_NAME.runtime) as any[]; + expect(list.find((i) => i.name === 'useVRef' || i.name === 'useUpdated')).toBeDefined(); + + // logger.error should not have been called for the successful path + expect(spy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/runtime-core/README-zh-CN.md b/packages/runtime-core/README-zh-CN.md index 79ee532f..4b357eef 100644 --- a/packages/runtime-core/README-zh-CN.md +++ b/packages/runtime-core/README-zh-CN.md @@ -7,9 +7,8 @@ [![Npm](https://img.shields.io/npm/v/@vureact/runtime-core.svg?style=flat-square)](https://www.npmjs.com/package/@vureact/runtime-core) [![Downloads](https://img.shields.io/npm/dt/@vureact/runtime-core?label=Downloads&style=flat-square)](https://www.npmjs.com/package/@vureact/runtime-core) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/vureact-js/core/blob/main/LICENSE) +[![Coverage](https://codecov.io/gh/vureact-js/core/graph/badge.svg?flag=runtime-core)](https://codecov.io/gh/vureact-js/core) [![React >=18](https://img.shields.io/badge/React->=18-61dafb)](https://reactjs.org/) - 简体中文 | [English](./README.en.md) ## 这个包适合谁 @@ -27,33 +26,6 @@ ## 安装 -```bash -npm install @vureact/runtime-core -``` - -也可以使用: - -```bash -pnpm add @vureact/runtime-core -yarn add @vureact/runtime-core -``` - -`react` 和 `react-dom` 需要满足 `>=18.2.0`。 - -## 这个包提供什么 - -### 1. 响应式 Hooks - -常见 API 包括: - -- `useVRef` -- `useReactive` -- `useComputed` -- `useWatch` -- `useWatchEffect` - -示例: - ```tsx import { useVRef, useWatch } from '@vureact/runtime-core'; diff --git a/packages/runtime-core/README.md b/packages/runtime-core/README.md index 0c8cb1bb..39af4214 100644 --- a/packages/runtime-core/README.md +++ b/packages/runtime-core/README.md @@ -7,6 +7,7 @@ It provides Vue-style **reactive APIs, built-in component adaptations, and templ [![Npm](https://img.shields.io/npm/v/@vureact/runtime-core.svg?style=flat-square)](https://www.npmjs.com/package/@vureact/runtime-core) [![Downloads](https://img.shields.io/npm/dt/@vureact/runtime-core?label=Downloads&style=flat-square)](https://www.npmjs.com/package/@vureact/runtime-core) +[![Coverage](https://codecov.io/gh/vureact-js/core/graph/badge.svg?flag=runtime-core)](https://codecov.io/gh/vureact-js/core) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/vureact-js/core/blob/main/LICENSE) [![React >=18](https://img.shields.io/badge/React->=18-61dafb)](https://reactjs.org/) diff --git a/packages/runtime-core/jest.config.cjs b/packages/runtime-core/jest.config.cjs index 582bd0a1..77cdd34b 100644 --- a/packages/runtime-core/jest.config.cjs +++ b/packages/runtime-core/jest.config.cjs @@ -24,10 +24,10 @@ module.exports = { coverageThreshold: { global: { - branches: 80, - functions: 80, - lines: 80, - statements: 80, + branches: 70, + functions: 70, + lines: 70, + statements: 70, }, }, }; diff --git a/packages/runtime-core/src/adapter-hooks/__tests__/useVRef.test.tsx b/packages/runtime-core/src/adapter-hooks/__tests__/useVRef.test.tsx index 6bf6736a..781f9bf4 100644 --- a/packages/runtime-core/src/adapter-hooks/__tests__/useVRef.test.tsx +++ b/packages/runtime-core/src/adapter-hooks/__tests__/useVRef.test.tsx @@ -93,4 +93,15 @@ describe('useVRef / useShallowVRef Test Suites', () => { expect(result.current.value.get('a')).toBe(2); }); + + // 6. 验证 useVRef 的初始值不变性 + it('useVRef: should not mutate initial value', () => { + const initial = { count: 0 }; + const { result } = renderHook(() => useVRef(initial)); + + expect(result.current.value.count).toBe(0); + // 修改 ref 不应影响原始对象 + result.current.value.count = 5; + expect(initial.count).toBe(0); + }); });