111
This commit is contained in:
@@ -22,9 +22,11 @@ const sceneResult = (message, sceneCode = 'sleep') => ({ success: true, status:
|
||||
function environment(overrides = {}) {
|
||||
const intervals = new Map(), timeouts = new Map(), storage = new Map(), disposed = [], mounted = [], toasts = []
|
||||
let id = 0
|
||||
const watchers = []
|
||||
const vue = {
|
||||
ref: value => ({ value }), reactive: value => value,
|
||||
computed: getter => ({ get value() { return getter() } }),
|
||||
computed: getter => ({ get value() { return typeof getter === 'function' ? getter() : getter.get() }, set value(value) { getter.set?.(value) } }),
|
||||
watch: (getter, callback) => { watchers.push({ getter, callback, value: getter() }) },
|
||||
onUnmounted: handler => disposed.push(handler), onMounted: handler => mounted.push(handler),
|
||||
}
|
||||
const healthApi = {
|
||||
@@ -64,7 +66,9 @@ function environment(overrides = {}) {
|
||||
vm.runInNewContext(source, context, { filename: file })
|
||||
return module.exports
|
||||
}
|
||||
return { load, healthApi, intervals, timeouts, toasts, vue, dispose: () => disposed.forEach(fn => fn()), mount: () => mounted.forEach(fn => fn()) }
|
||||
return { load, healthApi, intervals, timeouts, toasts, vue,
|
||||
flushWatches: () => { for (const watcher of watchers) { const value = watcher.getter(); if (value !== watcher.value) { watcher.callback(value, watcher.value); watcher.value = value } } },
|
||||
dispose: () => disposed.forEach(fn => fn()), mount: () => mounted.forEach(fn => fn()) }
|
||||
}
|
||||
|
||||
test('parameter conversion rejects junk and unavailable data; light zero and hardware limits are respected', () => {
|
||||
@@ -240,6 +244,164 @@ const device = (moduleKey, category, entities = [], extra = {}) => ({
|
||||
})
|
||||
const entity = (entityId, domain, state, attributes = {}, available = true) => ({ entityId, domain, state, attributes, available })
|
||||
|
||||
const standardLight = (id = 'light.strip', mode = 'color_temp', attributes = {}) => device('', 'Lighting', [entity(id, 'light', 'on', {
|
||||
supported_color_modes: [mode], min_color_temp_kelvin: 2700, max_color_temp_kelvin: 6500, supported_features: 4,
|
||||
brightness: 128, color_temp_kelvin: 3000, effect_list: ['阅读', '助眠'], effect: '阅读', ...attributes,
|
||||
})], { deviceId: id, primaryControlEntityId: id })
|
||||
|
||||
test('configured strip defaults to its supported sleep effect without inventing a reported effect or sending on page load', () => {
|
||||
const env = environment(), { useLightControls } = env.load('devices/lighting/useLightControls')
|
||||
const item = standardLight('light.strip', 'color_temp', { effect: null, effect_list: ['阅读模式', '助眠模式'] })
|
||||
item.moduleKey = 'sleep-light-strip'; item.defaultLightEffect = '助眠模式'
|
||||
let writes = 0
|
||||
const hook = useLightControls(() => item, () => false, async () => { writes++; return true })
|
||||
assert.equal(hook.effectTarget.value, '助眠模式'); assert.equal(hook.defaultEffect.value, '助眠模式')
|
||||
assert.equal(hook.capability.value.effect, undefined); assert.equal(writes, 0)
|
||||
item.entities[0].attributes.effect = '阅读模式'
|
||||
assert.equal(hook.effectTarget.value, '阅读模式'); assert.equal(hook.capability.value.effect, '阅读模式')
|
||||
item.defaultLightEffect = '未声明模式'; item.entities[0].attributes.effect = null
|
||||
assert.equal(hook.effectTarget.value, ''); assert.equal(hook.defaultEffect.value, '')
|
||||
})
|
||||
|
||||
test('multiple lights use only their own fade status and continue polling when the original light has stopped', async () => {
|
||||
let current = { entityId: 'light.primary', running: false, brightnessPercent: 0, mode: 'none', lights: [
|
||||
{ entityId: 'light.primary', running: false, brightnessPercent: 0, mode: 'none' },
|
||||
{ entityId: 'light.strip', running: true, brightnessPercent: 46, mode: 'sleep' },
|
||||
] }
|
||||
const env = environment({ lightTransitionStatus: async () => current })
|
||||
const helpers = env.load('devices/lighting/transitions'), { deviceSummary } = env.load('devices/deviceSummary')
|
||||
const strip = standardLight(); strip.moduleKey = 'sleep-light-strip'
|
||||
assert.equal(helpers.lightTransitionForDevice(strip, current).brightnessPercent, 46)
|
||||
assert.equal(helpers.anyLightTransitionRunning(current), true)
|
||||
assert.equal(helpers.lightTransitionForDevice(standardLight('light.unrelated'), current), undefined)
|
||||
const summary = deviceSummary(strip, { light: current })
|
||||
assert.equal(summary.statusText, '助眠渐暗中'); assert.equal(summary.metricText, '亮度 46%')
|
||||
let ended = 0
|
||||
const params = env.load('devices/parameterRegistry').createDeviceParameterState()
|
||||
params.lightBrightness = 10
|
||||
const hook = env.load('composables/useDeviceTransitions').useDeviceTransitions(params, async () => { ended++ })
|
||||
await hook.syncLightTransitionStatus()
|
||||
assert.equal(env.intervals.size, 1); assert.equal(params.lightBrightness, 10)
|
||||
current = { ...current, lights: current.lights.map(light => ({ ...light, running: false, brightnessPercent: 0 })) }
|
||||
for (const tick of [...env.intervals.values()]) tick()
|
||||
await flush(); assert.equal(env.intervals.size, 0); assert.equal(ended, 1)
|
||||
env.dispose()
|
||||
})
|
||||
|
||||
test('explicit entity markers and legacy single-light responses cannot leak into another lamp', () => {
|
||||
const helpers = environment().load('devices/lighting/transitions')
|
||||
const primary = standardLight('light.primary'); primary.moduleKey = 'lighting'
|
||||
const strip = standardLight()
|
||||
const legacy = { running: true, brightnessPercent: 50, mode: 'sleep' }
|
||||
assert.equal(helpers.lightTransitionForDevice(primary, legacy), legacy)
|
||||
assert.equal(helpers.lightTransitionForDevice(strip, legacy), undefined)
|
||||
assert.equal(helpers.lightTransitionForDevice(primary, { ...legacy, entityId: 'light.strip' }), undefined)
|
||||
})
|
||||
|
||||
test('a failed new-light fade is not reported as success merely because the original light finished', async () => {
|
||||
let current = { entityId: 'light.primary', running: true, success: true, brightnessPercent: 1, mode: 'sleep', lights: [
|
||||
{ entityId: 'light.primary', running: true, success: true, brightnessPercent: 1, mode: 'sleep' },
|
||||
{ entityId: 'light.strip', running: true, success: true, brightnessPercent: 2, mode: 'sleep' },
|
||||
] }
|
||||
const env = environment({ lightTransitionStatus: async () => current })
|
||||
const hook = env.load('composables/useDeviceControl').useDeviceControl(); await hook.loadStatus()
|
||||
current = { ...current, running: false, message: '原灯渐暗完成', lights: [
|
||||
{ entityId: 'light.primary', running: false, success: true, mode: 'sleep', message: '原灯渐暗完成' },
|
||||
{ entityId: 'light.strip', running: false, success: false, mode: 'sleep', message: '灯带离线,渐变中断' },
|
||||
] }
|
||||
for (const tick of [...env.intervals.values()]) tick()
|
||||
await flush(); await flush()
|
||||
assert.equal(hook.actionFeedback.value.type, 'fail')
|
||||
assert.equal(hook.actionFeedback.value.message, '灯带离线,渐变中断')
|
||||
env.dispose()
|
||||
})
|
||||
|
||||
test('standard light capabilities are per entity, not per category or another lamps configured parameters', () => {
|
||||
const { lightCapabilities } = environment().load('devices/lighting/capabilities')
|
||||
const item = standardLight(), capability = lightCapabilities(item)
|
||||
assert.equal(capability.available, true); assert.equal(capability.canDim, true); assert.equal(capability.brightnessPercent, 50)
|
||||
assert.equal(capability.temperatureRange.min, 2700); assert.equal(capability.temperatureRange.max, 6500)
|
||||
assert.equal(capability.colorTemperature, 3000); assert.equal(capability.effect, '阅读')
|
||||
const simple = lightCapabilities(standardLight('light.simple', 'onoff', { effect_list: [], supported_features: 0 }))
|
||||
assert.equal(simple.canDim, false); assert.equal(simple.temperatureRange, undefined); assert.equal(simple.effects.length, 0)
|
||||
const dim = lightCapabilities(standardLight('light.dim', 'brightness'))
|
||||
assert.equal(dim.canDim, true); assert.equal(dim.temperatureRange, undefined)
|
||||
const rgb = lightCapabilities(standardLight('light.rgb', 'rgb'))
|
||||
assert.equal(rgb.canDim, true); assert.equal(rgb.temperatureRange, undefined)
|
||||
})
|
||||
|
||||
test('light unknown main state, foreign primary and missing capability cannot borrow online auxiliary data', () => {
|
||||
const { lightCapabilities } = environment().load('devices/lighting/capabilities'), item = standardLight()
|
||||
item.entities.push(entity('light.foreign', 'light', 'on', { supported_color_modes: ['color_temp'] }))
|
||||
item.entities[0].state = 'unknown'; assert.equal(lightCapabilities(item).available, false)
|
||||
assert.equal(lightCapabilities(item).brightnessPercent, undefined); assert.equal(lightCapabilities(item).colorTemperature, undefined)
|
||||
item.entities[0].state = 'on'; item.entities[0].available = false
|
||||
assert.equal(lightCapabilities(item).available, false)
|
||||
item.primaryControlEntityId = 'light.missing'; assert.equal(lightCapabilities(item).entity, undefined)
|
||||
const noCapabilities = standardLight(); noCapabilities.entities[0].attributes = {}
|
||||
assert.equal(lightCapabilities(noCapabilities).canDim, false); assert.equal(lightCapabilities(noCapabilities).temperatureRange, undefined)
|
||||
})
|
||||
|
||||
test('light ranges, feature flags and reported readings reject malformed or unsupported values', () => {
|
||||
const { lightCapabilities } = environment().load('devices/lighting/capabilities')
|
||||
for (const min of [undefined, 0, -1, 2700.5, 'garbage'])
|
||||
assert.equal(lightCapabilities(standardLight('light.strip', 'color_temp', { min_color_temp_kelvin: min })).temperatureRange, undefined)
|
||||
for (const max of [undefined, 2700, 2000, Infinity, 3000000000])
|
||||
assert.equal(lightCapabilities(standardLight('light.strip', 'color_temp', { max_color_temp_kelvin: max })).temperatureRange, undefined)
|
||||
for (const supported_features of [undefined, 0, 3, -1, 4.5, Infinity])
|
||||
assert.equal(lightCapabilities(standardLight('light.strip', 'color_temp', { supported_features })).effects.length, 0)
|
||||
const item = standardLight('light.strip', 'color_temp', { brightness: null, color_temp_kelvin: null, effect: '未声明模式' })
|
||||
assert.equal(lightCapabilities(item).brightnessPercent, undefined); assert.equal(lightCapabilities(item).effect, undefined)
|
||||
item.entities[0].state = 'off'; assert.equal(lightCapabilities(item).brightnessPercent, 0)
|
||||
})
|
||||
|
||||
test('light drafts are instance-local and standard sends use exactly the displayed devices main entity', async () => {
|
||||
const env = environment(), { useLightControls } = env.load('devices/lighting/useLightControls'), writes = []
|
||||
let item = standardLight('light.first'), busy = false
|
||||
const hook = useLightControls(() => item, () => busy, async (...args) => { writes.push(args); return true })
|
||||
hook.brightnessTarget.value = 61; await hook.setBrightness()
|
||||
assert.equal(writes[0][0], 'light.first'); assert.equal(writes[0][1], 'light_brightness'); assert.equal(writes[0][2], 61); assert.equal(writes[0][3], 'light.first')
|
||||
hook.temperatureTarget.value = 6500; await hook.setTemperature()
|
||||
assert.equal(writes[1][1], 'light_color_temperature'); assert.equal(writes[1][2], 6500)
|
||||
hook.effectTarget.value = '助眠'; await hook.setEffect(); assert.equal(writes[2][1], 'light_effect')
|
||||
hook.temperatureTarget.value = 2200; assert.equal(await hook.setTemperature(), false)
|
||||
hook.effectTarget.value = '删除遥控器'; assert.equal(await hook.setEffect(), false)
|
||||
busy = true; hook.brightnessTarget.value = 60; assert.equal(await hook.setBrightness(), false)
|
||||
busy = false; hook.brightnessTarget.value = 87
|
||||
item = standardLight('light.second', 'brightness', { brightness: 51 }); env.flushWatches()
|
||||
assert.equal(hook.brightnessTarget.value, 20); assert.equal(hook.capability.value.temperatureRange, undefined)
|
||||
await hook.setBrightness(); assert.equal(writes[3][0], 'light.second'); assert.equal(writes[3][3], 'light.second')
|
||||
item.entities[0].state = 'unavailable'; assert.equal(await hook.setBrightness(), false)
|
||||
assert.equal(writes.length, 4)
|
||||
})
|
||||
|
||||
test('late light operation cannot reset another devices draft and failed operation restores true reading', async () => {
|
||||
const env = environment(), { useLightControls } = env.load('devices/lighting/useLightControls'), pending = deferred()
|
||||
let item = standardLight('light.first')
|
||||
const hook = useLightControls(() => item, () => false, () => pending.promise)
|
||||
hook.brightnessTarget.value = 70; const task = hook.setBrightness()
|
||||
item = standardLight('light.second'); env.flushWatches(); hook.brightnessTarget.value = 83
|
||||
pending.resolve(false); await task
|
||||
assert.equal(hook.brightnessTarget.value, 83)
|
||||
const failed = useLightControls(() => item, () => false, async () => false)
|
||||
failed.brightnessTarget.value = 70; await failed.setBrightness()
|
||||
assert.equal(failed.brightnessTarget.value, 50)
|
||||
})
|
||||
|
||||
test('new light overview does not inherit the configured bedroom lamps transition runtime', () => {
|
||||
const { deviceSummary } = environment().load('devices/deviceSummary'), item = standardLight()
|
||||
const summary = deviceSummary(item, { light: { running: true, mode: 'wake', brightnessPercent: 77 } })
|
||||
assert.equal(summary.statusText, '已点亮'); assert.equal(summary.metricText, '亮度 50% · 3000K')
|
||||
assert.equal(summary.canToggle, true); assert.equal(summary.icon, 'lighting')
|
||||
})
|
||||
|
||||
test('readonly generic button unknown state is not mislabeled as an unavailable action', () => {
|
||||
const { entityStateLabel } = environment().load('devices/entityState')
|
||||
assert.equal(entityStateLabel(entity('button.test', 'button', 'unknown')), '动作按钮(尚未触发)')
|
||||
assert.equal(entityStateLabel(entity('button.test', 'button', 'unavailable', {}, false)), '不可用')
|
||||
assert.equal(entityStateLabel(entity('sensor.test', 'sensor', 'unknown', {}, false)), '状态未回读')
|
||||
})
|
||||
|
||||
const outletDevice = () => device('outlet', 'Outlet', [entity('switch.service', 'switch', 'on'), entity('switch.power', 'switch', 'off')],
|
||||
{ primaryControlEntityId: 'switch.power' })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user