dimsum-e2e-tests 3.70.0-next.62 → 3.70.0-next.69

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 (30) hide show
  1. package/CHANGELOG.md +807 -767
  2. package/ds-apppicker/DSAppPickerCO.js +28 -1
  3. package/ds-apppicker/basic/DSAppPicker.basic.keyboard-nav.func.spec.js +18 -7
  4. package/ds-apppicker/closeAfterSelection/DSAppPicker.closeAfterSelection.func.spec.js +1 -1
  5. package/ds-apppicker/custom-trigger-focus/DSAppPicker.custom-trigger-focus.func.spec.js +82 -0
  6. package/ds-apppicker/label-expansion-dismissal/DSAppPicker.label-expansion-dismissal.visual.spec.js +108 -0
  7. package/ds-apppicker/reflow/DSAppPicker.reflow.visual.spec.js +5 -6
  8. package/ds-banner/DSBanner.text-resize.visual.spec.js +5 -2
  9. package/ds-banner/DSBannerCO.js +4 -0
  10. package/ds-card-v3/reflow/DSCardV3.reflow.visual.spec.js +2 -0
  11. package/ds-chip/label-expansion-dismissal/DSChip.label-expansion-dismissal.visual.spec.js +84 -0
  12. package/ds-dataviz/DSDataViz.axis.visual.spec.js +1 -2
  13. package/ds-dataviz/DSDataViz.ticks.visual.spec.js +1 -2
  14. package/ds-form-combobox-multi/aria-disabled/DSComboboxMulti.aria-disabled.visual.spec.js +4 -2
  15. package/ds-form-date-range-picker/DateRangePickerCO.js +6 -0
  16. package/ds-form-date-range-picker/picker-callbacks/DateRangePicker.picker-callbacks.func.spec.js +36 -0
  17. package/ds-global-header/GlobalHeaderCO.js +96 -2
  18. package/ds-global-header/app-picker-passthrough/GlobalHeader.app-picker-passthrough.func.spec.js +113 -0
  19. package/ds-global-header/popup-menu-focus/GlobalHeader.popup-menu-focus.func.spec.js +63 -0
  20. package/ds-global-header/popup-menu-slots/GlobalHeader.popup-menu-slots.func.spec.js +70 -0
  21. package/ds-global-header/search-toggle-slots/GlobalHeader.search-toggle-slots.func.spec.js +118 -0
  22. package/ds-global-header/toolbar-item-attributes/GlobalHeader.toolbar-item-attributes.func.spec.js +53 -0
  23. package/ds-grid/DSGridCO.js +20 -0
  24. package/ds-grid/layout-mode/DSGrid.layout-mode.func.spec.js +51 -0
  25. package/ds-grid/layout-mode/DSGrid.layout-mode.visual.spec.js +22 -0
  26. package/ds-hooks/DSHook.UseFontSizeMedia.visual.spec.js +5 -2
  27. package/ds-hooks/DSHookCO.js +55 -1
  28. package/ds-query-builder/DSQueryBuilder.visual.spec.js +6 -6
  29. package/package.json +149 -149
  30. package/paths.js +1 -0
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable import/no-relative-packages */
2
- import { PATH_E2E_GLOBAL_HEADER } from '../paths';
3
- import { PageObject, Urlbuilder, type } from '../helpers';
2
+ import { PATH_E2E_GLOBAL_HEADER, PATH_E2E_GLOBAL_HEADER_PUI18647 } from '../paths';
3
+ import { PageObject, Urlbuilder, getElementByIndex, type } from '../helpers';
4
4
 
