From 768a583d214b155afa16b93ab45e16b93032824f Mon Sep 17 00:00:00 2001 From: pharret31 Date: Thu, 5 Feb 2026 18:31:02 +0100 Subject: [PATCH 1/2] Add tests --- .../treeView.checkboxes.tests.js | 819 +++++++++++------- 1 file changed, 506 insertions(+), 313 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.checkboxes.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.checkboxes.tests.js index 71af6c4e30be..e2212c253218 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.checkboxes.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.checkboxes.tests.js @@ -22,150 +22,149 @@ const initTree = function(options) { return $('#treeView').dxTreeView(options); }; -QUnit.module('Checkboxes'); - -QUnit.test('Set intermediate state for parent if at least a one child is selected', function(assert) { - const data = $.extend(true, [], DATA[5]); - data[0].items[1].items[0].expanded = true; - data[0].items[1].items[1].expanded = true; - const $treeView = initTree({ - items: data, - showCheckBoxesMode: 'normal' - }); - - const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); - $(checkboxes[4]).trigger('dxclick'); - - assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), true); - assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), undefined); - assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); -}); +QUnit.module('Checkboxes', () => { + QUnit.test('Set intermediate state for parent if at least a one child is selected', function(assert) { + const data = $.extend(true, [], DATA[5]); + data[0].items[1].items[0].expanded = true; + data[0].items[1].items[1].expanded = true; + const $treeView = initTree({ + items: data, + showCheckBoxesMode: 'normal' + }); -QUnit.test('selectNodesRecursive = false', function(assert) { - const data = $.extend(true, [], DATA[5]); - data[0].items[1].items[0].expanded = true; - data[0].items[1].items[1].expanded = true; + const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); + $(checkboxes[4]).trigger('dxclick'); - const $treeView = initTree({ - items: data, - selectNodesRecursive: false, - showCheckBoxesMode: 'normal' + assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), true); + assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), undefined); + assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); }); - const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); - $(checkboxes[4]).trigger('dxclick'); + QUnit.test('selectNodesRecursive = false', function(assert) { + const data = $.extend(true, [], DATA[5]); + data[0].items[1].items[0].expanded = true; + data[0].items[1].items[1].expanded = true; - assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), true); - assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), false); -}); + const $treeView = initTree({ + items: data, + selectNodesRecursive: false, + showCheckBoxesMode: 'normal' + }); -QUnit.test('Remove intermediate state from parent if all children are unselected', function(assert) { - const data = $.extend(true, [], DATA[5]); - data[0].items[1].items[0].expanded = true; - data[0].items[1].items[1].expanded = true; + const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); + $(checkboxes[4]).trigger('dxclick'); - const $treeView = initTree({ - items: data, - showCheckBoxesMode: 'normal' + assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), true); + assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), false); }); - const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); - $(checkboxes[4]).trigger('dxclick'); - $(checkboxes[3]).trigger('dxclick'); - $(checkboxes[4]).trigger('dxclick'); - - assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), true); - assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), undefined); - assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); - - $(checkboxes[3]).trigger('dxclick'); - assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), false); -}); + QUnit.test('Remove intermediate state from parent if all children are unselected', function(assert) { + const data = $.extend(true, [], DATA[5]); + data[0].items[1].items[0].expanded = true; + data[0].items[1].items[1].expanded = true; -QUnit.test('Parent node should be selected if all children are selected', function(assert) { - const data = $.extend(true, [], DATA[5]); - data[0].items[1].items[0].expanded = true; - data[0].items[1].items[1].expanded = true; - const $treeView = initTree({ - items: data, - showCheckBoxesMode: 'normal' + const $treeView = initTree({ + items: data, + showCheckBoxesMode: 'normal' + }); + + const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); + $(checkboxes[4]).trigger('dxclick'); + $(checkboxes[3]).trigger('dxclick'); + $(checkboxes[4]).trigger('dxclick'); + + assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), true); + assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), undefined); + assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); + + $(checkboxes[3]).trigger('dxclick'); + assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), false); }); - const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); - $(checkboxes[4]).trigger('dxclick'); - $(checkboxes[3]).trigger('dxclick'); + QUnit.test('Parent node should be selected if all children are selected', function(assert) { + const data = $.extend(true, [], DATA[5]); + data[0].items[1].items[0].expanded = true; + data[0].items[1].items[1].expanded = true; + const $treeView = initTree({ + items: data, + showCheckBoxesMode: 'normal' + }); - assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), true); - assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), true); - assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), true); - assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); -}); + const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); + $(checkboxes[4]).trigger('dxclick'); + $(checkboxes[3]).trigger('dxclick'); -QUnit.test('All children should be selected/unselected after click on parent node', function(assert) { - const data = $.extend(true, [], DATA[5]); - data[0].items[1].items[0].expanded = true; - data[0].items[1].items[1].expanded = true; - const $treeView = initTree({ - items: data, - showCheckBoxesMode: 'normal' + assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), true); + assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), true); + assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), true); + assert.equal($(checkboxes[1]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); }); - const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); - - $(checkboxes[2]).trigger('dxclick'); + QUnit.test('All children should be selected/unselected after click on parent node', function(assert) { + const data = $.extend(true, [], DATA[5]); + data[0].items[1].items[0].expanded = true; + data[0].items[1].items[1].expanded = true; + const $treeView = initTree({ + items: data, + showCheckBoxesMode: 'normal' + }); - assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), true); - assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), true); - assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), true); + const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); - $(checkboxes[2]).trigger('dxclick'); + $(checkboxes[2]).trigger('dxclick'); - assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), false); - assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), false); -}); + assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), true); + assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), true); + assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), true); -QUnit.test('Regression: incorrect parent state', function(assert) { - const data = $.extend(true, [], data2); - data[2].expanded = true; + $(checkboxes[2]).trigger('dxclick'); - const $treeView = initTree({ - dataSource: data, - dataStructure: 'plain', - showCheckBoxesMode: 'normal' + assert.equal($(checkboxes[4]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[3]).dxCheckBox('instance').option('value'), false); + assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), false); }); - const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); + QUnit.test('Regression: incorrect parent state', function(assert) { + const data = $.extend(true, [], data2); + data[2].expanded = true; - $(checkboxes[3]).trigger('dxclick'); - $(checkboxes[4]).trigger('dxclick'); - $(checkboxes[5]).trigger('dxclick'); - $(checkboxes[6]).trigger('dxclick'); + const $treeView = initTree({ + dataSource: data, + dataStructure: 'plain', + showCheckBoxesMode: 'normal' + }); - assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), true); - assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); + const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); -}); + $(checkboxes[3]).trigger('dxclick'); + $(checkboxes[4]).trigger('dxclick'); + $(checkboxes[5]).trigger('dxclick'); + $(checkboxes[6]).trigger('dxclick'); -QUnit.test('T173381', function(assert) { - const $treeView = initTree({ - items: [ - { - id: 777, text: 'root', items: [ - { - id: 1, text: 'a', items: + assert.equal($(checkboxes[2]).dxCheckBox('instance').option('value'), true); + assert.equal($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); + + }); + + QUnit.test('T173381', function(assert) { + const $treeView = initTree({ + items: [ + { + id: 777, text: 'root', items: [ + { + id: 1, text: 'a', items: [ { id: 11, text: 'a.1', expanded: true, @@ -175,39 +174,39 @@ QUnit.test('T173381', function(assert) { ] }, { id: 12, text: 'a.2' }] - }, - { - id: 2, text: 'b', expanded: true, - items: [ - { id: 21, text: 'b.1' }, - { id: 22, text: 'b.2' } - ] - } - ] - } - ], - showCheckBoxesMode: 'normal' - }); - const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); + }, + { + id: 2, text: 'b', expanded: true, + items: [ + { id: 21, text: 'b.1' }, + { id: 22, text: 'b.2' } + ] + } + ] + } + ], + showCheckBoxesMode: 'normal' + }); + const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); - $(checkboxes[2]).trigger('dxclick'); - assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); + $(checkboxes[2]).trigger('dxclick'); + assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); - $(checkboxes[6]).trigger('dxclick'); - assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); + $(checkboxes[6]).trigger('dxclick'); + assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); - $(checkboxes[6]).trigger('dxclick'); - assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); -}); + $(checkboxes[6]).trigger('dxclick'); + assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); + }); -QUnit.test('T195986', function(assert) { - const $treeView = initTree({ - items: [ - { - id: 777, text: 'root', expanded: true, selected: true, - items: [ - { - id: 1, text: 'a', expanded: true, selected: true, items: + QUnit.test('T195986', function(assert) { + const $treeView = initTree({ + items: [ + { + id: 777, text: 'root', expanded: true, selected: true, + items: [ + { + id: 1, text: 'a', expanded: true, selected: true, items: [ { id: 11, text: 'a.1', expanded: true, selected: true, @@ -217,218 +216,412 @@ QUnit.test('T195986', function(assert) { ] } ] - } - ] - } - ], - showCheckBoxesMode: 'normal' + } + ] + } + ], + showCheckBoxesMode: 'normal' + }); + const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); + $(checkboxes[3]).trigger('dxclick'); + assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); + + $(checkboxes[3]).trigger('dxclick'); + assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), true); }); - const checkboxes = $treeView.find(`.${CHECKBOX_CLASS}`); - $(checkboxes[3]).trigger('dxclick'); - assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), undefined); - $(checkboxes[3]).trigger('dxclick'); - assert.strictEqual($(checkboxes[0]).dxCheckBox('instance').option('value'), true); -}); + const clickByItemCheckbox = (wrapper, item) => wrapper.getElement() + .find(`[aria-label="${item}"] .dx-checkbox`) + .eq(0).trigger('dxclick'); + + const clickBySelectAllCheckbox = (wrapper) => wrapper.getElement() + .find(`.${SELECT_ALL_CHECKBOX_CLASS}`) + .eq(0).trigger('dxclick'); + + ['none', 'normal', 'selectAll'].forEach((showCheckBoxesMode) => { + ['multiple', 'single'].forEach((selectionMode) => { + [false, true].forEach((selectNodesRecursive) => { + QUnit.test(`All deselected -> select middle level item. checkboxMode: ${showCheckBoxesMode}, selectionMode: ${selectionMode}, recursive: ${selectNodesRecursive} (T988753)`, function() { + const wrapper = new TreeViewTestWrapper({ + showCheckBoxesMode, selectNodesRecursive, selectionMode, + items: [ { text: 'item1', expanded: true, items: [ + { text: 'item1_1', expanded: true, items: [ { text: 'item1_1_1' } ] }, + { text: 'item1_2', expanded: true, items: [ { text: 'item1_2_1' } ] } ] + } ], + }); + + clickByItemCheckbox(wrapper, 'item1_1'); + + let expectedLog; + if(showCheckBoxesMode === 'none') { + expectedLog = []; + } else if(showCheckBoxesMode === 'normal' || selectionMode === 'single') { + expectedLog = ['itemSelectionChanged', 'selectionChanged']; + } else if(showCheckBoxesMode === 'selectAll') { + expectedLog = ['itemSelectionChanged', 'selectionChanged', 'selectAllValueChanged']; + } -const clickByItemCheckbox = (wrapper, item) => wrapper.getElement() - .find(`[aria-label="${item}"] .dx-checkbox`) - .eq(0).trigger('dxclick'); + wrapper.checkEventLog(expectedLog, 'after click by item1_1 checkbox'); + }); -const clickBySelectAllCheckbox = (wrapper) => wrapper.getElement() - .find(`.${SELECT_ALL_CHECKBOX_CLASS}`) - .eq(0).trigger('dxclick'); + QUnit.test(`All selected -> deselect middle level item. checkboxMode: ${showCheckBoxesMode}, selectionMode: ${selectionMode}, recursive: ${selectNodesRecursive} (T988753)`, function() { + const wrapper = new TreeViewTestWrapper({ + showCheckBoxesMode, selectNodesRecursive, selectionMode, + items: [ { text: 'item1', selected: true, expanded: true, items: [ + { text: 'item1_1', selected: true, expanded: true, items: [ { text: 'item1_1_1' } ] }, + { text: 'item1_2', selected: true, expanded: true, items: [ { text: 'item1_2_1' } ] } ] + } ], + }); + + clickByItemCheckbox(wrapper, 'item1_1'); + + let expectedLog; + if(showCheckBoxesMode === 'none') { + expectedLog = []; + } else if(selectionMode === 'single') { + expectedLog = ['itemSelectionChanged', 'itemSelectionChanged', 'selectionChanged']; + } else if(showCheckBoxesMode === 'normal') { + expectedLog = ['itemSelectionChanged', 'selectionChanged']; + } else if(showCheckBoxesMode === 'selectAll') { + expectedLog = selectNodesRecursive === false + ? ['itemSelectionChanged', 'selectionChanged'] + : ['itemSelectionChanged', 'selectionChanged', 'selectAllValueChanged']; + } -['none', 'normal', 'selectAll'].forEach((showCheckBoxesMode) => { - ['multiple', 'single'].forEach((selectionMode) => { - [false, true].forEach((selectNodesRecursive) => { - QUnit.test(`All deselected -> select middle level item. checkboxMode: ${showCheckBoxesMode}, selectionMode: ${selectionMode}, recursive: ${selectNodesRecursive} (T988753)`, function() { - const wrapper = new TreeViewTestWrapper({ - showCheckBoxesMode, selectNodesRecursive, selectionMode, - items: [ { text: 'item1', expanded: true, items: [ - { text: 'item1_1', expanded: true, items: [ { text: 'item1_1_1' } ] }, - { text: 'item1_2', expanded: true, items: [ { text: 'item1_2_1' } ] } ] - } ], + wrapper.checkEventLog(expectedLog, 'after click by item1_1 checkbox'); }); + }); + }); + }); - clickByItemCheckbox(wrapper, 'item1_1'); + QUnit.test('selectAll checkbox should have aria-label="Select All" attribute', function(assert) { + initTree({ + items: [ { text: 'item' } ], + showCheckBoxesMode: 'selectAll' + }); - let expectedLog; - if(showCheckBoxesMode === 'none') { - expectedLog = []; - } else if(showCheckBoxesMode === 'normal' || selectionMode === 'single') { - expectedLog = ['itemSelectionChanged', 'selectionChanged']; - } else if(showCheckBoxesMode === 'selectAll') { - expectedLog = ['itemSelectionChanged', 'selectionChanged', 'selectAllValueChanged']; - } + const $selectAllCheckbox = $(`.${SELECT_ALL_CHECKBOX_CLASS}`); - wrapper.checkEventLog(expectedLog, 'after click by item1_1 checkbox'); - }); + assert.strictEqual($selectAllCheckbox.attr('aria-label'), 'Select All'); + }); - QUnit.test(`All selected -> deselect middle level item. checkboxMode: ${showCheckBoxesMode}, selectionMode: ${selectionMode}, recursive: ${selectNodesRecursive} (T988753)`, function() { - const wrapper = new TreeViewTestWrapper({ - showCheckBoxesMode, selectNodesRecursive, selectionMode, - items: [ { text: 'item1', selected: true, expanded: true, items: [ - { text: 'item1_1', selected: true, expanded: true, items: [ { text: 'item1_1_1' } ] }, - { text: 'item1_2', selected: true, expanded: true, items: [ { text: 'item1_2_1' } ] } ] - } ], - }); + QUnit.test('checkbox should have aria-label="Check state" attribute', function(assert) { + initTree({ + items: [ { text: 'item' } ], + showCheckBoxesMode: 'normal' + }); - clickByItemCheckbox(wrapper, 'item1_1'); - - let expectedLog; - if(showCheckBoxesMode === 'none') { - expectedLog = []; - } else if(selectionMode === 'single') { - expectedLog = ['itemSelectionChanged', 'itemSelectionChanged', 'selectionChanged']; - } else if(showCheckBoxesMode === 'normal') { - expectedLog = ['itemSelectionChanged', 'selectionChanged']; - } else if(showCheckBoxesMode === 'selectAll') { - expectedLog = selectNodesRecursive === false - ? ['itemSelectionChanged', 'selectionChanged'] - : ['itemSelectionChanged', 'selectionChanged', 'selectAllValueChanged']; - } + const $checkbox = $(`.${CHECKBOX_CLASS}`); - wrapper.checkEventLog(expectedLog, 'after click by item1_1 checkbox'); - }); + assert.strictEqual($checkbox.attr('aria-label'), 'Check state'); + }); + + QUnit.test('checkbox should have a correct aria-label value based on localization (T1247518)', function(assert) { + const localizedCheckStateText = 'custom-select-all'; + localization.loadMessages({ 'en': { 'CheckState': localizedCheckStateText } }); + + initTree({ + items: [ { text: 'item' } ], + showCheckBoxesMode: 'normal' }); + + const $checkbox = $(`.${CHECKBOX_CLASS}`); + + assert.strictEqual($checkbox.attr('aria-label'), localizedCheckStateText, 'checkbox aria-label has correct localized value'); }); -}); -QUnit.test('selectAll checkbox should have aria-label="Select All" attribute', function(assert) { - initTree({ - items: [ { text: 'item' } ], - showCheckBoxesMode: 'selectAll' + QUnit.test('SelectAll checkBox should select all values on enter key when no items selected', function(assert) { + let selectAllValue = null; + const wrapper = new TreeViewTestWrapper({ + showCheckBoxesMode: 'selectAll', + focusStateEnabled: true, + items: [ { text: 'item1', items: [ { text: 'item1_1' }, { text: 'item1_2' } ] } ], + onSelectAllValueChanged: ({ value }) => { selectAllValue = value; } + }); + const $selectAll = wrapper.getElement().find(`.${SELECT_ALL_CHECKBOX_CLASS}`); + + $selectAll.trigger($.Event('keydown', { key: 'enter' })); + + assert.deepEqual(selectAllValue, true, 'all items selected'); }); - const $selectAllCheckbox = $(`.${SELECT_ALL_CHECKBOX_CLASS}`); + QUnit.test('SelectAll checkBox should delect all values on enter key when all items selected', function(assert) { + let selectAllValue = null; + const wrapper = new TreeViewTestWrapper({ + showCheckBoxesMode: 'selectAll', + focusStateEnabled: true, + items: [{ text: 'item1', selected: true, items: [ { text: 'item1_1' }, { text: 'item1_2' } ] + }], + onSelectAllValueChanged: ({ value }) => { selectAllValue = value; } + }); + const $selectAll = wrapper.getElement().find(`.${SELECT_ALL_CHECKBOX_CLASS}`); - assert.strictEqual($selectAllCheckbox.attr('aria-label'), 'Select All'); -}); + $selectAll.trigger($.Event('keydown', { key: 'enter' })); -QUnit.test('checkbox should have aria-label="Check state" attribute', function(assert) { - initTree({ - items: [ { text: 'item' } ], - showCheckBoxesMode: 'normal' + assert.deepEqual(selectAllValue, false, 'all items deselected'); }); - const $checkbox = $(`.${CHECKBOX_CLASS}`); + ['click', 'enter'].forEach((scenario) => { + [ + { + initialValue: undefined, + newValue: true, + items: [{ text: 'item1' }, { text: 'item2', selected: true }], + }, + { + initialValue: false, + newValue: true, + items: [{ text: 'item1' }, { text: 'item2' }], + }, + { + initialValue: true, + newValue: false, + items: [{ text: 'item1', selected: true }, { text: 'item2', selected: true }], + }, + ].forEach(({ initialValue, newValue, items }) => { + QUnit.test(`Select all checkbox should change value from ${initialValue} to ${newValue} on ${scenario}`, function(assert) { + const wrapper = new TreeViewTestWrapper({ + showCheckBoxesMode: 'selectAll', + focusStateEnabled: true, + items, + }); + const $selectAll = wrapper.getElement().find(`.${SELECT_ALL_CHECKBOX_CLASS}`); + const selectAll = $selectAll.dxCheckBox('instance'); - assert.strictEqual($checkbox.attr('aria-label'), 'Check state'); -}); + assert.strictEqual(selectAll.option('value'), initialValue, `initital value is ${initialValue}`); -QUnit.test('checkbox should have a correct aria-label value based on localization (T1247518)', function(assert) { - const localizedCheckStateText = 'custom-select-all'; - localization.loadMessages({ 'en': { 'CheckState': localizedCheckStateText } }); + if(scenario === 'click') { + $selectAll.trigger('dxclick'); + } else { + $selectAll.trigger($.Event('keydown', { key: 'enter' })); + } - initTree({ - items: [ { text: 'item' } ], - showCheckBoxesMode: 'normal' + assert.strictEqual(selectAll.option('value'), newValue, `new value is ${initialValue}`); + }); + }); }); - const $checkbox = $(`.${CHECKBOX_CLASS}`); - - assert.strictEqual($checkbox.attr('aria-label'), localizedCheckStateText, 'checkbox aria-label has correct localized value'); -}); + QUnit.test('SelectAll checkBox should have the same focusStateEnabled as treeView', function(assert) { + const wrapper = new TreeViewTestWrapper({ + showCheckBoxesMode: 'selectAll', + focusStateEnabled: false, + items: [{ text: 'item1' }], + }); + const selectAllCheckBox = wrapper.getElement().find(`.${SELECT_ALL_CHECKBOX_CLASS}`).dxCheckBox('instance'); -QUnit.test('SelectAll checkBox should select all values on enter key when no items selected', function(assert) { - let selectAllValue = null; - const wrapper = new TreeViewTestWrapper({ - showCheckBoxesMode: 'selectAll', - focusStateEnabled: true, - items: [ { text: 'item1', items: [ { text: 'item1_1' }, { text: 'item1_2' } ] } ], - onSelectAllValueChanged: ({ value }) => { selectAllValue = value; } + assert.deepEqual(selectAllCheckBox.option('focusStateEnabled'), false); }); - const $selectAll = wrapper.getElement().find(`.${SELECT_ALL_CHECKBOX_CLASS}`); - $selectAll.trigger($.Event('keydown', { key: 'enter' })); + QUnit.test('Check value of the selectAllValueChanged event (T988753)', function(assert) { + const selectAllValueChangedLog = []; + const wrapper = new TreeViewTestWrapper({ + showCheckBoxesMode: 'selectAll', + items: [ { text: 'item1', expanded: true, items: [ { text: 'item1_1' }, { text: 'item1_2' } ] } ], + onSelectAllValueChanged: (args) => { selectAllValueChangedLog.push(args.value); } + }); - assert.deepEqual(selectAllValue, true, 'all items selected'); -}); + clickByItemCheckbox(wrapper, 'item1_1'); + assert.deepEqual(selectAllValueChangedLog, [undefined], 'after click by item1_1'); + + clickByItemCheckbox(wrapper, 'item1_2'); + assert.deepEqual(selectAllValueChangedLog, [undefined, true], 'after click by item1_2'); -QUnit.test('SelectAll checkBox should delect all values on enter key when all items selected', function(assert) { - let selectAllValue = null; - const wrapper = new TreeViewTestWrapper({ - showCheckBoxesMode: 'selectAll', - focusStateEnabled: true, - items: [{ text: 'item1', selected: true, items: [ { text: 'item1_1' }, { text: 'item1_2' } ] - }], - onSelectAllValueChanged: ({ value }) => { selectAllValue = value; } + clickByItemCheckbox(wrapper, 'item1'); + assert.deepEqual(selectAllValueChangedLog, [undefined, true, false], 'after click by item1'); }); - const $selectAll = wrapper.getElement().find(`.${SELECT_ALL_CHECKBOX_CLASS}`); - $selectAll.trigger($.Event('keydown', { key: 'enter' })); + QUnit.module('allowDisabledNodeSelection', { + beforeEach: function(module) { + this.items = [{ + id: 1, + expanded: true, + items: [{ + id: 2, + expanded: true, + disabled: true, + items: [{ + id: 3, + selected: true, + }, { + id: 4, + }], + }], + }]; + }, + }, () => { + [ + { allowDisabledNodeSelection: true, ariaCheckedStates: ['mixed', 'mixed', 'true', 'false'] }, + { allowDisabledNodeSelection: false, ariaCheckedStates: ['false', 'false', 'true', 'false'] } + ].forEach((config) => { + QUnit.test(`initial selection state should be correct when allowDisabledNodeSelection: ${config.allowDisabledNodeSelection}`, function(assert) { + const treeView = initTree({ + items: this.items, + showCheckBoxesMode: 'normal', + allowDisabledNodeSelection: config.allowDisabledNodeSelection, + }).dxTreeView('instance'); + + const checkboxes = $(treeView.$element()).find(`.${CHECKBOX_CLASS}`); + + checkboxes.each((index, checkbox) => { + const ariaChecked = $(checkbox).attr('aria-checked'); + assert.strictEqual(ariaChecked, config.ariaCheckedStates[index], `checkbox ${index} has correct aria-checked state`); + }); + }); + }); - assert.deepEqual(selectAllValue, false, 'all items deselected'); -}); + QUnit.module('selectAll', { + beforeEach: function(module) { + this.selectedItems = [{ + id: 1, + selected: true, + expanded: true, + items: [{ + id: 2, + selected: true, + disabled: true, + expanded: true, + items: [{ + id: 3, + selected: true, + }], + }], + }]; + + this.unselectedItems = [{ + id: 1, + expanded: true, + items: [{ + id: 2, + disabled: true, + expanded: true, + items: [{ + id: 3, + }], + }], + }]; + }, + }, () => { + [ + { allowDisabledNodeSelection: true, ariaCheckedStates: ['true', 'true', 'true'] }, + { allowDisabledNodeSelection: false, ariaCheckedStates: ['true', 'false', 'true'] } + ].forEach((config) => { + QUnit.test(`selectAll should work correct when allowDisabledNodeSelection: ${config.allowDisabledNodeSelection}`, function(assert) { + const treeView = initTree({ + items: this.unselectedItems, + showCheckBoxesMode: 'normal', + allowDisabledNodeSelection: config.allowDisabledNodeSelection, + }).dxTreeView('instance'); + + treeView.selectAll(); + + const checkboxes = $(treeView.$element()).find(`.${CHECKBOX_CLASS}`); + + checkboxes.each((index, checkbox) => { + const ariaChecked = $(checkbox).attr('aria-checked'); + assert.strictEqual(ariaChecked, config.ariaCheckedStates[index], `checkbox ${index} has correct aria-checked state`); + }); + }); + }); -['click', 'enter'].forEach((scenario) => { - [ - { - initialValue: undefined, - newValue: true, - items: [{ text: 'item1' }, { text: 'item2', selected: true }], - }, - { - initialValue: false, - newValue: true, - items: [{ text: 'item1' }, { text: 'item2' }], - }, - { - initialValue: true, - newValue: false, - items: [{ text: 'item1', selected: true }, { text: 'item2', selected: true }], - }, - ].forEach(({ initialValue, newValue, items }) => { - QUnit.test(`Select all checkbox should change value from ${initialValue} to ${newValue} on ${scenario}`, function(assert) { - const wrapper = new TreeViewTestWrapper({ - showCheckBoxesMode: 'selectAll', - focusStateEnabled: true, - items, + [ + { allowDisabledNodeSelection: true, ariaCheckedStates: ['false', 'false', 'false'] }, + { allowDisabledNodeSelection: false, ariaCheckedStates: ['false', 'true', 'false'] } + ].forEach((config) => { + QUnit.test(`unselectAll should work correct when allowDisabledNodeSelection: ${config.allowDisabledNodeSelection}`, function(assert) { + const treeView = initTree({ + items: this.selectedItems, + showCheckBoxesMode: 'normal', + allowDisabledNodeSelection: config.allowDisabledNodeSelection, + }).dxTreeView('instance'); + + treeView.unselectAll(); + + const checkboxes = $(treeView.$element()).find(`.${CHECKBOX_CLASS}`); + + checkboxes.each((index, checkbox) => { + const ariaChecked = $(checkbox).attr('aria-checked'); + assert.strictEqual(ariaChecked, config.ariaCheckedStates[index], `checkbox ${index} has correct aria-checked state`); + }); + }); }); - const $selectAll = wrapper.getElement().find(`.${SELECT_ALL_CHECKBOX_CLASS}`); - const selectAll = $selectAll.dxCheckBox('instance'); - assert.strictEqual(selectAll.option('value'), initialValue, `initital value is ${initialValue}`); + // TODO: think about test from false to true for allowDisabledNodesSelection + QUnit.test('change option allowDisabledNodesSelection in runtime should change modes correctly', function(assert) { + const selectedStatesBefore = ['true', 'true', 'true']; + const unselectedStatesAfter = ['false', 'true', 'false']; - if(scenario === 'click') { - $selectAll.trigger('dxclick'); - } else { - $selectAll.trigger($.Event('keydown', { key: 'enter' })); - } + const treeView = initTree({ + items: this.unselectedItems, + showCheckBoxesMode: 'normal', + allowDisabledNodeSelection: true, + }).dxTreeView('instance'); - assert.strictEqual(selectAll.option('value'), newValue, `new value is ${initialValue}`); - }); - }); -}); + treeView.selectAll(); -QUnit.test('SelectAll checkBox should have the same focusStateEnabled as treeView', function(assert) { - const wrapper = new TreeViewTestWrapper({ - showCheckBoxesMode: 'selectAll', - focusStateEnabled: false, - items: [{ text: 'item1' }], - }); - const selectAllCheckBox = wrapper.getElement().find(`.${SELECT_ALL_CHECKBOX_CLASS}`).dxCheckBox('instance'); + const checkboxes = $(treeView.$element()).find(`.${CHECKBOX_CLASS}`); - assert.deepEqual(selectAllCheckBox.option('focusStateEnabled'), false); -}); + checkboxes.each((index, checkbox) => { + const ariaChecked = $(checkbox).attr('aria-checked'); + assert.strictEqual(ariaChecked, selectedStatesBefore[index], `checkbox ${index} has correct aria-checked state`); + }); -QUnit.test('Check value of the selectAllValueChanged event (T988753)', function(assert) { - const selectAllValueChangedLog = []; - const wrapper = new TreeViewTestWrapper({ - showCheckBoxesMode: 'selectAll', - items: [ { text: 'item1', expanded: true, items: [ { text: 'item1_1' }, { text: 'item1_2' } ] } ], - onSelectAllValueChanged: (args) => { selectAllValueChangedLog.push(args.value); } - }); + treeView.option('allowDisabledNodeSelection', false); - clickByItemCheckbox(wrapper, 'item1_1'); - assert.deepEqual(selectAllValueChangedLog, [undefined], 'after click by item1_1'); + treeView.unselectAll(); - clickByItemCheckbox(wrapper, 'item1_2'); - assert.deepEqual(selectAllValueChangedLog, [undefined, true], 'after click by item1_2'); + checkboxes.each((index, checkbox) => { + const ariaChecked = $(checkbox).attr('aria-checked'); + assert.strictEqual(ariaChecked, unselectedStatesAfter[index], `checkbox ${index} has correct aria-checked state`); + }); + }); + }); - clickByItemCheckbox(wrapper, 'item1'); - assert.deepEqual(selectAllValueChangedLog, [undefined, true, false], 'after click by item1'); + QUnit.module('recursive selection', () => { + [ + { allowDisabledNodeSelection: true, ariaCheckedStates: ['true', 'true', 'true', 'true'] }, + { allowDisabledNodeSelection: false, ariaCheckedStates: ['true', 'false', 'true', 'false'] } + ].forEach((config) => { + QUnit.test(`from parent to children when allowDisabledNodesSelection = ${config.allowDisabledNodeSelection}`, function(assert) { + const treeView = initTree({ + items: this.items, + showCheckBoxesMode: 'normal', + allowDisabledNodeSelection: config.allowDisabledNodeSelection, + }).dxTreeView('instance'); + + treeView.selectItem(1); + + const checkboxes = $(treeView.$element()).find(`.${CHECKBOX_CLASS}`); + + checkboxes.each((index, checkbox) => { + const ariaChecked = $(checkbox).attr('aria-checked'); + assert.strictEqual(ariaChecked, config.ariaCheckedStates[index], `checkbox ${index} has correct aria-checked state`); + }); + }); + }); + + [ + { allowDisabledNodeSelection: true, ariaCheckedStates: ['true', 'true', 'true', 'true'] }, + { allowDisabledNodeSelection: false, ariaCheckedStates: ['false', 'false', 'true', 'true'] } + ].forEach((config) => { + QUnit.test(`from children to parent when allowDisabledNodesSelection = ${config.allowDisabledNodeSelection}`, function(assert) { + const treeView = initTree({ + items: this.items, + showCheckBoxesMode: 'normal', + allowDisabledNodeSelection: config.allowDisabledNodeSelection, + }).dxTreeView('instance'); + + treeView.selectItem(4); + + const checkboxes = $(treeView.$element()).find(`.${CHECKBOX_CLASS}`); + + checkboxes.each((index, checkbox) => { + const ariaChecked = $(checkbox).attr('aria-checked'); + assert.strictEqual(ariaChecked, config.ariaCheckedStates[index], `checkbox ${index} has correct aria-checked state`); + }); + }); + }); + }); + }); }); QUnit.module('T988756', () => { From b9e0ac8a265729a6981c9c465ba007c809110a90 Mon Sep 17 00:00:00 2001 From: dmlvr Date: Fri, 6 Feb 2026 18:08:59 +0200 Subject: [PATCH 2/2] modified SelectAll logic --- .../hierarchical_collection/data_adapter.ts | 36 +++++++++++++++---- .../hierarchical_collection/data_converter.ts | 11 ++++++ .../__internal/ui/tree_view/tree_view.base.ts | 9 +++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts b/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts index 80d7039edbd1..aabc846bb30c 100644 --- a/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts +++ b/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts @@ -51,6 +51,8 @@ export interface DataAdapterOptions { onNodeChanged: (node: InternalNode) => void; searchMode: SearchMode; + + allowDisabledNodeSelection: boolean; } SearchBoxController.setEditorClass(TextBox); @@ -69,6 +71,7 @@ class DataAdapter { dataConverter: new HierarchicalDataConverter(), onNodeChanged: noop, sort: null, + allowDisabledNodeSelection: true, }; _selectedNodesKeys: ItemKey[] = []; @@ -162,8 +165,12 @@ class DataAdapter { return this.options.multipleSelection ? this.getData() : this.getFullData(); } - _isNodeVisible(node: InternalNode): boolean { - return node.internalFields.item.visible !== false; + _isNodeVisible(node?: InternalNode): boolean { + return node?.internalFields.item.visible !== false; + } + + _isNodeDisabled(node?: InternalNode): boolean { + return node?.internalFields.item.disabled === true; } _getByKey(data: (InternalNode | null)[], key: ItemKey): InternalNode | null { @@ -432,6 +439,10 @@ class DataAdapter { return this.options.dataConverter.getItemsCount(); } + _getDisabledItemsCount(): number { + return this.options.dataConverter.getDisabledItemsCount(); + } + getVisibleItemsCount(): number { return this.options.dataConverter.getVisibleItemsCount(); } @@ -509,7 +520,16 @@ class DataAdapter { : this._dataStructure; each(dataStructure, (_index: number, node: InternalNode) => { - if (node && this._isNodeVisible(node)) { + if (!this._isNodeVisible(node)) { + return; + } + + if (this.options.allowDisabledNodeSelection) { + this._setFieldState(node, SELECTED, state); + return; + } + + if (!this._isNodeDisabled(node)) { this._setFieldState(node, SELECTED, state); } }); @@ -522,10 +542,14 @@ class DataAdapter { } isAllSelected(): boolean | undefined { - if (this.getSelectedNodesKeys().length) { - return this.getSelectedNodesKeys().length === this.getVisibleItemsCount() ? true : undefined; + if (!this.getSelectedNodesKeys().length) { + return false; } - return false; + + const countedNodesAmount = this.getVisibleItemsCount() + - (!this.options.allowDisabledNodeSelection ? this._getDisabledItemsCount() : 0); + + return this.getSelectedNodesKeys().length === countedNodesAmount ? true : undefined; } toggleExpansion(key: ItemKey, state: boolean): void { diff --git a/packages/devextreme/js/__internal/ui/hierarchical_collection/data_converter.ts b/packages/devextreme/js/__internal/ui/hierarchical_collection/data_converter.ts index 396ff865186f..4f0f1c367662 100644 --- a/packages/devextreme/js/__internal/ui/hierarchical_collection/data_converter.ts +++ b/packages/devextreme/js/__internal/ui/hierarchical_collection/data_converter.ts @@ -72,6 +72,8 @@ class DataConverter { private _visibleItemsCount = 0; + private _disabledItemsCount = 0; + _indexByKey: Record = {}; private _dataAccessors!: DataAccessors; @@ -128,6 +130,10 @@ class DataConverter { this._visibleItemsCount += 1; } + if (item.disabled === true) { + this._disabledItemsCount += 1; + } + const { items, ...itemWithoutItems } = item; const node = { @@ -265,6 +271,10 @@ class DataConverter { return this._itemsCount; } + getDisabledItemsCount(): number { + return this._disabledItemsCount; + } + getVisibleItemsCount(): number { return this._visibleItemsCount; } @@ -302,6 +312,7 @@ class DataConverter { ): (InternalNode | null)[] { this._itemsCount = 0; this._visibleItemsCount = 0; + this._disabledItemsCount = 0; this._rootValue = rootValue; this._dataType = dataType; this._indexByKey = {}; diff --git a/packages/devextreme/js/__internal/ui/tree_view/tree_view.base.ts b/packages/devextreme/js/__internal/ui/tree_view/tree_view.base.ts index fd4680d6cbfd..500311730169 100644 --- a/packages/devextreme/js/__internal/ui/tree_view/tree_view.base.ts +++ b/packages/devextreme/js/__internal/ui/tree_view/tree_view.base.ts @@ -97,6 +97,8 @@ export interface TreeViewBaseProperties extends Properties, Omit< deferRendering?: boolean; _supportItemUrl?: boolean; + + allowDisabledNodeSelection?: boolean; } class TreeViewBase extends HierarchicalCollectionWidget { @@ -281,6 +283,7 @@ class TreeViewBase extends HierarchicalCollectionWidget