diff --git a/e2e/testcafe-devextreme/tests/treeList/apiMocks/tasksApiMock.ts b/e2e/testcafe-devextreme/tests/treeList/apiMocks/tasksApiMock.ts new file mode 100644 index 000000000000..1362f4e53ab8 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/treeList/apiMocks/tasksApiMock.ts @@ -0,0 +1,185 @@ +import { RequestMock } from 'testcafe'; + +export const tasksApiMock = RequestMock() + .onRequestTo(/\/api\/data\?filter=%5B%22Task_Parent_ID%22%2C%22%3D%22%2C0%5D/) + .respond( + { + data: [ + { + Task_ID: 1, + Task_Parent_ID: 0, + Task_Assigned_Employee_ID: 1, + Task_Assigned_Employee: null, + Task_Owner_ID: 1, + Task_Subject: 'Plans 2015', + Task_Start_Date: '2015-01-01T00:00:00', + Task_Due_Date: '2015-04-01T00:00:00', + Task_Status: 'Completed', + Task_Priority: 3, + Task_Completion: 100, + Has_Items: true, + }, + { + Task_ID: 2, + Task_Parent_ID: 0, + Task_Assigned_Employee_ID: 2, + Task_Assigned_Employee: null, + Task_Owner_ID: 1, + Task_Subject: 'Health Insurance', + Task_Start_Date: '2015-02-12T00:00:00', + Task_Due_Date: '2015-05-30T00:00:00', + Task_Status: 'In Progress', + Task_Priority: 3, + Task_Completion: 75, + Has_Items: true, + }, + ], + totalCount: -1, + groupCount: -1, + summary: null, + }, + 200, + { + 'access-control-allow-origin': '*', + }, + ) + .onRequestTo(/\/api\/data\?filter=%5B%22Task_Parent_ID%22%2C%22%3D%22%2C1%5D/) + .respond( + { + data: [ + { + Task_ID: 28, + Task_Parent_ID: 1, + Task_Assigned_Employee_ID: 7, + Task_Assigned_Employee: null, + Task_Owner_ID: 1, + Task_Subject: 'Prepare 2015 Financial', + Task_Start_Date: '2015-01-15T00:00:00', + Task_Due_Date: '2015-01-31T00:00:00', + Task_Status: 'Completed', + Task_Priority: 3, + Task_Completion: 100, + Has_Items: false, + }, + { + Task_ID: 29, + Task_Parent_ID: 1, + Task_Assigned_Employee_ID: 4, + Task_Assigned_Employee: null, + Task_Owner_ID: 1, + Task_Subject: 'Prepare 2015 Marketing Plan', + Task_Start_Date: '2015-01-01T00:00:00', + Task_Due_Date: '2015-01-31T00:00:00', + Task_Status: 'Completed', + Task_Priority: 3, + Task_Completion: 100, + Has_Items: true, + }, + { + Task_ID: 42, + Task_Parent_ID: 1, + Task_Assigned_Employee_ID: 3, + Task_Assigned_Employee: null, + Task_Owner_ID: 1, + Task_Subject: 'Deliver R&D Plans for 2015', + Task_Start_Date: '2015-03-01T00:00:00', + Task_Due_Date: '2015-03-10T00:00:00', + Task_Status: 'Completed', + Task_Priority: 3, + Task_Completion: 100, + Has_Items: true, + }, + ], + totalCount: -1, + groupCount: -1, + summary: null, + }, + 200, + { + 'access-control-allow-origin': '*', + }, + ) + .onRequestTo(/\/api\/data\?filter=%5B%5B%22Task_Subject%22%2C%22contains%22%2C%22google%22%5D%2C%22or%22%2C%5B%22Task_Status%22%2C%22contains%22%2C%22google%22%5D%5D/) + .respond( + { + data: [ + { + Task_ID: 32, + Task_Parent_ID: 29, + Task_Assigned_Employee_ID: 1, + Task_Assigned_Employee: null, + Task_Owner_ID: 4, + Task_Subject: 'Google AdWords Strategy', + Task_Start_Date: '2015-02-16T00:00:00', + Task_Due_Date: '2015-02-28T00:00:00', + Task_Status: 'Completed', + Task_Priority: 3, + Task_Completion: 100, + Has_Items: false, + }, + ], + totalCount: -1, + groupCount: -1, + summary: null, + }, + 200, + { + 'access-control-allow-origin': '*', + }, + ) + .onRequestTo(/\/api\/data\?filter=%5B%22Task_ID%22%2C%22%3D%22%2C29%5D/) + .respond( + { + data: [ + { + Task_ID: 29, + Task_Parent_ID: 1, + Task_Assigned_Employee_ID: 4, + Task_Assigned_Employee: null, + Task_Owner_ID: 1, + Task_Subject: 'Prepare 2015 Marketing Plan', + Task_Start_Date: '2015-01-01T00:00:00', + Task_Due_Date: '2015-01-31T00:00:00', + Task_Status: 'Completed', + Task_Priority: 3, + Task_Completion: 100, + Has_Items: true, + }, + ], + totalCount: -1, + groupCount: -1, + summary: null, + }, + 200, + { + 'access-control-allow-origin': '*', + }, + ) + .onRequestTo(/\/api\/data\?filter=%5B%22Task_ID%22%2C%22%3D%22%2C1%5D/) + .respond( + { + data: [ + { + Task_ID: 1, + Task_Parent_ID: 0, + Task_Assigned_Employee_ID: 1, + Task_Assigned_Employee: null, + Task_Owner_ID: 1, + Task_Subject: 'Plans 2015', + Task_Start_Date: '2015-01-01T00:00:00', + Task_Due_Date: '2015-04-01T00:00:00', + Task_Status: 'Completed', + Task_Priority: 3, + Task_Completion: 100, + Has_Items: true, + }, + ], + totalCount: -1, + groupCount: -1, + summary: null, + }, + 200, + { + 'access-control-allow-origin': '*', + }, + ); diff --git a/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-checked-all.png b/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-checked-all.png new file mode 100644 index 000000000000..d823fc523984 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-checked-all.png differ diff --git a/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-checked-searched.png b/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-checked-searched.png new file mode 100644 index 000000000000..67811de14fd1 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-checked-searched.png differ diff --git a/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-unchecked-all.png b/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-unchecked-all.png new file mode 100644 index 000000000000..28b49021a689 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-unchecked-all.png differ diff --git a/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-unchecked-searched.png b/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-unchecked-searched.png new file mode 100644 index 000000000000..6505e9c6caa5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/treeList/etalons/T1264312-selection-unchecked-searched.png differ diff --git a/e2e/testcafe-devextreme/tests/treeList/selection.ts b/e2e/testcafe-devextreme/tests/treeList/selection.ts index ae720233f706..7af582d19262 100644 --- a/e2e/testcafe-devextreme/tests/treeList/selection.ts +++ b/e2e/testcafe-devextreme/tests/treeList/selection.ts @@ -1,7 +1,10 @@ import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; import TreeList from 'devextreme-testcafe-models/treeList'; +import ExpandableCell from 'devextreme-testcafe-models/treeList/expandableCell'; + import url from '../../helpers/getPageUrl'; import { createWidget } from '../../helpers/createWidget'; +import { tasksApiMock } from './apiMocks/tasksApiMock'; fixture`Selection` .page(url(__dirname, '../container.html')); @@ -57,3 +60,81 @@ test('TreeList with selection and boolean data in first column should render rig mode: 'multiple', }, })); + +// T1264312 +test('TreeList restore selection after the search panel has cleared', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const treeList = new TreeList('#container'); + const dataRow = treeList.getDataRow(0); + const expandableCell = new ExpandableCell(dataRow.getDataCell(0)); + const searchBox = treeList.getSearchBox(); + + await t + .click(dataRow.getSelectCheckBox()) + .expect(dataRow.isSelected).ok(); + await t + .click(expandableCell.getExpandButton()) + .expect(expandableCell.isExpanded()).ok(); + await t.expect(await takeScreenshot('T1264312-selection-checked-all', treeList.element)).ok(); + + await t + .click(expandableCell.getCollapseButton()) + .typeText(searchBox.input, 'google') + .expect(expandableCell.isExpanded()).ok(); + await t.expect(await takeScreenshot('T1264312-selection-checked-searched', treeList.element)).ok(); + + await t + .click(dataRow.getSelectCheckBox()) + .expect(dataRow.isSelected).notOk(); + await t.expect(await takeScreenshot('T1264312-selection-unchecked-searched', treeList.element)).ok(); + + await t + .click(searchBox.getClearButton()) + .click(expandableCell.getExpandButton()) + .expect(expandableCell.isExpanded()).ok(); + await t.expect(await takeScreenshot('T1264312-selection-unchecked-all', treeList.element)).ok(); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async (t) => { + await t.addRequestHooks(tasksApiMock); + return createWidget('dxTreeList', () => ({ + dataSource: (window as any).DevExpress.data.AspNet.createStore({ + key: 'Task_ID', + loadUrl: 'https://api/data', + }), + selection: { mode: 'multiple', recursive: true, allowSelectAll: false }, + remoteOperations: { filtering: true, sorting: true, grouping: true }, + parentIdExpr: 'Task_Parent_ID', + hasItemsExpr: 'Has_Items', + searchPanel: { + visible: true, + }, + headerFilter: { + visible: true, + }, + showRowLines: true, + showBorders: true, + columnWidth: 180, + columns: [{ + dataField: 'Task_Subject', + width: 300, + }, { + dataField: 'Task_Assigned_Employee_ID', + caption: 'Assigned', + }, { + dataField: 'Task_Status', + caption: 'Status', + }, { + dataField: 'Task_Start_Date', + caption: 'Start Date', + dataType: 'date', + }, { + dataField: 'Task_Due_Date', + caption: 'Due Date', + dataType: 'date', + }, + ], + })); +}); diff --git a/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts b/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts index 0c5e512d1670..29c734c08309 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/selection/m_selection.ts @@ -260,6 +260,7 @@ const selection = (Base: ModuleType) => class SelectionCont private _updateSelectionStateCore(keys, isSelected) { const dataController = this._dataController; + this._selectionStateByKey = {}; for (let i = 0; i < keys.length; i++) { this._selectionStateByKey[keys[i]] = isSelected; // @ts-expect-error diff --git a/packages/testcafe-models/textBox.ts b/packages/testcafe-models/textBox.ts index 9879032b4f29..aafeaa6e7d99 100644 --- a/packages/testcafe-models/textBox.ts +++ b/packages/testcafe-models/textBox.ts @@ -6,6 +6,8 @@ const CLASS = { input: 'dx-texteditor-input', isInvalid: 'dx-invalid', label: 'dx-label', + buttonsContainer: 'dx-texteditor-buttons-container', + clearButton: 'dx-clear-button-area', }; export default class TextBox extends Widget { input: Selector; @@ -34,6 +36,10 @@ export default class TextBox extends Widget { return new ActionButton(this.element, index); } + getClearButton(): Selector { + return this.element.find(`.${CLASS.buttonsContainer}`).find(`.${CLASS.clearButton}`); + } + getLabel(): Selector { return this.element.find(`.${CLASS.label}`); } diff --git a/packages/testcafe-models/treeList/expandableCell.ts b/packages/testcafe-models/treeList/expandableCell.ts index ad1636fbff6e..cfbaa0e54bf6 100644 --- a/packages/testcafe-models/treeList/expandableCell.ts +++ b/packages/testcafe-models/treeList/expandableCell.ts @@ -2,7 +2,9 @@ import FocusableElement from "../internal/focusable"; import DataCell from "../dataGrid/data/cell"; const CLASS = { - expandButton: 'dx-treelist-icon-container', + actionContainer: 'dx-treelist-icon-container', + expandButton: 'dx-treelist-collapsed', + collapseButton: 'dx-treelist-expanded', }; export default class ExpandableCell extends FocusableElement { @@ -11,6 +13,14 @@ export default class ExpandableCell extends FocusableElement { } getExpandButton(): Selector { - return this.element.find(`.${CLASS.expandButton}`); + return this.element.find(`.${CLASS.actionContainer}`).find(`.${CLASS.expandButton}`); + } + + getCollapseButton(): Selector { + return this.element.find(`.${CLASS.actionContainer}`).find(`.${CLASS.collapseButton}`); + } + + isExpanded(): Promise { + return this.getCollapseButton().exists; } }