@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 +5 -3
- package/lib/cjs/components/combo_box/selectors.d.ts +15 -55
- package/lib/cjs/components/combo_box/selectors.js +15 -55
- package/lib/cjs/playwright/base_object.d.ts +15 -1
- package/lib/cjs/playwright/base_object.js +51 -1
- package/lib/cjs/playwright/components/combo_box/object.d.ts +26 -6
- package/lib/cjs/playwright/components/combo_box/object.js +93 -31
- package/lib/esm/components/combo_box/selectors.d.ts +15 -55
- package/lib/esm/components/combo_box/selectors.js +15 -55
- package/lib/esm/components/combo_box/selectors.js.map +1 -1
- package/lib/esm/playwright/base_object.d.ts +15 -1
- package/lib/esm/playwright/base_object.js +50 -1
- package/lib/esm/playwright/base_object.js.map +1 -1
- package/lib/esm/playwright/components/combo_box/object.d.ts +26 -6
- package/lib/esm/playwright/components/combo_box/object.js +83 -29
- package/lib/esm/playwright/components/combo_box/object.js.map +1 -1
- package/package.json +2 -2
- package/src/components/combo_box/README.md +33 -0
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# EUI Test Helpers
|
|
2
2
|
|
|
3
|
-
`@elastic/eui-test-helpers`
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
|
|
20
|
+
PILL_SELECTOR: string;
|
|
37
21
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
-
|
|
34
|
+
PILL_SELECTOR: '.euiComboBoxPill',
|
|
51
35
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
*
|
|
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.
|
|
49
|
-
//
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
95
|
-
*
|
|
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
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
|
|
20
|
+
PILL_SELECTOR: string;
|
|
37
21
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
-
|
|
27
|
+
PILL_SELECTOR: '.euiComboBoxPill',
|
|
44
28
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
|
38
|
-
*
|
|
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.
|
|
41
|
-
//
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
85
|
-
*
|
|
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
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
152
|
-
.
|
|
153
|
-
await
|
|
154
|
-
|
|
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.
|
|
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":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,OAAO,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"object.js","sourceRoot":"","sources":["../../../../../src/playwright/components/combo_box/object.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,OAAO,EAAE,UAAU,EAAoB,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAkB,SAAQ,UAAU;IAC/C,YAAY,KAAkB,EAAE,QAAgB;QAC9C,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,oBAAoB,CAAC,aAAa,CAAC,CAAC;IAC7D,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,kBAAkB,CACtB,MAAgB,EAChB,EAAE,OAAO,GAAG,IAAK,KAA2B,EAAE;QAE9C,iCAAiC;QACjC,MAAM,YAAY,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;QAC1C,yEAAyE;QACzE,qEAAqE;QACrE,MAAM,YAAY,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC;QAE9C,MAAM,aAAa,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEpE,0CAA0C;QAC1C,IACE,aAAa,CAAC,MAAM,KAAK,YAAY,CAAC,MAAM;YAC5C,aAAa,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,EAC5D,CAAC;YACD,OAAO;QACT,CAAC;QAED,6EAA6E;QAC7E,2EAA2E;QAC3E,8EAA8E;QAC9E,+EAA+E;QAC/E,uEAAuE;QACvE,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAC7D,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,YAAY,EAAE,CAAC;YACjC,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACvC,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAC3D,mEAAmE;YACnE,+DAA+D;YAC/D,mEAAmE;YACnE,sEAAsE;YACtE,qEAAqE;YACrE,4DAA4D;YAC5D,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAChC,CAAC;QAED,0EAA0E;QAC1E,kEAAkE;QAClE,MAAM,MAAM;aACT,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC;aACzF,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3B,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,wBAAwB,CAC5B,MAAgB,EAChB,EAAE,OAAO,GAAG,IAAK,KAA2B,EAAE;QAE9C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACzB,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnC,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAChC,CAAC;QACD,0EAA0E;QAC1E,kBAAkB;QAClB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,oBAAoB;QACxB,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACzB,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI;aAC1B,IAAI,EAAE;aACN,OAAO,CAAC,oBAAoB,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC/D,MAAM,WAAW,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAChD,OAAO,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,aAAa,EAAE,CAAC;IACzD,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnD,OAAO;QACT,CAAC;QAED,IAAI,MAAM,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC1B,MAAM,IAAI,CAAC,qBAAqB,EAAE,CAAC;YACnC,OAAO;QACT,CAAC;QAED,IAAI,MAAM,IAAI,CAAC,0BAA0B,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC/B,OAAO;QACT,CAAC;QAED,MAAM,IAAI,CAAC,uBAAuB,EAAE,CAAC;IACvC,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,kBAAkB;QACtB,IAAI,MAAM,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;QACpC,CAAC;QACD,IAAI,MAAM,IAAI,CAAC,0BAA0B,EAAE,EAAE,CAAC;YAC5C,OAAO,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IAEO,KAAK,CAAC,QAAQ;QACpB,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,qBAAqB;QACjC,OAAO,MAAM,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC7B,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YAC7C,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC;YACnD,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED;;;;;;;;OAQG;IACK,KAAK,CAAC,uBAAuB;QACnC,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QAEzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI;aACvB,IAAI,EAAE;aACN,OAAO,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QAElE,OAAO,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;YACpC,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;YAC3C,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC;YAC/B,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;QACtD,CAAC;QAED,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;IAChC,CAAC;IAEO,KAAK,CAAC,SAAS,CAAC,KAAa,EAAE,OAAO,GAAG,IAAK;QACpD,sEAAsE;QACtE,sCAAsC;QACtC,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QAEzB,sEAAsE;QACtE,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI;aACtB,IAAI,EAAE;aACN,OAAO,CAAC,oBAAoB,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;aAC3D,SAAS,CAAC,QAAQ,CAAC,CAAC;QACvB,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAEzE,0EAA0E;QAC1E,0EAA0E;QAC1E,2EAA2E;QAC3E,8EAA8E;QAC9E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,aAAa,EAAE,CAAC;QAC5C,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,OAAO,CAAC,CAAC;QACtE,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;YACpB,MAAM,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,KAAK,EAAE,CAAC;QACxC,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAC1C,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,iBAAiB;QAC7B,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAC1C,oEAAoE;QACpE,qEAAqE;QACrE,sEAAsE;QACtE,8DAA8D;QAC9D,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,KAAK,CAAC,0BAA0B;QACtC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YAAE,OAAO,KAAK,CAAC;QAC9C,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;YAAE,OAAO,KAAK,CAAC;QACzD,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC;IACzC,CAAC;IAEO,KAAK,CAAC,eAAe;QAC3B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACtD,OAAO,OAAO,EAAE,QAAQ,CAAC,uBAAuB,CAAC,IAAI,KAAK,CAAC;IAC7D,CAAC;IAED,IAAY,KAAK;QACf,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAAC,uBAAuB,CAAC,CAAC;IAC7E,CAAC;IAED,IAAY,WAAW;QACrB,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAAC,sBAAsB,CAAC,CAAC;IAC5E,CAAC;IAED,IAAY,KAAK;QACf,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,aAAa,CAAC,CAAC;IAC/D,CAAC;IAEO,KAAK,CAAC,WAAW;QACvB,OAAO,CACL,CAAC,MAAM,IAAI,CAAC,IAAI;aACb,OAAO,CAAC,oBAAoB,CAAC,8BAA8B,CAAC;aAC5D,KAAK,EAAE,CAAC,GAAG,CAAC,CAChB,CAAC;IACJ,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elastic/eui-test-helpers",
|
|
3
|
-
"version": "1.
|
|
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
|
+
|