This commit is contained in:
fuenmao
2026-11-13 14:03:38 +08:00
parent db0c578890
commit 09e8bded7e
22 changed files with 1309 additions and 607 deletions
+57 -3
View File
@@ -144,8 +144,9 @@ test('late login response cannot write a token or navigate after leaving the aut
const server = await createServer({ configFile: false, root: path.dirname(root), plugins: [vuePlugin()], appType: 'custom',
server: { middlewareMode: true, hmr: false }, optimizeDeps: { noDiscovery: true, include: [] } })
after(() => server.close())
async function render(name, values = {}, routePath = '/dashboard') {
const { default: view } = await server.ssrLoadModule(`/src/views/${name}.vue`)
async function render(name, values = {}, routePath = '/dashboard', props = {}) {
const componentPath = name.includes('/') ? name : `views/${name}`
const { default: view } = await server.ssrLoadModule(`/src/${componentPath}.vue`)
const component = { ...view, setup(props, context) {
const state = view.setup(props, context)
for (const [key, value] of Object.entries(values)) { assert.ok(isRef(state[key]), key); state[key].value = value }
@@ -153,7 +154,7 @@ async function render(name, values = {}, routePath = '/dashboard') {
} }
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/:pathMatch(.*)*', component }] })
await router.push(routePath); await router.isReady()
const app = createSSRApp({ render: () => h(component) }); app.use(router)
const app = createSSRApp({ render: () => h(component, props) }); app.use(router)
return renderToString(app)
}
@@ -171,6 +172,59 @@ test('home renders a prominent scene, collapsed presets, a real profile entry an
assert.ok(html.indexOf('健康评估</span>') < html.indexOf('我的</span>'))
})
test('automation card separates the rule switch from physical readback and preserves previous results while stopped', async () => {
const state = { available: true, enabled: false, actionLabel: '关闭窗帘', targetLabel: '美百帘窗帘', records: [
{ id: 'one', actionKey: 'curtain-close', targetLabel: '美百帘窗帘', triggeredAtMs: 1794380000000, result: 'readback-confirmed' },
{ id: 'two', actionKey: 'curtain-close', targetLabel: '美百帘窗帘', triggeredAtMs: 1794381000000, result: 'sent-readback-unconfirmed' },
] }
const props = { bindingId: 'simulation-left' }
const stopped = await render('components/Ib43AutomationCard', { status: state }, '/sleep-data', props)
assert.match(stopped, /已停止/)
assert.match(stopped, /开启联动/)
assert.match(stopped, /美百帘窗帘 · 已确认关闭/)
assert.match(stopped, /已发出关帘指令,未确认关闭/)
const started = await render('components/Ib43AutomationCard', { status: { ...state, enabled: true } }, '/sleep-data', props)
assert.match(started, /停止联动/)
assert.doesNotMatch(started, /开启联动/)
const both = await render('components/Ib43AutomationCard', { status: {
...state,
rules: [
{ actionKey: 'curtain-close', actionLabel: '关闭窗帘', enabled: false, targetLabel: '美百帘窗帘' },
{ actionKey: 'dehumidifier-humidity-preview', actionLabel: '湿度偏高时开启除湿机', enabled: true,
targetLabel: '除湿机', previewOnly: true, conditionLabel: '室内相对湿度超过 55% 时记录开机计划。' },
{ actionKey: 'dehumidifier-humidity-start', actionLabel: '湿度偏高时开启除湿机', enabled: false,
targetLabel: '除湿机', previewOnly: false, conditionLabel: '室内相对湿度超过 55% 且除湿机关机时,发送开机指令。' },
{ actionKey: 'climate-temperature-preview', actionLabel: '按室温判断空调模式', enabled: false,
targetLabel: '空调温控器2', previewOnly: true, conditionLabel: '室温高于 25℃ 时计划制冷,低于 22℃ 时计划制热。' },
{ actionKey: 'climate-heat-start', actionLabel: '室温偏低时开启空调制热', enabled: false,
targetLabel: '空调温控器2', previewOnly: false, conditionLabel: '室温低于 22℃ 且空调关机时,开启制热并设为 22℃。' },
{ actionKey: 'wake-fragrance-quick-release', actionLabel: '让香氛机「唤醒」出香一次', enabled: false,
targetLabel: '香氛机 唤醒', previewOnly: false },
{ actionKey: 'speaker-sleep-music', actionLabel: '用 Xiaomi Sound Pro 播放舒睡音乐', enabled: false,
targetLabel: 'Xiaomi Sound Pro', previewOnly: false },
],
records: [...state.records,
{ id: 'three', actionKey: 'dehumidifier-humidity-preview', targetLabel: '除湿机',
triggeredAtMs: 1794382000000, result: 'preview-would-start' },
{ id: 'four', actionKey: 'dehumidifier-humidity-start', targetLabel: '除湿机',
triggeredAtMs: 1794383000000, result: 'readback-confirmed' },
{ id: 'five', actionKey: 'climate-temperature-preview', targetLabel: '空调温控器2',
triggeredAtMs: 1794384000000, result: 'preview-climate-cool' },
{ id: 'six', actionKey: 'climate-heat-start', targetLabel: '空调温控器2',
triggeredAtMs: 1794385000000, result: 'readback-confirmed' },
{ id: 'seven', actionKey: 'wake-fragrance-quick-release', targetLabel: '香氛机 唤醒',
triggeredAtMs: 1794386000000, result: 'fragrance-release-sent' },
{ id: 'eight', actionKey: 'speaker-sleep-music', targetLabel: 'Xiaomi Sound Pro',
triggeredAtMs: 1794387000000, result: 'speaker-playback-sent' }],
} }, '/sleep-data', props)
for (const text of ['开启联动', '停止预演', '超过 55% 且除湿机关机时', '除湿机 · 已确认开启', '除湿机 · 预演通过,计划开启除湿机',
'按室温判断空调模式(预演)', '高于 25℃ 时计划制冷', '空调温控器2 · 预演通过,计划制冷',
'室温偏低时开启空调制热', '低于 22℃ 且空调关机时', '空调温控器2 · 已确认制热至 22℃',
'香氛机 唤醒 · 已发送一次快速出香指令', 'Xiaomi Sound Pro · 已发送舒睡音乐播放指令'])
assert.ok(both.includes(text), text)
})
const sleepBed = { id: 'left-bed', bedName: '卧室左侧', side: 'left', simulation: true, endedAtMs: null }
const rightBed = { id: 'right-bed', bedName: '卧室右侧', side: 'right', simulation: false, endedAtMs: null }
+8 -8
View File
@@ -81,12 +81,12 @@ test('parameter conversion rejects junk and unavailable data; light zero and har
const { createDeviceParameterState, syncDeviceParameters } = env.load('devices/parameterRegistry')
const params = createDeviceParameterState()
syncDeviceParameters(params, { brightness: control('off'), color_temp: control('on', { color_temp_kelvin: 6500 }),
volume: control('on', { volume_level: .56 }), fragrance_auto_light: control('on') })
volume: control('on', { volume_level: .56 }), uv: control('on') })
assert.equal(params.lightBrightness, 0); assert.equal(params.lightColorTemp, 5000)
assert.equal(params.speakerVolume, 56)
assert.equal(params.fragranceAutoLight, true)
syncDeviceParameters(params, { fragrance_auto_light: control('unavailable', {}, false), volume: control('on', { volume_level: 'NaN' }) })
assert.equal(params.fragranceAutoLight, true); assert.equal(params.speakerVolume, 56)
assert.equal(params.dehumidifierUv, true)
syncDeviceParameters(params, { uv: control('unavailable', {}, false), volume: control('on', { volume_level: 'NaN' }) })
assert.equal(params.dehumidifierUv, true); assert.equal(params.speakerVolume, 56)
})
test('parameter availability is scoped to the actual device; missing numeric attributes are not default readings', () => {
@@ -150,13 +150,13 @@ test('all-off can interrupt start; a late start result cannot overwrite off feed
})
test('failed parameter command restores last known reading even if refresh itself fails', async () => {
const env = environment({ homeAssistantStatus: async () => status({ fragrance_auto_light: control('off') }),
const env = environment({ homeAssistantStatus: async () => status({ uv: control('off') }),
setParameter: async () => ({ success: false, message: '失败' }) })
const hook = env.load('composables/useDeviceControl').useDeviceControl()
await hook.loadStatus(); hook.localParams.fragranceAutoLight = true
await hook.loadStatus(); hook.localParams.dehumidifierUv = true
env.healthApi.homeAssistantStatus = async () => { throw new Error('离线') }
assert.equal(await hook.applyParam('fragrance', 'fragrance_auto_light', true), false)
assert.equal(hook.localParams.fragranceAutoLight, false)
assert.equal(await hook.applyParam('dehumidifier', 'uv', true), false)
assert.equal(hook.localParams.dehumidifierUv, false)
assert.equal(hook.actionFeedback.value.message, '失败')
env.dispose(); assert.equal(env.timeouts.size, 0)
})
+24
View File
@@ -303,6 +303,30 @@ test('fragrance detail retains separate nightlight and unavailable quick-release
assert.ok(!html.includes('演示智能床垫'))
})
test('wake and sleep fragrance details share controls and show their own readings', async () => {
const sleepDuration = reading('number.sleep_duration', 'number', '3')
const wakeDuration = reading('number.wake_duration', 'number', '5')
const wakeButton = reading('button.wake_release', 'button', 'unknown')
const sleep = { ...devices[2], entities: [...devices[2].entities, sleepDuration] }
const wake = device('wake', '香氛机 唤醒', 'wake-fragrance', 'Fragrance',
[reading('switch.wake', 'switch', 'on'), wakeDuration, wakeButton], { primaryControlEntityId: 'switch.wake' })
const haStatus = ref({ connected: true, devices: [sleep, wake], controlParameters: {
fragrance_duration: sleepDuration, wake_fragrance_duration: wakeDuration, wake_fragrance_quick_release: wakeButton,
} })
const sleepHtml = (await render('/device-control/fragrance', { haStatus })).html
const wakeHtml = (await render('/device-control/wake', { haStatus })).html
for (const label of ['单次出香时长', '自动出香间隔', '香薰氛围灯', '人来增香', '感应夜灯', '无人时自动关灯', '立即出香']) {
assert.ok(sleepHtml.includes(label), label)
assert.ok(wakeHtml.includes(label), label)
}
assert.match(sleepHtml, />3 秒</)
assert.doesNotMatch(sleepHtml, />5 秒</)
assert.match(wakeHtml, />5 秒</)
assert.doesNotMatch(wakeHtml, />3 秒</)
assert.match(wakeHtml, /快速出香/)
assert.doesNotMatch(wakeHtml, /查看全部能力/)
})
test('each registered device detail renders its own parameters without requesting the network', async () => {
const cases = [
[devices[0], ['灯光亮度', '灯光色温调节']],