From 01c8d8ac38e3c7f5eff8a3a35571bff259d0e054 Mon Sep 17 00:00:00 2001 From: aspenrt78 <105655892+aspenrt78@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:18:14 -0600 Subject: [PATCH 1/3] fix: preserve nested YAML variable values during import --- src/utils/yamlImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/yamlImporter.ts b/src/utils/yamlImporter.ts index f53b8c8..b1bd2fb 100644 --- a/src/utils/yamlImporter.ts +++ b/src/utils/yamlImporter.ts @@ -140,7 +140,7 @@ export const parseButtonCardYaml = (yamlString: string): Partial = if (parsed.variables && typeof parsed.variables === 'object') { config.variables = Object.entries(parsed.variables).map(([name, value]) => ({ name, - value: String(value) + value: value !== null && typeof value === 'object' ? JSON.stringify(value) : String(value) })); } From 0363a35d680b37e88d890d501603448a82909a61 Mon Sep 17 00:00:00 2001 From: aspenrt78 <105655892+aspenrt78@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:18:33 -0600 Subject: [PATCH 2/3] test: exercise real YAML importer and generator round trips --- tests/yamlRoundTrip.test.cjs | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/yamlRoundTrip.test.cjs diff --git a/tests/yamlRoundTrip.test.cjs b/tests/yamlRoundTrip.test.cjs new file mode 100644 index 0000000..8ae19d5 --- /dev/null +++ b/tests/yamlRoundTrip.test.cjs @@ -0,0 +1,88 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const ts = require('typescript'); +const YAML = require('yaml'); + +// Run the actual TypeScript importer and generator without duplicating their logic. +require.extensions['.ts'] = (module, filename) => { + const source = fs.readFileSync(filename, 'utf8'); + const compiled = ts.transpileModule(source, { + fileName: filename, + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, + }); + module._compile(compiled.outputText, filename); +}; + +const { parseButtonCardYaml, validateImportedConfig } = require('../src/utils/yamlImporter.ts'); +const { generateYaml } = require('../src/utils/yamlGenerator.ts'); +const { DEFAULT_CONFIG } = require('../src/types.ts'); + +function roundTrip(source) { + const imported = validateImportedConfig(parseButtonCardYaml(YAML.stringify(source))); + const generated = generateYaml({ ...DEFAULT_CONFIG, ...imported }); + return { imported, generated, parsed: YAML.parse(generated) }; +} + +test('retains entity, icon, name, and essential show flags', () => { + const { parsed } = roundTrip({ + type: 'custom:button-card', + entity: 'light.kitchen', + name: 'Kitchen', + icon: 'mdi:lightbulb', + show_name: false, + show_icon: false, + }); + assert.equal(parsed.entity, 'light.kitchen'); + assert.equal(parsed.name, 'Kitchen'); + assert.equal(parsed.icon, 'mdi:lightbulb'); + assert.equal(parsed.show_name, false); + assert.equal(parsed.show_icon, false); +}); + +test('preserves nested variable objects and arrays without object-string coercion', () => { + const vars = { + values: { thresholds: [20, 50, 75], colors: { low: '#123456', high: '#abcdef' } }, + options: ['one', 'two'], + }; + const { imported, parsed, generated } = roundTrip({ + type: 'custom:button-card', + entity: 'sensor.temperature', + variables: vars, + }); + assert.deepEqual(JSON.parse(imported.variables.find(v => v.name === 'values').value), vars.values); + assert.deepEqual(parsed.variables, vars); + assert.equal(generated.includes('[object Object]'), false); +}); + +test('preserves action targets and service data', () => { + const { parsed } = roundTrip({ + type: 'custom:button-card', + entity: 'switch.fan', + tap_action: { + action: 'perform-action', + perform_action: 'light.turn_on', + target: { entity_id: ['light.kitchen', 'light.table'] }, + data: { brightness_pct: 45 }, + }, + }); + assert.equal(parsed.tap_action.perform_action, 'light.turn_on'); + assert.deepEqual(parsed.tap_action.target, { entity_id: ['light.kitchen', 'light.table'] }); + assert.equal(parsed.tap_action.data.brightness_pct, 45); +}); + +test('retains JavaScript action templates', () => { + const code = '[[[ return entity.state === "on"; ]]]'; + const { parsed } = roundTrip({ + type: 'custom:button-card', entity: 'switch.lamp', + tap_action: { action: 'javascript', javascript: code }, + }); + assert.equal(parsed.tap_action.action, 'javascript'); + assert.equal(parsed.tap_action.javascript.trim(), code); +}); + +test('rejects invalid YAML instead of fabricating an empty configuration', () => { + assert.throws(() => parseButtonCardYaml('entity: [unterminated'), /./); +}); From 4c4c04773eaa1f5acc2e418efa87aea0134273dc Mon Sep 17 00:00:00 2001 From: aspenrt78 <105655892+aspenrt78@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:19:09 -0600 Subject: [PATCH 3/3] test: match Vite's default-import interop in TS harness --- tests/yamlRoundTrip.test.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/yamlRoundTrip.test.cjs b/tests/yamlRoundTrip.test.cjs index 8ae19d5..5bb7b58 100644 --- a/tests/yamlRoundTrip.test.cjs +++ b/tests/yamlRoundTrip.test.cjs @@ -11,7 +11,7 @@ require.extensions['.ts'] = (module, filename) => { const source = fs.readFileSync(filename, 'utf8'); const compiled = ts.transpileModule(source, { fileName: filename, - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true }, }); module._compile(compiled.outputText, filename); };