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.
Files changed (23) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/ds-autocomplete/DSAutocompleteCO.js +34 -1
  3. package/ds-autocomplete/aria-busy-loading/DSAutocomplete.aria-busy-loading.axe-core.func.spec.js +50 -0
  4. package/ds-autocomplete/aria-busy-loading/DSAutocomplete.aria-busy-loading.func.spec.js +122 -0
  5. package/ds-autocomplete/aria-busy-loading/DSAutocomplete.aria-busy-loading.visual.spec.js +48 -0
  6. package/ds-data-table-async/DSDataTableCO.js +124 -2
  7. package/ds-data-table-async/components/HeaderCO.js +16 -1
  8. package/ds-data-table-async/dnd/DSDataTable.dnd-keyboard-scroll.func.spec.js +51 -0
  9. package/ds-data-table-async/filter-menu-close/DSDataTable.filter-menu-close.func.spec.js +57 -19
  10. package/ds-data-table-async/filter-popover-boundary/DSDataTable.filter-popover-boundary.func.spec.js +98 -0
  11. package/ds-data-table-async/filters/DSDataTable.filters-creatable-active-descendant.func.spec.js +51 -0
  12. package/ds-data-table-async/filters/DSDataTable.filters-creatable.func.spec.js +39 -0
  13. package/ds-data-table-async/filters/DSDataTable.filters.axe-core.func.spec.js +11 -9
  14. package/ds-data-table-async/helpers/getInputSelection.js +6 -0
  15. package/ds-data-table-async/helpers/index.js +1 -0
  16. package/ds-data-table-async/resizable/DSDataTable.resizerA11y.axe-core.func.spec.js +18 -21
  17. package/ds-data-table-async/resizable/DSDataTable.resizerA11y.func.spec.js +35 -65
  18. package/ds-hooks/useFocusTrap/UseFocusTrapCO.js +2 -0
  19. package/ds-hooks/useFocusTrap/UseFocusTrapToggle.func.spec.js +48 -0
  20. package/ds-slider-v2/DSSliderV2.func.spec.js +36 -25
  21. package/ds-slider-v2/DSSliderV2CO.js +10 -0
  22. package/ds-slider-v2/min-range/DSSliderV2.min-range.func.spec.js +69 -0
  23. 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
  }
@@ -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
- static getResizerAccessibleName = async (resizer) => {
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
- // SKIPPED pending fix for defect PUI-18785: an open per-column filter menu does not close when the
6
- // user clicks the column header cell or its drag-and-drop handle the column can even be dragged
7
- // with the menu open, and the menu drops alongside the column and stays open. These tests capture
8
- // the EXPECTED behavior (the menu must close on those interactions). Un-skip them once PUI-18785
9
- // lands, then create the Test ticket and replace PUI-XXXXX.
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.skip('PUI-XXXXX - DSDataTable:: filter menu closes on column header or drag interaction - Func', () => {
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 drag handle closes an open filter menu', async () => {
19
- const idHeader = await HeaderCO.getHeaderCellByIndex(0);
20
- await mouseOver(idHeader);
21
- await (await HeaderCO.getFilterBtnByIndex(0)).click();
22
- await expect(await FiltersCO.getFilterMenuContent()).toBeDisplayed();
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
- const dragHandle = await HeaderCO.getHeaderCellDragHandler(idHeader);
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('02: clicking the column header cell closes an open filter menu', async () => {
30
- const idHeader = await HeaderCO.getHeaderCellByIndex(0);
31
- await mouseOver(idHeader);
32
- await (await HeaderCO.getFilterBtnByIndex(0)).click();
33
- await expect(await FiltersCO.getFilterMenuContent()).toBeDisplayed();
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
  });