@elastic/eui-test-helpers 1.1.0-snapshot.1783939453438

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 (36) hide show
  1. package/LICENSE.txt +6 -0
  2. package/README.md +56 -0
  3. package/lib/cjs/components/combo_box/selectors.d.ts +72 -0
  4. package/lib/cjs/components/combo_box/selectors.js +86 -0
  5. package/lib/cjs/index.d.ts +2 -0
  6. package/lib/cjs/index.js +19 -0
  7. package/lib/cjs/playwright/base_object.d.ts +31 -0
  8. package/lib/cjs/playwright/base_object.js +50 -0
  9. package/lib/cjs/playwright/components/combo_box/object.d.ts +88 -0
  10. package/lib/cjs/playwright/components/combo_box/object.js +220 -0
  11. package/lib/cjs/selectors.d.ts +1 -0
  12. package/lib/cjs/selectors.js +12 -0
  13. package/lib/cjs/storybook.d.ts +7 -0
  14. package/lib/cjs/storybook.js +22 -0
  15. package/lib/esm/components/combo_box/selectors.d.ts +72 -0
  16. package/lib/esm/components/combo_box/selectors.js +80 -0
  17. package/lib/esm/components/combo_box/selectors.js.map +1 -0
  18. package/lib/esm/index.d.ts +2 -0
  19. package/lib/esm/index.js +10 -0
  20. package/lib/esm/index.js.map +1 -0
  21. package/lib/esm/playwright/base_object.d.ts +31 -0
  22. package/lib/esm/playwright/base_object.js +30 -0
  23. package/lib/esm/playwright/base_object.js.map +1 -0
  24. package/lib/esm/playwright/components/combo_box/object.d.ts +88 -0
  25. package/lib/esm/playwright/components/combo_box/object.js +210 -0
  26. package/lib/esm/playwright/components/combo_box/object.js.map +1 -0
  27. package/lib/esm/selectors.d.ts +1 -0
  28. package/lib/esm/selectors.js +9 -0
  29. package/lib/esm/selectors.js.map +1 -0
  30. package/lib/esm/storybook.d.ts +7 -0
  31. package/lib/esm/storybook.js +15 -0
  32. package/lib/esm/storybook.js.map +1 -0
  33. package/licenses/ELASTIC-LICENSE-2.0.md +93 -0
  34. package/licenses/SSPL-LICENSE.md +557 -0
  35. package/package.json +62 -0
  36. package/src/components/combo_box/README.md +29 -0
