Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const sendCommand = vi.fn()

vi.mock('@/js/stores/useStatesStore', () => ({
useStatesStore: () => ({ sendCommand })
}))

import ohInput from './oh-input.vue'

describe('oh-input.vue', () => {
beforeEach(() => {
sendCommand.mockClear()
})

it('does not extract a unit when the pattern ends with a value token', () => {
const unit = (ohInput as any).methods.extractUnit(`%1$tM %1$tS'%1$tL"`)
expect(unit).toBeNull()
})

it('does extract a unit when the pattern ends with a unit token', () => {
const unit = (ohInput as any).methods.extractUnit(`%.1f °C`)
expect(unit).toBe('°C')
})

it('does extract the %unit% pattern', () => {
const unit = (ohInput as any).methods.extractUnit(`%.1f %unit%`)
expect(unit).toBe('%unit%')
})

it('does strip of the unit from the value', () => {
let context = {
unit: '°C'
}
let value = (ohInput as any).methods.extractValue.call(context, '20 °C')
expect(value).toBe('20')

context = {
unit: 'complicated unit'
}
value = (ohInput as any).methods.extractValue.call(context, '20 complicated unit')
expect(value).toBe('20')
})

it('does not infer a unit from Number:Time state description with value tokens', () => {
const context = {
type: 'number',
config: { useDisplayState: true, item: 'Timer' },
item: { unitSymbol: 's', stateDescription: { pattern: `%1$tM %1$tS'%1$tL"` } },
context: { store: { Timer: { state: '60 s' } } },
extractUnit: (ohInput as any).methods.extractUnit
}

const unit = (ohInput as any).computed.unit.call(context)
expect(unit).toBe('s')
})

it("does use the item's display unit if available", () => {
const context = {
type: 'number',
config: { useDisplayState: true, item: 'Power' },
item: { unitSymbol: 'W', stateDescription: { pattern: '%.3f kW' } },
context: { store: { Power: { state: '6000 W' } } },
extractUnit: (ohInput as any).methods.extractUnit
}

const unit = (ohInput as any).computed.unit.call(context)
expect(unit).toBe('kW')
})

it("does use the item's unit if display unit is %unit%", () => {
const context = {
type: 'number',
systemUnit: 'W',
config: { useDisplayState: true, item: 'Power' },
item: { unitSymbol: 'W', stateDescription: { pattern: '%.3f %unit%' } },
context: { store: { Power: { state: '6000 W' } } },
extractUnit: (ohInput as any).methods.extractUnit
}

const unit = (ohInput as any).computed.unit.call(context)
expect(unit).toBe('W')
})

it('does extract the unit when trailing text follows it', () => {
const unit = (ohInput as any).methods.extractUnit(`%.0f kW is the power`)
expect(unit).toBe('kW')
})

it('does extract %unit% when trailing text follows it', () => {
const unit = (ohInput as any).methods.extractUnit(`%.0f %unit% is the power`)
expect(unit).toBe('%unit%')
})

it("does use the item's unit symbol when %unit% is followed by trailing text", () => {
const context = {
type: 'number',
config: { useDisplayState: true, item: 'Power' },
item: { unitSymbol: 'kW', stateDescription: { pattern: '%.0f %unit% is the power' } },
context: { store: { Power: { state: '6 kW' } } },
extractUnit: (ohInput as any).methods.extractUnit
}

const unit = (ohInput as any).computed.unit.call(context)
expect(unit).toBe('kW')
})

it('sends the pending value unchanged when no unit is available', () => {
const context = {
config: { item: 'Timer' },
pendingUpdate: '60',
unit: null
}

;(ohInput as any).methods.sendButtonClicked.call(context)

expect(sendCommand).toHaveBeenCalledWith('Timer', '60')
expect(context.pendingUpdate).toBeNull()
})

it('appends unit when available', () => {
const context = {
config: { item: 'Temperature' },
pendingUpdate: '60',
unit: '°C'
}

;(ohInput as any).methods.sendButtonClicked.call(context)

expect(sendCommand).toHaveBeenCalledWith('Temperature', '60 °C')
expect(context.pendingUpdate).toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -102,27 +102,22 @@ export default {
type() {
return this.config.type || getDefaultInputType(this.item?.type) || 'text'
},
// Returns the unit from the item's displayState, state description pattern or the item's unit symbol
/**
* The unit from the stateDescription pattern (if `useDisplayState` is enabled) or the item's unit symbol.
* @return {string|null}
*/
unit() {
if (this.type !== 'number') return null
if (!this.item?.unitSymbol) return null

const storeItem = this.context.store[this.config.item]
// When the state of a dimensioned item is UNDEF/NULL, item.displayState is undefined
// so we need to pull it out of the item's state description pattern
if (this.config.useDisplayState) {
const unit = this.extractUnit(storeItem.displayState || this.item.stateDescription?.pattern)
return unit === '%unit%' ? this.item.unitSymbol : unit
const pattern = this.item.stateDescription?.pattern
const unit = this.extractUnit(pattern)
if (unit === '%unit%') return this.item.unitSymbol
return unit ?? this.item.unitSymbol
}
return this.item.unitSymbol
},
// Returns the index of the last pattern in the stateDescription
// Example: displayState = "Some label %0.1f footext °C", returns 2
// Returns -1 if no pattern is found
valueIndexInDisplayState() {
const parts = this.item?.stateDescription?.pattern?.trim()?.split(/\s+/) || []
return parts.findLastIndex((part) => part.startsWith('%') && part !== '%unit%' && part !== '%%')
},
calendarParams() {
if (this.type !== 'datepicker') return null
let params = { dateFormat: { year: 'numeric', month: 'numeric', day: 'numeric' } }
Expand Down Expand Up @@ -232,22 +227,17 @@ export default {
},
extractUnit(pattern) {
if (!pattern) return null
return pattern.trim().split(/\s+/).pop()
const parts = pattern.trim().split(/\s+/)
// Ignore %unit% and %% placeholders while detecting actual value tokens.
const valueIndex = parts.findLastIndex((part) => part.startsWith('%') && part !== '%unit%' && part !== '%%')

if (valueIndex === -1 || valueIndex === parts.length - 1) return null
return parts[valueIndex + 1] ?? null
},
extractValue(pattern) {
if (!pattern) return null

const parts = pattern.trim().split(/\s+/)
switch (parts.length) {
case 0:
return null
case 1:
return pattern
case 2:
return parts[0]
default:
return parts[this.valueIndexInDisplayState]
}
const endIndex = pattern.lastIndexOf(this.unit)
if (endIndex < 0) return pattern
return pattern.substring(0, endIndex).trim()
}
}
}
Expand Down
Loading