feat: 重构 reminders — 简化 index.ts(Vue 组件已完备)+ 9 测试
This commit is contained in:
parent
f1621372af
commit
bd7360784a
@ -1,23 +1,20 @@
|
|||||||
// 提醒事项应用
|
// 提醒事项应用 — Vue 组件化
|
||||||
import { el, uid, localDateKey } from '../../utils'
|
import { h, render } from 'vue'
|
||||||
import { store as Store } from '../../composables/useStore'
|
|
||||||
import { ui as UI } from '../../composables/useUI'
|
|
||||||
import { Apps, stdMenus } from '../../composables/useApps'
|
import { Apps, stdMenus } from '../../composables/useApps'
|
||||||
|
import RemindersComponent from './Reminders.vue'
|
||||||
|
|
||||||
export const RemindersApp = {
|
export const RemindersApp = {
|
||||||
id: 'reminders', name: '提醒事项', icon: '/assets/icons/reminders.png',
|
id: 'reminders', name: '提醒事项', icon: '/assets/icons/reminders.png',
|
||||||
w: 720, h: 500, minW: 520, minH: 340,
|
w: 720, h: 500, minW: 520, minH: 340,
|
||||||
menus(win: any) { return stdMenus(this, { file: [{ label: '新建提醒事项', key: '⌘N', action: () => win?.appState?.focusNew() }] }) },
|
menus(win: any) {
|
||||||
colors: ['#0a84ff', '#ff9f0a', '#ff453a', '#32d74b', '#bf5af2'],
|
return stdMenus(this, {
|
||||||
store: { get() { return Store.get('reminders', { lists: [{ id:'l1',name:'提醒',color:'#0a84ff' },{ id:'l2',name:'工作',color:'#ff9f0a' },{ id:'l3',name:'家庭',color:'#32d74b' }], items: [{ id:uid(),list:'l1',title:'体验 macOS 网页版',due:'',done:false },{ id:uid(),list:'l2',title:'周五前提交周报',due:localDateKey(new Date(Date.now()+86400000*2)),done:false },{ id:uid(),list:'l3',title:'买牛奶和鸡蛋',due:'',done:true }] }) }, set(v:any) { Store.set('reminders',v) } },
|
file: [{ label: '新建提醒事项', key: '⌘N', action: () => win?.appState?.focusNew() }]
|
||||||
|
})
|
||||||
|
},
|
||||||
render(win: any) {
|
render(win: any) {
|
||||||
const st = win.appState = { data: this.store.get(), cur: 'l1' }; win.body.classList.add('rem-body')
|
const vnode = h(RemindersComponent, { win })
|
||||||
const side = el('div',{class:'rem-side'}), listEl = el('div',{class:'rem-items'}), newIn = el('input',{class:'text-input rem-new',type:'text',placeholder:'添加提醒事项,回车确认'}), main = el('div',{class:'rem-main'},listEl,el('div',{class:'rem-new-row'},newIn))
|
render(vnode, win.body)
|
||||||
win.body.append(side,main); const save = () => this.store.set(st.data); st.focusNew = () => (newIn as HTMLInputElement).focus()
|
},
|
||||||
const renderSide = () => { side.innerHTML=''; side.append(el('div',{class:'fb-side-title',text:'我的列表'})); st.data.lists.forEach((l:any) => { const row = el('div',{class:'fb-side-item'+(st.cur===l.id?' sel':'')},el('span',{class:'rem-color',style:{background:l.color}}),el('span',{class:'rem-lname',text:l.name}),el('span',{class:'rem-count',text:String(st.data.items.filter((i:any)=>i.list===l.id&&!i.done).length)})); row.addEventListener('click',()=>{st.cur=l.id;renderAll()}); side.append(row) }) }
|
|
||||||
const renderItems = () => { listEl.innerHTML=''; const items=st.data.items.filter((i:any)=>i.list===st.cur), open=items.filter((i:any)=>!i.done), done=items.filter((i:any)=>i.done); const mkRow=(it:any)=>{const cb=el('button',{class:'rem-check'+(it.done?' done':''),'aria-label':it.done?'标记为未完成':'标记为完成'});cb.addEventListener('click',()=>{it.done=!it.done;save();renderAll()});const ti=el('input',{class:'rem-title'+(it.done?' done':''),value:it.title});ti.addEventListener('change',()=>{it.title=(ti as HTMLInputElement).value.trim()||it.title;save();renderAll()});const di=el('input',{class:'rem-due',type:'date',value:it.due||''});di.addEventListener('change',()=>{it.due=(di as HTMLInputElement).value;save()});return el('div',{class:'rem-row'},cb,ti,di)}; open.forEach((it:any)=>listEl.append(mkRow(it))); if(done.length){listEl.append(el('div',{class:'fb-side-title',text:`已完成(${done.length})`}));done.forEach((it:any)=>listEl.append(mkRow(it)))}; if(!items.length)listEl.append(el('div',{class:'empty-state',style:{height:'140px'},text:'此列表为空'})) }
|
|
||||||
newIn.addEventListener('keydown',(e:KeyboardEvent)=>{e.stopPropagation();if(e.key==='Enter'&&(newIn as HTMLInputElement).value.trim()){st.data.items.push({id:uid(),list:st.cur,title:(newIn as HTMLInputElement).value.trim(),due:'',done:false});(newIn as HTMLInputElement).value='';save();renderAll()}})
|
|
||||||
const renderAll=()=>{renderSide();renderItems()};renderAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Apps.register(RemindersApp)
|
Apps.register(RemindersApp)
|
||||||
|
;(window as any).RemindersApp = RemindersApp
|
||||||
|
|||||||
28
tests/cases/36-reminders.test.ts
Normal file
28
tests/cases/36-reminders.test.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 36-reminders: Reminders Vue 组件化测试
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { h, render, nextTick } from 'vue'
|
||||||
|
import RemindersComponent from '../../src/apps/reminders/Reminders.vue'
|
||||||
|
import { store } from '../../src/composables/useStore'
|
||||||
|
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 { el, body, timers: [] as any[], data: {} } }
|
||||||
|
|
||||||
|
describe('36-reminders — Reminders Vue 组件化', () => {
|
||||||
|
let win: any
|
||||||
|
beforeEach(() => { localStorage.clear(); store.set('reminders', null); setupDOM(); win = makeMockWin() })
|
||||||
|
afterEach(() => { render(null, win.body); win.el.remove() })
|
||||||
|
function mount() { const v = h(RemindersComponent, { win }); render(v, win.body) }
|
||||||
|
|
||||||
|
it('显示 3 个列表', () => { mount(); expect(win.body.querySelectorAll('.fb-side-item').length).toBe(3) })
|
||||||
|
it('列表有颜色标识', () => { mount(); expect(win.body.querySelector('.rem-color')).toBeTruthy() })
|
||||||
|
it('显示未完成计数', () => { mount(); expect(win.body.querySelector('.rem-count')?.textContent).toBeTruthy() })
|
||||||
|
it('切换列表', async () => { mount(); const items = win.body.querySelectorAll('.fb-side-item'); (items[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(items[1].classList.contains('sel')).toBe(true) })
|
||||||
|
it('显示提醒事项', () => { mount(); expect(win.body.querySelector('.rem-title')).toBeTruthy() })
|
||||||
|
// ⚠️ jsdom 中 toggleItem 触发后 class 更新可能需额外 nextTick
|
||||||
|
it('勾选标记完成', async () => { mount(); const cb = win.body.querySelector('.rem-check') as HTMLElement; expect(cb).toBeTruthy(); const wasDone = cb.classList.contains('done'); cb.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); await nextTick(); const cb2 = win.body.querySelector('.rem-check') as HTMLElement; expect(cb2.classList.contains('done')).toBe(!wasDone) })
|
||||||
|
it('Enter 添加新事项', async () => { mount(); const input = win.body.querySelector('.rem-new') as HTMLInputElement; input.value = '新提醒'; input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); await nextTick(); expect(input.value).toBe('') })
|
||||||
|
it('空列表显示提示', () => { store.set('reminders', { lists: [{ id: 'e1', name: '空', color: '#ff9f0a' }], items: [] }); mount(); expect(win.body.querySelector('.empty-state')?.textContent).toContain('此列表为空') })
|
||||||
|
it('卸载', () => { mount(); render(null, win.body); expect(win.body.children.length).toBe(0) })
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user