From 5072070f89423e4e43258f63bc6aae0788817f38 Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Thu, 18 Dec 2025 12:54:18 +0100 Subject: [PATCH] vue-vanilla: Fix OneOfRenderer.vue not clearing some primitive values Replace isEmpty check with check against undefined because isEmpty always returns true for boolean values, 0, empty strings and objects without props. This lead to errors when switching from any of these to an array and trying to add elements there. Also adds unit tests for this. fixes #2506 --- .../vue-vanilla/src/complex/OneOfRenderer.vue | 3 +- .../tests/unit/complex/OneOfRenderer.spec.ts | 125 ++++++++++++++++++ packages/vue-vanilla/tests/unit/setup.ts | 14 ++ packages/vue-vanilla/tests/unit/util/index.ts | 1 + 4 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 packages/vue-vanilla/tests/unit/setup.ts diff --git a/packages/vue-vanilla/src/complex/OneOfRenderer.vue b/packages/vue-vanilla/src/complex/OneOfRenderer.vue index b676662bb9..4a0379f2b4 100644 --- a/packages/vue-vanilla/src/complex/OneOfRenderer.vue +++ b/packages/vue-vanilla/src/complex/OneOfRenderer.vue @@ -88,7 +88,6 @@ import { RendererProps, useJsonFormsOneOfControl, } from '@jsonforms/vue'; -import isEmpty from 'lodash/isEmpty'; import { defineComponent, inject, nextTick, ref } from 'vue'; import { useVanillaControl } from '../util'; import { ControlWrapper } from '../controls'; @@ -158,7 +157,7 @@ const controlRenderer = defineComponent({ const target = event.target as any; this.selectIndex = target.value; - if (this.control.enabled && !isEmpty(this.control.data)) { + if (this.control.enabled && this.control.data !== undefined) { this.showDialog(); nextTick(() => { this.newSelectedIndex = this.selectIndex; diff --git a/packages/vue-vanilla/tests/unit/complex/OneOfRenderer.spec.ts b/packages/vue-vanilla/tests/unit/complex/OneOfRenderer.spec.ts index d1bb27878a..afb7878dce 100644 --- a/packages/vue-vanilla/tests/unit/complex/OneOfRenderer.spec.ts +++ b/packages/vue-vanilla/tests/unit/complex/OneOfRenderer.spec.ts @@ -1,6 +1,42 @@ import { expect } from 'chai'; import { mountJsonForms } from '../util'; +const schemaWithVariousTypes = { + type: 'object', + properties: { + oneOfProp: { + title: 'Boolean or Array', + oneOf: [ + { + title: 'Boolean', + type: 'boolean', + }, + { + title: 'Array', + type: 'array', + items: { + type: 'string', + }, + }, + { + title: 'Object', + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + }, + ], + }, + }, +}; + +const uischemaWithVariousTypes = { + type: 'Control', + scope: '#/properties/oneOfProp', +}; + const schema = { title: 'My Object', oneOf: [ @@ -34,6 +70,95 @@ const uischema = { }; describe('OneOfRenderer.vue', () => { + it('shows confirmation dialog when switching from boolean true to array', async () => { + const wrapper = mountJsonForms( + { oneOfProp: true }, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + const dialog = wrapper.find('dialog'); + + expect(oneOfSelect.element.value).to.equal('0'); + expect(dialog.element.open).to.be.false; + + await oneOfSelect.setValue('1'); + + expect(dialog.element.open).to.be.true; + }); + + it('shows confirmation dialog when switching from boolean false to array', async () => { + const wrapper = mountJsonForms( + { oneOfProp: false }, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + const dialog = wrapper.find('dialog'); + + expect(oneOfSelect.element.value).to.equal('0'); + expect(dialog.element.open).to.be.false; + + await oneOfSelect.setValue('1'); + + expect(dialog.element.open).to.be.true; + }); + + it('allows adding items after switching from boolean to array', async () => { + const wrapper = mountJsonForms( + { oneOfProp: true }, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + + await oneOfSelect.setValue('1'); + + const confirmButton = wrapper.find('dialog button:last-child'); + await confirmButton.trigger('click'); + + const addButton = wrapper.find('.array-list-add'); + await addButton.trigger('click'); + + expect(wrapper.vm.data).to.deep.equal({ oneOfProp: [''] }); + }); + + it('does not show confirmation dialog when switching from undefined to array', async () => { + const wrapper = mountJsonForms( + {}, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + const dialog = wrapper.find('dialog'); + + await oneOfSelect.setValue('1'); + + expect(dialog.element.open).to.be.false; + expect(wrapper.vm.data).to.deep.equal({}); + }); + + it('allows adding items after switching from object to array', async () => { + const wrapper = mountJsonForms( + { oneOfProp: { name: 'test' } }, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + + expect(oneOfSelect.element.value).to.equal('2'); + + await oneOfSelect.setValue('1'); + + const confirmButton = wrapper.find('dialog button:last-child'); + await confirmButton.trigger('click'); + + const addButton = wrapper.find('.array-list-add'); + await addButton.trigger('click'); + + expect(wrapper.vm.data).to.deep.equal({ oneOfProp: [''] }); + }); + it('render has a class', () => { const wrapper = mountJsonForms({ variant: 'b', b: 'b' }, schema, uischema); expect(wrapper.find('div.one-of').exists()).to.be.true; diff --git a/packages/vue-vanilla/tests/unit/setup.ts b/packages/vue-vanilla/tests/unit/setup.ts new file mode 100644 index 0000000000..be8bced710 --- /dev/null +++ b/packages/vue-vanilla/tests/unit/setup.ts @@ -0,0 +1,14 @@ +// JSDOM doesn't support HTMLDialogElement.showModal() and close(), so we need to polyfill/mock them +// Also see https://github.com/jsdom/jsdom/issues/3294 +if (typeof HTMLDialogElement !== 'undefined') { + HTMLDialogElement.prototype.showModal = + HTMLDialogElement.prototype.showModal || + function (this: HTMLDialogElement) { + this.setAttribute('open', ''); + }; + HTMLDialogElement.prototype.close = + HTMLDialogElement.prototype.close || + function (this: HTMLDialogElement) { + this.removeAttribute('open'); + }; +} diff --git a/packages/vue-vanilla/tests/unit/util/index.ts b/packages/vue-vanilla/tests/unit/util/index.ts index bbebd88eb4..1eed8b4a5e 100644 --- a/packages/vue-vanilla/tests/unit/util/index.ts +++ b/packages/vue-vanilla/tests/unit/util/index.ts @@ -1 +1,2 @@ +import '../setup'; export * from './util';