Skip to content
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CardView from 'devextreme-testcafe-models/cardView';
import url from '../../../helpers/getPageUrl';
import { createWidget } from '../../../helpers/createWidget';
import { getCardFieldCaptions } from '../helpers/cardUtils';

fixture`CardView - ColumnChooser.Functional`
.page(url(__dirname, '../../container.html'));
Expand Down Expand Up @@ -184,3 +185,65 @@ test('ColumnChooser should receive and render custom texts', async (t) => {
}).after(async (t) => {
await t.eval(() => location.reload());
});

test('cards should update when column is hidden via column chooser (select mode) (T1324855)', async (t) => {
const cardView = new CardView('#container');

const initialCaptions = await getCardFieldCaptions(t, cardView, 3);
await t.expect(initialCaptions).eql(['A', 'B', 'C']);

await cardView.apiShowColumnChooser();

await t.click(cardView.getColumnChooser().getCheckbox(0));

const captionsAfterHide = await getCardFieldCaptions(t, cardView, 2);
await t.expect(captionsAfterHide).eql(['B', 'C']);

await t.click(cardView.getColumnChooser().getCheckbox(0));

const captionsAfterShow = await getCardFieldCaptions(t, cardView, 3);
await t.expect(captionsAfterShow).eql(['A', 'B', 'C']);
}).before(async () => createWidget('dxCardView', {
dataSource: [
{ a: 1, b: 2, c: 3 },
],
columns: ['a', 'b', 'c'],
columnChooser: {
enabled: true,
mode: 'select',
},
}));

test('cards should update when column is hidden via column chooser (dragAndDrop mode) (T1324855)', async (t) => {
const cardView = new CardView('#container');

const initialCaptions = await getCardFieldCaptions(t, cardView, 3);
await t.expect(initialCaptions).eql(['A', 'B', 'C']);

await cardView.apiShowColumnChooser();

await t.dragToElement(
cardView.getHeaderPanel().getHeaderItem(0).element,
cardView.getColumnChooser().content,
);

const captionsAfterHide = await getCardFieldCaptions(t, cardView, 2);
await t.expect(captionsAfterHide).eql(['B', 'C']);

await t.dragToElement(
cardView.getColumnChooser().getColumn(0),
cardView.getHeaderPanel().element,
);

const captionsAfterShow = await getCardFieldCaptions(t, cardView, 3);
await t.expect(captionsAfterShow).eql(['A', 'B', 'C']);
}).before(async () => createWidget('dxCardView', {
dataSource: [
{ a: 1, b: 2, c: 3 },
],
columns: ['a', 'b', 'c'],
columnChooser: {
enabled: true,
mode: 'dragAndDrop',
},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import TreeView from 'devextreme-testcafe-models/treeView';
import { Selector } from 'testcafe';
import url from '../../../helpers/getPageUrl';
import { createWidget } from '../../../helpers/createWidget';
import { getCardFieldCaptions } from '../helpers/cardUtils';
import {
arrayMoveToGap,
dragToColumnChooser,
Expand Down Expand Up @@ -305,3 +306,38 @@ test('drag from columnChooser to headerPanel: when allowReordering: false', asyn
mode: 'dragAndDrop',
},
}));

test('cards should update when columns are reordered (T1324855)', async (t) => {
const cardView = new CardView('#container');

const initialCaptions = await getCardFieldCaptions(t, cardView, 3);
await t.expect(initialCaptions).eql(['A', 'B', 'C']);

const headerPanel = cardView.getHeaderPanel();
const firstHeader = headerPanel.getHeaderItem(0).element;
const secondHeader = headerPanel.getHeaderItem(1).element;

await t.dragToElement(firstHeader, secondHeader, {
destinationOffsetX: -5,
destinationOffsetY: -20,
speed: 0.5,
});

// Wait for headers to update after drag
await t.expect(cardView.getHeaders().getHeaderItemNth(0).element.innerText).notEql('A');

const headerCaptions: string[] = [];
const headersCount = await cardView.getHeaders().getHeaderItemsElements().count;
for (let i = 0; i < headersCount; i += 1) {
headerCaptions.push(await cardView.getHeaders().getHeaderItemNth(i).element.innerText);
}

const cardCaptions = await getCardFieldCaptions(t, cardView, headersCount);
await t.expect(cardCaptions).eql(headerCaptions);
}).before(async () => createWidget('dxCardView', {
dataSource: [
{ a: 1, b: 2, c: 3 },
],
columns: ['a', 'b', 'c'],
allowColumnReordering: true,
}));
17 changes: 17 additions & 0 deletions e2e/testcafe-devextreme/tests/cardView/helpers/cardUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import CardView from 'devextreme-testcafe-models/cardView';

const getCardFieldCaptions = async (
t: TestController,
cardView: CardView,
expectedCount: number,
cardIndex = 0,
): Promise<string[]> => {
const card = cardView.getCard(cardIndex);
const captions = await card.getCaptions();

await t.expect(captions.length).eql(expectedCount);

return captions;
};

export { getCardFieldCaptions };
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,37 @@ describe('absence of multiple re-render', () => {
});
});
});