package/LICENSE.txt ADDED
@@ -0,0 +1,6 @@
1
+ Source code in this repository is covered by (i) a dual license under the Server
2
+ Side Public License, v 1 and the Elastic License 2.0 or (ii) an Apache License
3
+ 2.0 compatible license or (iii) solely under the Elastic License 2.0, in each
4
+ case, as noted in the applicable header. The default throughout the repository
5
+ is a dual license under the Server Side Public License, v 1 and the Elastic
6
+ License 2.0, unless the header specifies another license.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # EUI Test Helpers
2
+
3
+ `@elastic/eui-test-helpers` is a library of test helpers to use when testing EUI components
4
+ with [Cypress], [React Testing Library], or [Scout]. It provides assertion, find, and query helpers to make interacting
5
+ with EUI components easy and reliable. Say goodbye to finding CSS selectors to query that _seem_ right.
6
+
7
+ > [!NOTE]
8
+ > This library is in early stages of development and is missing many useful utilities. Please contribute or
9
+ > [open a feature request](https://github.com/elastic/eui/issues/new?template=feature_request.md) if you notice
10
+ > anything missing!
11
+
12
+ This library provides utilities to EUI components that are non-trivial to write tests for. Please use the built-in
13
+ methods for testing simple, native-like components that are provided by your testing framework of choice.
14
+
15
+ ## Installation
16
+
17
+ This library is versioned independently from `@elastic/eui`. Because the helpers target EUI's component DOM and `data-test-subj`s, pick a version compatible with the `@elastic/eui` version you're testing against.
18
+
19
+ ```shell
20
+ yarn add --dev @elastic/eui-test-helpers
21
+ ```
22
+
23
+ ## Playwright Component Objects
24
+
25
+ For Playwright/Scout consumers, this package ships **Component Objects** —
26
+ semantic wrappers around a single Playwright `Locator` that encapsulate
27
+ user-like interactions for a specific EUI component.
28
+
29
+ ```ts
30
+ import { EuiComboBoxObject } from '@elastic/eui-test-helpers';
31
+
32
+ const comboBox = new EuiComboBoxObject(page, 'dataViewSelector');
33
+ await comboBox.setSelectedOptions(['logs-*']);
34
+ expect(await comboBox.getSelectedOptions()).toEqual(['logs-*']);
35
+ ```
36
+
37
+ Every Component Object constructor takes `(scope, testSubj)`:
38
+ - `scope` — a Playwright `Page` or `Locator` to search within
39
+ - `testSubj` — the `data-test-subj` value you set on the component's root element in your app
40
+
41
+ `@playwright/test` is declared as a `peerDependency` — consumers provide
42
+ their own version at runtime.
43
+
44
+ ### Available Component Objects
45
+
46
+ | Component | Documentation |
47
+ |---|---|
48
+ | `EuiComboBoxObject` | [src/components/combo_box/README.md](src/components/combo_box/README.md) |
49
+
50
+ ## Contributing
51
+
52
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for design principles, directory structure, and how to add new Component Objects.
53
+
54
+ [Cypress]: https://github.com/cypress-io/cypress
55
+ [React Testing Library]: https://github.com/testing-library/react-testing-library
56
+ [Scout]: https://github.com/elastic/kibana/tree/main/src/platform/packages/shared/kbn-scout
@@ -0,0 +1,72 @@
1
+ /**
2
+ * List of available stable selectors for
3
+ * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}
4
+ */
5
+ export declare const EuiComboBoxSelectors: {
6
+ /**
7
+ * `data-test-subj` identifier of the inner input wrapper element
8
+ */
9
+ INPUT_WRAPPER_TEST_SUBJ: string;
10
+ /**
11
+ * `data-test-subj` identifier of the search input field
12
+ */
13
+ SEARCH_INPUT_TEST_SUBJ: string;
14
+ /**
15
+ * `data-test-subj` identifier of the options list
16
+ */
17
+ OPTIONS_LIST_TEST_SUBJ: string;
18
+ /**
19
+ * `data-test-subj` identifier of the options list toggle button
20
+ */
21
+ OPTIONS_LIST_TOGGLE_BUTTON_TEST_SUBJ: string;
22
+ /**
23
+ * `data-test-subj` identifier of the clear button
24
+ */
25
+ CLEAR_BUTTON_TEST_SUBJ: string;
26
+ /**
27
+ * CSS selector for the input wrapper in `singleSelection={{ asPlainText: true }}` mode.
28
+ * Present when the combo renders the selection inside the input instead of as pills.
29
+ */
30
+ PLAIN_TEXT_INPUT_WRAP_SELECTOR: string;
31
+ /**
32
+ * `data-test-subj` identifier of selected option pills.
33
+ * Only present in non-`asPlainText` mode — in `asPlainText` mode the
34
+ * selection is shown inside the input, not as pills.
35
+ */
36
+ PILL_TEST_SUBJ: string;
37
+ /**
38
+ * CSS selector for all options in a specific combo box's dropdown.
39
+ *
40
+ * `testSubj` is the consumer's `data-test-subj` on `<EuiComboBox>`. EUI
41
+ * propagates this to the options list as `${testSubj}-optionsList`,
42
+ * letting us disambiguate when multiple combo boxes coexist on one page.
43
+ *
44
+ * To target a specific option by label, compose with Playwright's
45
+ * `getByTitle` to avoid CSS-injection issues with labels that contain
46
+ * special characters (`"`, `]`, `\`):
47
+ *
48
+ * ```ts
49
+ * page
50
+ * .locator(EuiComboBoxSelectors.optionFor(testSubj))
51
+ * .and(page.getByTitle(label, { exact: true }))
52
+ * ```
53
+ *
54
+ * Note: the list may be virtualized — type the search string into the
55
+ * input before asserting on a specific option to ensure it is in DOM.
56
+ */
57
+ optionFor: (testSubj: string) => string;
58
+ /**
59
+ * CSS selector for all selected options in a specific combo box's dropdown.
60
+ * See `optionFor` for `testSubj` rationale and label-targeting guidance.
61
+ */
62
+ selectedOptionFor: (testSubj: string) => string;
63
+ /**
64
+ * CSS selector for a specific combo box's options list container (the
65
+ * dropdown portal). Matches whether the list has regular options or an
66
+ * empty-state entry (e.g. "Add X as a custom option" for `onCreateOption`).
67
+ *
68
+ * Use this when you only need to wait for the dropdown to open, not when
69
+ * you need to target a specific option inside it.
70
+ */
71
+ optionsListFor: (testSubj: string) => string;
72
+ };
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.EuiComboBoxSelectors = void 0;
7
+ /*
8
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
9
+ * or more contributor license agreements. Licensed under the Elastic License
10
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
11
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
12
+ * Side Public License, v 1.
13
+ */
14
+
15
+ /**
16
+ * List of available stable selectors for
17
+ * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}
18
+ */
19
+ const EuiComboBoxSelectors = exports.EuiComboBoxSelectors = {
20
+ /**
21
+ * `data-test-subj` identifier of the inner input wrapper element
22
+ */
23
+ INPUT_WRAPPER_TEST_SUBJ: 'comboBoxInput',
24
+ /**
25
+ * `data-test-subj` identifier of the search input field
26
+ */
27
+ SEARCH_INPUT_TEST_SUBJ: 'comboBoxSearchInput',
28
+ /**
29
+ * `data-test-subj` identifier of the options list
30
+ */
31
+ OPTIONS_LIST_TEST_SUBJ: 'comboBoxOptionsList',
32
+ /**
33
+ * `data-test-subj` identifier of the options list toggle button
34
+ */
35
+ OPTIONS_LIST_TOGGLE_BUTTON_TEST_SUBJ: 'comboBoxToggleListButton',
36
+ /**
37
+ * `data-test-subj` identifier of the clear button
38
+ */
39
+ CLEAR_BUTTON_TEST_SUBJ: 'comboBoxClearButton',
40
+ /**
41
+ * CSS selector for the input wrapper in `singleSelection={{ asPlainText: true }}` mode.
42
+ * Present when the combo renders the selection inside the input instead of as pills.
43
+ */
44
+ PLAIN_TEXT_INPUT_WRAP_SELECTOR: '.euiComboBox__inputWrap--plainText',
45
+ /**
46
+ * `data-test-subj` identifier of selected option pills.
47
+ * Only present in non-`asPlainText` mode — in `asPlainText` mode the
48
+ * selection is shown inside the input, not as pills.
49
+ */
50
+ PILL_TEST_SUBJ: 'euiComboBoxPill',
51
+ /**
52
+ * CSS selector for all options in a specific combo box's dropdown.
53
+ *
54
+ * `testSubj` is the consumer's `data-test-subj` on `<EuiComboBox>`. EUI
55
+ * propagates this to the options list as `${testSubj}-optionsList`,
56
+ * letting us disambiguate when multiple combo boxes coexist on one page.
57
+ *
58
+ * To target a specific option by label, compose with Playwright's
59
+ * `getByTitle` to avoid CSS-injection issues with labels that contain
60
+ * special characters (`"`, `]`, `\`):
61
+ *
62
+ * ```ts
63
+ * page
64
+ * .locator(EuiComboBoxSelectors.optionFor(testSubj))
65
+ * .and(page.getByTitle(label, { exact: true }))
66
+ * ```
67
+ *
68
+ * Note: the list may be virtualized — type the search string into the
69
+ * input before asserting on a specific option to ensure it is in DOM.
70
+ */
71
+ optionFor: testSubj => `[data-test-subj~="${testSubj}-optionsList"] [role="option"]`,
72
+ /**
73
+ * CSS selector for all selected options in a specific combo box's dropdown.
74
+ * See `optionFor` for `testSubj` rationale and label-targeting guidance.
75
+ */
76
+ selectedOptionFor: testSubj => `[data-test-subj~="${testSubj}-optionsList"] [role="option"][aria-selected="true"]`,
77
+ /**
78
+ * CSS selector for a specific combo box's options list container (the
79
+ * dropdown portal). Matches whether the list has regular options or an
80
+ * empty-state entry (e.g. "Add X as a custom option" for `onCreateOption`).
81
+ *
82
+ * Use this when you only need to wait for the dropdown to open, not when
83
+ * you need to target a specific option inside it.
84
+ */
85
+ optionsListFor: testSubj => `[data-test-subj~="${testSubj}-optionsList"]`
86
+ };
@@ -0,0 +1,2 @@
1
+ export { BaseObject, type ObjectScope } from './playwright/base_object';
2
+ export { EuiComboBoxObject } from './playwright/components/combo_box/object';
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "BaseObject", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _base_object.BaseObject;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "EuiComboBoxObject", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _object.EuiComboBoxObject;
16
+ }
17
+ });
18
+ var _base_object = require("./playwright/base_object");
19
+ var _object = require("./playwright/components/combo_box/object");
@@ -0,0 +1,31 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+ export type ObjectScope = Page | Locator | BaseObject;
3
+ /**
4
+ * Base class for Playwright Component Objects — semantic wrappers around a
5
+ * single root `Locator` resolved from a `data-test-subj` inside the given
6
+ * scope. Subclasses compose: pass another Component Object as `scope` to
7
+ * nest one inside the other's DOM subtree.
8
+ *
9
+ * Requires `testIdAttribute: 'data-test-subj'` in the Playwright config.
10
+ */
11
+ export declare abstract class BaseObject {
12
+ /**
13
+ * Available to subclasses for queries outside `root`'s subtree but within
14
+ * the original scope (e.g. siblings, related controls).
15
+ */
16
+ protected readonly scope: Page | Locator;
17
+ protected readonly root: Locator;
18
+ /**
19
+ * Retained so subclasses can disambiguate portal-rendered content per
20
+ * instance — e.g. EUI propagates this as `${testSubj}-optionsList` to a
21
+ * combo box's options list, letting us scope queries to the right combo
22
+ * when several exist on a page.
23
+ */
24
+ protected readonly testSubj: string;
25
+ constructor(scope: ObjectScope, testSubj: string);
26
+ /**
27
+ * Underlying `Locator` — escape hatch for assertions or scoping the
28
+ * Component Object's API doesn't cover.
29
+ */
30
+ get locator(): Locator;
31
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.BaseObject = void 0;
7
+ /*
8
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
9
+ * or more contributor license agreements. Licensed under the Elastic License
10
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
11
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
12
+ * Side Public License, v 1.
13
+ */
14
+
15
+ /**
16
+ * Base class for Playwright Component Objects — semantic wrappers around a
17
+ * single root `Locator` resolved from a `data-test-subj` inside the given
18
+ * scope. Subclasses compose: pass another Component Object as `scope` to
19
+ * nest one inside the other's DOM subtree.
20
+ *
21
+ * Requires `testIdAttribute: 'data-test-subj'` in the Playwright config.
22
+ */
23
+ class BaseObject {
24
+ /**
25
+ * Available to subclasses for queries outside `root`'s subtree but within
26
+ * the original scope (e.g. siblings, related controls).
27
+ */
28
+
29
+ /**
30
+ * Retained so subclasses can disambiguate portal-rendered content per
31
+ * instance — e.g. EUI propagates this as `${testSubj}-optionsList` to a
32
+ * combo box's options list, letting us scope queries to the right combo
33
+ * when several exist on a page.
34
+ */
35
+
36
+ constructor(scope, testSubj) {
37
+ this.scope = scope instanceof BaseObject ? scope.locator : scope;
38
+ this.root = this.scope.getByTestId(testSubj);
39
+ this.testSubj = testSubj;
40
+ }
41
+
42
+ /**
43
+ * Underlying `Locator` — escape hatch for assertions or scoping the
44
+ * Component Object's API doesn't cover.
45
+ */
46
+ get locator() {
47
+ return this.root;
48
+ }
49
+ }
50
+ exports.BaseObject = BaseObject;
@@ -0,0 +1,88 @@
1
+ import { BaseObject } from '../../base_object';
2
+ /**
3
+ * Playwright Component Object for {@link
4
+ * https://eui.elastic.co/docs/components/forms/selection/combo-box/ EuiComboBox}.
5
+ *
6
+ * `testSubj` must match the `data-test-subj` set by the consumer on the
7
+ * `<EuiComboBox>` element (the outer `.euiComboBox` wrapper, not the inner
8
+ * `comboBoxInput`).
9
+ */
10
+ export declare class EuiComboBoxObject extends BaseObject {
11
+ /**
12
+ * Replace the current selection with `labels`. Set-semantics: order-
13
+ * independent — already-selected labels are kept, missing ones are added,
14
+ * extras are removed. No-op if the current selection already matches.
15
+ *
16
+ * Throws with a descriptive message if any label never appears in the
17
+ * dropdown (catches test/data drift early).
18
+ */
19
+ setSelectedOptions(labels: string[]): Promise<void>;
20
+ /**
21
+ * Clear all selected options. No-op if nothing is selected.
22
+ *
23
+ * Auto-detects the combo box configuration and uses the appropriate strategy:
24
+ * - Pills present → {@link clickPillClearButtons}
25
+ * - `asPlainText` with a confirmed input selection → {@link deleteSearchInput}
26
+ * - Otherwise → {@link deselectAllFromDropdown}
27
+ *
28
+ * Use the explicit methods directly when you need full control over the
29
+ * clearing strategy (e.g. `onCreateOption` with selections not in the
30
+ * options list).
31
+ */
32
+ clear(): Promise<void>;
33
+ /**
34
+ * Currently selected option labels.
35
+ *
36
+ * - Multi-select / `singleSelection=true` → pill texts.
37
+ * - `singleSelection={{ asPlainText: true }}` → the input value. EUI
38
+ * renders no pills in this mode; the input IS the selection display.
39
+ * Works correctly with both `isClearable=true` (default) and
40
+ * `isClearable=false`.
41
+ * - Nothing selected → `[]`.
42
+ */
43
+ getSelectedOptions(): Promise<string[]>;
44
+ private hasPills;
45
+ /**
46
+ * Clicks the `×` button on each selected pill individually.
47
+ * Works regardless of `isClearable` — pill close buttons are always present.
48
+ * No-op if no pills are rendered.
49
+ */
50
+ private clickPillClearButtons;
51
+ /**
52
+ * Opens the dropdown and clicks each `aria-selected="true"` option to
53
+ * deselect it. Works for all `isClearable` and `singleSelection`
54
+ * configurations when the selected options are present in the options list.
55
+ *
56
+ * Does not work when the selection was created via `onCreateOption` and the
57
+ * created option was not added back to the `options` array — use
58
+ * {@link clickPillClearButtons} instead.
59
+ */
60
+ private deselectAllFromDropdown;
61
+ private addOption;
62
+ /**
63
+ * Focuses the search input and presses Backspace to clear the selection.
64
+ * Only valid in `singleSelection={{ asPlainText: true }}` mode where
65
+ * {@link hasConfirmedInputSelection} is true — EUI's `onKeyDown` handler
66
+ * fires `onRemoveOption` when Backspace is pressed with an empty `searchValue`.
67
+ */
68
+ private deleteSearchInput;
69
+ /**
70
+ * Returns true when the combo is in `asPlainText` mode and the input
71
+ * contains a *confirmed* selection (not unconfirmed typed text).
72
+ *
73
+ * In `asPlainText` mode the input IS the selection display: EUI renders
74
+ * the selected option's label directly in the input (no pills). An empty
75
+ * input means nothing is selected.
76
+ *
77
+ * When the user types text that matches no option and blurs (without
78
+ * `onCreateOption`), EUI marks the combo box invalid by adding
79
+ * `euiComboBox-isInvalid` to the root element. An invalid combo with a
80
+ * non-empty input has unconfirmed text, not a confirmed selection.
81
+ */
82
+ private hasConfirmedInputSelection;
83
+ private isMarkedInvalid;
84
+ private get input();
85
+ private get searchInput();
86
+ private get pills();
87
+ private isPlainText;
88
+ }
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.EuiComboBoxObject = void 0;
7
+ var _test = require("@playwright/test");
8
+ var _base_object = require("../../base_object");
9
+ var _selectors = require("../../../components/combo_box/selectors");
10
+ /*
11
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
12
+ * or more contributor license agreements. Licensed under the Elastic License
13
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
14
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
15
+ * Side Public License, v 1.
16
+ */
17
+
18
+ /**
19
+ * Playwright Component Object for {@link
20
+ * https://eui.elastic.co/docs/components/forms/selection/combo-box/ EuiComboBox}.
21
+ *
22
+ * `testSubj` must match the `data-test-subj` set by the consumer on the
23
+ * `<EuiComboBox>` element (the outer `.euiComboBox` wrapper, not the inner
24
+ * `comboBoxInput`).
25
+ */
26
+ class EuiComboBoxObject extends _base_object.BaseObject {
27
+ /**
28
+ * Replace the current selection with `labels`. Set-semantics: order-
29
+ * independent — already-selected labels are kept, missing ones are added,
30
+ * extras are removed. No-op if the current selection already matches.
31
+ *
32
+ * Throws with a descriptive message if any label never appears in the
33
+ * dropdown (catches test/data drift early).
34
+ */
35
+ async setSelectedOptions(labels) {
36
+ // Dedupe while preserving order.
37
+ const targetLabels = [...new Set(labels)];
38
+ // `[...arr].sort()` (not `arr.sort()`) — sort mutates in place; the copy
39
+ // avoids mutating either the consumer's input or our internal state.
40
+ const sortedTarget = [...targetLabels].sort();
41
+ const sortedCurrent = [...(await this.getSelectedOptions())].sort();
42
+
43
+ // Set-equality short-circuit (any order).
44
+ if (sortedCurrent.length === sortedTarget.length && sortedCurrent.every((label, i) => label === sortedTarget[i])) {
45
+ return;
46
+ }
47
+
48
+ // Naive replace — clear, then add each. A diff-based approach would do
49
+ // less DOM work but require a per-pill remove primitive we don't ship yet.
50
+ await this.clear();
51
+ for (const label of targetLabels) {
52
+ await this.addOption(label);
53
+ }
54
+ if (targetLabels.length > 0) {
55
+ // Blur the input to close the dropdown. Using blur() rather than a
56
+ // keyboard event avoids bubbling Escape to page-level handlers
57
+ // (modal/flyout close listeners) on the consumer page.
58
+ await this.searchInput.blur();
59
+ }
60
+ (0, _test.expect)([...(await this.getSelectedOptions())].sort()).toEqual(sortedTarget);
61
+ }
62
+
63
+ /**
64
+ * Clear all selected options. No-op if nothing is selected.
65
+ *
66
+ * Auto-detects the combo box configuration and uses the appropriate strategy:
67
+ * - Pills present → {@link clickPillClearButtons}
68
+ * - `asPlainText` with a confirmed input selection → {@link deleteSearchInput}
69
+ * - Otherwise → {@link deselectAllFromDropdown}
70
+ *
71
+ * Use the explicit methods directly when you need full control over the
72
+ * clearing strategy (e.g. `onCreateOption` with selections not in the
73
+ * options list).
74
+ */
75
+ async clear() {
76
+ if ((await this.getSelectedOptions()).length === 0) {
77
+ return;
78
+ }
79
+ if (await this.hasPills()) {
80
+ await this.clickPillClearButtons();
81
+ return;
82
+ }
83
+ if (await this.hasConfirmedInputSelection()) {
84
+ await this.deleteSearchInput();
85
+ return;
86
+ }
87
+ await this.deselectAllFromDropdown();
88
+ }
89
+
90
+ /**
91
+ * Currently selected option labels.
92
+ *
93
+ * - Multi-select / `singleSelection=true` → pill texts.
94
+ * - `singleSelection={{ asPlainText: true }}` → the input value. EUI
95
+ * renders no pills in this mode; the input IS the selection display.
96
+ * Works correctly with both `isClearable=true` (default) and
97
+ * `isClearable=false`.
98
+ * - Nothing selected → `[]`.
99
+ */
100
+ async getSelectedOptions() {
101
+ if (await this.hasPills()) {
102
+ return this.pills.allInnerTexts();
103
+ }
104
+ if (await this.hasConfirmedInputSelection()) {
105
+ return [await this.searchInput.inputValue()];
106
+ }
107
+ return [];
108
+ }
109
+ async hasPills() {
110
+ return (await this.pills.count()) > 0;
111
+ }
112
+
113
+ /**
114
+ * Clicks the `×` button on each selected pill individually.
115
+ * Works regardless of `isClearable` — pill close buttons are always present.
116
+ * No-op if no pills are rendered.
117
+ */
118
+ async clickPillClearButtons() {
119
+ while (await this.hasPills()) {
120
+ const countBefore = await this.pills.count();
121
+ await this.pills.first().locator('button').click();
122
+ await (0, _test.expect)(this.pills).not.toHaveCount(countBefore);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Opens the dropdown and clicks each `aria-selected="true"` option to
128
+ * deselect it. Works for all `isClearable` and `singleSelection`
129
+ * configurations when the selected options are present in the options list.
130
+ *
131
+ * Does not work when the selection was created via `onCreateOption` and the
132
+ * created option was not added back to the `options` array — use
133
+ * {@link clickPillClearButtons} instead.
134
+ */
135
+ async deselectAllFromDropdown() {
136
+ await this.input.click();
137
+ const selected = this.root.page().locator(_selectors.EuiComboBoxSelectors.selectedOptionFor(this.testSubj));
138
+ while ((await selected.count()) > 0) {
139
+ const countBefore = await selected.count();
140
+ await selected.first().click();
141
+ await (0, _test.expect)(selected).not.toHaveCount(countBefore);
142
+ }
143
+ await this.searchInput.blur();
144
+ }
145
+ async addOption(label) {
146
+ // Clicking the outer wrapper does not reliably open the dropdown; the
147
+ // inner `comboBoxInput` element does.
148
+ await this.input.click();
149
+
150
+ // Don't type to filter: EUI only sets an option's `title` while the input
151
+ // is empty, and the getByTitle match below relies on it. setSelectedOptions
152
+ // clears the selection first, so the list is unfiltered and every option
153
+ // renders its title.
154
+ //
155
+ // Options list is rendered in a portal outside `this.root`, so locate
156
+ // from page level. Use .and(getByTitle) rather than embedding the label
157
+ // in the CSS string — CSS attribute selectors break on labels containing
158
+ // quotes, brackets, or backslashes. getByTitle alone would search
159
+ // descendants; .and() intersects so it matches the option element itself.
160
+ const option = this.root.page().locator(_selectors.EuiComboBoxSelectors.optionFor(this.testSubj)).and(this.root.page().getByTitle(label, {
161
+ exact: true
162
+ }));
163
+ await option.waitFor({
164
+ state: 'visible'
165
+ });
166
+ await option.click();
167
+ }
168
+
169
+ /**
170
+ * Focuses the search input and presses Backspace to clear the selection.
171
+ * Only valid in `singleSelection={{ asPlainText: true }}` mode where
172
+ * {@link hasConfirmedInputSelection} is true — EUI's `onKeyDown` handler
173
+ * fires `onRemoveOption` when Backspace is pressed with an empty `searchValue`.
174
+ */
175
+ async deleteSearchInput() {
176
+ await this.searchInput.press('Backspace');
177
+ // Backspace triggers onRemoveOption (removes the selection) but the
178
+ // browser also fires a native input event that sets searchValue to a
179
+ // partial label string. fill('') cleans that up without restoring the
180
+ // selection (selectedOptions is already empty at this point).
181
+ await this.searchInput.fill('');
182
+ await (0, _test.expect)(this.searchInput).toHaveValue('');
183
+ }
184
+
185
+ /**
186
+ * Returns true when the combo is in `asPlainText` mode and the input
187
+ * contains a *confirmed* selection (not unconfirmed typed text).
188
+ *
189
+ * In `asPlainText` mode the input IS the selection display: EUI renders
190
+ * the selected option's label directly in the input (no pills). An empty
191
+ * input means nothing is selected.
192
+ *
193
+ * When the user types text that matches no option and blurs (without
194
+ * `onCreateOption`), EUI marks the combo box invalid by adding
195
+ * `euiComboBox-isInvalid` to the root element. An invalid combo with a
196
+ * non-empty input has unconfirmed text, not a confirmed selection.
197
+ */
198
+ async hasConfirmedInputSelection() {
199
+ if (!(await this.isPlainText())) return false;
200
+ if (!(await this.searchInput.inputValue())) return false;
201
+ return !(await this.isMarkedInvalid());
202
+ }
203
+ async isMarkedInvalid() {
204
+ const classes = await this.root.getAttribute('class');
205
+ return classes?.includes('euiComboBox-isInvalid') ?? false;
206
+ }
207
+ get input() {
208
+ return this.root.getByTestId(_selectors.EuiComboBoxSelectors.INPUT_WRAPPER_TEST_SUBJ);
209
+ }
210
+ get searchInput() {
211
+ return this.root.getByTestId(_selectors.EuiComboBoxSelectors.SEARCH_INPUT_TEST_SUBJ);
212
+ }
213
+ get pills() {
214
+ return this.root.getByTestId(_selectors.EuiComboBoxSelectors.PILL_TEST_SUBJ);
215
+ }
216
+ async isPlainText() {
217
+ return (await this.root.locator(_selectors.EuiComboBoxSelectors.PLAIN_TEXT_INPUT_WRAP_SELECTOR).count()) > 0;
218
+ }
219
+ }
220
+ exports.EuiComboBoxObject = EuiComboBoxObject;
@@ -0,0 +1 @@
1
+ export { EuiComboBoxSelectors } from './components/combo_box/selectors';
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "EuiComboBoxSelectors", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _selectors.EuiComboBoxSelectors;
10
+ }
11
+ });
12
+ var _selectors = require("./components/combo_box/selectors");
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns the Storybook iframe URL for a given story ID and optional args.
3
+ *
4
+ * @param id - The Storybook story ID (e.g. `'forms-euicombobox--playground'`)
5
+ * @param args - Optional semicolon-separated Storybook args string (e.g. `'data-test-subj:myCombo;singleSelection:true'`)
6
+ */
7
+ export declare const storyUrl: (id: string, args?: string) => string;