devextreme-vue
Version:
DevExtreme Vue UI and Visualization Components
535 lines (533 loc) • 21.1 kB
JavaScript
/*!
* 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 { reactive } from 'vue';
import { VMODEL_NAME } from '../vue-helper';
import Configuration, { bindOptionWatchers, setEmitOptionChangedFunc, } from '../configuration';
function createRootConfig(updateFunc) {
return new Configuration(updateFunc, null, {});
}
function createConfigWithExpectedChildren(children) {
return new Configuration(jest.fn(), null, {}, children);
}
describe('fullPath building', () => {
const testCases = [
{
msg: 'works for null',
name: null,
expected: null,
},
{
msg: 'works without owner',
name: 'abc',
expected: 'abc',
},
{
msg: 'works with owner',
name: 'abc',
ownerPath: 'def',
expected: 'def.abc',
},
{
msg: 'works for collection item',
name: 'abc',
collectionIndex: 123,
expected: 'abc[123]',
},
{
msg: 'works for collection item with owner',
name: 'abc',
ownerPath: 'def',
collectionIndex: 123,
expected: 'def.abc[123]',
},
];
for (const { msg, name, collectionIndex, ownerPath, expected, } of testCases) {
it(msg, () => {
const isCollection = collectionIndex !== undefined;
const ownerConfig = ownerPath ? { fullPath: ownerPath } : undefined;
expect(new Configuration(jest.fn(), name, {}, undefined, isCollection, collectionIndex, ownerConfig).fullPath).toBe(expected);
});
}
});
it('calls update from nested', () => {
const callback = jest.fn();
const root = createRootConfig(callback);
const nested = root.createNested('option', {});
nested.updateValue('prop', 123);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('option.prop', 123);
});
it('calls update from subnested', () => {
const callback = jest.fn();
const root = createRootConfig(callback);
const nested = root.createNested('option', {});
const subNested = nested.createNested('subOption', {});
subNested.updateValue('prop', 123);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('option.subOption.prop', 123);
});
it('calls update from nested collectionItem (first)', () => {
const callback = jest.fn();
const root = createRootConfig(callback);
const nested = root.createNested('option', {}, true);
root.createNested('option', {}, true);
nested.updateValue('prop', 123);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('option[0].prop', 123);
});
it('calls update from nested collectionItem (middle)', () => {
const callback = jest.fn();
const root = createRootConfig(callback);
root.createNested('option', {}, true);
const nested = root.createNested('option', {}, true);
root.createNested('option', {}, true);
nested.updateValue('prop', 123);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('option[1].prop', 123);
});
it('calls update from nested collectionItem (last)', () => {
const callback = jest.fn();
const root = createRootConfig(callback);
root.createNested('option', {}, true);
root.createNested('option', {}, true);
const nested = root.createNested('option', {}, true);
nested.updateValue('prop', 123);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('option[2].prop', 123);
});
it('calls update from nested collectionItem (the only)', () => {
const callback = jest.fn();
const root = createRootConfig(callback);
const nested = root.createNested('option', {}, true);
nested.updateValue('prop', 123);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('option[0].prop', 123);
});
it('binds option watchers', () => {
const updateValueFunc = jest.fn();
const $watchFunc = jest.fn();
const innerChanges = {};
bindOptionWatchers({
updateValue: updateValueFunc,
getOptionsToWatch: () => ['prop1'],
innerChanges: {},
}, {
$watch: $watchFunc,
}, innerChanges);
expect($watchFunc.mock.calls[0][0]).toBe('prop1');
const value = {};
$watchFunc.mock.calls[0][1](value);
expect(updateValueFunc).toHaveBeenCalledTimes(1);
expect(updateValueFunc.mock.calls[0][0]).toBe('prop1');
expect(updateValueFunc.mock.calls[0][1]).toBe(value);
});
it('should update only when raw value not equal', () => {
const updateValueFunc = jest.fn();
const $watchFunc = jest.fn();
const innerChanges = { prop1: 'test' };
bindOptionWatchers({
updateValue: updateValueFunc,
getOptionsToWatch: () => ['prop1'],
innerChanges: {},
}, {
$watch: $watchFunc,
}, innerChanges);
$watchFunc.mock.calls[0][1](reactive(innerChanges).prop1);
expect(updateValueFunc).toHaveBeenCalledTimes(0);
$watchFunc.mock.calls[0][1](reactive({ prop1: 'test1' }).prop1);
expect(updateValueFunc).toHaveBeenCalledTimes(1);
expect(updateValueFunc.mock.calls[0][0]).toBe('prop1');
expect(updateValueFunc.mock.calls[0][1]).toBe('test1');
});
describe('initial configuration', () => {
it('pulls value from nested', () => {
const root = createRootConfig(jest.fn());
const nested = root.createNested('option', {});
nested
.createNested('subOption', { prop: 123 })
.init(['prop']);
expect(root.getNestedOptionValues()).toMatchObject({
option: {
subOption: {
prop: 123,
},
},
});
});
it('pulls array of values from a coollectionItem nested (single value)', () => {
const root = createRootConfig(jest.fn());
root.createNested('options', { propA: 123 }, true);
expect(root.getNestedOptionValues()).toMatchObject({
options: [
{ propA: 123 },
],
});
});
it('pulls array of values from a coollectionItem nested (several values)', () => {
const root = createRootConfig(jest.fn());
root.createNested('options', { propA: 123 }, true);
root.createNested('options', { propA: 456, propB: 789 }, true);
expect(root.getNestedOptionValues()).toMatchObject({
options: [
{ propA: 123 },
{ propA: 456, propB: 789 },
],
});
});
it('pulls values from the last nested (not a coollectionItem)', () => {
const root = createRootConfig(jest.fn());
root.createNested('option', { propA: 123 });
root.createNested('option', { propA: 456, propB: 789 });
expect(root.getNestedOptionValues()).toMatchObject({
option: { propA: 456, propB: 789 },
});
});
it('pulls values from self and nested', () => {
const root = new Configuration(jest.fn(), null, { propA: 123 });
const nested = root.createNested('option', { propB: 456 });
nested.createNested('subOption', { propC: 789 });
expect(root.getNestedOptionValues()).toMatchObject({
option: {
propB: 456,
subOption: {
propC: 789,
},
},
});
expect(root.initialValues).toMatchObject({
propA: 123,
});
});
it('pulls empty value for correct option structure T728446', () => {
const root = createRootConfig(jest.fn());
const nested = root.createNested('option', {}, true);
nested.createNested('subOption', {});
expect(root.getNestedOptionValues()).toMatchObject({ option: [{ subOption: {} }] });
});
it('pulls values and ignores empty nested', () => {
const root = createRootConfig(jest.fn());
const nested = root.createNested('option', {});
nested.init(['empty']);
nested
.createNested('subOption', { prop: 123 })
.init(['prop']);
root.createNested('anotherOption', {});
nested.createNested('anotherSubOption', {});
expect(root.getNestedOptionValues()).toMatchObject({
option: {
subOption: {
prop: 123,
},
},
});
});
});
describe('collection items creation', () => {
describe('not-expected item .isCollectionItem prop', () => {
it('is true if isCollection arg is true', () => {
const owner = new Configuration(jest.fn(), null, {});
const nested = owner.createNested('name', {}, true);
expect(nested.isCollectionItem).toBeTruthy();
});
it('is false if isCollection arg is false', () => {
const owner = new Configuration(jest.fn(), null, {});
const nested = owner.createNested('name', {}, false);
expect(nested.isCollectionItem).toBeFalsy();
});
it('is false if isCollection arg is undefined', () => {
const owner = new Configuration(jest.fn(), null, {});
const nested = owner.createNested('name', {});
expect(nested.isCollectionItem).toBeFalsy();
});
});
describe('expectation of collection item', () => {
it('applied if isCollection arg is true', () => {
const owner = createConfigWithExpectedChildren({ abc: { isCollectionItem: true, optionName: 'def' } });
const nested = owner.createNested('abc', {}, true);
expect(nested.isCollectionItem).toBeTruthy();
expect(nested.name).toBe('def');
});
it('applied if isCollection arg is false', () => {
const owner = createConfigWithExpectedChildren({ abc: { isCollectionItem: true, optionName: 'def' } });
const nested = owner.createNested('abc', {}, false);
expect(nested.isCollectionItem).toBeTruthy();
expect(nested.name).toBe('def');
});
it('applied if isCollection arg is undefined', () => {
const owner = createConfigWithExpectedChildren({ abc: { isCollectionItem: true, optionName: 'def' } });
const nested = owner.createNested('abc', {});
expect(nested.isCollectionItem).toBeTruthy();
expect(nested.name).toBe('def');
});
});
describe('expected as collection item .isCollectionItem prop', () => {
it('is true if isCollection arg is true', () => {
const owner = createConfigWithExpectedChildren({ abc: { isCollectionItem: false, optionName: 'def' } });
const nested = owner.createNested('abc', {}, true);
expect(nested.isCollectionItem).toBeFalsy();
});
it('is true if isCollection arg is false', () => {
const owner = createConfigWithExpectedChildren({ abc: { isCollectionItem: false, optionName: 'def' } });
const nested = owner.createNested('abc', {}, false);
expect(nested.isCollectionItem).toBeFalsy();
});
it('is true if isCollection arg is undefined', () => {
const owner = createConfigWithExpectedChildren({ abc: { isCollectionItem: false, optionName: 'def' } });
const nested = owner.createNested('abc', {});
expect(nested.isCollectionItem).toBeFalsy();
expect(nested.name).toBe('def');
});
});
});
describe('options watch-list', () => {
it('includes option with initial values', () => {
const config = new Configuration(jest.fn(), null, { option1: 123, option2: 456 });
config.init(['option1']);
expect(config.getOptionsToWatch()).toEqual(['option1']);
});
it('includes option without initial values', () => {
const config = new Configuration(jest.fn(), null, {});
config.init(['option1']);
expect(config.getOptionsToWatch()).toEqual(['option1']);
});
it('excludes option if finds nested config with the same name', () => {
const config = new Configuration(jest.fn(), null, {});
config.init(['option1', 'theNestedOption']);
config.createNested('theNestedOption', {});
expect(config.getOptionsToWatch()).toEqual(['option1']);
});
});
describe('onOptionChanged', () => {
[
{
fullName: 'option',
value: 'new value',
previousValue: 'old value',
component: null,
},
{
fullName: 'option.nestedOption.subNestedOption',
value: 'any value',
previousValue: 'old value',
component: { option: (name) => name === 'option' && 'new value' },
},
{
fullName: 'option[0]',
value: 'any value',
previousValue: 'old value',
component: { option: (name) => name === 'option' && 'new value' },
},
{
fullName: 'option[0].nestedOption',
value: 'any value',
previousValue: 'old value',
component: { option: (name) => name === 'option' && 'new value' },
},
].map((optionChangedArgs) => {
it('emits from root configuration', () => {
const innerChanges = {};
const emitStub = jest.fn();
const config = new Configuration(jest.fn(), null, {});
const component = {
$emit: emitStub,
$props: { option: undefined },
$options: {
props: {
option: undefined,
},
},
};
setEmitOptionChangedFunc(config, component, innerChanges);
config.onOptionChanged(optionChangedArgs);
expect(emitStub).toHaveBeenCalledTimes(1);
expect(emitStub).toHaveBeenCalledWith('update:option', 'new value');
expect(innerChanges).toEqual({ option: 'new value' });
});
});
[
{
fullName: 'option.nestedOption',
value: 'new value',
previousValue: 'old value',
component: null,
},
{
fullName: 'option.nestedOption.subNestedOption',
value: 'any value',
previousValue: 'old value',
component: { option: (name) => name === 'option.nestedOption' && 'new value' },
},
].map((optionChangedArgs) => {
it('emits from nested configuration', () => {
const innerChanges = {};
const emitStub = jest.fn();
const config = new Configuration(jest.fn(), null, {});
const nestedConfig = config.createNested('option', {});
const component = {
$emit: emitStub,
$props: { nestedOption: undefined },
$options: {
props: {
nestedOption: undefined,
},
},
};
setEmitOptionChangedFunc(nestedConfig, component, innerChanges);
config.onOptionChanged(optionChangedArgs);
expect(emitStub).toHaveBeenCalledTimes(1);
expect(emitStub).toHaveBeenCalledWith('update:nestedOption', 'new value');
expect(innerChanges).toEqual({ nestedOption: 'new value' });
});
});
[
{
fullName: 'option[0].nestedOption',
value: 'new value',
previousValue: 'old value',
component: null,
},
{
fullName: 'option[0].nestedOption.subNestedOption',
value: 'any value',
previousValue: 'old value',
component: { option: (name) => name === 'option[0].nestedOption' && 'new value' },
},
].map((optionChangedArgs) => {
it('emits from nested collection configuration', () => {
const innerChanges = {};
const emitStub = jest.fn();
const config = new Configuration(jest.fn(), null, {});
const nestedConfig = config.createNested('option', {}, true);
const component = {
$emit: emitStub,
$props: { nestedOption: undefined },
$options: {
props: {
nestedOption: undefined,
},
},
};
setEmitOptionChangedFunc(nestedConfig, component, innerChanges);
config.onOptionChanged(optionChangedArgs);
expect(emitStub).toHaveBeenCalledTimes(1);
expect(emitStub).toHaveBeenCalledWith('update:nestedOption', 'new value');
expect(innerChanges).toEqual({ nestedOption: 'new value' });
});
});
[
{
fullName: 'option',
value: 'value',
previousValue: 'value',
component: null,
},
{
fullName: 'option',
value: [],
previousValue: [],
component: null,
},
].map((optionChangedArgs) => {
it('does not emit', () => {
const innerChanges = {};
const emitStub = jest.fn();
const config = new Configuration(jest.fn(), null, {});
const component = {
$: {},
$emit: emitStub,
$props: {},
$options: {},
};
setEmitOptionChangedFunc(config, component, innerChanges);
config.onOptionChanged(optionChangedArgs);
expect(emitStub).toHaveBeenCalledTimes(0);
});
});
// https://github.com/DevExpress/devextreme-vue/issues/330
it('emits once', () => {
const emitStubRoot = jest.fn();
const emitStubNested = jest.fn();
const component = {
$: {},
$emit: emitStubRoot,
$props: { option: undefined },
$options: {
props: {
option: undefined,
},
},
};
const config = new Configuration(jest.fn(), null, {});
setEmitOptionChangedFunc(config, component, {});
const nestedConfig1 = config.createNested('option', {}, true);
setEmitOptionChangedFunc(nestedConfig1, component, {});
const subNestedConfig = nestedConfig1.createNested('option', {}, false);
setEmitOptionChangedFunc(subNestedConfig, component, {});
const nestedConfig2 = config.createNested('option', {}, true);
setEmitOptionChangedFunc(nestedConfig2, component, {});
config.onOptionChanged({
fullName: 'option', value: 'new value', previousValue: 'old value', component: null,
});
expect(emitStubRoot).toHaveBeenCalledTimes(1);
expect(emitStubNested).toHaveBeenCalledTimes(0);
});
it('shouldn\'t emit if component does\'t have the prop', () => {
const emitStubRoot = jest.fn();
const config = new Configuration(jest.fn(), null, {});
const component = {
$emit: emitStubRoot,
$props: { option: undefined },
$options: {
props: {
option: undefined,
},
},
};
setEmitOptionChangedFunc(config, component, {});
config.onOptionChanged({
fullName: 'wrongName',
value: 'new value',
previousValue: 'old value',
component: null,
});
expect(emitStubRoot).toHaveBeenCalledTimes(0);
});
it('should use v-model event name and mutate innerChanges with VMODEL_NAME when option is "value" and v-model is active', () => {
const innerChanges = {};
const config = new Configuration(jest.fn(), null, {});
const emitStub = jest.fn();
const component = {
$emit: emitStub,
$props: { value: false, [VMODEL_NAME]: false },
$options: {
props: { value: false, [VMODEL_NAME]: false },
model: true,
},
$: {
vnode: {
props: { value: false, [VMODEL_NAME]: false },
},
},
};
setEmitOptionChangedFunc(config, component, innerChanges);
config.onOptionChanged({
fullName: 'value',
value: true,
previousValue: false,
component: null,
});
expect(emitStub).toHaveBeenCalledTimes(1);
expect(emitStub).toHaveBeenCalledWith(`update:${VMODEL_NAME}`, true);
expect(innerChanges[VMODEL_NAME]).toBe(true);
});
});