describe('reactivity to column option changes', () => {
const dataSource = [
{ id: 1, name: 'Audi' },
{ id: 2, name: 'BMW' },
];

const columns = [
{ dataField: 'id', caption: 'ID' },
{ dataField: 'name', caption: 'Name' },
];

it('should not cause extra re-render when sort/filter options change', () => {
const cardTemplate = jest.fn();

const container = document.createElement('div');
const cardView = new CardView(container, {
keyExpr: 'id',
dataSource,
columns,
cardTemplate,
sorting: {
mode: 'single',
},
} as CardViewOptions);

cardTemplate.mockClear();
cardView.columnOption('name', 'sortOrder', 'asc');

// Should be called dataSource.length times (once per card for data update),
// not dataSource.length * 2 (which would indicate extra re-render).
expect(cardTemplate).toBeCalledTimes(dataSource.length);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/co
import { DataController } from '@ts/grids/new/grid_core/data_controller/data_controller';
import { SearchController } from '@ts/grids/new/grid_core/search/index';

import type { CardInfo, Column, FieldInfo } from '../columns_controller/types';
import type {
CardInfo, Column, FieldInfo,
} from '../columns_controller/types';
import type { DataObject, Key } from '../data_controller/types';
import { getColumnLayoutKey } from './utils';

export class ItemsController {
private readonly selectedCardKeys = signal<Key[]>([]);
Expand All @@ -19,12 +22,22 @@ export class ItemsController {

public readonly additionalItems = signal<CardInfo[]>([]);

private readonly visibleColumnsLayout = computed(
() => JSON.stringify(
this.columnsController.visibleColumns.value
.map(getColumnLayoutKey),
),
);

public readonly items = computed(
() => {
// NOTE: We should trigger computed by search options change,
// But all work with these options encapsulated in SearchHighlightTextProcessor
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.searchController.highlightTextOptions.value;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.visibleColumnsLayout.value;

return this.dataController.items.value.map(
(item, itemIndex) => this.createCardInfo(
item,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { VisibleColumn } from '../columns_controller/types';

// NOTE: Column properties that are excluded from the layout key because they
// don't directly affect card rendering. Data changes caused by sort/filter
// arrive separately via dataController.items, so tracking these properties
// in visibleColumnsLayout would cause redundant re-renders (T1306983, T1309423).
const NON_LAYOUT_COLUMN_KEYS: ReadonlySet<string> = new Set([
'sortOrder',
'sortIndex',
'filterValues',
'filterType',
]);

export const getColumnLayoutKey = (column: VisibleColumn): string => {
const entries = Object.entries(column)
.filter(([key]) => !NON_LAYOUT_COLUMN_KEYS.has(key));

return JSON.stringify(entries);
};
13 changes: 13 additions & 0 deletions packages/testcafe-models/cardView/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,17 @@ export default class Card {
getHighlightedTexts(): Selector {
return this.element.find(`.${CLASS.fieldValue}__${CLASS.highlightedState}`);
}

async getCaptions(): Promise<string[]> {
const captionElements = this.element.find(`.${CLASS.fieldCaption}`);
const count = await captionElements.count;
const captions: string[] = [];

for (let i = 0; i < count; i += 1) {
const caption = await captionElements.nth(i).innerText;
captions.push(caption.replace(/:$/, ''));
}

return captions;
}
}
Loading