5
5
  export default class GlobalHeader extends PageObject {
6
6
  static basicURL = new Urlbuilder(PATH_E2E_GLOBAL_HEADER, 'basic');
@@ -31,6 +31,19 @@ export default class GlobalHeader extends PageObject {
31
31
 
32
32
  static withLayoutProvider = new Urlbuilder(PATH_E2E_GLOBAL_HEADER, 'with-layout-provider');
33
33
 
34
+ // PUI-18647 — one story per out-of-the-box toolbar item type, each a minimal header carrying only
35
+ // that item with every injection point wired to data-public="true".
36
+ static appPickerPassthroughURL = new Urlbuilder(PATH_E2E_GLOBAL_HEADER_PUI18647, 'app-picker-contract-passthrough');
37
+
38
+ static iconButtonItemURL = new Urlbuilder(PATH_E2E_GLOBAL_HEADER_PUI18647, 'icon-button-item-attributes');
39
+
40
+ static popupMenuSlotSurfaceURL = new Urlbuilder(PATH_E2E_GLOBAL_HEADER_PUI18647, 'popup-menu-slot-surface');
41
+
42
+ static searchToggleSlotsURL = new Urlbuilder(
43
+ PATH_E2E_GLOBAL_HEADER_PUI18647,
44
+ 'search-toggle-item-attributes-and-slots',
45
+ );
46
+
34
47
  // Selectors
35
48
  static async getGHBreadcrumbBtn(label) {
36
49
  return $(`//a[@data-testid="gh-breadcrumb-btn"]/span[contains(text(),"${label}")]/ancestor::a`);
@@ -108,6 +121,87 @@ export default class GlobalHeader extends PageObject {
108
121
  await spinButton.click();
109
122
  }
110
123
 
124
+ // PUI-18647 selectors
125
+
126
+ // Every element the stories wire to data-public="true". The assertions read this collection rather
127
+ // than probing one testid at a time: the contract under test is "the whole surface is reachable",
128
+ // so a slot silently dropping out has to fail the count, not slip through an unasserted gap.
129
+ static async getPublicNodes() {
130
+ return $$('[data-public="true"]');
131
+ }
132
+
133
+ static async getPublicNodeTestIds() {
134
+ const nodes = await GlobalHeader.getPublicNodes();
135
+ return Promise.all(Array.from({ length: nodes.length }, (_, index) => nodes[index].getAttribute('data-testid')));
136
+ }
137
+
138
+ // The stories render "Yes" here if any element in the page carries a type="ds-*" attribute. The
139
+ // toolbar `type` discriminator is internal routing and must never reach the DOM — `type` is a
140
+ // reserved HTML attribute.
141
+ static async getDsTypedItemExistFlag() {
142
+ return $('[data-testid="pui18647-ds-typed-item-exist"]');
143
+ }
144
+
145
+ static async getClickCount() {
146
+ return $('[data-testid="pui18647-click-count"]');
147
+ }
148
+
149
+ // A custom TriggerComponent handed to the app-picker item through componentProps. Its presence is
150
+ // what proves componentProps reaches DSAppPicker at all: it replaces the built-in trigger, so
151
+ // there is no `gh-toolbar-item` button in that story.
152
+ static async getAppPickerCustomTrigger() {
153
+ return $('[data-testid="pui18647-gh-apppicker-trigger-component"]');
154
+ }
155
+
156
+ // The popup-menu item's own root. Before PUI-18647 this element was an unstyled wrapper with no
157
+ // slot and no data-testid, so the item as a whole had no injection point.
158
+ static async getPopupMenuContainer() {
159
+ return $('[data-testid="ds-globalheader-menubar-item-popupmenu-container"]');
160
+ }
161
+
162
+ static async getPopupMenuItemButtons() {
163
+ return $$('[data-testid="ds-global-header-menubar-item-popupmenu-item-button"]');
164
+ }
165
+
166
+ static async getPopupMenuItemButtonByIndex(index = 0) {
167
+ return getElementByIndex(GlobalHeader.getPopupMenuItemButtons, index);
168
+ }
169
+
170
+ // The search-toggle item's root container. Item-level global attributes land HERE for this item
171
+ // type, not on the trigger button as with the sibling types.
172
+ static async getSearchToggleContainer() {
173
+ return $('[data-testid="ds-globalheader-menubar-item-searchtoggle-container"]');
174
+ }
175
+
176
+ static async getSearchTogglePrevButton() {
177
+ return $('[data-testid="ds-globalheader-searchtoggle-spinbutton-prev"]');
178
+ }
179
+
180
+ static async getSearchToggleNextButton() {
181
+ return $('[data-testid="ds-globalheader-searchtoggle-spinbutton-next"]');
182
+ }
183
+
184
+ // A neutral, non-focusable target outside any open overlay, used to assert that dismissing by
185
+ // pointer leaves focus where the click put it instead of yanking it back to the trigger.
186
+ // Anchored on a data-testid rather than on a tag: $('h1') matches Storybook's own hidden
187
+ // "No Preview" heading first, which is not clickable and is not part of the story at all.
188
+ static async getOutsideClickTarget() {
189
+ return GlobalHeader.getDsTypedItemExistFlag();
190
+ }
191
+
192
+ static async getSearchableRowByIndex(index = 0) {
193
+ return $(`[data-testid="pui18647-row-${index}"]`);
194
+ }
195
+
196
+ // The stories highlight the current result with an inline khaki background, so the selected row is
197
+ // read off the computed style rather than a semantic attribute — there is no semantic one to read.
198
+ // Matched on the rgb channels only: browsers disagree on whether to emit `rgb(...)` or `rgba(...,1)`
199
+ // and on internal spacing, and none of that is what the assertion is about.
200
+ static async isRowHighlighted(row) {
201
+ const { value } = await row.getCSSProperty('background-color');
202
+ return String(value).replace(/\s/g, '').includes('240,230,140');
203
+ }
204
+
111
205
  // Slots
112
206
 
113
207
  static async getGHBreadcrumbChevronlotByIndex(index = 0) {
@@ -0,0 +1,113 @@
1
+ import { Key } from 'webdriverio';
2
+ import GlobalHeaderCO from '../GlobalHeaderCO';
3
+ import DSAppPickerCO from '../../ds-apppicker/DSAppPickerCO';
4
+
5
+ // Tests for the fix in PUI-18647. Only a hand-maintained subset of props reached the out-of-the-box
6
+ // GlobalHeader -> DSAppPicker item; the rest were silently dropped, so the item could not customise
7
+ // the picker's slots at all. The reported symptom was `dsApppickerChip={{ 'data-public': true }}`
8
+ // never arriving, leaving LogRocket masking the app icons and labels (blocks ENCW-136789).
9
+ //
10
+ // The hand-off is now a full contract passthrough, which this spec proves along three channels:
11
+ // 1. the nine DSAppPicker slot props,
12
+ // 2. non-slot members of the same contract — TriggerComponent, onOpen/onClose, id,
13
+ // 3. GlobalHeader's own slots, which had to keep working across the refactor.
14
+ //
15
+ // Two a11y changes ride along and are pinned here because QA selectors depend on them: the trigger's
16
+ // aria-haspopup is now "dialog" rather than "true", and it no longer carries aria-expanded (a dialog
17
+ // announces its open state by being present). Open state is therefore asserted via the panel, never
18
+ // via the attribute.
19
+ //
20
+ // Chrome only: DOM attributes plus programmatic focus landings, identical in every engine. The
21
+ // `|| 'Chrome'` arm is mandatory (BrowserStack capitalises browserName; a lowercase-only guard
22
+ // registers zero tests and still exits 0).
23
+ if (
24
+ (!browser.capabilities['ice:options'].isPhone &&
25
+ !browser.capabilities['ice:options'].isTablet &&
26
+ browser.capabilities.browserName === 'chrome') ||
27
+ browser.capabilities.browserName === 'Chrome'
28
+ ) {
29
+ describe('PUI-19015 - GlobalHeader:: app-picker componentProps contract passthrough - Func', () => {
30
+ before('loading page', async () => {
31
+ const errorOnGo = await GlobalHeaderCO.appPickerPassthroughURL.go();
32
+ if (errorOnGo) throw errorOnGo;
33
+ });
34
+
35
+ it('01: should render the custom trigger handed through componentProps', async () => {
36
+ const trigger = await GlobalHeaderCO.getAppPickerCustomTrigger();
37
+ await trigger.waitForDisplayed();
38
+ await expect(trigger).toHaveText('Apps');
39
+
40
+ // componentProps.id reaches DSAppPicker and namespaces the generated trigger id.
41
+ await expect(await trigger.getAttribute('id')).toBe('pui18647-gh-apppicker-dialog-trigger-btn');
42
+
43
+ await expect(trigger).toHaveAttribute('aria-haspopup', 'dialog');
44
+ // Absent by design — asserting the panel is present is the supported way to read open state.
45
+ await expect(await trigger.getAttribute('aria-expanded')).toBe(null);
46
+
47
+ const dsTypedFlag = await GlobalHeaderCO.getDsTypedItemExistFlag();
48
+ await expect(dsTypedFlag).toHaveText('No');
49
+ });
50
+
51
+ it("02: should keep forwarding GlobalHeader's own slots while the picker is closed", async () => {
52
+ const publicTestIds = await GlobalHeaderCO.getPublicNodeTestIds();
53
+ await expect(publicTestIds).toEqual([
54
+ 'ds-global-header',
55
+ 'ds-global-header-logo-container',
56
+ 'ds-global-header-menubar-container',
57
+ 'ds-global-header-menubar-list',
58
+ 'ds-global-header-menubar-item',
59
+ ]);
60
+ });
61
+
62
+ it('03: should forward every app-picker slot prop and the onOpen callback', async () => {
63
+ await (await GlobalHeaderCO.getAppPickerCustomTrigger()).click();
64
+ await (await DSAppPickerCO.getAppPickerRoot()).waitForDisplayed();
65
+
66
+ // The story's onOpen re-scans the document, so the table only grows if that callback arrived
67
+ // too — the count below is simultaneously the slot assertion and the onOpen assertion.
68
+ const publicTestIds = await GlobalHeaderCO.getPublicNodeTestIds();
69
+ const countOf = (testId) => publicTestIds.filter((id) => id === testId).length;
70
+
71
+ // Three apps, two custom apps, one separator between the two sections.
72
+ await expect(countOf('ds-apppicker-root')).toBe(1);
73
+ await expect(countOf('ds-apppicker-group')).toBe(2);
74
+ await expect(countOf('ds-apppicker-row')).toBe(3);
75
+ await expect(countOf('ds-apppicker-title')).toBe(2);
76
+ await expect(countOf('ds-apppicker-item')).toBe(5);
77
+ await expect(countOf('ds-apppicker-separator')).toBe(1);
78
+ await expect(countOf('ds-apppicker-floating-wrapper')).toBe(1);
79
+ // dsApppickerChip — the prop the defect was raised about. One tagged node per app.
80
+ await expect(countOf('app-picker__chip')).toBe(5);
81
+
82
+ // The panel is a dialog named by the custom trigger, so the widened contract did not cost the
83
+ // picker its ARIA pattern.
84
+ const dialog = await DSAppPickerCO.getFloatingWrapper();
85
+ await expect(dialog).toHaveAttribute('role', 'dialog');
86
+ await expect(await dialog.getAttribute('aria-labelledby')).toBe(
87
+ await (await GlobalHeaderCO.getAppPickerCustomTrigger()).getAttribute('id'),
88
+ );
89
+
90
+ const dsTypedFlag = await GlobalHeaderCO.getDsTypedItemExistFlag();
91
+ await expect(dsTypedFlag).toHaveText('No');
92
+ });
93
+
94
+ it('04: should open onto the selected app with ArrowDown and ArrowUp', async () => {
95
+ // The toolbar is a menubar, so it opts the picker into openAndFocusOnArrowKeys. 'Reports' is
96
+ // the third app, so "landed on the selected app" cannot be read as "landed on the first app".
97
+ await browser.keys(Key.Escape);
98
+ const trigger = await GlobalHeaderCO.getAppPickerCustomTrigger();
99
+ await expect(trigger).toBeFocused();
100
+
101
+ await browser.keys(Key.ArrowDown);
102
+ await (await DSAppPickerCO.getAppPickerRoot()).waitForDisplayed();
103
+ await expect(await DSAppPickerCO.getChipByLabel('Reports')).toBeFocused();
104
+
105
+ // ArrowUp opens nothing before this change; it now lands on the same selected app.
106
+ await browser.keys(Key.Escape);
107
+ await expect(trigger).toBeFocused();
108
+ await browser.keys(Key.ArrowUp);
109
+ await (await DSAppPickerCO.getAppPickerRoot()).waitForDisplayed();
110
+ await expect(await DSAppPickerCO.getChipByLabel('Reports')).toBeFocused();
111
+ });
112
+ });
113
+ }
@@ -0,0 +1,63 @@
1
+ import { Key } from 'webdriverio';
2
+ import GlobalHeaderCO from '../GlobalHeaderCO';
3
+
4
+ // Tests for the popup-menu focus fixes shipped with PUI-18647:
5
+ //
6
+ // - (NEW) ArrowDown on the trigger opens onto the FIRST item; ArrowUp opens onto the LAST. ArrowUp
7
+ // did not open the menu at all before.
8
+ // - (FIX) Clicking outside no longer drags focus back to the trigger. It stays where the click put
9
+ // it, so focus is no longer lost arbitrarily on a pointer dismissal.
10
+ // - (unchanged) Escape still closes and returns focus to the trigger.
11
+ //
12
+ // Full browser matrix on purpose: pointer-driven dismissal and where focus settles afterwards is
13
+ // precisely the behaviour engines implement differently.
14
+ if (!browser.capabilities['ice:options'].isPhone && !browser.capabilities['ice:options'].isTablet) {
15
+ describe('PUI-19018 - GlobalHeader:: popup-menu open and focus return paths - Func', () => {
16
+ before('loading page', async () => {
17
+ const errorOnGo = await GlobalHeaderCO.popupMenuSlotSurfaceURL.go();
18
+ if (errorOnGo) throw errorOnGo;
19
+ });
20
+
21
+ it('01: should open onto the first menu item with ArrowDown', async () => {
22
+ // Focused programmatically rather than by clicking: a click would open the menu, and closing it
23
+ // again to get back to "closed trigger, focused" makes the starting state depend on where a
24
+ // dismissal leaves focus, which varies by engine. The ArrowDown behaviour is the case here.
25
+ const trigger = await GlobalHeaderCO.getGHToolbarItem(0);
26
+ await trigger.waitForDisplayed();
27
+ await browser.execute((el) => el.focus(), trigger);
28
+ await expect(trigger).toBeFocused();
29
+
30
+ await browser.keys(Key.ArrowDown);
31
+ await (await GlobalHeaderCO.getPopupContent()).waitForDisplayed();
32
+
33
+ const firstItem = await GlobalHeaderCO.getPopupMenuItemButtonByIndex(0);
34
+ await expect(firstItem).toBeFocused();
35
+ await expect(firstItem).toHaveText('Preferences');
36
+ });
37
+
38
+ it('02: should close and return focus to the trigger on Escape', async () => {
39
+ await browser.keys(Key.Escape);
40
+
41
+ await expect(await GlobalHeaderCO.getPopupContent()).not.toBeDisplayed();
42
+ await expect(await GlobalHeaderCO.getGHToolbarItem(0)).toBeFocused();
43
+ });
44
+
45
+ it('03: should open onto the last menu item with ArrowUp', async () => {
46
+ await browser.keys(Key.ArrowUp);
47
+ await (await GlobalHeaderCO.getPopupContent()).waitForDisplayed();
48
+
49
+ const lastItem = await GlobalHeaderCO.getPopupMenuItemButtonByIndex(1);
50
+ await expect(lastItem).toBeFocused();
51
+ await expect(lastItem).toHaveText('Sign out');
52
+ });
53
+
54
+ it('04: should close without returning focus to the trigger on an outside click', async () => {
55
+ await (await GlobalHeaderCO.getOutsideClickTarget()).click();
56
+
57
+ await expect(await GlobalHeaderCO.getPopupContent()).not.toBeDisplayed();
58
+ // The heading is not focusable, so focus settles on the body. The point is only that it did
59
+ // NOT get pulled back to the trigger, which is what used to happen.
60
+ await expect(await GlobalHeaderCO.getGHToolbarItem(0)).not.toBeFocused();
61
+ });
62
+ });
63
+ }
@@ -0,0 +1,70 @@
1
+ import { Key } from 'webdriverio';
2
+ import GlobalHeaderCO from '../GlobalHeaderCO';
3
+
4
+ // Tests for the new slot introduced in PUI-18647. The ds-popup-menu item's outermost element used to
5
+ // be an unstyled wrapper with no slot and no data-testid, so the item as a whole had no injection
6
+ // point — nowhere to reach it, nowhere for xstyled props to land. It is now
7
+ // `dsGlobalheaderMenubarItemPopupmenuContainer` / `ds-globalheader-menubar-item-popupmenu-container`.
8
+ //
9
+ // The change is additive: every pre-existing slot name and data-testid in this subtree is unchanged
10
+ // and sits in the same DOM position, so no existing selector moves. Both the new and the pre-existing
11
+ // slots are asserted here so a regression in either fails.
12
+ //
13
+ // Chrome only — pure DOM attribute assertions. The `|| 'Chrome'` arm is mandatory (BrowserStack
14
+ // capitalises browserName; a lowercase-only guard silently registers zero tests and still exits 0).
15
+ if (
16
+ (!browser.capabilities['ice:options'].isPhone &&
17
+ !browser.capabilities['ice:options'].isTablet &&
18
+ browser.capabilities.browserName === 'chrome') ||
19
+ browser.capabilities.browserName === 'Chrome'
20
+ ) {
21
+ describe('PUI-19017 - GlobalHeader:: popup-menu item slot surface - Func', () => {
22
+ before('loading page', async () => {
23
+ const errorOnGo = await GlobalHeaderCO.popupMenuSlotSurfaceURL.go();
24
+ if (errorOnGo) throw errorOnGo;
25
+ });
26
+
27
+ it('01: should expose the new item container slot while the menu is closed', async () => {
28
+ const container = await GlobalHeaderCO.getPopupMenuContainer();
29
+ await container.waitForDisplayed();
30
+ await expect(container).toHaveAttribute('data-dimsum-slot', 'dsGlobalheaderMenubarItemPopupmenuContainer');
31
+
32
+ // Closed, the reachable surface is the item container plus the trigger button. Item-level
33
+ // attributes land on the TRIGGER for this item type, not on the container.
34
+ const publicTestIds = await GlobalHeaderCO.getPublicNodeTestIds();
35
+ await expect(publicTestIds).toEqual(['ds-globalheader-menubar-item-popupmenu-container', 'gh-toolbar-item']);
36
+
37
+ const dsTypedFlag = await GlobalHeaderCO.getDsTypedItemExistFlag();
38
+ await expect(dsTypedFlag).toHaveText('No');
39
+ });
40
+
41
+ it('02: should expose every popup-menu slot once the menu is open', async () => {
42
+ const trigger = await GlobalHeaderCO.getGHToolbarItem(0);
43
+ await trigger.click();
44
+ await (await GlobalHeaderCO.getPopupContent()).waitForDisplayed();
45
+
46
+ // Two options and one separator, so the per-item slots appear twice. The exact testid spelling
47
+ // is asserted rather than normalised: the container and the item label use `ds-globalheader-`
48
+ // while their siblings use `ds-global-header-`, and that inconsistency is what consumers'
49
+ // selectors have to match.
50
+ const publicTestIds = await GlobalHeaderCO.getPublicNodeTestIds();
51
+ await expect(publicTestIds).toEqual([
52
+ 'ds-globalheader-menubar-item-popupmenu-container',
53
+ 'gh-toolbar-item',
54
+ 'ds-global-header-menubar-item-popupmenu-list',
55
+ 'ds-global-header-menubar-item-popupmenu-item',
56
+ 'ds-global-header-menubar-item-popupmenu-item-button',
57
+ 'ds-globalheader-menubar-item-popupmenu-item-label',
58
+ 'ds-global-header-menubar-item-popupmenu-separator',
59
+ 'ds-global-header-menubar-item-popupmenu-item',
60
+ 'ds-global-header-menubar-item-popupmenu-item-button',
61
+ 'ds-globalheader-menubar-item-popupmenu-item-label',
62
+ ]);
63
+
64
+ const dsTypedFlag = await GlobalHeaderCO.getDsTypedItemExistFlag();
65
+ await expect(dsTypedFlag).toHaveText('No');
66
+
67
+ await browser.keys(Key.Escape);
68
+ });
69
+ });
70
+ }
@@ -0,0 +1,118 @@
1
+ import { Key } from 'webdriverio';
2
+ import GlobalHeaderCO from '../GlobalHeaderCO';
3
+ import { type } from '../../helpers';
4
+
5
+ // Tests for the ds-search-toggle changes in PUI-18647:
6
+ //
7
+ // - (NEW) item-level attributes now land on the item's ROOT CONTAINER, not on the trigger button as
8
+ // with the sibling item types. The type declared that surface on its configuration type and
9
+ // forwarded none of it, so data-public on a search-toggle entry appeared nowhere.
10
+ // - (RENAMED) that container's data-testid moved from `ds-global-header-menubar-item-searchtoggle-
11
+ // container` to `ds-globalheader-menubar-item-searchtoggle-container`. The element itself is
12
+ // unchanged; commit 295f110651c dropped its legacy override in constants/index.ts, so the
13
+ // auto-generated post-convention value now wins. Asserted as the DOM currently renders it — if
14
+ // the legacy override is restored, this expectation flips back to the hyphenated spelling.
15
+ // - (BREAKING) the two spinbutton result-navigation identifiers were INVERTED: the control that
16
+ // triggered NEXT was slotted as PREVIOUS and vice versa. The accessible names were always right,
17
+ // so testid and accessible name now agree. This is the one breaking change for QA here.
18
+ // - (FIX) Escape was handled twice — on the input and again on the container it bubbles to — so one
19
+ // press collapsed twice and called onClear three times. It now fires once.
20
+ //
21
+ // Chrome only: attribute assertions plus click-to-state-change, both engine-independent. The
22
+ // `|| 'Chrome'` arm is mandatory (BrowserStack capitalises browserName; a lowercase-only guard
23
+ // registers zero tests and still exits 0).
24
+ if (
25
+ (!browser.capabilities['ice:options'].isPhone &&
26
+ !browser.capabilities['ice:options'].isTablet &&
27
+ browser.capabilities.browserName === 'chrome') ||
28
+ browser.capabilities.browserName === 'Chrome'
29
+ ) {
30
+ describe('PUI-19019 - GlobalHeader:: search-toggle item slots and result controls - Func', () => {
31
+ before('loading page', async () => {
32
+ const errorOnGo = await GlobalHeaderCO.searchToggleSlotsURL.go();
33
+ if (errorOnGo) throw errorOnGo;
34
+ });
35
+
36
+ it('01: should forward item-level attributes to the new item container while collapsed', async () => {
37
+ const container = await GlobalHeaderCO.getSearchToggleContainer();
38
+ await container.waitForDisplayed();
39
+ await expect(container).toHaveAttribute('data-dimsum-slot', 'dsGlobalheaderMenubarItemSearchtoggleContainer');
40
+
41
+ const publicTestIds = await GlobalHeaderCO.getPublicNodeTestIds();
42
+ await expect(publicTestIds).toEqual(['ds-globalheader-menubar-item-searchtoggle-container', 'gh-toolbar-item']);
43
+
44
+ const dsTypedFlag = await GlobalHeaderCO.getDsTypedItemExistFlag();
45
+ await expect(dsTypedFlag).toHaveText('No');
46
+ });
47
+
48
+ it('02: should expose the full search-toggle slot surface once expanded', async () => {
49
+ await (await GlobalHeaderCO.getGHToolbarItem(0)).click();
50
+ await (await GlobalHeaderCO.getSearchToggleInput()).waitForDisplayed();
51
+
52
+ const publicTestIds = await GlobalHeaderCO.getPublicNodeTestIds();
53
+ await expect(publicTestIds).toEqual([
54
+ 'ds-globalheader-menubar-item-searchtoggle-container',
55
+ 'gh-toolbar-item',
56
+ 'ds-globalheader-searchtoggle-expanded-content-wrapper',
57
+ 'ds-globalheader-searchtoggle-textinput',
58
+ 'ds-globalheader-searchtoggle-spinbutton-wrapper',
59
+ 'ds-globalheader-searchtoggle-spinbutton-text',
60
+ 'ds-globalheader-searchtoggle-spinbutton-separator',
61
+ 'ds-globalheader-searchtoggle-spinbutton-next',
62
+ 'ds-globalheader-searchtoggle-spinbutton-prev',
63
+ ]);
64
+ });
65
+
66
+ it('03: should match each spinbutton data-testid to its accessible name', async () => {
67
+ // The un-inversion. Note the DOM order is next-then-prev, which is what made the original
68
+ // swap easy to miss — so identity is read off the accessible name, never off position.
69
+ await expect(await GlobalHeaderCO.getSearchTogglePrevButton()).toHaveAttribute(
70
+ 'aria-label',
71
+ 'Previous search result',
72
+ );
73
+ await expect(await GlobalHeaderCO.getSearchToggleNextButton()).toHaveAttribute(
74
+ 'aria-label',
75
+ 'Next search result',
76
+ );
77
+ });
78
+
79
+ it('04: should navigate results forward with next and backward with prev', async () => {
80
+ const input = await GlobalHeaderCO.getSearchToggleInput();
81
+ await input.click();
82
+ await type('L');
83
+
84
+ const spinbuttonText = await GlobalHeaderCO.getSearchToggleSpinbutton();
85
+ await browser.waitUntil(async () => (await spinbuttonText.getText()) === '4', {
86
+ timeout: 3000,
87
+ timeoutMsg: 'search results did not populate after typing',
88
+ });
89
+
90
+ // Four rows match "L"; the first hit is row 0 and the last is row 4.
91
+ await (await GlobalHeaderCO.getSearchToggleNextButton()).click();
92
+ await expect(spinbuttonText).toHaveText('1 / 4');
93
+ const firstMatch = await GlobalHeaderCO.getSearchableRowByIndex(0);
94
+ await expect(await GlobalHeaderCO.isRowHighlighted(firstMatch)).toBe(true);
95
+
96
+ // Prev from the first result wraps to the last, proving the control moves backward.
97
+ await (await GlobalHeaderCO.getSearchTogglePrevButton()).click();
98
+ await expect(spinbuttonText).toHaveText('4 / 4');
99
+ const lastMatch = await GlobalHeaderCO.getSearchableRowByIndex(4);
100
+ await expect(await GlobalHeaderCO.isRowHighlighted(lastMatch)).toBe(true);
101
+ });
102
+
103
+ it('05: should collapse, clear and return focus on a single Escape', async () => {
104
+ await (await GlobalHeaderCO.getSearchToggleInput()).click();
105
+ await browser.keys(Key.Escape);
106
+
107
+ await expect(await GlobalHeaderCO.getSearchToggleInput()).not.toBeDisplayed();
108
+ await expect(await GlobalHeaderCO.getGHToolbarItem(0)).toBeFocused();
109
+
110
+ // Collapsing clears the query, so the highlight is gone and the reachable surface is back to
111
+ // the two collapsed-state nodes.
112
+ const lastMatch = await GlobalHeaderCO.getSearchableRowByIndex(4);
113
+ await expect(await GlobalHeaderCO.isRowHighlighted(lastMatch)).toBe(false);
114
+ const publicTestIds = await GlobalHeaderCO.getPublicNodeTestIds();
115
+ await expect(publicTestIds).toEqual(['ds-globalheader-menubar-item-searchtoggle-container', 'gh-toolbar-item']);
116
+ });
117
+ });
118
+ }
@@ -0,0 +1,53 @@
1
+ import GlobalHeaderCO from '../GlobalHeaderCO';
2
+
3
+ // Tests for the contract formalised in PUI-18647: item-level global attributes are forwarded to
4
+ // whatever an out-of-the-box toolbar item renders as its root. The typeless (icon button) item
5
+ // already behaved this way before the refactor — this pins the baseline so the widened contract
6
+ // cannot quietly move it.
7
+ //
8
+ // Also guards the `type` discriminator: it routes the item internally and must never reach the DOM,
9
+ // because `type` is a reserved HTML attribute.
10
+ //
11
+ // Chrome only: every assertion is a DOM attribute or a rendered text value, identical in every
12
+ // engine. The `|| 'Chrome'` arm is mandatory — BrowserStack capitalises browserName, and a
13
+ // lowercase-only guard registers zero tests while still exiting 0.
14
+ if (
15
+ (!browser.capabilities['ice:options'].isPhone &&
16
+ !browser.capabilities['ice:options'].isTablet &&
17
+ browser.capabilities.browserName === 'chrome') ||
18
+ browser.capabilities.browserName === 'Chrome'
19
+ ) {
20
+ describe('PUI-19016 - GlobalHeader:: typeless toolbar item attributes and onClick - Func', () => {
21
+ before('loading page', async () => {
22
+ const errorOnGo = await GlobalHeaderCO.iconButtonItemURL.go();
23
+ if (errorOnGo) throw errorOnGo;
24
+ });
25
+
26
+ it('01: should forward item-level attributes only to the rendered button', async () => {
27
+ const button = await GlobalHeaderCO.getGHToolbarItem(0);
28
+ await button.waitForDisplayed();
29
+
30
+ // The story wires data-public at item level only, so the button is the single element that may
31
+ // carry it. Asserting the whole collection rather than just the button is what catches the
32
+ // attribute leaking onto an ancestor.
33
+ const publicTestIds = await GlobalHeaderCO.getPublicNodeTestIds();
34
+ await expect(publicTestIds).toEqual(['gh-toolbar-item']);
35
+
36
+ await expect(button).toHaveAttribute('data-dimsum-slot', 'dsGlobalheaderMenubarItemButton');
37
+ // The button's own type, not the toolbar item's routing discriminator.
38
+ await expect(button).toHaveAttribute('type', 'button');
39
+
40
+ const dsTypedFlag = await GlobalHeaderCO.getDsTypedItemExistFlag();
41
+ await expect(dsTypedFlag).toHaveText('No');
42
+ });
43
+
44
+ it('02: should forward the item-level onClick handler to the rendered button', async () => {
45
+ const clickCount = await GlobalHeaderCO.getClickCount();
46
+ await expect(clickCount).toHaveText('0');
47
+
48
+ await (await GlobalHeaderCO.getGHToolbarItem(0)).click();
49
+
50
+ await expect(clickCount).toHaveText('1');
51
+ });
52
+ });
53
+ }
@@ -43,6 +43,8 @@ export default class DSGridCO extends PageObject {
43
43
 
44
44
  static breakpointsV2 = new Urlbuilder(PATH_E2E_GRID, 'with-breakpoints-v-2-test');
45
45
 
46
+ static layoutMode = new Urlbuilder(PATH_E2E_GRID, 'with-layout-mode-test');
47
+
46
48
  static getUrl(component = 'basic') {
47
49
  return PageObject.getUrl(PATH_E2E_GRID, component);
48
50
  }
@@ -70,6 +72,24 @@ export default class DSGridCO extends PageObject {
70
72
  return $(`//div[text()="Scrollable Body"]`);
71
73
  }
72
74
 
75
+ static async getGridByTestId(testId) {
76
+ return $(`[data-testid="${testId}"]`);
77
+ }
78
+
79
+ // Computed grid-template-columns, e.g. "640px 640px 640px". Returned as a string so callers can
80
+ // compare two grids to each other without depending on the resolved px values.
81
+ static async getResolvedColumns(testId) {
82
+ const grid = await this.getGridByTestId(testId);
83
+ await grid.waitForDisplayed();
84
+ return String((await grid.getCSSProperty('grid-template-columns')).value);
85
+ }
86
+
87
+ // Number of resolved column tracks. This is the only cross-browser-stable signal of which
88
+ // breakpoint definition won, and it never asserts px values.
89
+ static async getResolvedColumnCount(testId) {
90
+ return (await this.getResolvedColumns(testId)).trim().split(/\s+/).filter(Boolean).length;
91
+ }
92
+
73
93
  // Snapshots
74
94
  static snapshotPath(example = 'basic') {
75
95
  return PageObject.getSnapshotPathBuilder('Grid', example);
@@ -0,0 +1,51 @@
1
+ import DSGridCO from '../DSGridCO';
2
+
3
+ // Coverage for the withLayoutMode opt-in shipped in PUI-17417: an opted-in Grid collapses its
4
+ // responsive cols/rows/span OBJECT definitions to the single value selected by the ancestor
5
+ // DSLayoutProvider layoutMode instead of the viewport media-query ladder. The feature has no DOM
6
+ // footprint - the only observable effect is the emitted CSS, since the collapsed array takes the
7
+ // Array.isArray branch of manageCols and emits ONE unconditional grid-template-columns. So we
8
+ // assert the resolved column COUNT (whitespace separated tracks of the computed value), never the
9
+ // px values. Chrome only: a computed CSS read is identical across engines.
10
+ if (
11
+ (!browser.capabilities['ice:options'].isPhone &&
12
+ !browser.capabilities['ice:options'].isTablet &&
13
+ browser.capabilities.browserName === 'chrome') ||
14
+ browser.capabilities.browserName === 'Chrome'
15
+ ) {
16
+ describe('PUI-18978 - DSGrid:: withLayoutMode breakpoint resolution - Func', () => {
17
+ before('loading page', async () => {
18
+ const errorOnGo = await DSGridCO.layoutMode.go();
19
+ if (errorOnGo) throw errorOnGo;
20
+ });
21
+
22
+ it('01: should resolve cols from the ancestor layoutMode when opted in', async () => {
23
+ await expect(await DSGridCO.getResolvedColumnCount('ds-grid-layout-mode-l')).toEqual(3);
24
+ await expect(await DSGridCO.getResolvedColumnCount('ds-grid-layout-mode-m')).toEqual(2);
25
+ await expect(await DSGridCO.getResolvedColumnCount('ds-grid-layout-mode-s')).toEqual(1);
26
+ });
27
+
28
+ it('02: should honor an explicit xsmall definition, keeping xs distinct from s', async () => {
29
+ await expect(await DSGridCO.getResolvedColumnCount('ds-grid-layout-mode-xs-explicit')).toEqual(1);
30
+ await expect(await DSGridCO.getResolvedColumnCount('ds-grid-layout-mode-s-explicit')).toEqual(2);
31
+ });
32
+
33
+ it('03: should resolve xs to the small definition when xsmall is not declared', async () => {
34
+ await expect(await DSGridCO.getResolvedColumnCount('ds-grid-layout-mode-xs-backfilled')).toEqual(1);
35
+ });
36
+
37
+ it('04: should ignore the ancestor layoutMode when not opted in', async () => {
38
+ // Width independent invariant: opting out inside a provider must render exactly what the same
39
+ // grid renders with no provider at all. The layoutMode path can never satisfy this.
40
+ const optedOut = await DSGridCO.getResolvedColumns('ds-grid-opt-out-in-provider');
41
+ const noProvider = await DSGridCO.getResolvedColumns('ds-grid-opt-out-no-provider');
42
+ await expect(optedOut).toEqual(noProvider);
43
+
44
+ // The failure mode being guarded against is the provider mode s leaking in, which is exactly
45
+ // 1 column. The correct value is whatever the viewport resolves to (3 above 1024px), so we
46
+ // assert more than one column rather than pinning a viewport width.
47
+ const columnCount = await DSGridCO.getResolvedColumnCount('ds-grid-opt-out-in-provider');
48
+ await expect(columnCount).toBeGreaterThan(1);
49
+ });
50
+ });
51
+ }
@@ -0,0 +1,22 @@
1
+ import DSGridCO from '../DSGridCO';
2
+
3
+ // Coverage for the withLayoutMode opt-in shipped in PUI-17417. Every grid in the story shares one
4
+ // responsive cols definition and one viewport width, so this single snapshot is the human readable
5
+ // proof that each grid lays out for the DSLayoutProvider mode of the container it lives in - and
6
+ // that the two opted-out grids stay viewport driven.
7
+ if (!browser.capabilities['ice:options'].isPhone && !browser.capabilities['ice:options'].isTablet) {
8
+ describe('PUI-18979 - DSGrid:: withLayoutMode breakpoint resolution - Visual', () => {
9
+ before('loading page', async () => {
10
+ const errorOnGo = await DSGridCO.layoutMode.go();
11
+ if (errorOnGo) throw errorOnGo;
12
+ });
13
+
14
+ it('01: should display each grid laid out for its own LayoutProvider mode', async () => {
15
+ const firstGrid = await DSGridCO.getGridByTestId('ds-grid-layout-mode-l');
16
+ await firstGrid.waitForDisplayed();
17
+
18
+ const snapshot = await browser.percyCheckScreenshot(DSGridCO.snapshotPath('layout-mode'));
19
+ await expect(snapshot).toEqual(0);
20
+ });
21
+ });
22
+ }