Skip to content
2 changes: 2 additions & 0 deletions packages/devextreme/js/__internal/grids/new/grid_core/di.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { HeaderFilterViewController } from './filtering/header_filter/view_contr
import * as FilterControllerModule from './filtering/index';
import { ItemsController } from './items_controller/items_controller';
import { KeyboardNavigationController } from './keyboard_navigation/index';
import { OptionsValidationController } from './options_validation/index';
import { PagerView } from './pager/view';
import { SearchController } from './search/controller';
import { SearchView } from './search/view';
Expand Down Expand Up @@ -57,4 +58,5 @@ export function register(diContext: DIContext): void {
diContext.register(ClearFilterVisitor);
diContext.register(GetAppliedFilterVisitor);
diContext.register(KeyboardNavigationController);
diContext.register(OptionsValidationController);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { isDefined } from '@js/core/utils/type';
import { DataController } from '@ts/grids/new/grid_core/data_controller/index';

import { throwError } from './utils';

export class OptionsValidationController {
public static dependencies = [
DataController,
] as const;

constructor(
private readonly dataController: DataController,
) {
}

public validateKeyExpr(): void {
const keyExpr = this.dataController.dataSource.peek().key();

if (!isDefined(keyExpr)) {
throwError('E1042', 'CardView');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { OptionsValidationController } from './controller';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import errors from '@js/ui/widget/ui.errors';

export const throwError = (errorCode?: string, message?: string): void => {
throw errors.Error(errorCode, message);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/* eslint-disable spellcheck/spell-checker, @stylistic/max-len */
import {
afterEach, beforeEach, describe, expect, it, jest,
} from '@jest/globals';
import $ from '@js/core/renderer';
import CardView from '@ts/grids/new/card_view/widget';
import type { Options as GridCoreOptions } from '@ts/grids/new/grid_core/options';
import { throwError } from '@ts/grids/new/grid_core/options_validation/utils';
import { rerender } from 'inferno';

const SELECTORS = {
cardView: '.dx-cardview',
card: '.dx-cardview-card',
cardCheckbox: '.dx-checkbox-container',
selectAllButton: '[aria-label="Select all"]',
};

const setup = (options: GridCoreOptions = {}): CardView => {
const container = document.createElement('div');
const { body } = document;
body.append(container);

const cardView = new CardView(container, options);
rerender();
return cardView;
};

const getCardElements = (): NodeListOf<Element> => document.querySelectorAll(SELECTORS.card);

const getCardCheckboxes = (): NodeListOf<Element> => document.querySelectorAll(SELECTORS.cardCheckbox);

const getSelectAllButton = (): Element | null => document.querySelector(SELECTORS.selectAllButton);

const checkError = (): void => expect(throwError).toHaveBeenCalledWith('E1042', 'CardView');

jest.mock('@ts/grids/new/grid_core/options_validation/utils', () => ({
throwError: jest.fn().mockImplementation(() => ({})),
}));

describe('when keyExpr is missing', () => {
afterEach(() => {
const cardView = document.querySelector(SELECTORS.cardView);
// @ts-expect-error bad typed renderer
$(cardView ?? undefined as any)?.dxCardView('dispose');
document.body.innerHTML = '';
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('selection mode single', () => {
it('shouldn\'t throw E1042 error if keyExpr is missing and selection', () => {
setup({
dataSource: [{ value: 'test1' }, { value: 'test2' }],
selection: {
mode: 'single',
},
});

const cardElements = getCardElements();
expect(cardElements.length).toEqual(2);
});

it('should throw E1042 error on card click selection', () => {
setup({
dataSource: [{ value: 'test1' }, { value: 'test2' }],
selection: {
mode: 'single',
},
});

const cardElements = getCardElements();
cardElements[0].dispatchEvent(new MouseEvent('click'));

checkError();
});

it('should throw E1042 error on initial selectedCardKeys', () => {
setup({
dataSource: [{ value: 'test1' }, { value: 'test2' }],
selection: {
mode: 'single',
},
selectedCardKeys: [0],
});

checkError();
});

it('should throw E1042 error on runtime selectedCardKeys update', () => {
const cardView = setup({
dataSource: [{ value: 'test1' }, { value: 'test2' }],
selection: {
mode: 'single',
},
});

cardView.instance().option('selectedCardKeys', [1]);

checkError();
});
});

describe('selection mode multiple', () => {
it('shouldn\'t throw E1042 error if keyExpr is missing and selection', () => {
setup({
dataSource: [{ value: 'test1' }, { value: 'test2' }],
selection: {
mode: 'multiple',
},
});

const cardElements = getCardElements();
expect(cardElements.length).toEqual(2);
});

it('should throw E1042 error on checkbox click selection', () => {
setup({
dataSource: [{ value: 'test1' }, { value: 'test2' }],
selection: {
mode: 'multiple',
showCheckBoxesMode: 'always',
},
});

const cardCheckboxes = getCardCheckboxes();
cardCheckboxes[0].dispatchEvent(new MouseEvent('click', { bubbles: true }));

checkError();
});

it('should throw E1042 error on selectAll toolbar button click', () => {
setup({
dataSource: [{ value: 'test1' }, { value: 'test2' }],
selection: {
mode: 'multiple',
showCheckBoxesMode: 'always',
allowSelectAll: true,
},
});

const selectAllButton = getSelectAllButton();
selectAllButton?.dispatchEvent(new MouseEvent('click'));

checkError();
});

it('should throw E1042 error on initial selectedCardKeys', () => {
setup({
dataSource: [{ value: 'test1' }, { value: 'test2' }],
selection: {
mode: 'multiple',
showCheckBoxesMode: 'always',
},
selectedCardKeys: [0, 1],
});

checkError();
});

it('should throw E1042 error on runtime selectedCardKeys update', () => {
const cardView = setup({
dataSource: [{ value: 'test1' }, { value: 'test2' }],
selection: {
mode: 'multiple',
showCheckBoxesMode: 'always',
},
});

cardView.instance().option('selectedCardKeys', [1]);

checkError();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,171 @@ describe('SelectionController', () => {
});
});

describe('when selection changes via selectionChanged callback', () => {
it('should update both selectionController and itemsController selection states when selecting single card', () => {
const cardData = { id: 1, value: 'test' };
const {
selectionController,
itemsController,
} = setup({
keyExpr: 'id',
dataSource: [cardData],
selection: {
mode: 'multiple',
},
});

// Set up the spy before calling the method
const setSelectionStateSpy = jest.spyOn(itemsController, 'setSelectionState');

// Mock the selectionChanged private method call with test data
const selectionChangedEvent = {
addedItemKeys: [1],
removedItemKeys: [],
selectedItemKeys: [1],
selectedItems: [cardData],
};

// Call the private method directly
// @ts-expect-error - accessing private method
selectionController.selectionChanged(selectionChangedEvent);

// Verify that both controllers were updated
expect(selectionController.getSelectedCardKeys()).toEqual([1]);

// Check that the itemsController was updated with the same keys
expect(setSelectionStateSpy).toHaveBeenCalledWith([1]);

// Verify the UI would show correct selection state
const items = itemsController.items.peek();
expect(items[0].isSelected).toBe(true);
});

it('should update both selectionController and itemsController when selecting multiple cards', () => {
const cardsData = [
{ id: 1, value: 'test1' },
{ id: 2, value: 'test2' },
{ id: 3, value: 'test3' },
];
const {
selectionController,
itemsController,
} = setup({
keyExpr: 'id',
dataSource: cardsData,
selection: {
mode: 'multiple',
},
});

// Set up the spy before calling the method
const setSelectionStateSpy = jest.spyOn(itemsController, 'setSelectionState');

// Mock the selectionChanged private method call with test data
const selectionChangedEvent = {
addedItemKeys: [1, 3],
removedItemKeys: [],
selectedItemKeys: [1, 3],
selectedItems: [cardsData[0], cardsData[2]],
};

// Call the private method directly
// @ts-expect-error - accessing private method
selectionController.selectionChanged(selectionChangedEvent);

// Verify that both controllers were updated
expect(selectionController.getSelectedCardKeys()).toEqual([1, 3]);

// Check that the itemsController was updated with the same keys
expect(setSelectionStateSpy).toHaveBeenCalledWith([1, 3]);

// Verify the UI would show correct selection state
const items = itemsController.items.peek();
expect(items[0].isSelected).toBe(true);
expect(items[1].isSelected).toBe(false);
expect(items[2].isSelected).toBe(true);
});

it('should update both selectionController and itemsController when deselecting cards', () => {
const cardsData = [
{ id: 1, value: 'test1' },
{ id: 2, value: 'test2' },
{ id: 3, value: 'test3' },
];
const {
selectionController,
itemsController,
} = setup({
keyExpr: 'id',
dataSource: cardsData,
selection: {
mode: 'multiple',
},
selectedCardKeys: [1, 2, 3],
});

// Initially all cards should be selected
expect(itemsController.items.peek()[0].isSelected).toBe(true);
expect(itemsController.items.peek()[1].isSelected).toBe(true);
expect(itemsController.items.peek()[2].isSelected).toBe(true);

// Set up the spy before calling the method
const setSelectionStateSpy = jest.spyOn(itemsController, 'setSelectionState');

// Mock the selectionChanged event when deselecting card #2
const selectionChangedEvent = {
addedItemKeys: [],
removedItemKeys: [2],
selectedItemKeys: [1, 3],
selectedItems: [cardsData[0], cardsData[2]],
};

// Call the private method directly
// @ts-expect-error - accessing private method
selectionController.selectionChanged(selectionChangedEvent);

// Verify that both controllers were updated
expect(selectionController.getSelectedCardKeys()).toEqual([1, 3]);

// Check that the itemsController was updated with the same keys
expect(setSelectionStateSpy).toHaveBeenCalledWith([1, 3]);

// Verify the UI would show correct selection state
const items = itemsController.items.peek();
expect(items[0].isSelected).toBe(true);
expect(items[1].isSelected).toBe(false);
expect(items[2].isSelected).toBe(true);
});

it('should throw error E1042 if keyExpr is missing and selectionChanged', () => {
const cardData = { id: 1, value: 'test' };
const {
selectionController,
} = setup({
dataSource: [cardData],
selection: {
mode: 'multiple',
},
});

// Mock the selectionChanged private method call with test data
const selectionChangedEvent = {
addedItemKeys: [1],
removedItemKeys: [],
selectedItemKeys: [1],
selectedItems: [cardData],
};

try {
// Call the private method directly
// @ts-expect-error - accessing private method
selectionController.selectionChanged(selectionChangedEvent);
} catch (error) {
expect((error as { __id: string }).__id).toEqual('E1042');
}
});
});

describe('when selecting all cards', () => {
it('should be called', () => {
const selectionChangedMockFn = jest.fn();
Expand Down
Loading
Loading