@elastic/eui-test-helpers 1.1.0 → 1.2.0

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/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # EUI Test Helpers
2
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.
3
+ `@elastic/eui-test-helpers` provides helpers for interacting with EUI components reliably in
4
+ consumer tests no more guessing at CSS selectors that only _seem_ right. Today it ships
5
+ **Playwright Component Objects** for [Scout]/[Playwright] consumers; [Cypress] and
6
+ [React Testing Library] helpers may follow.
6
7
 
7
8
  > [!NOTE]
8
9
  > This library is in early stages of development and is missing many useful utilities. Please contribute or
@@ -51,6 +52,7 @@ their own version at runtime.
51
52
 
52
53
  See [CONTRIBUTING.md](CONTRIBUTING.md) for design principles, directory structure, and how to add new Component Objects.
53
54
 
55
+ [Playwright]: https://playwright.dev
54
56
  [Cypress]: https://github.com/cypress-io/cypress
55
57
  [React Testing Library]: https://github.com/testing-library/react-testing-library
56
58
  [Scout]: https://github.com/elastic/kibana/tree/main/src/platform/packages/shared/kbn-scout
@@ -1,72 +1,32 @@
1
1
  /**
2
- * List of available stable selectors for
3
- * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}
2
+ * Stable selectors for
3
+ * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}.
4
+ * `*_SELECTOR` values are CSS; `*_TEST_SUBJ` values are `data-test-subj` names.
4
5
  */
5
6
  export declare const EuiComboBoxSelectors: {
6
- /**
7
- * `data-test-subj` identifier of the inner input wrapper element
8
- */
7
+ /** Root element (outer `.euiComboBox` wrapper carrying the consumer's `data-test-subj`). */
8
+ ROOT_SELECTOR: string;
9
9
  INPUT_WRAPPER_TEST_SUBJ: string;
10
- /**
11
- * `data-test-subj` identifier of the search input field
12
- */
13
10
  SEARCH_INPUT_TEST_SUBJ: string;
14
- /**
15
- * `data-test-subj` identifier of the options list
16
- */
17
11
  OPTIONS_LIST_TEST_SUBJ: string;
18
- /**
19
- * `data-test-subj` identifier of the options list toggle button
20
- */
21
12
  OPTIONS_LIST_TOGGLE_BUTTON_TEST_SUBJ: string;
22
- /**
23
- * `data-test-subj` identifier of the clear button
24
- */
25
13
  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
14
  PLAIN_TEXT_INPUT_WRAP_SELECTOR: string;
31
15
  /**
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.
16
+ * Selected option pills. Read by class, not `data-test-subj`: EUI spreads an
17
+ * option's own `data-test-subj` onto its pill after the `euiComboBoxPill`
18
+ * default, so a per-option subj would override it; the class always holds.
35
19
  */
36
- PILL_TEST_SUBJ: string;
20
+ PILL_SELECTOR: string;
37
21
  /**
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.
22
+ * Options in a specific combo's dropdown. EUI propagates the consumer's
23
+ * `data-test-subj` to the list as `${testSubj}-optionsList`, so this scopes to
24
+ * one combo when several coexist. The list may be virtualized — type to filter
25
+ * before asserting on a specific option.
56
26
  */
57
27
  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
- */
28
+ /** Selected options in a specific combo's dropdown (see `optionFor`). */
62
29
  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
- */
30
+ /** A specific combo's options list container (the dropdown portal). */
71
31
  optionsListFor: (testSubj: string) => string;
72
32
  };
@@ -13,74 +13,34 @@ exports.EuiComboBoxSelectors = void 0;
13
13
  */
14
14
 
15
15
  /**
16
- * List of available stable selectors for
17
- * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}
16
+ * Stable selectors for
17
+ * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}.
18
+ * `*_SELECTOR` values are CSS; `*_TEST_SUBJ` values are `data-test-subj` names.
18
19
  */
19
20
  const EuiComboBoxSelectors = exports.EuiComboBoxSelectors = {
20
- /**
21
- * `data-test-subj` identifier of the inner input wrapper element
22
- */
21
+ /** Root element (outer `.euiComboBox` wrapper carrying the consumer's `data-test-subj`). */
22
+ ROOT_SELECTOR: '.euiComboBox',
23
23
  INPUT_WRAPPER_TEST_SUBJ: 'comboBoxInput',
24
- /**
25
- * `data-test-subj` identifier of the search input field
26
- */
27
24
  SEARCH_INPUT_TEST_SUBJ: 'comboBoxSearchInput',
28
- /**
29
- * `data-test-subj` identifier of the options list
30
- */
31
25
  OPTIONS_LIST_TEST_SUBJ: 'comboBoxOptionsList',
32
- /**
33
- * `data-test-subj` identifier of the options list toggle button
34
- */
35
26
  OPTIONS_LIST_TOGGLE_BUTTON_TEST_SUBJ: 'comboBoxToggleListButton',
36
- /**
37
- * `data-test-subj` identifier of the clear button
38
- */
39
27
  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
28
  PLAIN_TEXT_INPUT_WRAP_SELECTOR: '.euiComboBox__inputWrap--plainText',
45
29
  /**
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.
30
+ * Selected option pills. Read by class, not `data-test-subj`: EUI spreads an
31
+ * option's own `data-test-subj` onto its pill after the `euiComboBoxPill`
32
+ * default, so a per-option subj would override it; the class always holds.
49
33
  */
50
- PILL_TEST_SUBJ: 'euiComboBoxPill',
34
+ PILL_SELECTOR: '.euiComboBoxPill',
51
35
  /**
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.
36
+ * Options in a specific combo's dropdown. EUI propagates the consumer's
37
+ * `data-test-subj` to the list as `${testSubj}-optionsList`, so this scopes to
38
+ * one combo when several coexist. The list may be virtualized — type to filter
39
+ * before asserting on a specific option.
70
40
  */
71
41
  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
- */
42
+ /** Selected options in a specific combo's dropdown (see `optionFor`). */
76
43
  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
- */
44
+ /** A specific combo's options list container (the dropdown portal). */
85
45
  optionsListFor: testSubj => `[data-test-subj~="${testSubj}-optionsList"]`
86
46
  };
