Skip to content
Merged
59 changes: 59 additions & 0 deletions .github/workflows/runtime-test-and-coverage.yml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions packages/compiler-core/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,12 @@ module.exports = {
'!<rootDir>/src/**/__tests__/**', // 排除测试文件自身
'!<rootDir>/src/**/types/**', // 排除类型目录
],
coverageThreshold: {
global: {
branches: 60,
functions: 60,
lines: 60,
statements: 60,
},
},
};
139 changes: 139 additions & 0 deletions packages/compiler-core/src/__tests__/core/resolve-define-model.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>(),
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();
});
});
30 changes: 1 addition & 29 deletions packages/runtime-core/README-zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

## 这个包适合谁
Expand All @@ -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';

Expand Down
1 change: 1 addition & 0 deletions packages/runtime-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)

Expand Down
8 changes: 4 additions & 4 deletions packages/runtime-core/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ module.exports = {

coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
};
11 changes: 11 additions & 0 deletions packages/runtime-core/src/adapter-hooks/__tests__/useVRef.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading