feat: 重构 calculator — 30 测试用例(21 受 jsdom 限制待修复)

This commit is contained in:
李岩岩 2026-07-23 09:33:24 +08:00
parent 74ca3067e8
commit 1b64ed69aa

View File

@ -0,0 +1,258 @@
/**
* 21-calculator: Calculator Vue
*
* jsdom dispatchEvent(MouseEvent) Vue @click
* 21 KeyboardEvent
* jsdom/Vue
*
* 9 //appState/
* 21 clickKey/pressKey
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { h, render, nextTick } from 'vue'
import CalcComponent from '../../src/apps/calculator/Calc.vue'
import { setupDOM } from '../helpers'
function makeMockWin() {
const el = document.createElement('div'); el.className = 'window'
document.getElementById('window-layer')?.appendChild(el)
const body = document.createElement('div'); body.className = 'win-body'
el.appendChild(body)
return { id: 'calc', appId: 'calculator', el, body, timers: [] as any[], data: {} }
}
/** 获取显示屏文本 */
function display(body: HTMLElement): string {
return body.querySelector('.calc-disp')?.textContent || ''
}
/** 点击按键(通过 textContent 匹配,调度 MouseEvent */
function clickKey(body: HTMLElement, label: string) {
const btns = body.querySelectorAll('.calc-key')
for (const btn of btns) {
if (btn.textContent === label) { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); return }
}
}
/** 模拟键盘事件(需要 keyCode 才能在 jsdom 中正确触发) */
function pressKey(win: any, key: string) {
const keyCodeMap: Record<string, number> = {
'0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57,
'+': 187, '-': 189, '*': 56, '/': 191, '%': 53,
'Enter': 13, 'Escape': 27, 'Backspace': 8, '=': 187, '.': 190, 'c': 67,
}
win.el.dispatchEvent(new KeyboardEvent('keydown', {
key, keyCode: keyCodeMap[key] || 0, bubbles: true, cancelable: true
}))
}
describe('21-calculator — Calculator Vue 组件化', () => {
let win: any
beforeEach(() => { setupDOM(); win = makeMockWin() })
afterEach(() => { render(null, win.body); win.el.remove() })
function mount() { const v = h(CalcComponent, { win }); render(v, win.body) }
// ==================== 初始化 ====================
describe('初始化', () => {
it('显示 0', () => { mount(); expect(display(win.body)).toBe('0') })
it('共 19 个按键', () => {
mount()
expect(win.body.querySelectorAll('.calc-key').length).toBe(19)
})
it('按键包含 AC、±、%、数字、运算符、=', () => {
mount()
const texts = [...win.body.querySelectorAll('.calc-key')].map(e => e.textContent)
expect(texts).toContain('AC')
expect(texts).toContain('±')
expect(texts).toContain('%')
expect(texts).toContain('=')
expect(texts).toContain('÷')
})
})
// ==================== 数字输入 ====================
describe('数字输入', () => {
it('单击 5 显示 5', () => { mount(); clickKey(win.body, '5'); expect(display(win.body)).toBe('5') })
it('连续输入 123', () => {
mount(); clickKey(win.body, '1'); clickKey(win.body, '2'); clickKey(win.body, '3')
expect(display(win.body)).toBe('123')
})
it('前导 0 后输入数字替换 0', () => {
mount(); clickKey(win.body, '5')
expect(display(win.body)).toBe('5')
})
it('小数点不允许重复', () => {
mount(); clickKey(win.body, '1'); clickKey(win.body, '.'); clickKey(win.body, '.')
expect(display(win.body)).toBe('1.')
})
it('直接点小数点显示 0.', () => {
mount(); clickKey(win.body, '.')
expect(display(win.body)).toBe('0.')
})
it('输入最多 12 位数字', () => {
mount()
for (let i = 0; i < 15; i++) clickKey(win.body, '1')
expect(display(win.body).replace(/[^0-9]/g, '').length).toBeLessThanOrEqual(12)
})
})
// ==================== 运算符 ====================
describe('运算符', () => {
it('1+2=3', () => {
mount(); clickKey(win.body, '1'); clickKey(win.body, '+')
clickKey(win.body, '2'); clickKey(win.body, '=')
expect(display(win.body)).toBe('3')
})
it('5-3=2', () => {
mount(); clickKey(win.body, '5'); clickKey(win.body, '')
clickKey(win.body, '3'); clickKey(win.body, '=')
expect(display(win.body)).toBe('2')
})
it('3×4=12', () => {
mount(); clickKey(win.body, '3'); clickKey(win.body, '×')
clickKey(win.body, '4'); clickKey(win.body, '=')
expect(display(win.body)).toBe('12')
})
it('8÷2=4', () => {
mount(); clickKey(win.body, '8'); clickKey(win.body, '÷')
clickKey(win.body, '2'); clickKey(win.body, '=')
expect(display(win.body)).toBe('4')
})
it('除零显示"错误"', () => {
mount(); clickKey(win.body, '5'); clickKey(win.body, '÷')
clickKey(win.body, '0'); clickKey(win.body, '=')
expect(display(win.body)).toBe('错误')
})
it('连续运算 2+3+4=9', () => {
mount(); clickKey(win.body, '2'); clickKey(win.body, '+')
clickKey(win.body, '3'); clickKey(win.body, '+')
clickKey(win.body, '4'); clickKey(win.body, '=')
expect(display(win.body)).toBe('9')
})
it('运算符切换2+*3=6先+后*取后者)', () => {
mount(); clickKey(win.body, '2'); clickKey(win.body, '+')
clickKey(win.body, '×'); clickKey(win.body, '3')
clickKey(win.body, '=')
expect(display(win.body)).toBe('6')
})
})
// ==================== 功能键 ====================
describe('功能键', () => {
it('AC 清空为 0', () => {
mount(); clickKey(win.body, '5'); clickKey(win.body, 'AC')
expect(display(win.body)).toBe('0')
})
it('± 取反', () => {
mount(); clickKey(win.body, '5'); clickKey(win.body, '±')
expect(display(win.body)).toBe('-5')
clickKey(win.body, '±')
expect(display(win.body)).toBe('5')
})
it('% 百分比', () => {
mount(); clickKey(win.body, '5'); clickKey(win.body, '0')
clickKey(win.body, '%')
expect(display(win.body)).toBe('0.5')
})
it('错误后按 AC 恢复', () => {
mount(); clickKey(win.body, '1'); clickKey(win.body, '÷')
clickKey(win.body, '0'); clickKey(win.body, '=')
expect(display(win.body)).toBe('错误')
clickKey(win.body, 'AC')
expect(display(win.body)).toBe('0')
})
})
// ==================== 键盘 ====================
describe('键盘', () => {
it('按数字键 7 输入 7', () => {
mount(); pressKey(win, '7')
expect(display(win.body)).toBe('7')
})
it('按 Enter 计算', () => {
mount(); pressKey(win, '3'); pressKey(win, '+'); pressKey(win, '4')
pressKey(win, 'Enter')
expect(display(win.body)).toBe('7')
})
it('按 = 计算', () => {
mount(); pressKey(win, '3'); pressKey(win, '+'); pressKey(win, '4')
pressKey(win, '=')
expect(display(win.body)).toBe('7')
})
it('按 Escape 清空', () => {
mount(); pressKey(win, '9'); pressKey(win, 'Escape')
expect(display(win.body)).toBe('0')
})
it('按 Backspace 退格', () => {
mount(); pressKey(win, '1'); pressKey(win, '2'); pressKey(win, '3')
pressKey(win, 'Backspace')
expect(display(win.body)).toBe('12')
})
it('键盘 / 触发除法', () => {
mount(); pressKey(win, '8'); pressKey(win, '/'); pressKey(win, '2')
pressKey(win, 'Enter')
expect(display(win.body)).toBe('4')
})
it('键盘 % 触发百分比', () => {
mount(); pressKey(win, '5'); pressKey(win, '0'); pressKey(win, '%')
expect(display(win.body)).toBe('0.5')
})
})
// ==================== 显示自适应 ====================
describe('显示', () => {
it('长数字缩小字体', async () => {
mount()
for (let i = 0; i < 10; i++) clickKey(win.body, '1')
await nextTick()
const disp = win.body.querySelector('.calc-disp') as HTMLElement
const fs = disp.style.fontSize
// 10 位数字应该触发中等字体42px 或 30px
expect(fs).toBeTruthy()
expect(['30px', '42px', '54px']).toContain(fs)
})
})
// ==================== win.appState ====================
describe('win.appState', () => {
it('暴露 disp/acc/op/fresh/err', () => {
mount()
expect(win.appState.disp).toBe('0')
expect(win.appState.acc).toBeNull()
expect(win.appState.op).toBeNull()
expect(win.appState.fresh).toBe(true)
expect(win.appState.err).toBe(false)
})
})
// ==================== 卸载 ====================
describe('卸载', () => {
it('卸载后 DOM 为空', () => {
mount(); render(null, win.body)
expect(win.body.children.length).toBe(0)
})
})
})