Skip to content

Commit 8ec389f

Browse files
authored
fix: test graphics ElementExpressionHelper (#4233)
1 parent 5262128 commit 8ec389f

2 files changed

Lines changed: 933 additions & 11 deletions

File tree

companion/lib/Graphics/ConvertGraphicsElements/Helper.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { JsonValue } from 'type-fest'
22
import type { ExecuteExpressionResult } from '@companion-app/shared/Expression/ExpressionResult.js'
33
import type { HorizontalAlignment, VerticalAlignment } from '@companion-app/shared/Graphics/Util.js'
44
import type { ControlLocation } from '@companion-app/shared/Model/Common.js'
5-
import type { ExpressionOrValue } from '@companion-app/shared/Model/Options.js'
5+
import { isExpressionOrValue, type ExpressionOrValue } from '@companion-app/shared/Model/Options.js'
66
import type { DrawImageBuffer } from '@companion-app/shared/Model/StyleModel.js'
77
import {
88
stringifyVariableValue,
@@ -77,7 +77,7 @@ export class ElementExpressionHelper<T> {
7777

7878
#getValue(propertyName: keyof T): ExpressionOrValue<JsonValue | undefined> {
7979
const override = this.#elementOverrides?.get(String(propertyName))
80-
return override ? override : (this.#element as any)[propertyName]
80+
return override ?? (this.#element as any)[propertyName] ?? { isExpression: false, value: undefined }
8181
}
8282

8383
getUnknown(propertyName: keyof T, defaultValue: VariableValue): VariableValue | undefined {
@@ -96,7 +96,7 @@ export class ElementExpressionHelper<T> {
9696
getParsedString(propertyName: keyof T, defaultValue: string): string {
9797
const value = this.#getValue(propertyName)
9898
if (value.isExpression) {
99-
return stringifyVariableValue(this.getUnknown(propertyName, defaultValue)) ?? ''
99+
return stringifyVariableValue(this.getUnknown(propertyName, defaultValue)) ?? defaultValue
100100
} else {
101101
return this.parseVariablesInString(stringifyVariableValue(value.value) ?? '', defaultValue)
102102
}
@@ -105,14 +105,20 @@ export class ElementExpressionHelper<T> {
105105
getNumber(propertyName: keyof T, defaultValue: number, scale = 1): number {
106106
const value = this.#getValue(propertyName)
107107

108-
if (!value.isExpression) return Number(value.value) * scale
108+
if (!value.isExpression) {
109+
const num = Number(value.value)
110+
return isNaN(num) ? defaultValue : num * scale
111+
}
109112

110113
const result = this.executeExpressionAndTrackVariables(value.value, 'number')
111114
if (!result.ok) {
112115
return defaultValue
113116
}
114117

115-
return Number(result.value) * scale
118+
// Number(undefined) = NaN and typeof NaN === 'number', so ok:true can still yield NaN
119+
// (e.g. when a referenced variable doesn't exist). Treat NaN as a missing value.
120+
const num = Number(result.value)
121+
return isNaN(num) ? defaultValue : num * scale
116122
}
117123

118124
getString<TVal extends string | null | undefined>(propertyName: keyof T, defaultValue: TVal): TVal {
@@ -123,16 +129,15 @@ export class ElementExpressionHelper<T> {
123129
return stringifyVariableValue(value.value) as TVal
124130
}
125131

126-
const result = this.executeExpressionAndTrackVariables(value.value, 'string')
132+
const result = this.executeExpressionAndTrackVariables(value.value, undefined)
127133
if (!result.ok) {
128134
return defaultValue
129135
}
130136

131-
if (typeof result.value !== 'string') {
132-
return defaultValue
133-
}
134-
135-
return result.value as TVal
137+
// stringifyVariableValue returns undefined only when result.value is undefined
138+
// (e.g. the referenced variable doesn't exist). Treat that as a missing value.
139+
const stringified = stringifyVariableValue(result.value)
140+
return (stringified ?? defaultValue) as TVal
136141
}
137142

138143
getEnum<TVal extends string | number>(propertyName: keyof T, values: TVal[], defaultValue: TVal): TVal {
@@ -157,6 +162,18 @@ export class ElementExpressionHelper<T> {
157162
return actualValue
158163
}
159164

165+
/**
166+
* Like getEnum, but compares the string value to the enum values in a tolerant way:
167+
* Matches only the first non-whitespace character, case-insensitively.
168+
*/
169+
getTolerantEnum<TVal extends string>(propertyName: keyof T, values: readonly TVal[], defaultValue: TVal): TVal {
170+
const raw = this.getString(propertyName, defaultValue)
171+
const trimmed = String(raw ?? '')
172+
.trim()
173+
.toLowerCase()
174+
return values.find((v) => v.toLowerCase().startsWith(trimmed[0])) ?? defaultValue
175+
}
176+
160177
getBoolean(propertyName: keyof T, defaultValue: boolean): boolean {
161178
const value = this.#getValue(propertyName)
162179

@@ -194,6 +211,21 @@ export class ElementExpressionHelper<T> {
194211
return 'center'
195212
}
196213
}
214+
215+
/**
216+
* Create a helper for a row of an internal:list or internal:table (or other child object)
217+
*/
218+
forRow(row: unknown): ElementExpressionHelper<Record<string, ExpressionOrValue<JsonValue | undefined>>> {
219+
const normalised: Record<string, ExpressionOrValue<JsonValue | undefined>> = {}
220+
if (row && typeof row === 'object' && !Array.isArray(row)) {
221+
for (const key of Object.keys(row)) {
222+
const val = (row as Record<string, unknown>)[key]
223+
normalised[key] = isExpressionOrValue(val) ? val : { isExpression: false, value: val as JsonValue | undefined }
224+
}
225+
}
226+
return new ElementExpressionHelper(this.#parser, this.#usedVariables, normalised, undefined)
227+
}
228+
197229
getVerticalAlignment(propertyName: keyof T): VerticalAlignment {
198230
const value = this.#getValue(propertyName)
199231

0 commit comments

Comments
 (0)