@elastic/eui-test-helpers 1.1.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.
Files changed (35) 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 +61 -0
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.storyUrl = 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
+ * Returns the Storybook iframe URL for a given story ID and optional args.
17
+ *
18
+ * @param id - The Storybook story ID (e.g. `'forms-euicombobox--playground'`)
19
+ * @param args - Optional semicolon-separated Storybook args string (e.g. `'data-test-subj:myCombo;singleSelection:true'`)
20
+ */
21
+ const storyUrl = (id, args) => `/iframe.html?id=${id}&viewMode=story${args ? `&args=${args}` : ''}`;
22
+ exports.storyUrl = storyUrl;
@@ -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,80 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ * or more contributor license agreements. Licensed under the Elastic License
4
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
5
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
6
+ * Side Public License, v 1.
7
+ */
8
+ /**
9
+ * List of available stable selectors for
10
+ * {@link https://eui.elastic.co/docs/components/forms/selection/combo-box/|EuiComboBox}
11
+ */
12
+ export const EuiComboBoxSelectors = {
13
+ /**
14
+ * `data-test-subj` identifier of the inner input wrapper element
15
+ */
16
+ INPUT_WRAPPER_TEST_SUBJ: 'comboBoxInput',
17
+ /**
18
+ * `data-test-subj` identifier of the search input field
19
+ */
20
+ SEARCH_INPUT_TEST_SUBJ: 'comboBoxSearchInput',
21
+ /**
22
+ * `data-test-subj` identifier of the options list
23
+ */
24
+ OPTIONS_LIST_TEST_SUBJ: 'comboBoxOptionsList',
25
+ /**
26
+ * `data-test-subj` identifier of the options list toggle button
27
+ */
28
+ OPTIONS_LIST_TOGGLE_BUTTON_TEST_SUBJ: 'comboBoxToggleListButton',
29
+ /**
30
+ * `data-test-subj` identifier of the clear button
31
+ */
32
+ 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
+ PLAIN_TEXT_INPUT_WRAP_SELECTOR: '.euiComboBox__inputWrap--plainText',
38
+ /**
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.
42
+ */
43
+ PILL_TEST_SUBJ: 'euiComboBoxPill',
44
+ /**
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.
63
+ */
64
+ 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
+ */
69
+ 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
+ */
78
+ optionsListFor: (testSubj) => `[data-test-subj~="${testSubj}-optionsList"]`,
79
+ };
80
+ //# sourceMappingURL=selectors.js.map
@@ -0,0 +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"}
@@ -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,10 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ * or more contributor license agreements. Licensed under the Elastic License
4
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
5
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
6
+ * Side Public License, v 1.
7
+ */
8
+ export { BaseObject } from './playwright/base_object';
9
+ export { EuiComboBoxObject } from './playwright/components/combo_box/object';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAoB,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAC"}
@@ -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,30 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ * or more contributor license agreements. Licensed under the Elastic License
4
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
5
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
6
+ * Side Public License, v 1.
7
+ */
8
+ /**
9
+ * Base class for Playwright Component Objects — semantic wrappers around a
10
+ * single root `Locator` resolved from a `data-test-subj` inside the given
11
+ * scope. Subclasses compose: pass another Component Object as `scope` to
12
+ * nest one inside the other's DOM subtree.
13
+ *
14
+ * Requires `testIdAttribute: 'data-test-subj'` in the Playwright config.
15
+ */
16
+ export class BaseObject {
17
+ constructor(scope, testSubj) {
18
+ this.scope = scope instanceof BaseObject ? scope.locator : scope;
19
+ this.root = this.scope.getByTestId(testSubj);
20
+ this.testSubj = testSubj;
21
+ }
22
+ /**
23
+ * Underlying `Locator` — escape hatch for assertions or scoping the
24
+ * Component Object's API doesn't cover.
25
+ */
26
+ get locator() {
27
+ return this.root;
28
+ }
29
+ }
30
+ //# sourceMappingURL=base_object.js.map
@@ -0,0 +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"}
@@ -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,210 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ * or more contributor license agreements. Licensed under the Elastic License
4
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
5
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
6
+ * Side Public License, v 1.
7
+ */
8
+ import { expect } from '@playwright/test';
9
+ import { BaseObject } from '../../base_object';
10
+ import { EuiComboBoxSelectors } from '../../../components/combo_box/selectors';
11
+ /**
12
+ * Playwright Component Object for {@link
13
+ * https://eui.elastic.co/docs/components/forms/selection/combo-box/ EuiComboBox}.
14
+ *
15
+ * `testSubj` must match the `data-test-subj` set by the consumer on the
16
+ * `<EuiComboBox>` element (the outer `.euiComboBox` wrapper, not the inner
17
+ * `comboBoxInput`).
18
+ */
19
+ export class EuiComboBoxObject extends BaseObject {
20
+ /**
21
+ * Replace the current selection with `labels`. Set-semantics: order-
22
+ * independent — already-selected labels are kept, missing ones are added,
23
+ * extras are removed. No-op if the current selection already matches.
24
+ *
25
+ * Throws with a descriptive message if any label never appears in the
26
+ * dropdown (catches test/data drift early).
27
+ */
28
+ async setSelectedOptions(labels) {
29
+ // Dedupe while preserving order.
30
+ const targetLabels = [...new Set(labels)];
31
+ // `[...arr].sort()` (not `arr.sort()`) — sort mutates in place; the copy
32
+ // avoids mutating either the consumer's input or our internal state.
33
+ const sortedTarget = [...targetLabels].sort();
34
+ const sortedCurrent = [...(await this.getSelectedOptions())].sort();
35
+ // Set-equality short-circuit (any order).
36
+ if (sortedCurrent.length === sortedTarget.length &&
37
+ sortedCurrent.every((label, i) => label === sortedTarget[i])) {
38
+ return;
39
+ }
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();
43
+ for (const label of targetLabels) {
44
+ await this.addOption(label);
45
+ }
46
+ if (targetLabels.length > 0) {
47
+ // Blur the input to close the dropdown. Using blur() rather than a
48
+ // keyboard event avoids bubbling Escape to page-level handlers
49
+ // (modal/flyout close listeners) on the consumer page.
50
+ await this.searchInput.blur();
51
+ }
52
+ expect([...(await this.getSelectedOptions())].sort()).toEqual(sortedTarget);
53
+ }
54
+ /**
55
+ * Clear all selected options. No-op if nothing is selected.
56
+ *
57
+ * Auto-detects the combo box configuration and uses the appropriate strategy:
58
+ * - Pills present → {@link clickPillClearButtons}
59
+ * - `asPlainText` with a confirmed input selection → {@link deleteSearchInput}
60
+ * - Otherwise → {@link deselectAllFromDropdown}
61
+ *
62
+ * Use the explicit methods directly when you need full control over the
63
+ * clearing strategy (e.g. `onCreateOption` with selections not in the
64
+ * options list).
65
+ */
66
+ async clear() {
67
+ if ((await this.getSelectedOptions()).length === 0) {
68
+ return;
69
+ }
70
+ if (await this.hasPills()) {
71
+ await this.clickPillClearButtons();
72
+ return;
73
+ }
74
+ if (await this.hasConfirmedInputSelection()) {
75
+ await this.deleteSearchInput();
76
+ return;
77
+ }
78
+ await this.deselectAllFromDropdown();
79
+ }
80
+ /**
81
+ * Currently selected option labels.
82
+ *
83
+ * - 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`.
88
+ * - Nothing selected → `[]`.
89
+ */
90
+ async getSelectedOptions() {
91
+ if (await this.hasPills()) {
92
+ return this.pills.allInnerTexts();
93
+ }
94
+ if (await this.hasConfirmedInputSelection()) {
95
+ return [await this.searchInput.inputValue()];
96
+ }
97
+ return [];
98
+ }
99
+ async hasPills() {
100
+ return (await this.pills.count()) > 0;
101
+ }
102
+ /**
103
+ * Clicks the `×` button on each selected pill individually.
104
+ * Works regardless of `isClearable` — pill close buttons are always present.
105
+ * No-op if no pills are rendered.
106
+ */
107
+ async clickPillClearButtons() {
108
+ while (await this.hasPills()) {
109
+ const countBefore = await this.pills.count();
110
+ await this.pills.first().locator('button').click();
111
+ await expect(this.pills).not.toHaveCount(countBefore);
112
+ }
113
+ }
114
+ /**
115
+ * Opens the dropdown and clicks each `aria-selected="true"` option to
116
+ * deselect it. Works for all `isClearable` and `singleSelection`
117
+ * configurations when the selected options are present in the options list.
118
+ *
119
+ * Does not work when the selection was created via `onCreateOption` and the
120
+ * created option was not added back to the `options` array — use
121
+ * {@link clickPillClearButtons} instead.
122
+ */
123
+ async deselectAllFromDropdown() {
124
+ await this.input.click();
125
+ const selected = this.root
126
+ .page()
127
+ .locator(EuiComboBoxSelectors.selectedOptionFor(this.testSubj));
128
+ while ((await selected.count()) > 0) {
129
+ const countBefore = await selected.count();
130
+ await selected.first().click();
131
+ await expect(selected).not.toHaveCount(countBefore);
132
+ }
133
+ await this.searchInput.blur();
134
+ }
135
+ async addOption(label) {
136
+ // Clicking the outer wrapper does not reliably open the dropdown; the
137
+ // inner `comboBoxInput` element does.
138
+ 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
150
+ .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();
155
+ }
156
+ /**
157
+ * Focuses the search input and presses Backspace to clear the selection.
158
+ * Only valid in `singleSelection={{ asPlainText: true }}` mode where
159
+ * {@link hasConfirmedInputSelection} is true — EUI's `onKeyDown` handler
160
+ * fires `onRemoveOption` when Backspace is pressed with an empty `searchValue`.
161
+ */
162
+ async deleteSearchInput() {
163
+ await this.searchInput.press('Backspace');
164
+ // Backspace triggers onRemoveOption (removes the selection) but the
165
+ // browser also fires a native input event that sets searchValue to a
166
+ // partial label string. fill('') cleans that up without restoring the
167
+ // selection (selectedOptions is already empty at this point).
168
+ await this.searchInput.fill('');
169
+ await expect(this.searchInput).toHaveValue('');
170
+ }
171
+ /**
172
+ * Returns true when the combo is in `asPlainText` mode and the input
173
+ * contains a *confirmed* selection (not unconfirmed typed text).
174
+ *
175
+ * In `asPlainText` mode the input IS the selection display: EUI renders
176
+ * the selected option's label directly in the input (no pills). An empty
177
+ * input means nothing is selected.
178
+ *
179
+ * When the user types text that matches no option and blurs (without
180
+ * `onCreateOption`), EUI marks the combo box invalid by adding
181
+ * `euiComboBox-isInvalid` to the root element. An invalid combo with a
182
+ * non-empty input has unconfirmed text, not a confirmed selection.
183
+ */
184
+ async hasConfirmedInputSelection() {
185
+ if (!(await this.isPlainText()))
186
+ return false;
187
+ if (!(await this.searchInput.inputValue()))
188
+ return false;
189
+ return !(await this.isMarkedInvalid());
190
+ }
191
+ async isMarkedInvalid() {
192
+ const classes = await this.root.getAttribute('class');
193
+ return classes?.includes('euiComboBox-isInvalid') ?? false;
194
+ }
195
+ get input() {
196
+ return this.root.getByTestId(EuiComboBoxSelectors.INPUT_WRAPPER_TEST_SUBJ);
197
+ }
198
+ get searchInput() {
199
+ return this.root.getByTestId(EuiComboBoxSelectors.SEARCH_INPUT_TEST_SUBJ);
200
+ }
201
+ get pills() {
202
+ return this.root.getByTestId(EuiComboBoxSelectors.PILL_TEST_SUBJ);
203
+ }
204
+ async isPlainText() {
205
+ return ((await this.root
206
+ .locator(EuiComboBoxSelectors.PLAIN_TEXT_INPUT_WRAP_SELECTOR)
207
+ .count()) > 0);
208
+ }
209
+ }
210
+ //# sourceMappingURL=object.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object.js","sourceRoot":"","sources":["../../../../../src/playwright/components/combo_box/object.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { EuiComboBoxSelectors } from './components/combo_box/selectors';
@@ -0,0 +1,9 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ * or more contributor license agreements. Licensed under the Elastic License
4
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
5
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
6
+ * Side Public License, v 1.
7
+ */
8
+ export { EuiComboBoxSelectors } from './components/combo_box/selectors';
9
+ //# sourceMappingURL=selectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectors.js","sourceRoot":"","sources":["../../src/selectors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC"}
@@ -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;
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ * or more contributor license agreements. Licensed under the Elastic License
4
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
5
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
6
+ * Side Public License, v 1.
7
+ */
8
+ /**
9
+ * Returns the Storybook iframe URL for a given story ID and optional args.
10
+ *
11
+ * @param id - The Storybook story ID (e.g. `'forms-euicombobox--playground'`)
12
+ * @param args - Optional semicolon-separated Storybook args string (e.g. `'data-test-subj:myCombo;singleSelection:true'`)
13
+ */
14
+ export const storyUrl = (id, args) => `/iframe.html?id=${id}&viewMode=story${args ? `&args=${args}` : ''}`;
15
+ //# sourceMappingURL=storybook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storybook.js","sourceRoot":"","sources":["../../src/storybook.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EAAU,EAAE,IAAa,EAAU,EAAE,CAC5D,mBAAmB,EAAE,kBAAkB,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC"}