@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.
- package/LICENSE.txt +6 -0
- package/README.md +56 -0
- package/lib/cjs/components/combo_box/selectors.d.ts +72 -0
- package/lib/cjs/components/combo_box/selectors.js +86 -0
- package/lib/cjs/index.d.ts +2 -0
- package/lib/cjs/index.js +19 -0
- package/lib/cjs/playwright/base_object.d.ts +31 -0
- package/lib/cjs/playwright/base_object.js +50 -0
- package/lib/cjs/playwright/components/combo_box/object.d.ts +88 -0
- package/lib/cjs/playwright/components/combo_box/object.js +220 -0
- package/lib/cjs/selectors.d.ts +1 -0
- package/lib/cjs/selectors.js +12 -0
- package/lib/cjs/storybook.d.ts +7 -0
- package/lib/cjs/storybook.js +22 -0
- package/lib/esm/components/combo_box/selectors.d.ts +72 -0
- package/lib/esm/components/combo_box/selectors.js +80 -0
- package/lib/esm/components/combo_box/selectors.js.map +1 -0
- package/lib/esm/index.d.ts +2 -0
- package/lib/esm/index.js +10 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/playwright/base_object.d.ts +31 -0
- package/lib/esm/playwright/base_object.js +30 -0
- package/lib/esm/playwright/base_object.js.map +1 -0
- package/lib/esm/playwright/components/combo_box/object.d.ts +88 -0
- package/lib/esm/playwright/components/combo_box/object.js +210 -0
- package/lib/esm/playwright/components/combo_box/object.js.map +1 -0
- package/lib/esm/selectors.d.ts +1 -0
- package/lib/esm/selectors.js +9 -0
- package/lib/esm/selectors.js.map +1 -0
- package/lib/esm/storybook.d.ts +7 -0
- package/lib/esm/storybook.js +15 -0
- package/lib/esm/storybook.js.map +1 -0
- package/licenses/ELASTIC-LICENSE-2.0.md +93 -0
- package/licenses/SSPL-LICENSE.md +557 -0
- package/package.json +61 -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
|
+
};
|
package/lib/cjs/index.js
ADDED
|
@@ -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;
|