dimsum-e2e-tests 3.70.0-next.57 → 3.70.0-next.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/ds-autocomplete/DSAutocompleteCO.js +34 -1
- package/ds-autocomplete/aria-busy-loading/DSAutocomplete.aria-busy-loading.axe-core.func.spec.js +50 -0
- package/ds-autocomplete/aria-busy-loading/DSAutocomplete.aria-busy-loading.func.spec.js +122 -0
- package/ds-autocomplete/aria-busy-loading/DSAutocomplete.aria-busy-loading.visual.spec.js +48 -0
- package/ds-data-table-async/DSDataTableCO.js +124 -2
- package/ds-data-table-async/components/HeaderCO.js +16 -1
- package/ds-data-table-async/dnd/DSDataTable.dnd-keyboard-scroll.func.spec.js +51 -0
- package/ds-data-table-async/filter-menu-close/DSDataTable.filter-menu-close.func.spec.js +57 -19
- package/ds-data-table-async/filter-popover-boundary/DSDataTable.filter-popover-boundary.func.spec.js +98 -0
- package/ds-data-table-async/filters/DSDataTable.filters-creatable-active-descendant.func.spec.js +51 -0
- package/ds-data-table-async/filters/DSDataTable.filters-creatable.func.spec.js +39 -0
- package/ds-data-table-async/filters/DSDataTable.filters.axe-core.func.spec.js +11 -9
- package/ds-data-table-async/helpers/getInputSelection.js +6 -0
- package/ds-data-table-async/helpers/index.js +1 -0
- package/ds-data-table-async/resizable/DSDataTable.resizerA11y.axe-core.func.spec.js +18 -21
- package/ds-data-table-async/resizable/DSDataTable.resizerA11y.func.spec.js +35 -65
- package/ds-hooks/useFocusTrap/UseFocusTrapCO.js +2 -0
- package/ds-hooks/useFocusTrap/UseFocusTrapToggle.func.spec.js +48 -0
- package/ds-slider-v2/DSSliderV2.func.spec.js +36 -25
- package/ds-slider-v2/DSSliderV2CO.js +10 -0
- package/ds-slider-v2/min-range/DSSliderV2.min-range.func.spec.js +69 -0
- package/package.json +149 -149
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## 3.70.0-next.59 (2026-08-12)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- dimsum:: analysis of size for date-time-input and data-table [PUI-18345](https://jira.elliemae.io/browse/PUI-18345) ([#8285](https://git.elliemae.io/platform-ui/dimsum/issues/8285)) ([70cd13f](https://git.elliemae.io/platform-ui/dimsum/commit/70cd13f8e4f928805fdd8ec7dff1f0042d33abdb))
|
|
11
|
+
|
|
12
|
+
## [3.70.0-next.58](https://git.elliemae.io/platform-ui/dimsum/compare/v3.70.0-next.57...v3.70.0-next.58) (2026-08-11)
|
|
13
|
+
|
|
14
|
+
**Note:** Version bump only for package dimsum-e2e-tests
|
|
15
|
+
|
|
6
16
|
## [3.70.0-next.57](https://git.elliemae.io/platform-ui/dimsum/compare/v3.70.0-next.56...v3.70.0-next.57) (2026-08-11)
|
|
7
17
|
|
|
8
18
|
**Note:** Version bump only for package dimsum-e2e-tests
|
|
@@ -14,6 +14,14 @@ export default class DSAutocompleteCO extends PageObject {
|
|
|
14
14
|
|
|
15
15
|
static floatingAfterFiltering = new Urlbuilder(PATH_E2E_AUTOCOMPLETE, 'floating-after-filtering');
|
|
16
16
|
|
|
17
|
+
static loadingTest = new Urlbuilder(PATH_E2E_AUTOCOMPLETE, 'loading-test');
|
|
18
|
+
|
|
19
|
+
static skeletonTest = new Urlbuilder(PATH_E2E_AUTOCOMPLETE, 'skeleton-test');
|
|
20
|
+
|
|
21
|
+
static loadingInputProps = new Urlbuilder(PATH_E2E_AUTOCOMPLETE, 'loading-input-props-test');
|
|
22
|
+
|
|
23
|
+
static skeletonInputProps = new Urlbuilder(PATH_E2E_AUTOCOMPLETE, 'skeleton-input-props-test');
|
|
24
|
+
|
|
17
25
|
// Snapshots
|
|
18
26
|
static snapshotPath(example = 'basic') {
|
|
19
27
|
return PageObject.getSnapshotPathBuilder('Autocomplete', example);
|
|
@@ -37,8 +45,10 @@ export default class DSAutocompleteCO extends PageObject {
|
|
|
37
45
|
return $$('[data-dimsum-slot="dsAutocompleteContainer"]')[index];
|
|
38
46
|
}
|
|
39
47
|
|
|
48
|
+
// the option is a nested slot: the rendered element owns dsMenuitemMenuItemWrapper and carries the
|
|
49
|
+
// autocomplete slot on data-dimsum-parent-slot, which is where the custom attributes land
|
|
40
50
|
static async getOptionSlot(index = 0) {
|
|
41
|
-
return $$('[data-dimsum-slot="dsAutocompleteOption"]')[index];
|
|
51
|
+
return $$('[data-dimsum-parent-slot="dsAutocompleteOption"]')[index];
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
static async getInputSlot(index = 0) {
|
|
@@ -48,4 +58,27 @@ export default class DSAutocompleteCO extends PageObject {
|
|
|
48
58
|
static async getMenulistSlot(index = 0) {
|
|
49
59
|
return $$('[data-dimsum-slot="dsAutocompleteMenuList"]')[index];
|
|
50
60
|
}
|
|
61
|
+
|
|
62
|
+
// loading / skeleton (busy listbox) states
|
|
63
|
+
static async getLoadingListbox() {
|
|
64
|
+
return $('[data-testid="autocomplete-loading"]');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static async getLoadingIndicator() {
|
|
68
|
+
return $('[data-testid="ds-autocomplete-loading-indicator"]');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static async getSkeletonListbox() {
|
|
72
|
+
return $('[data-testid="autocomplete-skeleton"]');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static async getSkeletonMenuItems() {
|
|
76
|
+
return $$('[data-testid="ds-autocomplete-skeleton-menu-item"]');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// the input Autocomplete renders itself (onValueChange / inputProps API); the deprecated
|
|
80
|
+
// children API renders a consumer-owned DSInputText instead, which carries no autocomplete testid
|
|
81
|
+
static async getAutocompleteInput() {
|
|
82
|
+
return $('[data-testid="ds-autocomplete-input"]');
|
|
83
|
+
}
|
|
51
84
|
}
|
package/ds-autocomplete/aria-busy-loading/DSAutocomplete.aria-busy-loading.axe-core.func.spec.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Key } from 'webdriverio';
|
|
2
|
+
import DSAutocompleteCO from '../DSAutocompleteCO';
|
|
3
|
+
import { axeCoreCheck, type } from '../../helpers';
|
|
4
|
+
|
|
5
|
+
// Coverage for the loading state added in PUI-17137 ([Autocomplete] Add Support for Loading State).
|
|
6
|
+
// The busy listbox surfaces are the riskiest part for axe: role="listbox" normally requires option
|
|
7
|
+
// children (satisfied via aria-busy) and the spinner's role="status" is a forbidden listbox child
|
|
8
|
+
// (removed from the accessibility tree via aria-hidden). This spec guards both.
|
|
9
|
+
if (
|
|
10
|
+
(!browser.capabilities['ice:options'].isPhone &&
|
|
11
|
+
!browser.capabilities['ice:options'].isTablet &&
|
|
12
|
+
browser.capabilities.browserName === 'chrome') ||
|
|
13
|
+
browser.capabilities.browserName === 'Chrome'
|
|
14
|
+
) {
|
|
15
|
+
describe('PUI-18864 - DSAutocomplete:: busy listbox while isLoading -Axe-Core', () => {
|
|
16
|
+
before('loading page', async () => {
|
|
17
|
+
const errorOnGo = await DSAutocompleteCO.loadingTest.go();
|
|
18
|
+
if (errorOnGo) throw errorOnGo;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('01: should have no accessibility violations while the loading spinner is shown', async () => {
|
|
22
|
+
await browser.keys(Key.Tab);
|
|
23
|
+
await type('4');
|
|
24
|
+
|
|
25
|
+
const loading = await DSAutocompleteCO.getLoadingListbox();
|
|
26
|
+
await loading.waitForDisplayed();
|
|
27
|
+
|
|
28
|
+
const result = await axeCoreCheck();
|
|
29
|
+
expect(result.length).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('PUI-18865 - DSAutocomplete:: busy listbox while isSkeleton -Axe-Core', () => {
|
|
34
|
+
before('loading page', async () => {
|
|
35
|
+
const errorOnGo = await DSAutocompleteCO.skeletonTest.go();
|
|
36
|
+
if (errorOnGo) throw errorOnGo;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('01: should have no accessibility violations while the skeleton menu is shown', async () => {
|
|
40
|
+
await browser.keys(Key.Tab);
|
|
41
|
+
await type('4');
|
|
42
|
+
|
|
43
|
+
const skeleton = await DSAutocompleteCO.getSkeletonListbox();
|
|
44
|
+
await skeleton.waitForDisplayed();
|
|
45
|
+
|
|
46
|
+
const result = await axeCoreCheck();
|
|
47
|
+
expect(result.length).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Key } from 'webdriverio';
|
|
2
|
+
import DSAutocompleteCO from '../DSAutocompleteCO';
|
|
3
|
+
import { type } from '../../helpers';
|
|
4
|
+
|
|
5
|
+
// Coverage for the loading state added in PUI-17137 ([Autocomplete] Add Support for Loading State).
|
|
6
|
+
// While options are being fetched the options array is still empty, so LoadingContainer /
|
|
7
|
+
// SkeletonContainer keep the popover open on their own and carry the canonical listbox id +
|
|
8
|
+
// role="listbox" + aria-busy + aria-label. Only one of skeleton / loading / options list is ever
|
|
9
|
+
// mounted at a time, so the listbox id is never duplicated in the DOM.
|
|
10
|
+
//
|
|
11
|
+
// The last two describes exercise the `onValueChange` + `inputProps` API, which is the only path
|
|
12
|
+
// where Autocomplete renders its own input and therefore wires role="combobox" + aria-expanded +
|
|
13
|
+
// aria-controls onto it. The deprecated `children` API (first two describes) renders a
|
|
14
|
+
// consumer-owned input that receives none of those attributes.
|
|
15
|
+
//
|
|
16
|
+
// Pure DOM-attribute assertions -> Chrome-only guard.
|
|
17
|
+
if (
|
|
18
|
+
(!browser.capabilities['ice:options'].isPhone &&
|
|
19
|
+
!browser.capabilities['ice:options'].isTablet &&
|
|
20
|
+
browser.capabilities.browserName === 'chrome') ||
|
|
21
|
+
browser.capabilities.browserName === 'Chrome'
|
|
22
|
+
) {
|
|
23
|
+
describe('PUI-18860 - DSAutocomplete:: busy listbox while isLoading -Func', () => {
|
|
24
|
+
before('loading page', async () => {
|
|
25
|
+
const errorOnGo = await DSAutocompleteCO.loadingTest.go();
|
|
26
|
+
if (errorOnGo) throw errorOnGo;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('01: should render a busy loading listbox and hide the options list', async () => {
|
|
30
|
+
await browser.keys(Key.Tab);
|
|
31
|
+
await type('4');
|
|
32
|
+
|
|
33
|
+
const loading = await DSAutocompleteCO.getLoadingListbox();
|
|
34
|
+
await loading.waitForDisplayed();
|
|
35
|
+
|
|
36
|
+
await expect(loading).toHaveAttribute('role', 'listbox');
|
|
37
|
+
await expect(loading).toHaveAttribute('aria-busy', 'true');
|
|
38
|
+
await expect(loading).toHaveAttribute('aria-label', 'Loading options please wait');
|
|
39
|
+
|
|
40
|
+
// the spinner exposes role="status", a forbidden child of role="listbox", so it must be
|
|
41
|
+
// removed from the accessibility tree while staying visible
|
|
42
|
+
const indicator = await DSAutocompleteCO.getLoadingIndicator();
|
|
43
|
+
await expect(indicator).toBeDisplayedInViewport();
|
|
44
|
+
await expect(indicator).toHaveAttribute('aria-hidden', 'true');
|
|
45
|
+
|
|
46
|
+
// only one of loading / skeleton / options list is mounted at a time
|
|
47
|
+
const menulist = await DSAutocompleteCO.getAutocompleteMenuList();
|
|
48
|
+
await expect(menulist).not.toBeExisting();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('PUI-18861 - DSAutocomplete:: busy listbox while isSkeleton -Func', () => {
|
|
53
|
+
before('loading page', async () => {
|
|
54
|
+
const errorOnGo = await DSAutocompleteCO.skeletonTest.go();
|
|
55
|
+
if (errorOnGo) throw errorOnGo;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('01: should render a busy skeleton listbox with five presentational items', async () => {
|
|
59
|
+
await browser.keys(Key.Tab);
|
|
60
|
+
await type('4');
|
|
61
|
+
|
|
62
|
+
const skeleton = await DSAutocompleteCO.getSkeletonListbox();
|
|
63
|
+
await skeleton.waitForDisplayed();
|
|
64
|
+
|
|
65
|
+
await expect(skeleton).toHaveAttribute('role', 'listbox');
|
|
66
|
+
await expect(skeleton).toHaveAttribute('aria-busy', 'true');
|
|
67
|
+
await expect(skeleton).toHaveAttribute('aria-label', 'Loading options please wait');
|
|
68
|
+
|
|
69
|
+
const skeletonItems = await DSAutocompleteCO.getSkeletonMenuItems();
|
|
70
|
+
await expect(skeletonItems).toHaveLength(5);
|
|
71
|
+
// the count above is what pins SKELETON_AMOUNT; every item is rendered from the same JSX
|
|
72
|
+
// literal, so one sample is enough to prove role="presentation" reaches the DOM and keeps
|
|
73
|
+
// the busy listbox free of fake options
|
|
74
|
+
await expect(skeletonItems[2]).toHaveAttribute('role', 'presentation');
|
|
75
|
+
|
|
76
|
+
const menulist = await DSAutocompleteCO.getAutocompleteMenuList();
|
|
77
|
+
await expect(menulist).not.toBeExisting();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('PUI-18862 - DSAutocomplete:: input aria-controls linkage while isLoading -Func', () => {
|
|
82
|
+
before('loading page', async () => {
|
|
83
|
+
const errorOnGo = await DSAutocompleteCO.loadingInputProps.go();
|
|
84
|
+
if (errorOnGo) throw errorOnGo;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('01: should point the input aria-controls at the busy loading listbox', async () => {
|
|
88
|
+
await browser.keys(Key.Tab);
|
|
89
|
+
await type('4');
|
|
90
|
+
|
|
91
|
+
const loading = await DSAutocompleteCO.getLoadingListbox();
|
|
92
|
+
await loading.waitForDisplayed();
|
|
93
|
+
const loadingId = await loading.getAttribute('id');
|
|
94
|
+
|
|
95
|
+
const input = await DSAutocompleteCO.getAutocompleteInput();
|
|
96
|
+
await expect(input).toHaveAttribute('role', 'combobox');
|
|
97
|
+
await expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
98
|
+
await expect(input).toHaveAttribute('aria-controls', loadingId);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('PUI-18863 - DSAutocomplete:: input aria-controls linkage while isSkeleton -Func', () => {
|
|
103
|
+
before('loading page', async () => {
|
|
104
|
+
const errorOnGo = await DSAutocompleteCO.skeletonInputProps.go();
|
|
105
|
+
if (errorOnGo) throw errorOnGo;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('01: should point the input aria-controls at the busy skeleton listbox', async () => {
|
|
109
|
+
await browser.keys(Key.Tab);
|
|
110
|
+
await type('4');
|
|
111
|
+
|
|
112
|
+
const skeleton = await DSAutocompleteCO.getSkeletonListbox();
|
|
113
|
+
await skeleton.waitForDisplayed();
|
|
114
|
+
const skeletonId = await skeleton.getAttribute('id');
|
|
115
|
+
|
|
116
|
+
const input = await DSAutocompleteCO.getAutocompleteInput();
|
|
117
|
+
await expect(input).toHaveAttribute('role', 'combobox');
|
|
118
|
+
await expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
119
|
+
await expect(input).toHaveAttribute('aria-controls', skeletonId);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Key } from 'webdriverio';
|
|
2
|
+
import DSAutocompleteCO from '../DSAutocompleteCO';
|
|
3
|
+
import { type } from '../../helpers';
|
|
4
|
+
|
|
5
|
+
// Coverage for the loading state added in PUI-17137 ([Autocomplete] Add Support for Loading State).
|
|
6
|
+
// The acceptance criteria is the loading example from the InputText spec, so these snapshots pin the
|
|
7
|
+
// two menu appearances: the centered indeterminate spinner and the five skeleton menu items.
|
|
8
|
+
// Percy freezes CSS animations, so both the spinner rotation and the skeleton shimmer are captured
|
|
9
|
+
// deterministically.
|
|
10
|
+
if (!browser.capabilities['ice:options'].isPhone && !browser.capabilities['ice:options'].isTablet) {
|
|
11
|
+
describe('PUI-18866 - DSAutocomplete:: loading spinner menu -Visual', () => {
|
|
12
|
+
before('loading page', async () => {
|
|
13
|
+
const errorOnGo = await DSAutocompleteCO.loadingTest.go();
|
|
14
|
+
if (errorOnGo) throw errorOnGo;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('01: should display the loading spinner centered in the menu', async () => {
|
|
18
|
+
await browser.keys(Key.Tab);
|
|
19
|
+
await type('4');
|
|
20
|
+
|
|
21
|
+
const loading = await DSAutocompleteCO.getLoadingListbox();
|
|
22
|
+
await loading.waitForDisplayed();
|
|
23
|
+
|
|
24
|
+
const snapshot = await browser.percyCheckScreenshot(
|
|
25
|
+
DSAutocompleteCO.snapshotPath('autocomplete-loading-spinner-menu'),
|
|
26
|
+
);
|
|
27
|
+
await expect(snapshot).toEqual(0);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('PUI-18867 - DSAutocomplete:: skeleton menu items -Visual', () => {
|
|
32
|
+
before('loading page', async () => {
|
|
33
|
+
const errorOnGo = await DSAutocompleteCO.skeletonTest.go();
|
|
34
|
+
if (errorOnGo) throw errorOnGo;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('01: should display five skeleton menu items in the menu', async () => {
|
|
38
|
+
await browser.keys(Key.Tab);
|
|
39
|
+
await type('4');
|
|
40
|
+
|
|
41
|
+
const skeleton = await DSAutocompleteCO.getSkeletonListbox();
|
|
42
|
+
await skeleton.waitForDisplayed();
|
|
43
|
+
|
|
44
|
+
const snapshot = await browser.percyCheckScreenshot(DSAutocompleteCO.snapshotPath('autocomplete-skeleton-menu'));
|
|
45
|
+
await expect(snapshot).toEqual(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -33,6 +33,8 @@ export default class DataTableCO extends PageObject {
|
|
|
33
33
|
|
|
34
34
|
static DndColumnsFixedWidth = new Urlbuilder(PATH_E2E_DATATABLE_DND, 'dnd-columns-fixed-width');
|
|
35
35
|
|
|
36
|
+
static keyboardDragScroll = new Urlbuilder(PATH_E2E_DATATABLE_DND, 'keyboard-drag-scroll-test');
|
|
37
|
+
|
|
36
38
|
// Filters
|
|
37
39
|
|
|
38
40
|
static singleSelect = new Urlbuilder(PATH_E2E_DATATABLE_FILTERS, 'single-select-filter');
|
|
@@ -200,10 +202,10 @@ export default class DataTableCO extends PageObject {
|
|
|
200
202
|
'grouping-with-hidden-and-resizable-columns',
|
|
201
203
|
);
|
|
202
204
|
|
|
205
|
+
// PUI-18812 removed the resizerA11yMode prop, so there is one resizer shape and one story. The
|
|
206
|
+
// "SeparatorMode" name is historical — kept to avoid churning this URL and every spec using it.
|
|
203
207
|
static resizerSeparatorMode = new Urlbuilder(PATH_E2E_DATATABLE_COLUMN, 'resizer-separator-mode-test');
|
|
204
208
|
|
|
205
|
-
static resizerSliderMode = new Urlbuilder(PATH_E2E_DATATABLE_COLUMN, 'resizer-slider-mode-test');
|
|
206
|
-
|
|
207
209
|
// Text Wrapping
|
|
208
210
|
|
|
209
211
|
static textWrapTruncColumn = new Urlbuilder(PATH_E2E_DATATABLE_TEXT_WRAPPING, 'text-wrap-truncate-column');
|
|
@@ -470,6 +472,126 @@ export default class DataTableCO extends PageObject {
|
|
|
470
472
|
};
|
|
471
473
|
});
|
|
472
474
|
|
|
475
|
+
// PUI-15735 — creatable filter aria-activedescendant validity
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Returns the combobox input's aria-activedescendant together with the ids of every option
|
|
479
|
+
* currently rendered in the listbox, in one round-trip. Valid states are an empty
|
|
480
|
+
* aria-activedescendant or one whose value appears in optionIds.
|
|
481
|
+
*/
|
|
482
|
+
static getFilterActiveDescendantState = async () =>
|
|
483
|
+
browser.execute(() => {
|
|
484
|
+
const input = document.querySelector('[data-testid="combobox-input"]');
|
|
485
|
+
const options = Array.from(document.querySelectorAll('[data-testid="combobox-option"]'));
|
|
486
|
+
return {
|
|
487
|
+
activeDescendant: input ? input.getAttribute('aria-activedescendant') : null,
|
|
488
|
+
optionIds: options.map((option) => option.id),
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// PUI-8826 — keyboard row drag must not smooth-scroll
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Patches the DOM scroll APIs so every programmatic scroll from this point on records its
|
|
496
|
+
* `behavior`. dnd-kit calls scrollTo/scrollBy directly on the scroll container's DOM node rather
|
|
497
|
+
* than through React, so patching the shared prototype captures every call the drag makes.
|
|
498
|
+
* Install after the story is loaded and before the drag starts.
|
|
499
|
+
*/
|
|
500
|
+
static installScrollSpy = async () =>
|
|
501
|
+
browser.execute(() => {
|
|
502
|
+
window.dimsumScrollCalls = [];
|
|
503
|
+
['scrollTo', 'scrollBy'].forEach((methodName) => {
|
|
504
|
+
const original = Element.prototype[methodName];
|
|
505
|
+
// A function expression, not an arrow, so `this` stays the scrolled element.
|
|
506
|
+
Element.prototype[methodName] = function scrollSpy(...args) {
|
|
507
|
+
const [options] = args;
|
|
508
|
+
if (options && typeof options === 'object') {
|
|
509
|
+
window.dimsumScrollCalls.push({ method: methodName, behavior: options.behavior });
|
|
510
|
+
}
|
|
511
|
+
return original.apply(this, args);
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
static getScrollSpyCalls = async () => browser.execute(() => window.dimsumScrollCalls || []);
|
|
517
|
+
|
|
518
|
+
/** Scroll offset of the virtualized viewport — proves the drag actually scrolled the list. */
|
|
519
|
+
static getScrollableContainerScrollTop = async () =>
|
|
520
|
+
browser.execute(() => {
|
|
521
|
+
const container = document.querySelector('[data-testid="data-table-scrollable-container"]');
|
|
522
|
+
return container ? container.scrollTop : null;
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
/** Id-cell text of whichever row currently owns focus — the dragged row after a keyboard drop. */
|
|
526
|
+
static getFocusedRowIdCellText = async () =>
|
|
527
|
+
browser.execute(() => {
|
|
528
|
+
const active = document.activeElement;
|
|
529
|
+
const row = active && active.closest ? active.closest('[data-testid="data-table-row"]') : null;
|
|
530
|
+
const cells = row ? row.querySelectorAll('[data-testid="data-table-cell"]') : [];
|
|
531
|
+
// Cell 0 is the drag-handle cell; the id column follows it.
|
|
532
|
+
return cells.length > 1 ? cells[1].textContent.trim() : null;
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// PUI-17187 — filter popover clamped to the table's own scrollport during horizontal scroll
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Horizontal-scroll geometry of one column, read live instead of hardcoded. Returns the header
|
|
539
|
+
* cell's offset range inside the scrolled content plus the scrollport's own width, which is what
|
|
540
|
+
* the two scroll targets are derived from: colLeft - clientWidth + a sliver still leaves the
|
|
541
|
+
* column partially visible at the left edge, while colRight puts it fully out of view. Fixed-layout
|
|
542
|
+
* column widths shift with viewport and theme, so a hardcoded scrollLeft would be silently wrong
|
|
543
|
+
* on another browser or breakpoint.
|
|
544
|
+
*/
|
|
545
|
+
static getColumnScrollGeometry = async (columnName) =>
|
|
546
|
+
browser.execute((name) => {
|
|
547
|
+
const container = document.querySelector('[data-testid="data-table-scrollable-container"]');
|
|
548
|
+
const headers = Array.from(document.querySelectorAll('[data-testid="data-table-header"]'));
|
|
549
|
+
const target = headers.find((header) => (header.textContent || '').toLowerCase().includes(name.toLowerCase()));
|
|
550
|
+
if (!container || !target) return null;
|
|
551
|
+
// Derived from bounding rects plus the CURRENT scrollLeft, not offsetLeft/offsetWidth:
|
|
552
|
+
// offsetLeft is measured against the nearest positioned ancestor, which is not guaranteed to
|
|
553
|
+
// be the scrolled content, so it silently yields the wrong scroll target. This form is exact
|
|
554
|
+
// regardless of which ancestor happens to be positioned.
|
|
555
|
+
const targetRect = target.getBoundingClientRect();
|
|
556
|
+
const containerRect = container.getBoundingClientRect();
|
|
557
|
+
const colLeft = targetRect.left - containerRect.left + container.scrollLeft;
|
|
558
|
+
return {
|
|
559
|
+
colLeft,
|
|
560
|
+
colRight: colLeft + targetRect.width,
|
|
561
|
+
clientWidth: container.clientWidth,
|
|
562
|
+
maxScrollLeft: container.scrollWidth - container.clientWidth,
|
|
563
|
+
};
|
|
564
|
+
}, columnName);
|
|
565
|
+
|
|
566
|
+
static setScrollableContainerScrollLeft = async (value) =>
|
|
567
|
+
browser.execute((next) => {
|
|
568
|
+
const container = document.querySelector('[data-testid="data-table-scrollable-container"]');
|
|
569
|
+
if (container) container.scrollLeft = next;
|
|
570
|
+
}, value);
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Popover and scrollport horizontal bounds in one round-trip, so the two rects are always sampled
|
|
574
|
+
* from the same frame — reading them separately can straddle a reposition and compare a stale
|
|
575
|
+
* popover x against a fresh container x.
|
|
576
|
+
*/
|
|
577
|
+
static getFilterPopoverBoundaryRects = async () =>
|
|
578
|
+
browser.execute(() => {
|
|
579
|
+
const popover = document.querySelector('[data-testid="data-table-filter-menu-content"]');
|
|
580
|
+
const container = document.querySelector('[data-testid="data-table-scrollable-container"]');
|
|
581
|
+
if (!popover || !container) return null;
|
|
582
|
+
const popoverRect = popover.getBoundingClientRect();
|
|
583
|
+
const containerRect = container.getBoundingClientRect();
|
|
584
|
+
const styles = window.getComputedStyle(popover);
|
|
585
|
+
return {
|
|
586
|
+
popoverLeft: popoverRect.left,
|
|
587
|
+
popoverRight: popoverRect.right,
|
|
588
|
+
containerLeft: containerRect.left,
|
|
589
|
+
containerRight: containerRect.right,
|
|
590
|
+
opacity: styles.opacity,
|
|
591
|
+
pointerEvents: styles.pointerEvents,
|
|
592
|
+
};
|
|
593
|
+
});
|
|
594
|
+
|
|
473
595
|
// Snapshots
|
|
474
596
|
static snapshotPath(example = 'basic') {
|
|
475
597
|
return PageObject.getSnapshotPathBuilder('DataTable', example, 'ds-data-table');
|
|
@@ -83,12 +83,27 @@ export default class HeaderCO extends PageObject {
|
|
|
83
83
|
// as the getResizeHandler selector fix (separate PR); kept distinct so these a11y specs run standalone.
|
|
84
84
|
static getResizer = async (index) => $$('[data-testid="ds-datatable-resizer"]')[index];
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Text of the node the resizer's aria-labelledby points at. This is a WIRING check only — it
|
|
88
|
+
* proves the reference resolves to the intended span, NOT that the browser computes a name from
|
|
89
|
+
* it. Use getResizerComputedName for that. (The two can disagree: the name span is display:none,
|
|
90
|
+
* and whether a hidden aria-labelledby target contributes to the computed name is exactly the
|
|
91
|
+
* thing this must not assume.)
|
|
92
|
+
*/
|
|
93
|
+
static getResizerLabelledByText = async (resizer) => {
|
|
87
94
|
const labelledById = await resizer.getAttribute('aria-labelledby');
|
|
88
95
|
if (!labelledById) return null;
|
|
89
96
|
return (await $(`#${labelledById}`)).getProperty('textContent');
|
|
90
97
|
};
|
|
91
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The accessible name the BROWSER computes for the resizer, via the W3C WebDriver
|
|
101
|
+
* "Get Computed Label" endpoint — the accname algorithm's real output, not a re-implementation.
|
|
102
|
+
* Only meaningful while the handle is exposed: at rest it is aria-hidden, so it is outside the
|
|
103
|
+
* accessibility tree and has no computed name by design.
|
|
104
|
+
*/
|
|
105
|
+
static getResizerComputedName = async (resizer) => resizer.getComputedLabel();
|
|
106
|
+
|
|
92
107
|
// Snapshots
|
|
93
108
|
static snapshotPath(example = 'basic') {
|
|
94
109
|
return PageObject.getSnapshotPathBuilder('DataTable', example, 'ds-data-table');
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Key } from 'webdriverio';
|
|
2
|
+
import DSDataTableCO from '../DSDataTableCO';
|
|
3
|
+
import { RowCO } from '../components';
|
|
4
|
+
|
|
5
|
+
// Tests for the fix introduced in PUI-8826. Holding ArrowDown during a keyboard row drag fired
|
|
6
|
+
// dnd-kit's default smooth scroll on every key-repeat keydown, re-targeting the in-flight scroll
|
|
7
|
+
// animation before it settled — this read as the dragged row "flickering". The fix pins the
|
|
8
|
+
// KeyboardSensor to scrollBehavior: 'auto', so repeated keydowns produce instant jumps instead.
|
|
9
|
+
const REPEATED_PRESSES = 15;
|
|
10
|
+
|
|
11
|
+
if (
|
|
12
|
+
(!browser.capabilities['ice:options'].isPhone &&
|
|
13
|
+
!browser.capabilities['ice:options'].isTablet &&
|
|
14
|
+
browser.capabilities.browserName === 'chrome') ||
|
|
15
|
+
browser.capabilities.browserName === 'Chrome'
|
|
16
|
+
) {
|
|
17
|
+
describe('PUI-18883 - DSDataTable:: keyboard row drag scroll on held ArrowDown - Func', () => {
|
|
18
|
+
before('loading page', async () => {
|
|
19
|
+
const errorOnGo = await DSDataTableCO.keyboardDragScroll.go();
|
|
20
|
+
if (errorOnGo) throw errorOnGo;
|
|
21
|
+
await DSDataTableCO.waitForDataTable();
|
|
22
|
+
await DSDataTableCO.installScrollSpy();
|
|
23
|
+
});
|
|
24
|
+
it('01: the drag survives a burst of ArrowDown presses', async () => {
|
|
25
|
+
const dragHandle = await RowCO.getRowDragHlandlerByIndex(0);
|
|
26
|
+
await dragHandle.click();
|
|
27
|
+
await browser.keys(Key.Space);
|
|
28
|
+
await expect(dragHandle).toHaveAttribute('aria-pressed', 'true');
|
|
29
|
+
|
|
30
|
+
await DSDataTableCO.pressArrow('down', REPEATED_PRESSES);
|
|
31
|
+
// Still picked up and still holding focus — the repeat burst neither dropped the row nor
|
|
32
|
+
// stole focus mid-scroll.
|
|
33
|
+
await expect(dragHandle).toHaveAttribute('aria-pressed', 'true');
|
|
34
|
+
await expect(dragHandle).toBeFocused();
|
|
35
|
+
});
|
|
36
|
+
it('02: the burst scrolled the viewport and never used behavior "smooth"', async () => {
|
|
37
|
+
// Sanity first: a 200px viewport over 40 rows means the drag must have scrolled. Without
|
|
38
|
+
// this the behavior assertion below could pass without exercising the fix at all.
|
|
39
|
+
const scrollTop = await DSDataTableCO.getScrollableContainerScrollTop();
|
|
40
|
+
await expect(scrollTop).toBeGreaterThan(0);
|
|
41
|
+
|
|
42
|
+
const scrollCalls = await DSDataTableCO.getScrollSpyCalls();
|
|
43
|
+
await expect(scrollCalls.length).toBeGreaterThan(0);
|
|
44
|
+
await expect(scrollCalls.every((call) => call.behavior === 'auto')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('03: dropping with Space reorders the list and keeps the dragged row focused', async () => {
|
|
47
|
+
await browser.keys(Key.Space);
|
|
48
|
+
await expect(await DSDataTableCO.getFocusedRowIdCellText()).toBe('1');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -1,38 +1,76 @@
|
|
|
1
|
-
import { mouseOver } from '../../helpers';
|
|
2
1
|
import DSDataTableCO from '../DSDataTableCO';
|
|
3
2
|
import { HeaderCO, FiltersCO } from '../components';
|
|
4
3
|
|
|
5
|
-
//
|
|
6
|
-
// user
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Tests for the fix introduced in PUI-18785 (an open per-column filter menu did not close when the
|
|
5
|
+
// user clicked that column's header cell, grabbed its drag-and-drop handle, or grabbed its resize
|
|
6
|
+
// handle — the filter popover's floating reference is the whole header cell, so the click-outside
|
|
7
|
+
// detection never saw any of those interactions as "outside", and the menu could be dragged around
|
|
8
|
+
// the table while open).
|
|
9
|
+
//
|
|
10
|
+
// The SortByColumn story is the repro combination named in the defect: per-column filters +
|
|
11
|
+
// dragAndDropColumns + isResizeable on the same table.
|
|
12
|
+
//
|
|
13
|
+
// The target is the Country column: it is filterable but canSort is false, so a header click has
|
|
14
|
+
// only the filter to close and never also triggers a sort — the assertions stay about the menu and
|
|
15
|
+
// nothing else. That also makes FiltersCO.openFilterPopperByColumnName safe to use here even though
|
|
16
|
+
// it clicks the header cell to reveal the affordance; hovering instead is not portable, because in
|
|
17
|
+
// Safari the filter button stays aria-hidden/tabindex="-1" and never becomes interactable.
|
|
18
|
+
//
|
|
19
|
+
// Deviation from the ticket, asserted deliberately: PUI-18785's Expected Results asked that a
|
|
20
|
+
// column "should not be draggable while its filter menu is open". The shipped fix instead closes
|
|
21
|
+
// the menu and lets the same single gesture continue into the drag/resize, so these tests assert
|
|
22
|
+
// that shipped behavior — the menu closes, and the interaction is not swallowed just to close it.
|
|
23
|
+
const COUNTRY_COLUMN = 'country';
|
|
24
|
+
const COUNTRY_COLUMN_INDEX = 3;
|
|
25
|
+
|
|
26
|
+
const openCountryFilterMenu = async () => {
|
|
27
|
+
await FiltersCO.openFilterPopperByColumnName(COUNTRY_COLUMN);
|
|
28
|
+
await expect(await FiltersCO.getFilterMenuContent()).toBeDisplayed();
|
|
29
|
+
};
|
|
30
|
+
|
|
10
31
|
if (!browser.capabilities['ice:options'].isPhone && !browser.capabilities['ice:options'].isTablet) {
|
|
11
|
-
describe
|
|
32
|
+
describe('PUI-18887 - DSDataTable:: filter menu closes on header, drag and resize - Func', () => {
|
|
12
33
|
beforeEach(async () => {
|
|
13
34
|
const errorOnGo = await DSDataTableCO.sortByColumn.go();
|
|
14
35
|
if (errorOnGo) throw errorOnGo;
|
|
15
36
|
await DSDataTableCO.waitForDataTable();
|
|
16
37
|
});
|
|
17
38
|
|
|
18
|
-
it('01: clicking the column
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
await
|
|
39
|
+
it('01: clicking the column header closes an open filter menu and keeps the trigger', async () => {
|
|
40
|
+
await openCountryFilterMenu();
|
|
41
|
+
|
|
42
|
+
const countryHeader = await HeaderCO.getHeaderCellByText(COUNTRY_COLUMN);
|
|
43
|
+
await countryHeader.click();
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
await expect(await FiltersCO.getFilterMenuContent()).not.toBeDisplayed();
|
|
46
|
+
// Closing the menu must not take its own trigger with it. An over-broad fix that removed the
|
|
47
|
+
// whole filter affordance would still satisfy the assertion above, so the trigger is checked
|
|
48
|
+
// explicitly. Existence rather than visibility: the filter button is toggled with display:none
|
|
49
|
+
// by the header's own affordance logic, so a visibility assertion would be testing that
|
|
50
|
+
// unrelated hover/active state instead of the fix.
|
|
51
|
+
const filterBtn = await FiltersCO.getHeaderCellFilterButtonByHeaderCell(countryHeader, 0);
|
|
52
|
+
await expect(filterBtn).toBeExisting();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('02: activating the column drag handle closes an open filter menu', async () => {
|
|
56
|
+
await openCountryFilterMenu();
|
|
57
|
+
|
|
58
|
+
const countryHeader = await HeaderCO.getHeaderCellByText(COUNTRY_COLUMN);
|
|
59
|
+
const dragHandle = await HeaderCO.getHeaderCellDragHandler(countryHeader);
|
|
25
60
|
await dragHandle.click();
|
|
61
|
+
|
|
26
62
|
await expect(await FiltersCO.getFilterMenuContent()).not.toBeDisplayed();
|
|
27
63
|
});
|
|
28
64
|
|
|
29
|
-
it('
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
65
|
+
it('03: grabbing the column resize handle closes an open filter menu', async () => {
|
|
66
|
+
await openCountryFilterMenu();
|
|
67
|
+
|
|
68
|
+
// A real press-move-release on the 4px handle, which is what fires onResizeStart. Asserting
|
|
69
|
+
// the resize itself is out of scope here — that is PUI-18764's job; this test only proves the
|
|
70
|
+
// press reaches the resizer and closes the menu.
|
|
71
|
+
const resizer = await HeaderCO.getResizeHandler(COUNTRY_COLUMN_INDEX);
|
|
72
|
+
await resizer.dragAndDrop({ x: 30, y: 0 });
|
|
34
73
|
|
|
35
|
-
await idHeader.click();
|
|
36
74
|
await expect(await FiltersCO.getFilterMenuContent()).not.toBeDisplayed();
|
|
37
75
|
});
|
|
38
76
|
});
|