dimsum-e2e-tests 3.70.0-next.63 → 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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,42 @@
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.69 (2026-08-27)
7
+
8
+ ### Bug Fixes
9
+
10
+ - ds-form-date-range-picker:: add picker callback E2E coverage [PUI-18907](https://jira.elliemae.io/browse/PUI-18907) ([#8343](https://github.com/intcx/PLATFORM-UI.dimsum/issues/8343)) ([74b1724](https://github.com/intcx/PLATFORM-UI.dimsum/commit/74b1724d92c8bc91ee2fdb9106237da382dea311))
11
+
12
+ ## 3.70.0-next.68 (2026-08-26)
13
+
14
+ ### Bug Fixes
15
+
16
+ - ds-pills-v2:: pitfalls label requirement disclaimer and input/error association [PUI-18617](https://jira.elliemae.io/browse/PUI-18617) ([#8335](https://github.com/intcx/PLATFORM-UI.dimsum/issues/8335)) ([050fb4b](https://github.com/intcx/PLATFORM-UI.dimsum/commit/050fb4b91cfc8292f9d7474cbb6c0496d5205661))
17
+
18
+ ## 3.70.0-next.67 (2026-08-26)
19
+
20
+ ### Bug Fixes
21
+
22
+ - ds-global-header:: restored unintentional test-id change to usual one [PUI-19020](https://jira.elliemae.io/browse/PUI-19020) ([#8337](https://github.com/intcx/PLATFORM-UI.dimsum/issues/8337)) ([149cc79](https://github.com/intcx/PLATFORM-UI.dimsum/commit/149cc797c44bf15f3723ec7a19d73311755bf471))
23
+
24
+ ## 3.70.0-next.66 (2026-08-26)
25
+
26
+ ### Features
27
+
28
+ - ds-global-header:: E2E coverage for toolbar item contract passthrough [PUI-18647](https://jira.elliemae.io/browse/PUI-18647) ([#8332](https://github.com/intcx/PLATFORM-UI.dimsum/issues/8332)) ([ad4a2bb](https://github.com/intcx/PLATFORM-UI.dimsum/commit/ad4a2bbb5027f343605cfd164fe5d6265ebd6e9e))
29
+
30
+ ## 3.70.0-next.65 (2026-08-25)
31
+
32
+ ### Bug Fixes
33
+
34
+ - ds-form-date-range-picker:: forward props onPickerOpen and onPickerClose to the calendar [PUI-18907](https://jira.elliemae.io/browse/PUI-18907) ([#8334](https://github.com/intcx/PLATFORM-UI.dimsum/issues/8334)) ([dbbe29c](https://github.com/intcx/PLATFORM-UI.dimsum/commit/dbbe29cabefa96a848441565d4e14a6c94e99bac))
35
+
36
+ ## 3.70.0-next.64 (2026-08-25)
37
+
38
+ ### Features
39
+
40
+ - dimsum:: stories as correct typechecked deliverables [PUI-18610](https://jira.elliemae.io/browse/PUI-18610) ([#8331](https://github.com/intcx/PLATFORM-UI.dimsum/issues/8331)) ([2c4da4c](https://github.com/intcx/PLATFORM-UI.dimsum/commit/2c4da4cda51ce2a4b4e597926f257d144c1494f1))
41
+
6
42
  ## 3.70.0-next.63 (2026-08-22)
7
43
 
8
44
  **Note:** Version bump only for package dimsum-e2e-tests
@@ -18,6 +18,13 @@ export default class DSAppPickerCO extends PageObject {
18
18
 
19
19
  static reflowURL = new Urlbuilder(`${PATH_E2E_APP_PICKER}`, 'reflow-test');
20
20
 
21
+ // PUI-18647 — the two custom-trigger APIs side by side. Storybook kebab-cases the
22
+ // `PUI18647CustomTriggers...` export with a hyphen after the prefix, hence `pui-18647`.
23
+ static customTriggerFocusURL = new Urlbuilder(
24
+ `${PATH_E2E_APP_PICKER}`,
25
+ 'pui-18647-custom-triggers-focus-parity-with-deprecated',
26
+ );
27
+
21
28
  // Snapshots
22
29
  static snapshotPath(example = 'basic') {
23
30
  return PageObject.getSnapshotPathBuilder('AppPicker', example);
@@ -136,6 +143,20 @@ export default class DSAppPickerCO extends PageObject {
136
143
  return (await element.getAttribute('aria-current')) === 'true';
137
144
  }
138
145
 
146
+ // PUI-18647 selectors
147
+
148
+ // The supported API: DSAppPicker hands the trigger renderer `innerRef` plus the internal handlers,
149
+ // so it can drive the first-app/selected-app focus logic on open.
150
+ static async getSupportedCustomTrigger() {
151
+ return $('[data-testid="pui18647-trigger-component"]');
152
+ }
153
+
154
+ // The deprecated API: `renderTrigger` receives only `ref`, so it cannot reach those handlers. It
155
+ // keeps its pre-existing behaviour by design — this is not a bug, it is the documented delta.
156
+ static async getDeprecatedCustomTrigger() {
157
+ return $('[data-testid="pui18647-render-trigger"]');
158
+ }
159
+
139
160
  // Slots
140
161
 
141
162
  static async getItemSlotByIndex(index = 0) {
@@ -0,0 +1,82 @@
1
+ import { Key } from 'webdriverio';
2
+ import DSAppPickerCO from '../DSAppPickerCO';
3
+
4
+ // Tests for the change introduced in PUI-18647 (GlobalHeader -> DSAppPicker contract passthrough).
5
+ // Widening the hand-off meant a consumer can finally supply a custom trigger, which exposed two
6
+ // different focus stories that now need pinning:
7
+ //
8
+ // - `TriggerComponent` (supported) receives `innerRef` AND the internal open handlers, so a keyboard
9
+ // open can run the selected-app focus logic and land on the selected chip.
10
+ // - `renderTrigger` (deprecated) receives only `ref`, so it cannot reach those handlers and falls
11
+ // back to focusing the panel on every path. Unchanged by design.
12
+ //
13
+ // The mouse path is the RE-BASELINE: a TriggerComponent picker opened with the mouse used to land on
14
+ // the FIRST app and now lands on the panel, matching the built-in trigger. Kept on the full browser
15
+ // matrix because pointer-driven focus is exactly where engines disagree.
16
+ if (!browser.capabilities['ice:options'].isPhone && !browser.capabilities['ice:options'].isTablet) {
17
+ describe('PUI-19014 - DSAppPicker:: custom trigger open focus destinations - Func', () => {
18
+ before('loading page', async () => {
19
+ const errorOnGo = await DSAppPickerCO.customTriggerFocusURL.go();
20
+ if (errorOnGo) throw errorOnGo;
21
+ });
22
+
23
+ it('01: should focus the panel when the supported trigger is opened with the mouse', async () => {
24
+ const trigger = await DSAppPickerCO.getSupportedCustomTrigger();
25
+ await trigger.waitForDisplayed();
26
+ await trigger.click();
27
+
28
+ const panel = await DSAppPickerCO.getAppPickerRoot();
29
+ await panel.waitForDisplayed();
30
+ await expect(panel).toBeFocused();
31
+ });
32
+
33
+ it('02: should focus the selected app when the supported trigger is opened with Enter', async () => {
34
+ // Escape closes the panel opened in 01 and returns focus to the trigger, which is also the
35
+ // starting state this case needs.
36
+ await browser.keys(Key.Escape);
37
+ const trigger = await DSAppPickerCO.getSupportedCustomTrigger();
38
+ await expect(trigger).toBeFocused();
39
+
40
+ await browser.keys(Key.Enter);
41
+ await (await DSAppPickerCO.getAppPickerRoot()).waitForDisplayed();
42
+
43
+ // 'Reports' is deliberately the third app, so "landed on the selected app" cannot be mistaken
44
+ // for "landed on the first app".
45
+ const selectedChip = await DSAppPickerCO.getChipByLabel('Reports');
46
+ await expect(selectedChip).toBeFocused();
47
+ await expect(await DSAppPickerCO.isSelected(selectedChip)).toBe(true);
48
+ });
49
+
50
+ it('03: should focus the panel when the deprecated trigger is opened with the mouse', async () => {
51
+ await browser.keys(Key.Escape);
52
+
53
+ const trigger = await DSAppPickerCO.getDeprecatedCustomTrigger();
54
+ await trigger.waitForDisplayed();
55
+ await trigger.click();
56
+
57
+ const panel = await DSAppPickerCO.getAppPickerRoot();
58
+ await panel.waitForDisplayed();
59
+ await expect(panel).toBeFocused();
60
+ });
61
+
62
+ it('04: should focus the panel when the deprecated trigger is opened with Enter', async () => {
63
+ // The panel 03 left open is controlled from the story and has no Escape handler, so it is
64
+ // dismissed by clicking its trigger again. Where a dismissal click leaves focus is NOT what
65
+ // this case is about and is not consistent across engines — clicking a <button> does not focus
66
+ // it on macOS, and the panel unmounting mid-click can blur. So the keyboard starting state is
67
+ // established explicitly instead of being inherited from the click.
68
+ const trigger = await DSAppPickerCO.getDeprecatedCustomTrigger();
69
+ await trigger.click();
70
+ await (await DSAppPickerCO.getAppPickerRoot()).waitForDisplayed({ reverse: true });
71
+
72
+ await browser.execute((el) => el.focus(), trigger);
73
+ await expect(trigger).toBeFocused();
74
+
75
+ await browser.keys(Key.Enter);
76
+
77
+ const panel = await DSAppPickerCO.getAppPickerRoot();
78
+ await panel.waitForDisplayed();
79
+ await expect(panel).toBeFocused();
80
+ });
81
+ });
82
+ }
@@ -0,0 +1,108 @@
1
+ import { Key } from 'webdriverio';
2
+ import DSAppPickerCO from '../DSAppPickerCO';
3
+ import { mouseOver } from '../../helpers';
4
+
5
+ if (!browser.capabilities['ice:options'].isPhone && !browser.capabilities['ice:options'].isTablet) {
6
+ const PARKED_X = 10;
7
+ const PARKED_Y = 10;
8
+
9
+ // Enabled and long enough to truncate. Chip 1 of the apps list is used rather than the first one,
10
+ // which is natively disabled and tracked separately under PUI-18981.
11
+ const TRUNCATING_CHIP = 'Pipeline but with a really long label that will truncate';
12
+
13
+ const ARIA_DISABLED_CHIP = 'Dashboard but with a really long label that will truncate';
14
+
15
+ describe('PUI-18986 - DSAppPicker:: Escape dismisses the expanded chip label - Visual', () => {
16
+ before('loading page', async () => {
17
+ const errorOnGo = await DSAppPickerCO.basicURL.go();
18
+ if (errorOnGo) throw errorOnGo;
19
+ await browser.maximizeWindow();
20
+ await DSAppPickerCO.openPanel();
21
+ });
22
+
23
+ it('01: truncated chip expands its label on hover', async () => {
24
+ await mouseOver(await DSAppPickerCO.getChipByLabel(TRUNCATING_CHIP));
25
+ const snapshot = await browser.percyCheckScreenshot(DSAppPickerCO.snapshotPath('esc-dismiss-hover-expanded'));
26
+ await expect(snapshot).toEqual(0);
27
+ });
28
+
29
+ it('02: Escape collapses the hover-expanded label and keeps the panel open', async () => {
30
+ await browser.keys(Key.Escape);
31
+ const snapshot = await browser.percyCheckScreenshot(DSAppPickerCO.snapshotPath('esc-dismiss-hover-collapsed'));
32
+ await expect(snapshot).toEqual(0);
33
+ });
34
+
35
+ it('03: label expands again once the pointer leaves and returns', async () => {
36
+ await mouseOver(null, PARKED_X, PARKED_Y);
37
+ await mouseOver(await DSAppPickerCO.getChipByLabel(TRUNCATING_CHIP));
38
+ const snapshot = await browser.percyCheckScreenshot(DSAppPickerCO.snapshotPath('esc-dismiss-rehover-expanded'));
39
+ await expect(snapshot).toEqual(0);
40
+ });
41
+
42
+ it('04: truncated chip expands its label on focus alone', async () => {
43
+ await mouseOver(null, PARKED_X, PARKED_Y);
44
+ await browser.keys(Key.Tab);
45
+ await expect(await DSAppPickerCO.getChipByLabel(TRUNCATING_CHIP)).toBeFocused();
46
+ const snapshot = await browser.percyCheckScreenshot(DSAppPickerCO.snapshotPath('esc-dismiss-focus-expanded'));
47
+ await expect(snapshot).toEqual(0);
48
+ });
49
+
50
+ it('05: Escape collapses the focus-expanded label without moving focus', async () => {
51
+ await browser.keys(Key.Escape);
52
+ await expect(await DSAppPickerCO.getChipByLabel(TRUNCATING_CHIP)).toBeFocused();
53
+ const snapshot = await browser.percyCheckScreenshot(DSAppPickerCO.snapshotPath('esc-dismiss-focus-collapsed'));
54
+ await expect(snapshot).toEqual(0);
55
+ });
56
+
57
+ it('06: a further Escape closes the panel once the label is collapsed', async () => {
58
+ await browser.keys(Key.Escape);
59
+ await (await DSAppPickerCO.getAppPickerRoot()).waitForDisplayed({ reverse: true });
60
+ const snapshot = await browser.percyCheckScreenshot(DSAppPickerCO.snapshotPath('esc-dismiss-panel-closed'));
61
+ await expect(snapshot).toEqual(0);
62
+ });
63
+ });
64
+
65
+ describe('PUI-18987 - DSAppPicker:: Escape dismisses the expanded aria-disabled chip label - Visual', () => {
66
+ before('loading page', async () => {
67
+ const errorOnGo = await DSAppPickerCO.applyAriaDisabledURL.go();
68
+ if (errorOnGo) throw errorOnGo;
69
+ await browser.maximizeWindow();
70
+ await DSAppPickerCO.openPanel();
71
+ });
72
+
73
+ it('01: aria-disabled truncated chip expands its label on hover', async () => {
74
+ await mouseOver(await DSAppPickerCO.getChipByLabel(ARIA_DISABLED_CHIP));
75
+ const snapshot = await browser.percyCheckScreenshot(
76
+ DSAppPickerCO.snapshotPath('aria-disabled-esc-dismiss-hover-expanded'),
77
+ );
78
+ await expect(snapshot).toEqual(0);
79
+ });
80
+
81
+ it('02: Escape collapses the hover-expanded label and keeps the panel open', async () => {
82
+ await browser.keys(Key.Escape);
83
+ const snapshot = await browser.percyCheckScreenshot(
84
+ DSAppPickerCO.snapshotPath('aria-disabled-esc-dismiss-hover-collapsed'),
85
+ );
86
+ await expect(snapshot).toEqual(0);
87
+ });
88
+
89
+ it('03: aria-disabled truncated chip expands its label on keyboard focus', async () => {
90
+ await mouseOver(null, PARKED_X, PARKED_Y);
91
+ await browser.keys(Key.Tab);
92
+ await expect(await DSAppPickerCO.getChipByLabel(ARIA_DISABLED_CHIP)).toBeFocused();
93
+ const snapshot = await browser.percyCheckScreenshot(
94
+ DSAppPickerCO.snapshotPath('aria-disabled-esc-dismiss-focus-expanded'),
95
+ );
96
+ await expect(snapshot).toEqual(0);
97
+ });
98
+
99
+ it('04: Escape collapses the focus-expanded label without moving focus', async () => {
100
+ await browser.keys(Key.Escape);
101
+ await expect(await DSAppPickerCO.getChipByLabel(ARIA_DISABLED_CHIP)).toBeFocused();
102
+ const snapshot = await browser.percyCheckScreenshot(
103
+ DSAppPickerCO.snapshotPath('aria-disabled-esc-dismiss-focus-collapsed'),
104
+ );
105
+ await expect(snapshot).toEqual(0);
106
+ });
107
+ });
108
+ }
@@ -0,0 +1,84 @@
1
+ import { Key } from 'webdriverio';
2
+ import DSChipCO from '../DSChipCO';
3
+ import { mouseOver } from '../../helpers';
4
+
5
+ if (!browser.capabilities['ice:options'].isPhone && !browser.capabilities['ice:options'].isTablet) {
6
+ const PARKED_X = 10;
7
+ const PARKED_Y = 10;
8
+
9
+ // Indexes into the basic-test story grid:
10
+ // 0 Large | 1 Small | 2 Selected | 3 Truncated with large label | 4 Disabled | 5 wrapText with large label
11
+ const TRUNCATING_CHIP = 3;
12
+ const WRAP_TEXT_CHIP = 5;
13
+
14
+ // DSChipCO deliberately has no label lookup, so the tab target is reached by walking the tab order.
15
+ // The natively disabled chip drops out of it on its own, which keeps this independent of the story's
16
+ // exact number of focus stops.
17
+ const MAX_TAB_STOPS = 12;
18
+ const tabUntilFocused = async (index) => {
19
+ const target = await DSChipCO.getChipButtonByIndex(index);
20
+ for (let i = 0; i < MAX_TAB_STOPS; i += 1) {
21
+ // eslint-disable-next-line no-await-in-loop
22
+ if (await target.isFocused()) return target;
23
+ // eslint-disable-next-line no-await-in-loop
24
+ await browser.keys(Key.Tab);
25
+ }
26
+ throw new Error(`chip ${index} never took focus within ${MAX_TAB_STOPS} tab stops`);
27
+ };
28
+
29
+ describe('PUI-19009 - DSChip:: Escape dismisses the expanded truncated chip label - Visual', () => {
30
+ before('loading page', async () => {
31
+ const errorOnGo = await DSChipCO.basicURL.go();
32
+ if (errorOnGo) throw errorOnGo;
33
+ await browser.maximizeWindow();
34
+ await (await DSChipCO.getChipByIndex(TRUNCATING_CHIP)).waitForDisplayed();
35
+ });
36
+
37
+ it('01: truncated chip expands its label on hover', async () => {
38
+ await mouseOver(await DSChipCO.getChipByIndex(TRUNCATING_CHIP));
39
+ const snapshot = await browser.percyCheckScreenshot(DSChipCO.snapshotPath('esc-dismiss-hover-expanded'));
40
+ await expect(snapshot).toEqual(0);
41
+ });
42
+
43
+ it('02: Escape collapses the hover-expanded label without moving the pointer', async () => {
44
+ await browser.keys(Key.Escape);
45
+ const snapshot = await browser.percyCheckScreenshot(DSChipCO.snapshotPath('esc-dismiss-hover-collapsed'));
46
+ await expect(snapshot).toEqual(0);
47
+ });
48
+
49
+ it('03: label stays collapsed while the pointer remains on the chip', async () => {
50
+ await mouseOver(await DSChipCO.getChipByIndex(TRUNCATING_CHIP), 12, 12);
51
+ const snapshot = await browser.percyCheckScreenshot(DSChipCO.snapshotPath('esc-dismiss-hover-still-collapsed'));
52
+ await expect(snapshot).toEqual(0);
53
+ });
54
+
55
+ it('04: label expands again once the pointer leaves and returns', async () => {
56
+ await mouseOver(null, PARKED_X, PARKED_Y);
57
+ await mouseOver(await DSChipCO.getChipByIndex(TRUNCATING_CHIP));
58
+ const snapshot = await browser.percyCheckScreenshot(DSChipCO.snapshotPath('esc-dismiss-rehover-expanded'));
59
+ await expect(snapshot).toEqual(0);
60
+ });
61
+
62
+ it('05: truncated chip expands its label on focus alone', async () => {
63
+ await mouseOver(null, PARKED_X, PARKED_Y);
64
+ const chipButton = await tabUntilFocused(TRUNCATING_CHIP);
65
+ await expect(chipButton).toBeFocused();
66
+ const snapshot = await browser.percyCheckScreenshot(DSChipCO.snapshotPath('esc-dismiss-focus-expanded'));
67
+ await expect(snapshot).toEqual(0);
68
+ });
69
+
70
+ it('06: Escape collapses the focus-expanded label without moving focus', async () => {
71
+ await browser.keys(Key.Escape);
72
+ await expect(await DSChipCO.getChipButtonByIndex(TRUNCATING_CHIP)).toBeFocused();
73
+ const snapshot = await browser.percyCheckScreenshot(DSChipCO.snapshotPath('esc-dismiss-focus-collapsed'));
74
+ await expect(snapshot).toEqual(0);
75
+ });
76
+
77
+ it('07: a wrapText chip never expands, so Escape leaves it untouched', async () => {
78
+ await mouseOver(await DSChipCO.getChipByIndex(WRAP_TEXT_CHIP));
79
+ await browser.keys(Key.Escape);
80
+ const snapshot = await browser.percyCheckScreenshot(DSChipCO.snapshotPath('esc-dismiss-wrap-text-unchanged'));
81
+ await expect(snapshot).toEqual(0);
82
+ });
83
+ });
84
+ }
@@ -30,6 +30,8 @@ export default class DateRangePickerCO extends PageObject {
30
30
  'apply-aria-disabled-invalid-selection-test',
31
31
  );
32
32
 
33
+ static pickerCallbacks = new Urlbuilder(PATH_E2E_CONTROLLED_FORM_DATE_RANGE_PICKER, 'picker-callbacks-test');
34
+
33
35
  // Story elements
34
36
 
35
37
  static getNextMonthBtn = async () => $('[data-testid="ds-controlled-date-time-picker-calendar-next-month"]');
@@ -68,6 +70,10 @@ export default class DateRangePickerCO extends PageObject {
68
70
  static getClearableByIndex = async (index = 0) =>
69
71
  $$(`[data-testid="ds-controlled-date-time-picker-clear-btn"]`)[index];
70
72
 
73
+ static getOpenCallbackCount = async () => $('[data-testid="picker-open-count"]');
74
+
75
+ static getCloseCallbackCount = async () => $('[data-testid="picker-close-count"]');
76
+
71
77
  // TimeInput
72
78
  static getHourInput = async () => $('[data-testid="ds-controlled-date-time-picker-hour-input"]');
73
79
 
@@ -0,0 +1,36 @@
1
+ import { Key } from 'webdriverio';
2
+ import DateRangePickerCO from '../DateRangePickerCO';
3
+
4
+ if (
5
+ (!browser.capabilities['ice:options'].isPhone &&
6
+ !browser.capabilities['ice:options'].isTablet &&
7
+ browser.capabilities.browserName === 'chrome') ||
8
+ browser.capabilities.browserName === 'Chrome'
9
+ ) {
10
+ // Tests for the fix introduced in PUI-18907 (forward picker open and close callbacks).
11
+ describe('PUI-19082 - DSControlledDateRangePicker:: picker open and close callbacks - Func', () => {
12
+ before('loading page', async () => {
13
+ const errorOnGo = await DateRangePickerCO.pickerCallbacks.go();
14
+ if (errorOnGo) throw errorOnGo;
15
+ });
16
+
17
+ it('01: should invoke onPickerOpen when the calendar opens', async () => {
18
+ const calendarTrigger = await DateRangePickerCO.getPickerBtn();
19
+ const openCallbackCount = await DateRangePickerCO.getOpenCallbackCount();
20
+
21
+ await expect(openCallbackCount).toHaveText('0');
22
+ await calendarTrigger.click();
23
+ await expect(await DateRangePickerCO.getCalendarDialog()).toBeDisplayedInViewport();
24
+ await expect(openCallbackCount).toHaveText('1');
25
+ });
26
+
27
+ it('02: should invoke onPickerClose when the calendar closes', async () => {
28
+ const closeCallbackCount = await DateRangePickerCO.getCloseCallbackCount();
29
+
30
+ await expect(closeCallbackCount).toHaveText('0');
31
+ await browser.keys(Key.Escape);
32
+ await expect(await DateRangePickerCO.getCalendarDialog()).not.toBeDisplayedInViewport();
33
+ await expect(closeCallbackCount).toHaveText('1');
34
+ });
35
+ });
36
+ }
@@ -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
+ }