UNPKG

devextreme-vue

Version:

DevExtreme Vue UI and Visualization Components

1,145 lines (1,144 loc) • 74.7 kB
/*! * devextreme-vue * Version: 25.1.5 * Build date: Wed Sep 03 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file in the root of the project for details. * * https://github.com/DevExpress/devextreme-vue */ import { PatchFlags } from '@vue/shared'; import { mount } from '@vue/test-utils'; import * as events from 'devextreme/events'; import config from 'devextreme/core/config'; import { createVNode, defineComponent, h, nextTick, renderSlot, ref, } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import { pullConfigComponents } from '../children-processing'; import globalConfig from '../config'; import Configuration from '../configuration'; import { getNodeOptions } from '../vue-helper'; import { prepareComponentConfig, prepareExtensionComponentConfig, prepareConfigurationComponentConfig, } from '../index'; function createComponent(config) { prepareComponentConfig(config); return defineComponent(config); } function createConfigurationComponent(config) { prepareConfigurationComponentConfig(config); return defineComponent(config); } function createExtensionComponent(config) { prepareExtensionComponentConfig(config); return defineComponent(config); } const eventHandlers = {}; const Widget = { option: jest.fn(), resetOption: jest.fn(), dispose: jest.fn(), on: (event, handler) => { eventHandlers[event] = handler; }, fire: (event, args) => { if (!eventHandlers[event]) { throw new Error(`no handler registered for '${event}'`); } eventHandlers[event](args); }, beginUpdate: jest.fn(), endUpdate: jest.fn(), }; const CustomPlugin = { install: (app) => { app.test = 'test data'; }, }; function createWidget(_, options) { if (options.onInitializing) { options.onInitializing.call(Widget); } return Widget; } const WidgetClass = jest.fn(createWidget); const TestComponent = createComponent({ beforeCreate() { this.$_WidgetClass = WidgetClass; this.$_hasAsyncTemplate = true; }, props: { prop1: Number, prop2: Array, sampleProp: String, templateName: String, }, model: { prop: 'prop1', event: 'update:prop1', }, }); function skipIntegrationOptions(options) { const result = { ...options }; delete result.integrationOptions; delete result.onInitializing; delete result.ref; delete result.id; return result; } function buildTestConfigCtor() { return createConfigurationComponent({ props: { prop1: Number, prop2: String, sampleProp: String, }, }); } jest.setTimeout(1000); beforeEach(() => { jest.clearAllMocks(); }); describe('component rendering', () => { it('correctly renders', () => { const wrapper = mount(TestComponent); expect(wrapper.html()).toBe('<div></div>'); }); it('calls widget creation', () => { mount(TestComponent); expect(WidgetClass).toHaveBeenCalledTimes(1); expect(Widget.beginUpdate).toHaveBeenCalledTimes(1); expect(Widget.endUpdate).toHaveBeenCalledTimes(1); }); it('passes id to element', () => { const vm = defineComponent({ template: '<test-component id=\'my-id\'/>', components: { TestComponent, }, }); const wrapper = mount(vm); expect(wrapper.element.id).toBe('my-id'); }); it('passes class to element', () => { const vm = defineComponent({ template: '<test-component class=\'my-class my-class2\'/>', components: { TestComponent, }, }); const wrapper = mount(vm); expect(wrapper.element.className).toBe('my-class my-class2'); }); describe('correctly forwards classes', () => { it('forwards correct attrs in the render method', async () => { const component = defineComponent({ template: ` <test-component id="component" class="chat" :class="{'dx-chat-disabled': isDisabled}"></test-component> <button @click="toggleDisabledState($event)">Click me</button> `, components: { TestComponent }, setup() { const isDisabled = ref(false); function toggleDisabledState() { isDisabled.value = !isDisabled.value; } return { isDisabled, toggleDisabledState }; }, }); const wrapper = mount(component); const componentContainer = wrapper.find('#component'); await wrapper.find('button').trigger('click'); expect(componentContainer.element.className).toBe('chat dx-chat-disabled'); const attrsPassedToVNodeInRenderMethod = wrapper.vm.$.subTree?.children?.[0]?.component?.subTree?.props?.class; const expectedClasses = 'chat dx-chat-disabled'; expect(attrsPassedToVNodeInRenderMethod).toBe(expectedClasses); expect(componentContainer.element.className).toBe(expectedClasses); }); it('forwards correct classes when a dynamic and static attrs were defined', async () => { const component = defineComponent({ template: ` <test-component id="component" class="chat" :class="{'dx-chat-disabled': isDisabled}"></test-component> <button @click="toggleDisabledState($event)">Click me</button> `, components: { TestComponent }, setup() { const isDisabled = ref(false); function toggleDisabledState() { isDisabled.value = !isDisabled.value; } return { isDisabled, toggleDisabledState }; }, }); const wrapper = mount(component); const componentContainer = wrapper.find('#component'); componentContainer.element.classList.add('should-be-removed-class', 'dx-chat', 'dx-hover'); await wrapper.find('button').trigger('click'); expect(componentContainer.element.className).toBe('chat dx-chat dx-hover dx-chat-disabled'); await wrapper.find('button').trigger('click'); expect(componentContainer.element.className).toBe('chat dx-chat dx-hover'); }); it('forwards correct classes when only a dynamic attr was defined', async () => { const component = defineComponent({ template: ` <test-component id="component" :class="{'dx-chat-disabled': isDisabled}"></test-component> <button @click="toggleDisabledState($event)">Click me</button> `, components: { TestComponent }, setup() { const isDisabled = ref(false); function toggleDisabledState() { isDisabled.value = !isDisabled.value; } return { isDisabled, toggleDisabledState }; }, }); const wrapper = mount(component); const componentContainer = wrapper.find('#component'); componentContainer.element.classList.add('should-be-removed-class', 'dx-chat', 'dx-hover'); await wrapper.find('button').trigger('click'); expect(componentContainer.element.className).toBe('dx-chat dx-hover dx-chat-disabled'); await wrapper.find('button').trigger('click'); expect(componentContainer.element.className).toBe('dx-chat dx-hover'); }); }); it('passes styles to element', () => { const vm = defineComponent({ template: '<test-component style=\'height: 10px; width: 20px;\'/>', components: { TestComponent, }, }); const wrapper = mount(vm); expect(wrapper.element.outerHTML).toBe('<div style="height: 10px; width: 20px;"></div>'); }); it('creates nested component', () => { mount(defineComponent({ template: '<test-component><test-component/></test-component>', components: { TestComponent, }, })); expect(WidgetClass.mock.instances.length).toBe(2); expect(WidgetClass.mock.instances[1]).toEqual({}); }); it('correctly sets the buy now link', () => { expect(config().buyNowLink).toBe('https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeVue.aspx'); }); it('correctly sets the help link', () => { expect(config().licensingDocLink).toBe('https://go.devexpress.com/Licensing_Documentation_DevExtremeVue.aspx'); }); describe('options', () => { it('watch prop changing to undefined', (done) => { const wrapper = mount(TestComponent, { props: { sampleProp: 'test', }, }); wrapper.vm.$_config.updateValue = jest.fn(); wrapper.setProps({ sampleProp: undefined }); nextTick(() => { expect(wrapper.vm.$_config.updateValue).toBeCalled(); done(); }); }); it('pass the same template name as in props', () => { const vm = defineComponent({ template: `<test-component id="component" templateName="myTemplate"> <template #myTemplate> content </template> </test-component>`, components: { TestComponent, }, }); const wrapper = mount(vm); const component = wrapper.getComponent('#component'); expect(WidgetClass.mock.calls[0][0]).toBe(component.vm.$el); expect(skipIntegrationOptions(WidgetClass.mock.calls[0][1])).toEqual({ templateName: 'myTemplate', templatesRenderAsynchronously: true, }); }); it('pass template name as default', () => { const vm = defineComponent({ template: `<test-component id="component"> <template #templateName> content </template> </test-component>`, components: { TestComponent, }, }); const wrapper = mount(vm); const component = wrapper.getComponent('#component'); expect(WidgetClass.mock.calls[0][0]).toBe(component.vm.$el); expect(skipIntegrationOptions(WidgetClass.mock.calls[0][1])).toEqual({ templateName: 'templateName', templatesRenderAsynchronously: true, }); }); it('pass props to option on mounting', () => { const wrapper = mount(TestComponent, { props: { sampleProp: 'default', }, }); expect(WidgetClass.mock.calls[0][0]).toBe(wrapper.element); expect(skipIntegrationOptions(WidgetClass.mock.calls[0][1])).toEqual({ sampleProp: 'default', templatesRenderAsynchronously: true, }); }); it('subscribes to optionChanged', () => { mount(TestComponent, { props: { sampleProp: 'default', }, }); expect(eventHandlers).toHaveProperty('optionChanged'); }); it('watch prop changing', async () => { const wrapper = mount(TestComponent, { props: { sampleProp: 'default', }, }); await wrapper.setProps({ sampleProp: 'new' }); expect(Widget.option).toHaveBeenCalledTimes(1); expect(Widget.option).toHaveBeenCalledWith('sampleProp', 'new'); }); it('component shouldn\'t update array value(by default)', async () => { expect.assertions(1); const component = defineComponent({ template: ` <button @click="testData.push(2)">Click me</button> <test-component :prop2="testData"></test-component> `, components: { TestComponent, }, data() { return { testData: [1], }; }, }); const wrapper = mount(component); await wrapper.find('button').trigger('click'); await nextTick(() => { expect(Widget.option).toHaveBeenCalledTimes(0); }); }); it('component updates array value if the deepWatch flag equal true', async () => { expect.assertions(2); globalConfig({ deepWatch: true }); const component = defineComponent({ template: ` <button @click="testData.push(2)">Click me</button> <test-component :prop2="testData"></test-component> `, components: { TestComponent, }, data() { return { testData: [1], }; }, }); const wrapper = mount(component); await wrapper.find('button').trigger('click'); await nextTick(() => { expect(Widget.option).toHaveBeenCalledTimes(1); expect(Widget.option).toHaveBeenCalledWith('prop2', [1, 2]); globalConfig({ deepWatch: false }); }); }); }); describe('configuration', () => { const Nested = buildTestConfigCtor(); Nested.$_optionName = 'nestedOption'; it('creates configuration', () => { const wrapper = mount(TestComponent); expect(wrapper.vm.$_config).not.toBeNull(); }); it('updates pendingOptions from a widget component configuration updateFunc', () => { const wrapper = mount(TestComponent); const pendingOptions = wrapper.vm.$_pendingOptions; const name = 'abc'; const value = {}; wrapper.vm.$_config.updateFunc(name, value); expect(pendingOptions[name]).toEqual(value); }); it('initializes nested config', () => { const vm = defineComponent({ template: '<test-component id="component">' + ' <nested :prop1="123" />' + '</test-component>', components: { TestComponent, Nested, }, }); const wrapper = mount(vm); const config = wrapper.getComponent('#component').vm.$_config; expect(config.nested).toHaveLength(1); expect(config.nested[0].name).toBe('nestedOption'); expect(config.nested[0].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(config.nested[0].initialValues).toEqual({ prop1: 123 }); expect(config.nested[0].isCollectionItem).toBeFalsy(); }); it('initializes nested config (collectionItem)', () => { const NestedCollectionItem = buildTestConfigCtor(); NestedCollectionItem.$_optionName = 'nestedOption'; NestedCollectionItem.$_isCollectionItem = true; const vm = defineComponent({ template: '<test-component id="component">' + ' <nested-collection-item :prop1="123" />' + '</test-component>', components: { TestComponent, NestedCollectionItem, }, }); const wrapper = mount(vm); const config = wrapper.getComponent('#component').vm.$_config; expect(config.nested).toHaveLength(1); expect(config.nested[0].name).toBe('nestedOption'); expect(config.nested[0].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(config.nested[0].initialValues).toEqual({ prop1: 123 }); expect(config.nested[0].isCollectionItem).toBeTruthy(); expect(config.nested[0].collectionItemIndex).toBe(0); }); it('initializes nested config (several collectionItems)', () => { const NestedCollectionItem = buildTestConfigCtor(); NestedCollectionItem.$_optionName = 'nestedOption'; NestedCollectionItem.$_isCollectionItem = true; const vm = defineComponent({ template: '<test-component id="component">' + ' <nested-collection-item :prop1="123" />' + ' <nested-collection-item :prop1="456" prop2="abc" sample-prop="test" />' + ' <nested-collection-item prop2="def" />' + '</test-component>', components: { TestComponent, NestedCollectionItem, }, }); const wrapper = mount(vm); const config = wrapper.getComponent('#component').vm.$_config; expect(config.nested).toHaveLength(3); expect(config.nested[0].name).toBe('nestedOption'); expect(config.nested[0].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(config.nested[0].initialValues).toEqual({ prop1: 123 }); expect(config.nested[0].isCollectionItem).toBeTruthy(); expect(config.nested[0].collectionItemIndex).toBe(0); expect(config.nested[1].name).toBe('nestedOption'); expect(config.nested[1].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(config.nested[1].initialValues).toEqual({ prop1: 456, prop2: 'abc', sampleProp: 'test' }); expect(config.nested[1].isCollectionItem).toBeTruthy(); expect(config.nested[1].collectionItemIndex).toBe(1); expect(config.nested[2].name).toBe('nestedOption'); expect(config.nested[2].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(config.nested[2].initialValues).toEqual({ prop2: 'def' }); expect(config.nested[2].isCollectionItem).toBeTruthy(); expect(config.nested[2].collectionItemIndex).toBe(2); }); it('initializes nested config (using v-for)', () => { const NestedCollectionItem = buildTestConfigCtor(); NestedCollectionItem.$_optionName = 'nestedOption'; NestedCollectionItem.$_isCollectionItem = true; const vm = defineComponent({ template: '<test-component id="component">' + ' <nested-collection-item v-for="(item, index) in items" :key="index" :prop1="item.value" />' + '</test-component>', data() { return { items: [{ value: 123 }, { value: 321 }, { value: 432 }], }; }, components: { TestComponent, NestedCollectionItem, }, }); const wrapper = mount(vm); const config = wrapper.getComponent('#component').vm.$_config; expect(config.nested).toHaveLength(3); expect(config.nested[0].name).toBe('nestedOption'); expect(config.nested[0].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(config.nested[0].initialValues).toEqual({ key: 0, prop1: 123 }); expect(config.nested[0].isCollectionItem).toBeTruthy(); expect(config.nested[0].collectionItemIndex).toBe(0); expect(config.nested[1].name).toBe('nestedOption'); expect(config.nested[1].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(config.nested[1].initialValues).toEqual({ key: 1, prop1: 321 }); expect(config.nested[1].isCollectionItem).toBeTruthy(); expect(config.nested[1].collectionItemIndex).toBe(1); expect(config.nested[2].name).toBe('nestedOption'); expect(config.nested[2].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(config.nested[2].initialValues).toEqual({ key: 2, prop1: 432 }); expect(config.nested[2].isCollectionItem).toBeTruthy(); expect(config.nested[2].collectionItemIndex).toBe(2); }); it('shouldn\'t add fragment as children (v-for)', () => { const NestedCollectionItem = buildTestConfigCtor(); NestedCollectionItem.$_optionName = 'nestedOption'; NestedCollectionItem.$_isCollectionItem = true; const vm = defineComponent({ template: '<test-component id="component">' + ' <nested-collection-item v-for="(item, index) in items" :key="index" :prop1="item.value" />' + '</test-component>', data() { return { items: [{ value: 123 }, { value: 321 }, { value: 432 }], }; }, components: { TestComponent, NestedCollectionItem, }, }); const wrapper = mount(vm); const component = wrapper.getComponent('#component').vm; expect(component.$.subTree.children).toHaveLength(3); }); it('should find nested options if they wrapped to some kind of fragment elements', () => { const NestedCollectionItem = buildTestConfigCtor(); NestedCollectionItem.$_optionName = 'nestedOption'; NestedCollectionItem.$_isCollectionItem = true; const vm = defineComponent({ template: `<test-component id="component"> <template v-if="hasNested"> <template v-for="(item, index) in items"> <nested-collection-item :prop1="item.value" /> </template> </template> </test-component>`, data() { return { hasNested: true, items: [{ value: 123 }, { value: 321 }, { value: 432 }], }; }, components: { TestComponent, NestedCollectionItem, }, }); const wrapper = mount(vm); const component = wrapper.getComponent('#component').vm; expect(component.$.subTree.children).toHaveLength(3); }); it('initializes nested config predefined prop', () => { const predefinedValue = {}; const NestedWithPredefined = buildTestConfigCtor(); NestedWithPredefined.$_optionName = 'nestedOption'; NestedWithPredefined.$_predefinedProps = { predefinedProp: predefinedValue, }; const vm = defineComponent({ template: '<test-component id="component">' + ' <nested-with-predefined />' + '</test-component>', components: { TestComponent, NestedWithPredefined, }, }); const wrapper = mount(vm); const config = wrapper.getComponent('#component').vm.$_config; const initialValues = config.getNestedOptionValues(); expect(initialValues).toHaveProperty('nestedOption'); expect(initialValues.nestedOption).toHaveProperty('predefinedProp'); expect(initialValues.nestedOption.predefinedProp).toBe(predefinedValue); }); it('initializes sub-nested config', () => { const SubNested = buildTestConfigCtor(); SubNested.$_optionName = 'subNestedOption'; const vm = defineComponent({ template: '<test-component id="component">' + ' <nested :prop1="123">' + ' <sub-nested prop2="abc"/>' + ' </nested>' + '</test-component>', components: { TestComponent, Nested, SubNested, }, }); const wrapper = mount(vm); const config = wrapper.getComponent('#component').vm.$_config; expect(config.nested).toHaveLength(1); const nestedConfig = config.nested[0]; expect(nestedConfig.nested).toHaveLength(1); expect(nestedConfig.nested[0].name).toBe('subNestedOption'); expect(nestedConfig.nested[0].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(nestedConfig.nested[0].initialValues).toEqual({ prop2: 'abc' }); expect(nestedConfig.nested[0].isCollectionItem).toBeFalsy(); }); it('initializes sub-nested config (collectionItem)', () => { const SubNested = buildTestConfigCtor(); SubNested.$_optionName = 'subNestedOption'; SubNested.$_isCollectionItem = true; const vm = defineComponent({ template: '<test-component id="component">' + ' <nested :prop1="123">' + ' <sub-nested prop2="abc"/>' + ' </nested>' + '</test-component>', components: { TestComponent, Nested, SubNested, }, }); const wrapper = mount(vm); const config = wrapper.getComponent('#component').vm.$_config; expect(config.nested).toHaveLength(1); const nestedConfig = config.nested[0]; expect(nestedConfig.nested).toHaveLength(1); expect(nestedConfig.nested[0].name).toBe('subNestedOption'); expect(nestedConfig.nested[0].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(nestedConfig.nested[0].initialValues).toEqual({ prop2: 'abc' }); expect(nestedConfig.nested[0].isCollectionItem).toBeTruthy(); expect(nestedConfig.nested[0].collectionItemIndex).toBe(0); }); it('initializes sub-nested config (multiple collectionItems)', () => { const NestedCollectionItem = buildTestConfigCtor(); NestedCollectionItem.$_optionName = 'subNestedOption'; NestedCollectionItem.$_isCollectionItem = true; const vm = defineComponent({ template: '<test-component id="component">' + ' <nested>' + ' <nested-collection-item :prop1="123" />' + ' <nested-collection-item :prop1="456" prop2="abc" />' + ' <nested-collection-item prop2="def" />' + ' </nested>' + '</test-component>', components: { TestComponent, Nested, NestedCollectionItem, }, }); const wrapper = mount(vm); const config = wrapper.getComponent('#component').vm.$_config; expect(config.nested).toHaveLength(1); const nestedConfig = config.nested[0]; expect(nestedConfig.nested).toHaveLength(3); expect(nestedConfig.nested[0].name).toBe('subNestedOption'); expect(nestedConfig.nested[0].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(nestedConfig.nested[0].initialValues).toEqual({ prop1: 123 }); expect(nestedConfig.nested[0].isCollectionItem).toBeTruthy(); expect(nestedConfig.nested[0].collectionItemIndex).toBe(0); expect(nestedConfig.nested[1].name).toBe('subNestedOption'); expect(nestedConfig.nested[1].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(nestedConfig.nested[1].initialValues).toEqual({ prop1: 456, prop2: 'abc' }); expect(nestedConfig.nested[1].isCollectionItem).toBeTruthy(); expect(nestedConfig.nested[1].collectionItemIndex).toBe(1); expect(nestedConfig.nested[2].name).toBe('subNestedOption'); expect(nestedConfig.nested[2].options).toEqual(['prop1', 'prop2', 'sampleProp']); expect(nestedConfig.nested[2].initialValues).toEqual({ prop2: 'def' }); expect(nestedConfig.nested[2].isCollectionItem).toBeTruthy(); expect(nestedConfig.nested[2].collectionItemIndex).toBe(2); }); describe('expectedChildren', () => { it('initialized for widget component', () => { const expected = {}; const WidgetComponent = createComponent({ beforeCreate() { this.$_WidgetClass = WidgetClass; this.$_expectedChildren = expected; }, }); const wrapper = mount(WidgetComponent); expect(wrapper.vm.$_config.expectedChildren).toBe(expected); }); it('initialized for config component', () => { const expected = {}; const ConfigComponent = buildTestConfigCtor(); ConfigComponent.$_optionName = 'nestedOption'; ConfigComponent.$_expectedChildren = expected; const vm = defineComponent({ template: '<test-component id="component">' + ' <config-component />' + '</test-component>', components: { TestComponent, ConfigComponent, }, }); const wrapper = mount(vm); const widgetConfig = wrapper.getComponent('#component').vm.$_config; expect(widgetConfig.nested[0].expectedChildren).toBe(expected); }); }); }); describe('nested option', () => { const Nested = buildTestConfigCtor(); Nested.$_optionName = 'nestedOption'; it('pulls initital values', () => { const vm = defineComponent({ template: '<test-component id="component">' + ' <nested :prop1="123" />' + '</test-component>', components: { TestComponent, Nested, }, }); const wrapper = mount(vm); const component = wrapper.getComponent('#component'); expect(WidgetClass.mock.calls[0][0]).toBe(component.vm.$el); expect(skipIntegrationOptions(WidgetClass.mock.calls[0][1])).toEqual({ nestedOption: { prop1: 123, }, templatesRenderAsynchronously: true, }); }); it('watches option changes', (done) => { const vm = defineComponent({ template: '<test-component>' + ' <nested :prop1="value" />' + '</test-component>', components: { TestComponent, Nested, }, props: ['value'], }); const wrapper = mount(vm, { props: { value: 123, }, }); wrapper.setProps({ value: 456 }); nextTick(() => { expect(Widget.option).toHaveBeenCalledTimes(1); expect(Widget.option).toHaveBeenCalledWith('nestedOption.prop1', 456); done(); }); }); it('add nested component by condition', (done) => { const vm = defineComponent({ template: '<test-component>' + ' <nested v-if="showNest" :prop1="123" />' + '</test-component>', components: { TestComponent, Nested, }, props: { showNest: { type: Boolean, value: false, }, }, }); const wrapper = mount(vm); wrapper.setProps({ showNest: true }); nextTick(() => { expect(Widget.option).toHaveBeenCalledWith('nestedOption', { key: 0, prop1: 123 }); done(); }); }); it('remove nested component by condition', (done) => { const NestedCollectionItem = buildTestConfigCtor(); NestedCollectionItem.$_optionName = 'nestedOption'; NestedCollectionItem.$_isCollectionItem = true; const vm = defineComponent({ template: '<test-component>' + ' <nested-collection-item v-if="show" :prop1="123" />' + ' <nested-collection-item :prop1="321" />' + '</test-component>', components: { TestComponent, NestedCollectionItem, }, props: { show: { type: Boolean, default: true, }, }, }); const wrapper = mount(vm); wrapper.setProps({ show: false }); nextTick(() => { expect(Widget.option).toHaveBeenCalledWith('nestedOption', [{ prop1: 321 }]); done(); }); }); it('should update only part of collection components', (done) => { const NestedCollectionItem = buildTestConfigCtor(); NestedCollectionItem.$_optionName = 'nestedOption'; NestedCollectionItem.$_isCollectionItem = true; const vm = defineComponent({ template: '<test-component>' + ' <nested-collection-item>' + ' <nested-collection-item>' + ' <nested-collection-item v-if="show" :prop1="123">' + ' </nested-collection-item>' + ' <nested-collection-item :prop1="321">' + ' </nested-collection-item>' + ' </nested-collection-item>' + ' </nested-collection-item>' + '</test-component>', components: { TestComponent, NestedCollectionItem, }, props: { show: { type: Boolean, default: true, }, }, }); const wrapper = mount(vm); wrapper.setProps({ show: false }); nextTick(() => { expect(Widget.option) .toHaveBeenCalledWith('nestedOption[0].nestedOption[0].nestedOption', [{ prop1: 321 }]); done(); }); }); it('should update only part of collection components (remove all subnested)', (done) => { const NestedCollectionItem = buildTestConfigCtor(); NestedCollectionItem.$_optionName = 'nestedOption'; NestedCollectionItem.$_isCollectionItem = true; const vm = defineComponent({ template: '<test-component>' + ' <nested-collection-item>' + ' <nested-collection-item>' + ' <nested-collection-item v-if="show" :prop1="123">' + ' </nested-collection-item>' + ' <nested-collection-item v-if="show" :prop1="321">' + ' </nested-collection-item>' + ' </nested-collection-item>' + ' </nested-collection-item>' + '</test-component>', components: { TestComponent, NestedCollectionItem, }, props: { show: { type: Boolean, default: true, }, }, }); const wrapper = mount(vm); wrapper.setProps({ show: false }); nextTick(() => { expect(Widget.option).toHaveBeenCalledWith('nestedOption[0].nestedOption[0].nestedOption', undefined); done(); }); }); it('reset nested component', (done) => { const vm = defineComponent({ template: '<test-component>' + ' <nested v-if="show" :prop1="123" />' + '</test-component>', components: { TestComponent, Nested, }, props: { show: { type: Boolean, default: true, }, }, }); const wrapper = mount(vm); wrapper.setProps({ show: false }); nextTick(() => { expect(Widget.resetOption).toHaveBeenCalledWith('nestedOption'); done(); }); }); }); function renderTemplate(name, model, container, index) { model = model || {}; container = container || document.createElement('div'); const { render } = WidgetClass.mock.calls[0][1].integrationOptions.templates[name]; return render({ container, model, index, }); } describe('template', () => { const DX_TEMPLATE_WRAPPER = 'dx-template-wrapper'; const componentWithTemplate = defineComponent({ template: `<test-component :prop1='prop1Value'> <template #test v-if='renderTemplate'>content</template> </test-component>`, components: { TestComponent, }, props: { renderTemplate: { type: Boolean, value: false, }, prop1Value: { type: Number, value: 1, }, }, }); function renderItemTemplate(model, container, index) { return renderTemplate('item', model, container, index); } it('passes integrationOptions to widget', () => { const vm = defineComponent({ template: `<test-component> <template #item> <div>1</div> </template> <template #content> <div>1</div> </template> <div>1</div> </test-component>`, components: { TestComponent, }, }); mount(vm); const { integrationOptions } = WidgetClass.mock.calls[0][1]; expect(integrationOptions).toBeDefined(); expect(integrationOptions.templates).toBeDefined(); expect(integrationOptions.templates.item).toBeDefined(); expect(typeof integrationOptions.templates.item.render).toBe('function'); expect(integrationOptions.templates.content).toBeDefined(); expect(typeof integrationOptions.templates.content.render).toBe('function'); expect(integrationOptions.templates.default).toBeUndefined(); }); it('pass correct template name', () => { const vm = defineComponent({ template: `<test-component templateName="myTemplate"> <template #item> <div>1</div> </template> <template #content> <div>1</div> </template> <div>1</div> </test-component>`, components: { TestComponent, }, }); mount(vm); const { integrationOptions } = WidgetClass.mock.calls[0][1]; expect(integrationOptions).toBeDefined(); expect(integrationOptions.templates).toBeDefined(); expect(integrationOptions.templates.item).toBeDefined(); expect(typeof integrationOptions.templates.item.render).toBe('function'); expect(integrationOptions.templates.content).toBeDefined(); expect(typeof integrationOptions.templates.content.render).toBe('function'); expect(integrationOptions.templates.default).toBeUndefined(); }); it('passes \'integrationOptions.templates\' on update', () => { const wrapper = mount(componentWithTemplate); wrapper.setProps({ renderTemplate: true, }); nextTick(() => { expect(Widget.option.mock.calls[0][0]).toEqual('integrationOptions.templates'); expect(Widget.option.mock.calls[0][1].test.render).toBeInstanceOf(Function); }); }); it('passes \'integrationOptions.templates\' on update before other options', () => { const wrapper = mount(componentWithTemplate); wrapper.setProps({ renderTemplate: true, prop1Value: 2, }); nextTick(() => { expect(Widget.option.mock.calls[0][0]).toEqual('integrationOptions.templates'); expect(Widget.option.mock.calls[1]).toEqual(['test', 'test']); expect(Widget.option.mock.calls[2]).toEqual(['prop1', 2]); }); }); describe('with DOM', () => { let fixture; beforeEach(() => { fixture = document.createElement('div'); fixture.id = 'fixture'; document.body.appendChild(fixture); }); afterEach(() => { fixture.remove(); }); it('template content should be rendered in DOM', () => { let mountedInDom; const ChildComponent = defineComponent({ template: '<div>Test</div>', mounted() { mountedInDom = document.body.contains(this.$el); }, }); const instance = defineComponent({ template: `<test-component id="component"> <div class="template-container"></div> <template #tmpl> <child-component/> </template> </test-component>`, components: { TestComponent, ChildComponent, }, }); const wrapper = mount(instance, { attachTo: document.getElementById('fixture') }); renderTemplate('tmpl', {}, wrapper.getComponent('#component').vm.$el.querySelector('.template-container')); expect(mountedInDom).toBeTruthy(); }); }); it('does not unnecessarily pass \'integrationOptions.templates\'', () => { const wrapper = mount(componentWithTemplate, { props: { renderTemplate: true, prop1Value: 1, }, }); wrapper.setProps({ prop1Value: 2, }); wrapper.setProps({ prop1Value: 3, }); expect(Widget.option.mock.calls.find((call) => call[0] === 'integrationOptions.templates')).toBeUndefined(); }); it('renders', () => { const vm = defineComponent({ template: `<test-component> <template #item> <div>Template</div> </template> </test-component>`, components: { TestComponent, }, }); mount(vm); const renderedTemplate = renderItemTemplate(); expect(renderedTemplate.nodeName).toBe('DIV'); expect(renderedTemplate.className).toBe(DX_TEMPLATE_WRAPPER); expect(renderedTemplate.innerHTML).toBe('Template'); }); it('renders template with several children', () => { const vm = defineComponent({ template: `<test-component> <template #item> <div>child1</div> <div>child2</div> </template> </test-component>`, components: { TestComponent, }, }); mount(vm); const container = document.createElement('div'); renderItemTemplate({}, container); expect(container.innerHTML).toBe('<span style="display: none;"></span><div>child1</div><div>child2</div>'); }); it('unmounts template with root element node', () => { const vm = defineComponent({ template: `<test-component> <template #item> <div>child1</div> </template> </test-component>`, components: { TestComponent, }, }); mount(vm); const container = document.createElement('div'); renderItemTemplate({}, container); events.triggerHandler(container.children[0], 'dxremove'); expect(container.children.length).toEqual(0); }); it('unmounts template with text content', () => { const vm = defineComponent({ template: `<test-component> <template #item> Template_text_content </template> </test-component>`, components: { TestComponent, }, }); mount(vm); const container = document.createElement('div'); renderItemTemplate({}, container); events.triggerHandler(container.children[0], 'dxremove'); expect(container.children.length).toEqual(0); }); it('template should have globalProperties of parent', () => { let templateGlobalProperties; const CustomComponent = defineComponent({ template: '<div></div>', mounted() { templateGlobalProperties = this.$.appContext.config.globalProperties; }, }); const vm = defineComponent({ template: `<test-component id="component"> <template #item> <CustomComponent></CustomComponent> </template> </test-component>`, components: { TestComponent, CustomComponent, }, }); const config = { globalProperties: { name: 'test' }, }; // @ts-expect-error mount(vm, { global: { config } }); renderItemTemplate(); expect(templateGlobalProperties).toEqual(config.globalProperties); }); it('template should have custom plugins data', () => { let testData; const CustomComponent = defineComponent({ template: '<div></div>', mounted() { testData = this.$.appContext.app.test; }, }); const vm = defineComponent({ template: `<test-component id="component"> <template #item> <CustomComponent></CustomComponent> </template> </test-component>`, components: { TestComponent, CustomComponent, }, }); mount(vm, { global: { plugins: [CustomPlugin], }, }); renderItemTemplate(); e