test(web): exprEvaluator + useDebouncedValue 单元测试 — 24 个用例
exprEvaluator(19): 等值/不等/AND/OR/NOT/括号/短路运算/ missing field/type coercion/visibleWhen 便捷函数。 useDebouncedValue(5): 初始值/防抖/快速更新重置/自定义延迟/数值类型。
This commit is contained in:
78
apps/web/src/hooks/useDebouncedValue.test.ts
Normal file
78
apps/web/src/hooks/useDebouncedValue.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useDebouncedValue } from './useDebouncedValue'
|
||||
|
||||
describe('useDebouncedValue', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('returns initial value immediately', () => {
|
||||
const { result } = renderHook(() => useDebouncedValue('hello'))
|
||||
expect(result.current).toBe('hello')
|
||||
})
|
||||
|
||||
it('debounces value updates', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value }) => useDebouncedValue(value, 300),
|
||||
{ initialProps: { value: 'a' } },
|
||||
)
|
||||
|
||||
expect(result.current).toBe('a')
|
||||
|
||||
rerender({ value: 'b' })
|
||||
expect(result.current).toBe('a')
|
||||
|
||||
act(() => { vi.advanceTimersByTime(299) })
|
||||
expect(result.current).toBe('a')
|
||||
|
||||
act(() => { vi.advanceTimersByTime(1) })
|
||||
expect(result.current).toBe('b')
|
||||
})
|
||||
|
||||
it('resets timer on rapid updates', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value }) => useDebouncedValue(value, 200),
|
||||
{ initialProps: { value: 'a' } },
|
||||
)
|
||||
|
||||
rerender({ value: 'b' })
|
||||
act(() => { vi.advanceTimersByTime(100) })
|
||||
|
||||
rerender({ value: 'c' })
|
||||
act(() => { vi.advanceTimersByTime(100) })
|
||||
expect(result.current).toBe('a')
|
||||
|
||||
act(() => { vi.advanceTimersByTime(100) })
|
||||
expect(result.current).toBe('c')
|
||||
})
|
||||
|
||||
it('uses custom delay', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value }) => useDebouncedValue(value, 500),
|
||||
{ initialProps: { value: 'x' } },
|
||||
)
|
||||
|
||||
rerender({ value: 'y' })
|
||||
act(() => { vi.advanceTimersByTime(499) })
|
||||
expect(result.current).toBe('x')
|
||||
|
||||
act(() => { vi.advanceTimersByTime(1) })
|
||||
expect(result.current).toBe('y')
|
||||
})
|
||||
|
||||
it('works with numeric values', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value }) => useDebouncedValue(value, 100),
|
||||
{ initialProps: { value: 0 } },
|
||||
)
|
||||
|
||||
rerender({ value: 42 })
|
||||
act(() => { vi.advanceTimersByTime(100) })
|
||||
expect(result.current).toBe(42)
|
||||
})
|
||||
})
|
||||
138
apps/web/src/utils/exprEvaluator.test.ts
Normal file
138
apps/web/src/utils/exprEvaluator.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseExpr, evaluateExpr, evaluateVisibleWhen } from './exprEvaluator'
|
||||
|
||||
describe('parseExpr', () => {
|
||||
it('parses equality expression', () => {
|
||||
const ast = parseExpr("status == 'active'")
|
||||
expect(ast).toEqual({
|
||||
type: 'eq',
|
||||
field: 'status',
|
||||
value: 'active',
|
||||
})
|
||||
})
|
||||
|
||||
it('parses inequality expression', () => {
|
||||
const ast = parseExpr("type != 'internal'")
|
||||
expect(ast).toEqual({
|
||||
type: 'neq',
|
||||
field: 'type',
|
||||
value: 'internal',
|
||||
})
|
||||
})
|
||||
|
||||
it('parses AND expression', () => {
|
||||
const ast = parseExpr("status == 'active' AND role == 'admin'")
|
||||
expect(ast).toEqual({
|
||||
type: 'and',
|
||||
left: { type: 'eq', field: 'status', value: 'active' },
|
||||
right: { type: 'eq', field: 'role', value: 'admin' },
|
||||
})
|
||||
})
|
||||
|
||||
it('parses OR expression', () => {
|
||||
const ast = parseExpr("status == 'active' OR status == 'pending'")
|
||||
expect(ast).toEqual({
|
||||
type: 'or',
|
||||
left: { type: 'eq', field: 'status', value: 'active' },
|
||||
right: { type: 'eq', field: 'status', value: 'pending' },
|
||||
})
|
||||
})
|
||||
|
||||
it('parses NOT expression', () => {
|
||||
const ast = parseExpr("NOT status == 'deleted'")
|
||||
expect(ast).toEqual({
|
||||
type: 'not',
|
||||
operand: { type: 'eq', field: 'status', value: 'deleted' },
|
||||
})
|
||||
})
|
||||
|
||||
it('parses parenthesized expression', () => {
|
||||
const ast = parseExpr("(status == 'a' OR status == 'b') AND role == 'admin'")
|
||||
expect(ast?.type).toBe('and')
|
||||
expect(ast?.left?.type).toBe('or')
|
||||
})
|
||||
|
||||
it('returns null for empty input', () => {
|
||||
expect(parseExpr('')).toBeNull()
|
||||
})
|
||||
|
||||
it('parses && and || operators', () => {
|
||||
const ast = parseExpr("a == '1' && b == '2' || c == '3'")
|
||||
expect(ast).toBeDefined()
|
||||
expect(ast?.type).toBe('or')
|
||||
})
|
||||
})
|
||||
|
||||
describe('evaluateExpr', () => {
|
||||
it('evaluates equality true', () => {
|
||||
const ast = parseExpr("status == 'active'")!
|
||||
expect(evaluateExpr(ast, { status: 'active' })).toBe(true)
|
||||
})
|
||||
|
||||
it('evaluates equality false', () => {
|
||||
const ast = parseExpr("status == 'active'")!
|
||||
expect(evaluateExpr(ast, { status: 'inactive' })).toBe(false)
|
||||
})
|
||||
|
||||
it('evaluates inequality', () => {
|
||||
const ast = parseExpr("status != 'deleted'")!
|
||||
expect(evaluateExpr(ast, { status: 'active' })).toBe(true)
|
||||
expect(evaluateExpr(ast, { status: 'deleted' })).toBe(false)
|
||||
})
|
||||
|
||||
it('evaluates AND', () => {
|
||||
const ast = parseExpr("a == '1' AND b == '2'")!
|
||||
expect(evaluateExpr(ast, { a: '1', b: '2' })).toBe(true)
|
||||
expect(evaluateExpr(ast, { a: '1', b: '3' })).toBe(false)
|
||||
expect(evaluateExpr(ast, { a: '0', b: '2' })).toBe(false)
|
||||
})
|
||||
|
||||
it('evaluates OR', () => {
|
||||
const ast = parseExpr("a == '1' OR b == '2'")!
|
||||
expect(evaluateExpr(ast, { a: '1', b: 'x' })).toBe(true)
|
||||
expect(evaluateExpr(ast, { a: 'x', b: '2' })).toBe(true)
|
||||
expect(evaluateExpr(ast, { a: 'x', b: 'x' })).toBe(false)
|
||||
})
|
||||
|
||||
it('evaluates NOT', () => {
|
||||
const ast = parseExpr("NOT a == '1'")!
|
||||
expect(evaluateExpr(ast, { a: '1' })).toBe(false)
|
||||
expect(evaluateExpr(ast, { a: '2' })).toBe(true)
|
||||
})
|
||||
|
||||
it('handles missing field as empty string', () => {
|
||||
const ast = parseExpr("status == ''")!
|
||||
expect(evaluateExpr(ast, {})).toBe(true)
|
||||
})
|
||||
|
||||
it('converts non-string values to string', () => {
|
||||
const ast = parseExpr("count == '5'")!
|
||||
expect(evaluateExpr(ast, { count: 5 })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('evaluateVisibleWhen', () => {
|
||||
it('returns true for undefined expression', () => {
|
||||
expect(evaluateVisibleWhen(undefined, {})).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true for empty string expression', () => {
|
||||
expect(evaluateVisibleWhen('', {})).toBe(true)
|
||||
})
|
||||
|
||||
it('evaluates complex expression', () => {
|
||||
expect(
|
||||
evaluateVisibleWhen("type == 'doctor' AND status == 'active'", {
|
||||
type: 'doctor',
|
||||
status: 'active',
|
||||
}),
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
evaluateVisibleWhen("type == 'doctor' AND status == 'active'", {
|
||||
type: 'doctor',
|
||||
status: 'inactive',
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user