@@ -22,10 +22,24 @@ export declare abstract class BaseObject {
22
22
  * when several exist on a page.
23
23
  */
24
24
  protected readonly testSubj: string;
25
- constructor(scope: ObjectScope, testSubj: string);
25
+ /**
26
+ * CSS selector the root must match to be this component (e.g. `.euiComboBox`).
27
+ * When set, {@link assertComponent} enforces it.
28
+ */
29
+ protected readonly componentSelector?: string;
30
+ private componentVerified;
31
+ constructor(scope: ObjectScope, testSubj: string, componentSelector?: string);
26
32
  /**
27
33
  * Underlying `Locator` — escape hatch for assertions or scoping the
28
34
  * Component Object's API doesn't cover.
29
35
  */
30
36
  get locator(): Locator;
37
+ /**
38
+ * Throw if the element at `testSubj` isn't this component (a `data-test-subj`
39
+ * isn't unique to a component type). Run automatically before every public
40
+ * method via the constructor's Proxy. Memoized. Skips when the element is
41
+ * absent (that's the calling method's concern, not a misuse) and no-op without
42
+ * a `componentSelector`.
43
+ */
44
+ protected assertComponent(): Promise<void>;
31
45
  }
@@ -33,10 +33,36 @@ class BaseObject {
33
33
  * when several exist on a page.
34
34
  */
35
35
 
36
- constructor(scope, testSubj) {
36
+ /**
37
+ * CSS selector the root must match to be this component (e.g. `.euiComboBox`).
38
+ * When set, {@link assertComponent} enforces it.
39
+ */
40
+
41
+ componentVerified = false;
42
+ constructor(scope, testSubj, componentSelector) {
37
43
  this.scope = scope instanceof BaseObject ? scope.locator : scope;
38
44
  this.root = this.scope.getByTestId(testSubj);
39
45
  this.testSubj = testSubj;
46
+ this.componentSelector = componentSelector;
47
+
48
+ // Guard every async public method. A constructor can't be async, so instead
49
+ // of checking there, a Proxy awaits assertComponent() before each call. Only
50
+ // async methods are wrapped — the guard is async, so sync methods can't be
51
+ // guarded and pass through untouched (keeping their sync return type).
52
+ // Methods run with `target` as `this`, so internal calls don't re-guard.
53
+ return new Proxy(this, {
54
+ get(target, prop) {
55
+ const value = Reflect.get(target, prop, target);
56
+ const isGuardable = typeof value === 'function' && value.constructor.name === 'AsyncFunction' && prop !== 'assertComponent' && !(prop in Object.prototype);
57
+ if (!isGuardable) {
58
+ return value;
59
+ }
60
+ return async (...args) => {
61
+ await target.assertComponent();
62
+ return Reflect.apply(value, target, args);
63
+ };
64
+ }
65
+ });
40
66
  }
41
67
 
42
68
  /**
@@ -46,5 +72,29 @@ class BaseObject {
46
72
  get locator() {
47
73
  return this.root;
48
74
  }
75
+
76
+ /**
77
+ * Throw if the element at `testSubj` isn't this component (a `data-test-subj`
78
+ * isn't unique to a component type). Run automatically before every public
79
+ * method via the constructor's Proxy. Memoized. Skips when the element is
80
+ * absent (that's the calling method's concern, not a misuse) and no-op without
81
+ * a `componentSelector`.
82
+ */
83
+ async assertComponent() {
84
+ if (this.componentVerified || !this.componentSelector) {
85
+ return;
86
+ }
87
+ // Only verify when the element is present. An absent element isn't a misuse
88
+ // to flag — the calling method handles absence itself (e.g. clear() no-ops) —
89
+ // so skip rather than block waiting for something that may never appear.
90
+ if ((await this.root.count()) === 0) {
91
+ return;
92
+ }
93
+ const matches = await this.root.and(this.scope.locator(this.componentSelector)).count();
94
+ if (matches === 0) {
95
+ throw new Error(`Expected the element with data-test-subj "${this.testSubj}" to match "${this.componentSelector}", ` + `but it does not. Are you using the right Component Object for this element?`);
96
+ }
97
+ this.componentVerified = true;
98
+ }
49
99
  }
50
100
  exports.BaseObject = BaseObject;
@@ -1,4 +1,4 @@
1
- import { BaseObject } from '../../base_object';
1
+ import { BaseObject, type ObjectScope } from '../../base_object';
2
2
  /**
3
3
  * Playwright Component Object for {@link
4
4
  * https://eui.elastic.co/docs/components/forms/selection/combo-box/ EuiComboBox}.
@@ -8,6 +8,7 @@ import { BaseObject } from '../../base_object';
8
8
  * `comboBoxInput`).
9
9
  */
10
10
  export declare class EuiComboBoxObject extends BaseObject {
11
+ constructor(scope: ObjectScope, testSubj: string);
11
12
  /**
12
13
  * Replace the current selection with `labels`. Set-semantics: order-
13
14
  * independent — already-selected labels are kept, missing ones are added,
@@ -15,8 +16,29 @@ export declare class EuiComboBoxObject extends BaseObject {
15
16
  *
16
17
  * Throws with a descriptive message if any label never appears in the
17
18
  * dropdown (catches test/data drift early).
19
+ *
20
+ * `timeout` bounds how long each option is awaited in the dropdown after
21
+ * typing — raise it for slow / server-backed combos whose options load
22
+ * asynchronously.
23
+ */
24
+ setSelectedOptions(labels: string[], { timeout }?: {
25
+ timeout?: number;
26
+ }): Promise<void>;
27
+ /**
28
+ * Set free-text values on an `onCreateOption` combo box by typing and
29
+ * committing each with Enter, then verifying it was accepted. Unlike
30
+ * {@link setSelectedOptions}, this creates values rather than picking
31
+ * existing options.
32
+ */
33
+ setCustomSelectedOptions(labels: string[], { timeout }?: {
34
+ timeout?: number;
35
+ }): Promise<void>;
36
+ /**
37
+ * Open the dropdown and return the labels of the currently visible options.
38
+ * Virtualized lists mount only a subset, so this is the visible slice — not
39
+ * guaranteed to be every option.
18
40
  */
19
- setSelectedOptions(labels: string[]): Promise<void>;
41
+ getAllVisibleOptions(): Promise<string[]>;
20
42
  /**
21
43
  * Clear all selected options. No-op if nothing is selected.
22
44
  *
@@ -34,10 +56,8 @@ export declare class EuiComboBoxObject extends BaseObject {
34
56
  * Currently selected option labels.
35
57
  *
36
58
  * - 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`.
59
+ * - `singleSelection={{ asPlainText: true }}` → the input value (EUI renders
60
+ * no pills in this mode).
41
61
  * - Nothing selected → `[]`.
42
62
  */
43
63
  getSelectedOptions(): Promise<string[]>;
@@ -24,6 +24,10 @@ var _selectors = require("../../../components/combo_box/selectors");
24
24
  * `comboBoxInput`).
25
25
  */
26
26
  class EuiComboBoxObject extends _base_object.BaseObject {
27
+ constructor(scope, testSubj) {
28
+ super(scope, testSubj, _selectors.EuiComboBoxSelectors.ROOT_SELECTOR);
29
+ }
30
+
27
31
  /**
28
32
  * Replace the current selection with `labels`. Set-semantics: order-
29
33
  * independent — already-selected labels are kept, missing ones are added,
@@ -31,8 +35,14 @@ class EuiComboBoxObject extends _base_object.BaseObject {
31
35
  *
32
36
  * Throws with a descriptive message if any label never appears in the
33
37
  * dropdown (catches test/data drift early).
38
+ *
39
+ * `timeout` bounds how long each option is awaited in the dropdown after
40
+ * typing — raise it for slow / server-backed combos whose options load
41
+ * asynchronously.
34
42
  */
35
- async setSelectedOptions(labels) {
43
+ async setSelectedOptions(labels, {
44
+ timeout = 2_500
45
+ } = {}) {
36
46
  // Dedupe while preserving order.
37
47
  const targetLabels = [...new Set(labels)];
38
48
  // `[...arr].sort()` (not `arr.sort()`) — sort mutates in place; the copy
@@ -45,19 +55,70 @@ class EuiComboBoxObject extends _base_object.BaseObject {
45
55
  return;
46
56
  }
47
57
 
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();
58
+ // Naive replace — clear, then add each. Exception: asPlainText single-select
59
+ // replaces its selection when a new option is picked, so clearing first is
60
+ // unnecessary — and its input can hold a non-clearable default (a placeholder
61
+ // rendered as the value) that clear() can't empty. Still clear when the target
62
+ // is empty (nothing to select would leave the old selection in place).
63
+ if (targetLabels.length === 0 || !(await this.isPlainText())) {
64
+ await this.clear();
65
+ }
51
66
  for (const label of targetLabels) {
52
- await this.addOption(label);
67
+ await this.addOption(label, timeout);
53
68
  }
54
- if (targetLabels.length > 0) {
69
+ if (targetLabels.length > 0 && !(await this.isPlainText())) {
55
70
  // Blur the input to close the dropdown. Using blur() rather than a
56
71
  // keyboard event avoids bubbling Escape to page-level handlers
57
- // (modal/flyout close listeners) on the consumer page.
72
+ // (modal/flyout close listeners) on the consumer page. Skipped for
73
+ // asPlainText: picking an option there already commits and closes the
74
+ // dropdown, and an extra blur can race the (often parent-controlled)
75
+ // selectedOptions update and discard the just-picked value.
76
+ await this.searchInput.blur();
77
+ }
78
+
79
+ // Poll rather than assert once: an asPlainText selection is committed via
80
+ // the consumer's onChange, which can land a tick after the click.
81
+ await _test.expect.poll(() => this.getSelectedOptions().then(options => [...options].sort()), {
82
+ timeout
83
+ }).toEqual(sortedTarget);
84
+ }
85
+
86
+ /**
87
+ * Set free-text values on an `onCreateOption` combo box by typing and
88
+ * committing each with Enter, then verifying it was accepted. Unlike
89
+ * {@link setSelectedOptions}, this creates values rather than picking
90
+ * existing options.
91
+ */
92
+ async setCustomSelectedOptions(labels, {
93
+ timeout = 2_500
94
+ } = {}) {
95
+ for (const label of labels) {
96
+ await this.input.click();
97
+ await this.searchInput.fill(label);
98
+ await this.searchInput.press('Enter');
58
99
  await this.searchInput.blur();
59
100
  }
60
- (0, _test.expect)([...(await this.getSelectedOptions())].sort()).toEqual(sortedTarget);
101
+ // The typed value equals the resulting pill/input label, so membership is
102
+ // an exact check.
103
+ for (const label of labels) {
104
+ await _test.expect.poll(() => this.getSelectedOptions(), {
105
+ timeout
106
+ }).toContain(label);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Open the dropdown and return the labels of the currently visible options.
112
+ * Virtualized lists mount only a subset, so this is the visible slice — not
113
+ * guaranteed to be every option.
114
+ */
115
+ async getAllVisibleOptions() {
116
+ await this.input.click();
117
+ const optionsList = this.root.page().locator(_selectors.EuiComboBoxSelectors.optionsListFor(this.testSubj));
118
+ await optionsList.waitFor({
119
+ state: 'visible'
120
+ });
121
+ return optionsList.getByRole('option').allInnerTexts();
61
122
  }
62
123
 
63
124
  /**
@@ -91,10 +152,8 @@ class EuiComboBoxObject extends _base_object.BaseObject {
91
152
  * Currently selected option labels.
92
153
  *
93
154
  * - 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`.
155
+ * - `singleSelection={{ asPlainText: true }}` → the input value (EUI renders
156
+ * no pills in this mode).
98
157
  * - Nothing selected → `[]`.
99
158
  */
100
159
  async getSelectedOptions() {
@@ -142,28 +201,31 @@ class EuiComboBoxObject extends _base_object.BaseObject {
142
201
  }
143
202
  await this.searchInput.blur();
144
203
  }
145
- async addOption(label) {
204
+ async addOption(label, timeout = 2_500) {
146
205
  // Clicking the outer wrapper does not reliably open the dropdown; the
147
206
  // inner `comboBoxInput` element does.
148
207
  await this.input.click();
149
208
 
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();
209
+ // Type to filter, then wait for the (now narrowed) options to render.
210
+ await this.searchInput.fill(label);
211
+ const options = this.root.page().locator(_selectors.EuiComboBoxSelectors.optionsListFor(this.testSubj)).getByRole('option');
212
+ await _test.expect.poll(() => options.count(), {
213
+ timeout
214
+ }).toBeGreaterThan(0);
215
+
216
+ // Match on each option's full text so "ip" doesn't select "clientip" (EUI
217
+ // wraps the matched substring in `<mark>` while filtering, so a text-node
218
+ // lookup would match inside a longer option). If the text is truncated and
219
+ // has no exact match, fall back to keyboard-selecting the highlighted option.
220
+ const trimmed = label.trim();
221
+ const texts = await options.allInnerTexts();
222
+ const exactIndex = texts.findIndex(text => text.trim() === trimmed);
223
+ if (exactIndex >= 0) {
224
+ await options.nth(exactIndex).click();
225
+ } else {
226
+ await this.searchInput.press('ArrowDown');
227
+ await this.searchInput.press('Enter');
228
+ }
167
229
  }
168
230
 
169
231
  /**
@@ -211,7 +273,7 @@ class EuiComboBoxObject extends _base_object.BaseObject {
211
273
  return this.root.getByTestId(_selectors.EuiComboBoxSelectors.SEARCH_INPUT_TEST_SUBJ);
212
274
  }
213
275
  get pills() {
214
- return this.root.getByTestId(_selectors.EuiComboBoxSelectors.PILL_TEST_SUBJ);
276
+ return this.root.locator(_selectors.EuiComboBoxSelectors.PILL_SELECTOR);
215
277
  }
216
278
  async isPlainText() {
217
279
  return (await this.root.locator(_selectors.EuiComboBoxSelectors.PLAIN_TEXT_INPUT_WRAP_SELECTOR).count()) > 0;
@@ -1,72 +1,32 @@
1
1
  /**
2
- * List of available stable selectors for
3
- * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}
2
+ * Stable selectors for
3
+ * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}.
4
+ * `*_SELECTOR` values are CSS; `*_TEST_SUBJ` values are `data-test-subj` names.
4
5
  */
5
6
  export declare const EuiComboBoxSelectors: {
6
- /**
7
- * `data-test-subj` identifier of the inner input wrapper element
8
- */
7
+ /** Root element (outer `.euiComboBox` wrapper carrying the consumer's `data-test-subj`). */
8
+ ROOT_SELECTOR: string;
9
9
  INPUT_WRAPPER_TEST_SUBJ: string;
10
- /**
11
- * `data-test-subj` identifier of the search input field
12
- */
13
10
  SEARCH_INPUT_TEST_SUBJ: string;
14
- /**
15
- * `data-test-subj` identifier of the options list
16
- */
17
11
  OPTIONS_LIST_TEST_SUBJ: string;
18
- /**
19
- * `data-test-subj` identifier of the options list toggle button
20
- */
21
12
  OPTIONS_LIST_TOGGLE_BUTTON_TEST_SUBJ: string;
22
- /**
23
- * `data-test-subj` identifier of the clear button
24
- */
25
13
  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
14
  PLAIN_TEXT_INPUT_WRAP_SELECTOR: string;
31
15
  /**
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.
16
+ * Selected option pills. Read by class, not `data-test-subj`: EUI spreads an
17
+ * option's own `data-test-subj` onto its pill after the `euiComboBoxPill`
18
+ * default, so a per-option subj would override it; the class always holds.
35
19
  */
36
- PILL_TEST_SUBJ: string;
20
+ PILL_SELECTOR: string;
37
21
  /**
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.
22
+ * Options in a specific combo's dropdown. EUI propagates the consumer's
23
+ * `data-test-subj` to the list as `${testSubj}-optionsList`, so this scopes to
24
+ * one combo when several coexist. The list may be virtualized — type to filter
25
+ * before asserting on a specific option.
56
26
  */
57
27
  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
- */
28
+ /** Selected options in a specific combo's dropdown (see `optionFor`). */
62
29
  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
- */
30
+ /** A specific combo's options list container (the dropdown portal). */
71
31
  optionsListFor: (testSubj: string) => string;
72
32
  };
@@ -6,75 +6,35 @@
6
6
  * Side Public License, v 1.
7
7
  */
8
8
  /**
9
- * List of available stable selectors for
10
- * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}
9
+ * Stable selectors for
10
+ * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}.
11
+ * `*_SELECTOR` values are CSS; `*_TEST_SUBJ` values are `data-test-subj` names.
11
12
  */
12
13
  export const EuiComboBoxSelectors = {
13
- /**
14
- * `data-test-subj` identifier of the inner input wrapper element
15
- */
14
+ /** Root element (outer `.euiComboBox` wrapper carrying the consumer's `data-test-subj`). */
15
+ ROOT_SELECTOR: '.euiComboBox',
16
16
  INPUT_WRAPPER_TEST_SUBJ: 'comboBoxInput',
17
- /**
18
- * `data-test-subj` identifier of the search input field
19
- */
20
17
  SEARCH_INPUT_TEST_SUBJ: 'comboBoxSearchInput',
21
- /**
22
- * `data-test-subj` identifier of the options list
23
- */
24
18
  OPTIONS_LIST_TEST_SUBJ: 'comboBoxOptionsList',
25
- /**
26
- * `data-test-subj` identifier of the options list toggle button
27
- */
28
19
  OPTIONS_LIST_TOGGLE_BUTTON_TEST_SUBJ: 'comboBoxToggleListButton',
29
- /**
30
- * `data-test-subj` identifier of the clear button
31
- */
32
20
  CLEAR_BUTTON_TEST_SUBJ: 'comboBoxClearButton',
33
- /**
34
- * CSS selector for the input wrapper in `singleSelection={{ asPlainText: true }}` mode.
35
- * Present when the combo renders the selection inside the input instead of as pills.
36
- */
37
21
  PLAIN_TEXT_INPUT_WRAP_SELECTOR: '.euiComboBox__inputWrap--plainText',
38
22
  /**
39
- * `data-test-subj` identifier of selected option pills.
40
- * Only present in non-`asPlainText` mode in `asPlainText` mode the
41
- * selection is shown inside the input, not as pills.
23
+ * Selected option pills. Read by class, not `data-test-subj`: EUI spreads an
24
+ * option's own `data-test-subj` onto its pill after the `euiComboBoxPill`
25
+ * default, so a per-option subj would override it; the class always holds.
42
26
  */
43
- PILL_TEST_SUBJ: 'euiComboBoxPill',
27
+ PILL_SELECTOR: '.euiComboBoxPill',
44
28
  /**
45
- * CSS selector for all options in a specific combo box's dropdown.
46
- *
47
- * `testSubj` is the consumer's `data-test-subj` on `<EuiComboBox>`. EUI
48
- * propagates this to the options list as `${testSubj}-optionsList`,
49
- * letting us disambiguate when multiple combo boxes coexist on one page.
50
- *
51
- * To target a specific option by label, compose with Playwright's
52
- * `getByTitle` to avoid CSS-injection issues with labels that contain
53
- * special characters (`"`, `]`, `\`):
54
- *
55
- * ```ts
56
- * page
57
- * .locator(EuiComboBoxSelectors.optionFor(testSubj))
58
- * .and(page.getByTitle(label, { exact: true }))
59
- * ```
60
- *
61
- * Note: the list may be virtualized — type the search string into the
62
- * input before asserting on a specific option to ensure it is in DOM.
29
+ * Options in a specific combo's dropdown. EUI propagates the consumer's
30
+ * `data-test-subj` to the list as `${testSubj}-optionsList`, so this scopes to
31
+ * one combo when several coexist. The list may be virtualized — type to filter
32
+ * before asserting on a specific option.
63
33
  */
64
34
  optionFor: (testSubj) => `[data-test-subj~="${testSubj}-optionsList"] [role="option"]`,
65
- /**
66
- * CSS selector for all selected options in a specific combo box's dropdown.
67
- * See `optionFor` for `testSubj` rationale and label-targeting guidance.
68
- */
35
+ /** Selected options in a specific combo's dropdown (see `optionFor`). */
69
36
  selectedOptionFor: (testSubj) => `[data-test-subj~="${testSubj}-optionsList"] [role="option"][aria-selected="true"]`,
70
- /**
71
- * CSS selector for a specific combo box's options list container (the
72
- * dropdown portal). Matches whether the list has regular options or an
73
- * empty-state entry (e.g. "Add X as a custom option" for `onCreateOption`).
74
- *
75
- * Use this when you only need to wait for the dropdown to open, not when
76
- * you need to target a specific option inside it.
77
- */
37
+ /** A specific combo's options list container (the dropdown portal). */
78
38
  optionsListFor: (testSubj) => `[data-test-subj~="${testSubj}-optionsList"]`,
79
39
  };
80
40
  //# sourceMappingURL=selectors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"selectors.js","sourceRoot":"","sources":["../../../../src/components/combo_box/selectors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC;;OAEG;IACH,uBAAuB,EAAE,eAAe;IAExC;;OAEG;IACH,sBAAsB,EAAE,qBAAqB;IAE7C;;OAEG;IACH,sBAAsB,EAAE,qBAAqB;IAE7C;;OAEG;IACH,oCAAoC,EAAE,0BAA0B;IAEhE;;OAEG;IACH,sBAAsB,EAAE,qBAAqB;IAE7C;;;OAGG;IACH,8BAA8B,EAAE,oCAAoC;IAEpE;;;;OAIG;IACH,cAAc,EAAE,iBAAiB;IAEjC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,EAAE,CAAC,QAAgB,EAAU,EAAE,CACtC,qBAAqB,QAAQ,gCAAgC;IAE/D;;;OAGG;IACH,iBAAiB,EAAE,CAAC,QAAgB,EAAU,EAAE,CAC9C,qBAAqB,QAAQ,sDAAsD;IAErF;;;;;;;OAOG;IACH,cAAc,EAAE,CAAC,QAAgB,EAAU,EAAE,CAC3C,qBAAqB,QAAQ,gBAAgB;CAChD,CAAC"}
1
+ {"version":3,"file":"selectors.js","sourceRoot":"","sources":["../../../../src/components/combo_box/selectors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,4FAA4F;IAC5F,aAAa,EAAE,cAAc;IAE7B,uBAAuB,EAAE,eAAe;IAExC,sBAAsB,EAAE,qBAAqB;IAE7C,sBAAsB,EAAE,qBAAqB;IAE7C,oCAAoC,EAAE,0BAA0B;IAEhE,sBAAsB,EAAE,qBAAqB;IAE7C,8BAA8B,EAAE,oCAAoC;IAEpE;;;;OAIG;IACH,aAAa,EAAE,kBAAkB;IAEjC;;;;;OAKG;IACH,SAAS,EAAE,CAAC,QAAgB,EAAU,EAAE,CACtC,qBAAqB,QAAQ,gCAAgC;IAE/D,yEAAyE;IACzE,iBAAiB,EAAE,CAAC,QAAgB,EAAU,EAAE,CAC9C,qBAAqB,QAAQ,sDAAsD;IAErF,uEAAuE;IACvE,cAAc,EAAE,CAAC,QAAgB,EAAU,EAAE,CAC3C,qBAAqB,QAAQ,gBAAgB;CAChD,CAAC"}
@@ -22,10 +22,24 @@ export declare abstract class BaseObject {
22
22
  * when several exist on a page.
23
23
  */
24
24
  protected readonly testSubj: string;
25
- constructor(scope: ObjectScope, testSubj: string);
25
+ /**
26
+ * CSS selector the root must match to be this component (e.g. `.euiComboBox`).
27
+ * When set, {@link assertComponent} enforces it.
28
+ */
29
+ protected readonly componentSelector?: string;
30
+ private componentVerified;
31
+ constructor(scope: ObjectScope, testSubj: string, componentSelector?: string);
26
32
  /**
27
33
  * Underlying `Locator` — escape hatch for assertions or scoping the
28
34
  * Component Object's API doesn't cover.
29
35
  */
30
36
  get locator(): Locator;
37
+ /**
38
+ * Throw if the element at `testSubj` isn't this component (a `data-test-subj`
39
+ * isn't unique to a component type). Run automatically before every public
40
+ * method via the constructor's Proxy. Memoized. Skips when the element is
41
+ * absent (that's the calling method's concern, not a misuse) and no-op without
42
+ * a `componentSelector`.
43
+ */
44
+ protected assertComponent(): Promise<void>;
31
45
  }
@@ -14,10 +14,33 @@
14
14
  * Requires `testIdAttribute: 'data-test-subj'` in the Playwright config.
15
15
  */
16
16
  export class BaseObject {
17
- constructor(scope, testSubj) {
17
+ constructor(scope, testSubj, componentSelector) {
18
+ this.componentVerified = false;
18
19
  this.scope = scope instanceof BaseObject ? scope.locator : scope;
19
20
  this.root = this.scope.getByTestId(testSubj);
20
21
  this.testSubj = testSubj;
22
+ this.componentSelector = componentSelector;
23
+ // Guard every async public method. A constructor can't be async, so instead
24
+ // of checking there, a Proxy awaits assertComponent() before each call. Only
25
+ // async methods are wrapped — the guard is async, so sync methods can't be
26
+ // guarded and pass through untouched (keeping their sync return type).
27
+ // Methods run with `target` as `this`, so internal calls don't re-guard.
28
+ return new Proxy(this, {
29
+ get(target, prop) {
30
+ const value = Reflect.get(target, prop, target);
31
+ const isGuardable = typeof value === 'function' &&
32
+ value.constructor.name === 'AsyncFunction' &&
33
+ prop !== 'assertComponent' &&
34
+ !(prop in Object.prototype);
35
+ if (!isGuardable) {
36
+ return value;
37
+ }
38
+ return async (...args) => {
39
+ await target.assertComponent();
40
+ return Reflect.apply(value, target, args);
41
+ };
42
+ },
43
+ });
21
44
  }
22
45
  /**
23
46
  * Underlying `Locator` — escape hatch for assertions or scoping the
@@ -26,5 +49,31 @@ export class BaseObject {
26
49
  get locator() {
27
50
  return this.root;
28
51
  }
52
+ /**
53
+ * Throw if the element at `testSubj` isn't this component (a `data-test-subj`
54
+ * isn't unique to a component type). Run automatically before every public
55
+ * method via the constructor's Proxy. Memoized. Skips when the element is
56
+ * absent (that's the calling method's concern, not a misuse) and no-op without
57
+ * a `componentSelector`.
58
+ */
59
+ async assertComponent() {
60
+ if (this.componentVerified || !this.componentSelector) {
61
+ return;
62
+ }
63
+ // Only verify when the element is present. An absent element isn't a misuse
64
+ // to flag — the calling method handles absence itself (e.g. clear() no-ops) —
65
+ // so skip rather than block waiting for something that may never appear.
66
+ if ((await this.root.count()) === 0) {
67
+ return;
68
+ }
69
+ const matches = await this.root
70
+ .and(this.scope.locator(this.componentSelector))
71
+ .count();
72
+ if (matches === 0) {
73
+ throw new Error(`Expected the element with data-test-subj "${this.testSubj}" to match "${this.componentSelector}", ` +
74
+ `but it does not. Are you using the right Component Object for this element?`);
75
+ }
76
+ this.componentVerified = true;
77
+ }
29
78
  }
30
79
  //# sourceMappingURL=base_object.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"base_object.js","sourceRoot":"","sources":["../../../src/playwright/base_object.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH;;;;;;;GAOG;AACH,MAAM,OAAgB,UAAU;IAiB9B,YAAY,KAAkB,EAAE,QAAgB;QAC9C,IAAI,CAAC,KAAK,GAAG,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QACjE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACH,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;CACF"}
1
+ {"version":3,"file":"base_object.js","sourceRoot":"","sources":["../../../src/playwright/base_object.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH;;;;;;;GAOG;AACH,MAAM,OAAgB,UAAU;IAyB9B,YAAY,KAAkB,EAAE,QAAgB,EAAE,iBAA0B;QAFpE,sBAAiB,GAAG,KAAK,CAAC;QAGhC,IAAI,CAAC,KAAK,GAAG,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QACjE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAE3C,4EAA4E;QAC5E,6EAA6E;QAC7E,2EAA2E;QAC3E,uEAAuE;QACvE,yEAAyE;QACzE,OAAO,IAAI,KAAK,CAAC,IAAI,EAAE;YACrB,GAAG,CAAC,MAAM,EAAE,IAAI;gBACd,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;gBAChD,MAAM,WAAW,GACf,OAAO,KAAK,KAAK,UAAU;oBAC3B,KAAK,CAAC,WAAW,CAAC,IAAI,KAAK,eAAe;oBAC1C,IAAI,KAAK,iBAAiB;oBAC1B,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC;gBAE9B,IAAI,CAAC,WAAW,EAAE,CAAC;oBACjB,OAAO,KAAK,CAAC;gBACf,CAAC;gBAED,OAAO,KAAK,EAAE,GAAG,IAAe,EAAE,EAAE;oBAClC,MAAM,MAAM,CAAC,eAAe,EAAE,CAAC;oBAC/B,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;gBAC5C,CAAC,CAAC;YACJ,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;;;;OAMG;IACO,KAAK,CAAC,eAAe;QAC7B,IAAI,IAAI,CAAC,iBAAiB,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACtD,OAAO;QACT,CAAC;QACD,4EAA4E;QAC5E,8EAA8E;QAC9E,yEAAyE;QACzE,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;YACpC,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI;aAC5B,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;aAC/C,KAAK,EAAE,CAAC;QACX,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CACb,6CAA6C,IAAI,CAAC,QAAQ,eAAe,IAAI,CAAC,iBAAiB,KAAK;gBAClG,6EAA6E,CAChF,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAChC,CAAC;CACF"}
@@ -1,4 +1,4 @@
1
- import { BaseObject } from '../../base_object';
1
+ import { BaseObject, type ObjectScope } from '../../base_object';
2
2
  /**
3
3
  * Playwright Component Object for {@link
4
4
  * https://eui.elastic.co/docs/components/forms/selection/combo-box/ EuiComboBox}.
@@ -8,6 +8,7 @@ import { BaseObject } from '../../base_object';
8
8
  * `comboBoxInput`).
9
9
  */
10
10
  export declare class EuiComboBoxObject extends BaseObject {
11
+ constructor(scope: ObjectScope, testSubj: string);
11
12
  /**
12
13
  * Replace the current selection with `labels`. Set-semantics: order-
13
14
  * independent — already-selected labels are kept, missing ones are added,
@@ -15,8 +16,29 @@ export declare class EuiComboBoxObject extends BaseObject {
15
16
  *
16
17
  * Throws with a descriptive message if any label never appears in the
17
18
  * dropdown (catches test/data drift early).
19
+ *
20
+ * `timeout` bounds how long each option is awaited in the dropdown after
21
+ * typing — raise it for slow / server-backed combos whose options load
22
+ * asynchronously.
23
+ */
24
+ setSelectedOptions(labels: string[], { timeout }?: {
25
+ timeout?: number;
26
+ }): Promise<void>;
27
+ /**
28
+ * Set free-text values on an `onCreateOption` combo box by typing and
29
+ * committing each with Enter, then verifying it was accepted. Unlike
30
+ * {@link setSelectedOptions}, this creates values rather than picking
31
+ * existing options.
32
+ */
33
+ setCustomSelectedOptions(labels: string[], { timeout }?: {
34
+ timeout?: number;
35
+ }): Promise<void>;
36
+ /**
37
+ * Open the dropdown and return the labels of the currently visible options.
38
+ * Virtualized lists mount only a subset, so this is the visible slice — not
39
+ * guaranteed to be every option.
18
40
  */
19
- setSelectedOptions(labels: string[]): Promise<void>;
41
+ getAllVisibleOptions(): Promise<string[]>;
20
42
  /**
21
43
  * Clear all selected options. No-op if nothing is selected.
22
44
  *
@@ -34,10 +56,8 @@ export declare class EuiComboBoxObject extends BaseObject {
34
56
  * Currently selected option labels.
35
57
  *
36
58
  * - 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`.
59
+ * - `singleSelection={{ asPlainText: true }}` → the input value (EUI renders
60
+ * no pills in this mode).
41
61
  * - Nothing selected → `[]`.
42
62
  */
43
63
  getSelectedOptions(): Promise<string[]>;
@@ -17,6 +17,9 @@ import { EuiComboBoxSelectors } from '../../../components/combo_box/selectors';
17
17
  * `comboBoxInput`).
18
18
  */
19
19
  export class EuiComboBoxObject extends BaseObject {
20
+ constructor(scope, testSubj) {
21
+ super(scope, testSubj, EuiComboBoxSelectors.ROOT_SELECTOR);
22
+ }
20
23
  /**
21
24
  * Replace the current selection with `labels`. Set-semantics: order-
22
25
  * independent — already-selected labels are kept, missing ones are added,
@@ -24,8 +27,12 @@ export class EuiComboBoxObject extends BaseObject {
24
27
  *
25
28
  * Throws with a descriptive message if any label never appears in the
26
29
  * dropdown (catches test/data drift early).
30
+ *
31
+ * `timeout` bounds how long each option is awaited in the dropdown after
32
+ * typing — raise it for slow / server-backed combos whose options load
33
+ * asynchronously.
27
34
  */
28
- async setSelectedOptions(labels) {
35
+ async setSelectedOptions(labels, { timeout = 2500 } = {}) {
29
36
  // Dedupe while preserving order.
30
37
  const targetLabels = [...new Set(labels)];
31
38
  // `[...arr].sort()` (not `arr.sort()`) — sort mutates in place; the copy
@@ -37,19 +44,63 @@ export class EuiComboBoxObject extends BaseObject {
37
44
  sortedCurrent.every((label, i) => label === sortedTarget[i])) {
38
45
  return;
39
46
  }
40
- // Naive replace — clear, then add each. A diff-based approach would do
41
- // less DOM work but require a per-pill remove primitive we don't ship yet.
42
- await this.clear();
47
+ // Naive replace — clear, then add each. Exception: asPlainText single-select
48
+ // replaces its selection when a new option is picked, so clearing first is
49
+ // unnecessary — and its input can hold a non-clearable default (a placeholder
50
+ // rendered as the value) that clear() can't empty. Still clear when the target
51
+ // is empty (nothing to select would leave the old selection in place).
52
+ if (targetLabels.length === 0 || !(await this.isPlainText())) {
53
+ await this.clear();
54
+ }
43
55
  for (const label of targetLabels) {
44
- await this.addOption(label);
56
+ await this.addOption(label, timeout);
45
57
  }
46
- if (targetLabels.length > 0) {
58
+ if (targetLabels.length > 0 && !(await this.isPlainText())) {
47
59
  // Blur the input to close the dropdown. Using blur() rather than a
48
60
  // keyboard event avoids bubbling Escape to page-level handlers
49
- // (modal/flyout close listeners) on the consumer page.
61
+ // (modal/flyout close listeners) on the consumer page. Skipped for
62
+ // asPlainText: picking an option there already commits and closes the
63
+ // dropdown, and an extra blur can race the (often parent-controlled)
64
+ // selectedOptions update and discard the just-picked value.
65
+ await this.searchInput.blur();
66
+ }
67
+ // Poll rather than assert once: an asPlainText selection is committed via
68
+ // the consumer's onChange, which can land a tick after the click.
69
+ await expect
70
+ .poll(() => this.getSelectedOptions().then((options) => [...options].sort()), { timeout })
71
+ .toEqual(sortedTarget);
72
+ }
73
+ /**
74
+ * Set free-text values on an `onCreateOption` combo box by typing and
75
+ * committing each with Enter, then verifying it was accepted. Unlike
76
+ * {@link setSelectedOptions}, this creates values rather than picking
77
+ * existing options.
78
+ */
79
+ async setCustomSelectedOptions(labels, { timeout = 2500 } = {}) {
80
+ for (const label of labels) {
81
+ await this.input.click();
82
+ await this.searchInput.fill(label);
83
+ await this.searchInput.press('Enter');
50
84
  await this.searchInput.blur();
51
85
  }
52
- expect([...(await this.getSelectedOptions())].sort()).toEqual(sortedTarget);
86
+ // The typed value equals the resulting pill/input label, so membership is
87
+ // an exact check.
88
+ for (const label of labels) {
89
+ await expect.poll(() => this.getSelectedOptions(), { timeout }).toContain(label);
90
+ }
91
+ }
92
+ /**
93
+ * Open the dropdown and return the labels of the currently visible options.
94
+ * Virtualized lists mount only a subset, so this is the visible slice — not
95
+ * guaranteed to be every option.
96
+ */
97
+ async getAllVisibleOptions() {
98
+ await this.input.click();
99
+ const optionsList = this.root
100
+ .page()
101
+ .locator(EuiComboBoxSelectors.optionsListFor(this.testSubj));
102
+ await optionsList.waitFor({ state: 'visible' });
103
+ return optionsList.getByRole('option').allInnerTexts();
53
104
  }
54
105
  /**
55
106
  * Clear all selected options. No-op if nothing is selected.
@@ -81,10 +132,8 @@ export class EuiComboBoxObject extends BaseObject {
81
132
  * Currently selected option labels.
82
133
  *
83
134
  * - Multi-select / `singleSelection=true` → pill texts.
84
- * - `singleSelection={{ asPlainText: true }}` → the input value. EUI
85
- * renders no pills in this mode; the input IS the selection display.
86
- * Works correctly with both `isClearable=true` (default) and
87
- * `isClearable=false`.
135
+ * - `singleSelection={{ asPlainText: true }}` → the input value (EUI renders
136
+ * no pills in this mode).
88
137
  * - Nothing selected → `[]`.
89
138
  */
90
139
  async getSelectedOptions() {
@@ -132,26 +181,31 @@ export class EuiComboBoxObject extends BaseObject {
132
181
  }
133
182
  await this.searchInput.blur();
134
183
  }
135
- async addOption(label) {
184
+ async addOption(label, timeout = 2500) {
136
185
  // Clicking the outer wrapper does not reliably open the dropdown; the
137
186
  // inner `comboBoxInput` element does.
138
187
  await this.input.click();
139
- // Don't type to filter: EUI only sets an option's `title` while the input
140
- // is empty, and the getByTitle match below relies on it. setSelectedOptions
141
- // clears the selection first, so the list is unfiltered and every option
142
- // renders its title.
143
- //
144
- // Options list is rendered in a portal outside `this.root`, so locate
145
- // from page level. Use .and(getByTitle) rather than embedding the label
146
- // in the CSS string — CSS attribute selectors break on labels containing
147
- // quotes, brackets, or backslashes. getByTitle alone would search
148
- // descendants; .and() intersects so it matches the option element itself.
149
- const option = this.root
188
+ // Type to filter, then wait for the (now narrowed) options to render.
189
+ await this.searchInput.fill(label);
190
+ const options = this.root
150
191
  .page()
151
- .locator(EuiComboBoxSelectors.optionFor(this.testSubj))
152
- .and(this.root.page().getByTitle(label, { exact: true }));
153
- await option.waitFor({ state: 'visible' });
154
- await option.click();
192
+ .locator(EuiComboBoxSelectors.optionsListFor(this.testSubj))
193
+ .getByRole('option');
194
+ await expect.poll(() => options.count(), { timeout }).toBeGreaterThan(0);
195
+ // Match on each option's full text so "ip" doesn't select "clientip" (EUI
196
+ // wraps the matched substring in `<mark>` while filtering, so a text-node
197
+ // lookup would match inside a longer option). If the text is truncated and
198
+ // has no exact match, fall back to keyboard-selecting the highlighted option.
199
+ const trimmed = label.trim();
200
+ const texts = await options.allInnerTexts();
201
+ const exactIndex = texts.findIndex((text) => text.trim() === trimmed);
202
+ if (exactIndex >= 0) {
203
+ await options.nth(exactIndex).click();
204
+ }
205
+ else {
206
+ await this.searchInput.press('ArrowDown');
207
+ await this.searchInput.press('Enter');
208
+ }
155
209
  }
156
210
  /**
157
211
  * Focuses the search input and presses Backspace to clear the selection.
@@ -199,7 +253,7 @@ export class EuiComboBoxObject extends BaseObject {
199
253
  return this.root.getByTestId(EuiComboBoxSelectors.SEARCH_INPUT_TEST_SUBJ);
200
254
  }
201
255
  get pills() {
202
- return this.root.getByTestId(EuiComboBoxSelectors.PILL_TEST_SUBJ);
256
+ return this.root.locator(EuiComboBoxSelectors.PILL_SELECTOR);
203
257
  }
204
258
  async isPlainText() {
205
259
  return ((await this.root
@@ -1 +1 @@
1
- {"version":3,"file":"object.js","sourceRoot":"","sources":["../../../../../src/playwright/components/combo_box/object.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"object.js","sourceRoot":"","sources":["../../../../../src/playwright/components/combo_box/object.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elastic/eui-test-helpers",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A library of EUI test helpers for RTL, Cypress and Scout",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "repository": {
@@ -58,4 +58,4 @@
58
58
  "README.md",
59
59
  "licenses"
60
60
  ]
61
- }
61
+ }
@@ -0,0 +1,33 @@
1
+ # EuiComboBoxObject
2
+
3
+ Playwright Component Object for [EuiComboBox](https://eui.elastic.co/docs/components/forms/selection/combo-box/).
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import { EuiComboBoxObject } from '@elastic/eui-test-helpers';
9
+
10
+ const comboBox = new EuiComboBoxObject(page, 'myComboBox');
11
+ await comboBox.setSelectedOptions(['Option A', 'Option B']);
12
+ expect(await comboBox.getSelectedOptions()).toEqual(['Option A', 'Option B']);
13
+ ```
14
+
15
+ Set `data-test-subj` on the outer `<EuiComboBox>` wrapper (`.euiComboBox`), not on the inner `comboBoxInput`.
16
+
17
+ ## API
18
+
19
+ EuiComboBox has two selection modes that affect how the methods behave:
20
+
21
+ - **Pill mode** — `singleSelection={false}` (default) or `singleSelection={true}`: selected options render as pill elements.
22
+ - **Plain-text mode** — `singleSelection={{ asPlainText: true }}`: the selected option's label appears directly in the search input.
23
+
24
+ | Method | Description |
25
+ |---|---|
26
+ | `getSelectedOptions()` | Returns the selected option labels as `string[]`. |
27
+ | `setSelectedOptions(labels, { timeout? })` | Selects existing options (replaces the current selection; no-op if already matching). `timeout` (default `2500`ms) bounds how long each option is awaited after typing — raise it for async / fetch-as-you-type combos. |
28
+ | `setCustomSelectedOptions(labels, { timeout? })` | Creates free-text values via `onCreateOption`. Use instead of `setSelectedOptions` when the values don't pre-exist as options. `timeout` defaults to `2500`ms. |
29
+ | `getAllVisibleOptions()` | Opens the dropdown and returns the labels of the currently visible options as `string[]` (virtualized lists mount only a subset). |
30
+ | `clear()` | Clears all selected options. No-op if nothing is selected. |
31
+
32
+ The Component Object verifies the `data-test-subj` element is actually an `EuiComboBox` (via the `.euiComboBox` class) and throws a clear error otherwise — so pointing it at a different component that happens to share a `data-test-subj` fails loudly instead of silently misbehaving.
33
+