chore: match selected options by both value and label (#19316)

This commit is contained in:
Pavel Feldman 2022-12-07 09:04:32 -08:00 committed by GitHub
parent fd22d8bde1
commit 7aa3935dcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 68 additions and 31 deletions

View file

@ -997,6 +997,10 @@ completely visible as defined by
* since: v1.14 * since: v1.14
- returns: <[Array]<[string]>> - returns: <[Array]<[string]>>
Selects option or options in `<select>`.
**Details**
This method waits for [actionability](../actionability.md) checks, waits until all specified options are present in the `<select>` element and selects these options. This method waits for [actionability](../actionability.md) checks, waits until all specified options are present in the `<select>` element and selects these options.
If the target element is not a `<select>` element, this method throws an error. However, if the element is inside the `<label>` element that has an associated [control](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control), the control will be used instead. If the target element is not a `<select>` element, this method throws an error. However, if the element is inside the `<label>` element that has an associated [control](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control), the control will be used instead.
@ -1007,56 +1011,59 @@ Triggers a `change` and `input` event once all the provided options have been se
**Usage** **Usage**
```html
<select multiple>
<option value="red">Red</div>
<option value="green">Green</div>
<option value="blue">Blue</div>
</select>
```
```js ```js
// single selection matching the value // single selection matching the value or label
element.selectOption('blue'); element.selectOption('blue');
// single selection matching the label // single selection matching the label
element.selectOption({ label: 'Blue' }); element.selectOption({ label: 'Blue' });
// multiple selection // multiple selection for red, green and blue options
element.selectOption(['red', 'green', 'blue']); element.selectOption(['red', 'green', 'blue']);
``` ```
```java ```java
// single selection matching the value // single selection matching the value or label
element.selectOption("blue"); element.selectOption("blue");
// single selection matching the label // single selection matching the label
element.selectOption(new SelectOption().setLabel("Blue")); element.selectOption(new SelectOption().setLabel("Blue"));
// multiple selection // multiple selection for blue, red and second option
element.selectOption(new String[] {"red", "green", "blue"}); element.selectOption(new String[] {"red", "green", "blue"});
``` ```
```python async ```python async
# single selection matching the value # single selection matching the value or label
await element.select_option("blue") await element.select_option("blue")
# single selection matching the label # single selection matching the label
await element.select_option(label="blue") await element.select_option(label="blue")
# multiple selection # multiple selection for blue, red and second option
await element.select_option(value=["red", "green", "blue"]) await element.select_option(value=["red", "green", "blue"])
``` ```
```python sync ```python sync
# single selection matching the value # single selection matching the value or label
element.select_option("blue") element.select_option("blue")
# single selection matching both the label # single selection matching the label
element.select_option(label="blue") element.select_option(label="blue")
# multiple selection # multiple selection for blue, red and second option
element.select_option(value=["red", "green", "blue"]) element.select_option(value=["red", "green", "blue"])
``` ```
```csharp ```csharp
// single selection matching the value // single selection matching the value or label
await element.SelectOptionAsync(new[] { "blue" }); await element.SelectOptionAsync(new[] { "blue" });
// single selection matching the label // single selection matching the label
await element.SelectOptionAsync(new[] { new SelectOptionValue() { Label = "blue" } }); await element.SelectOptionAsync(new[] { new SelectOptionValue() { Label = "blue" } });
// multiple selection
await element.SelectOptionAsync(new[] { "red", "green", "blue" });
// multiple selection for blue, red and second option // multiple selection for blue, red and second option
await element.SelectOptionAsync(new[] { await element.SelectOptionAsync(new[] { "red", "green", "blue" });
new SelectOptionValue() { Label = "blue" },
new SelectOptionValue() { Index = 2 },
new SelectOptionValue() { Value = "red" }});
``` ```
### param: Locator.selectOption.values = %%-select-options-values-%% ### param: Locator.selectOption.values = %%-select-options-values-%%

View file

@ -697,7 +697,7 @@ Whether to allow sites to register Service workers. Defaults to `'allow'`.
- `index` ?<[int]> Matches by the index. Optional. - `index` ?<[int]> Matches by the index. Optional.
Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the
first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option first option matching one of the passed options is selected. String values are matching both values and labels. Option
is considered matching if all specified properties match. is considered matching if all specified properties match.
## wait-for-navigation-url ## wait-for-navigation-url

View file

@ -252,7 +252,7 @@ export function convertSelectOptionValues(values: string | api.ElementHandle | S
if (values[0] instanceof ElementHandle) if (values[0] instanceof ElementHandle)
return { elements: (values as ElementHandle[]).map((v: ElementHandle) => v._elementChannel) }; return { elements: (values as ElementHandle[]).map((v: ElementHandle) => v._elementChannel) };
if (isString(values[0])) if (isString(values[0]))
return { options: (values as string[]).map(value => ({ value })) }; return { options: (values as string[]).map(valueOrLabel => ({ valueOrLabel })) };
return { options: values as SelectOption[] }; return { options: values as SelectOption[] };
} }

View file

@ -32,7 +32,7 @@ export type Env = { [key: string]: string | number | boolean | undefined };
export type WaitForEventOptions = Function | { predicate?: Function, timeout?: number }; export type WaitForEventOptions = Function | { predicate?: Function, timeout?: number };
export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number }; export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number };
export type SelectOption = { value?: string, label?: string, index?: number }; export type SelectOption = { value?: string, label?: string, index?: number, valueOrLabel?: string };
export type SelectOptionOptions = { force?: boolean, timeout?: number, noWaitAfter?: boolean }; export type SelectOptionOptions = { force?: boolean, timeout?: number, noWaitAfter?: boolean };
export type FilePayload = { name: string, mimeType: string, buffer: Buffer }; export type FilePayload = { name: string, mimeType: string, buffer: Buffer };
export type StorageState = { export type StorageState = {

View file

@ -1465,6 +1465,7 @@ scheme.FrameSelectOptionParams = tObject({
strict: tOptional(tBoolean), strict: tOptional(tBoolean),
elements: tOptional(tArray(tChannel(['ElementHandle']))), elements: tOptional(tArray(tChannel(['ElementHandle']))),
options: tOptional(tArray(tObject({ options: tOptional(tArray(tObject({
valueOrLabel: tOptional(tString),
value: tOptional(tString), value: tOptional(tString),
label: tOptional(tString), label: tOptional(tString),
index: tOptional(tNumber), index: tOptional(tNumber),
@ -1833,6 +1834,7 @@ scheme.ElementHandleScrollIntoViewIfNeededResult = tOptional(tObject({}));
scheme.ElementHandleSelectOptionParams = tObject({ scheme.ElementHandleSelectOptionParams = tObject({
elements: tOptional(tArray(tChannel(['ElementHandle']))), elements: tOptional(tArray(tChannel(['ElementHandle']))),
options: tOptional(tArray(tObject({ options: tOptional(tArray(tObject({
valueOrLabel: tOptional(tString),
value: tOptional(tString), value: tOptional(tString),
label: tOptional(tString), label: tOptional(tString),
index: tOptional(tNumber), index: tOptional(tNumber),

View file

@ -621,8 +621,9 @@ export class InjectedScript {
throw this.createStacklessError(`Unexpected element state "${state}"`); throw this.createStacklessError(`Unexpected element state "${state}"`);
} }
selectOptions(optionsToSelect: (Node | { value?: string, label?: string, index?: number })[], selectOptions(optionsToSelect: (Node | { valueOrLabel?: string, value?: string, label?: string, index?: number })[],
node: Node, progress: InjectedScriptProgress): string[] | 'error:notconnected' | symbol { node: Node, progress: InjectedScriptProgress): string[] | 'error:notconnected' | symbol {
const element = this.retarget(node, 'follow-label'); const element = this.retarget(node, 'follow-label');
if (!element) if (!element)
return 'error:notconnected'; return 'error:notconnected';
@ -634,10 +635,12 @@ export class InjectedScript {
let remainingOptionsToSelect = optionsToSelect.slice(); let remainingOptionsToSelect = optionsToSelect.slice();
for (let index = 0; index < options.length; index++) { for (let index = 0; index < options.length; index++) {
const option = options[index]; const option = options[index];
const filter = (optionToSelect: Node | { value?: string, label?: string, index?: number }) => { const filter = (optionToSelect: Node | { valueOrLabel?: string, value?: string, label?: string, index?: number }) => {
if (optionToSelect instanceof Node) if (optionToSelect instanceof Node)
return option === optionToSelect; return option === optionToSelect;
let matches = true; let matches = true;
if (optionToSelect.valueOrLabel !== undefined)
matches = matches && (optionToSelect.valueOrLabel === option.value || optionToSelect.valueOrLabel === option.label);
if (optionToSelect.value !== undefined) if (optionToSelect.value !== undefined)
matches = matches && optionToSelect.value === option.value; matches = matches && optionToSelect.value === option.value;
if (optionToSelect.label !== undefined) if (optionToSelect.label !== undefined)

View file

@ -3517,8 +3517,8 @@ export interface Page {
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be * @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be
* used. * used.
* @param values Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise * @param values Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise
* only the first option matching one of the passed options is selected. String values are equivalent to * only the first option matching one of the passed options is selected. String values are matching both values and
* `{value:'string'}`. Option is considered matching if all specified properties match. * labels. Option is considered matching if all specified properties match.
* @param options * @param options
*/ */
selectOption(selector: string, values: null|string|ElementHandle|Array<string>|{ selectOption(selector: string, values: null|string|ElementHandle|Array<string>|{
@ -6389,8 +6389,8 @@ export interface Frame {
* *
* @param selector A selector to query for. * @param selector A selector to query for.
* @param values Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise * @param values Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise
* only the first option matching one of the passed options is selected. String values are equivalent to * only the first option matching one of the passed options is selected. String values are matching both values and
* `{value:'string'}`. Option is considered matching if all specified properties match. * labels. Option is considered matching if all specified properties match.
* @param options * @param options
*/ */
selectOption(selector: string, values: null|string|ElementHandle|Array<string>|{ selectOption(selector: string, values: null|string|ElementHandle|Array<string>|{
@ -9316,8 +9316,8 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
* ``` * ```
* *
* @param values Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise * @param values Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise
* only the first option matching one of the passed options is selected. String values are equivalent to * only the first option matching one of the passed options is selected. String values are matching both values and
* `{value:'string'}`. Option is considered matching if all specified properties match. * labels. Option is considered matching if all specified properties match.
* @param options * @param options
*/ */
selectOption(values: null|string|ElementHandle|Array<string>|{ selectOption(values: null|string|ElementHandle|Array<string>|{
@ -10923,6 +10923,10 @@ export interface Locator {
}): Promise<void>; }): Promise<void>;
/** /**
* Selects option or options in `<select>`.
*
* **Details**
*
* This method waits for [actionability](https://playwright.dev/docs/actionability) checks, waits until all specified options are present in * This method waits for [actionability](https://playwright.dev/docs/actionability) checks, waits until all specified options are present in
* the `<select>` element and selects these options. * the `<select>` element and selects these options.
* *
@ -10937,20 +10941,28 @@ export interface Locator {
* *
* **Usage** * **Usage**
* *
* ```html
* <select multiple>
* <option value="red">Red</div>
* <option value="green">Green</div>
* <option value="blue">Blue</div>
* </select>
* ```
*
* ```js * ```js
* // single selection matching the value * // single selection matching the value or label
* element.selectOption('blue'); * element.selectOption('blue');
* *
* // single selection matching the label * // single selection matching the label
* element.selectOption({ label: 'Blue' }); * element.selectOption({ label: 'Blue' });
* *
* // multiple selection * // multiple selection for blue, red and second option
* element.selectOption(['red', 'green', 'blue']); * element.selectOption(['red', 'green', 'blue']);
* ``` * ```
* *
* @param values Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise * @param values Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise
* only the first option matching one of the passed options is selected. String values are equivalent to * only the first option matching one of the passed options is selected. String values are matching both values and
* `{value:'string'}`. Option is considered matching if all specified properties match. * labels. Option is considered matching if all specified properties match.
* @param options * @param options
*/ */
selectOption(values: null|string|ElementHandle|Array<string>|{ selectOption(values: null|string|ElementHandle|Array<string>|{

View file

@ -2651,6 +2651,7 @@ export type FrameSelectOptionParams = {
strict?: boolean, strict?: boolean,
elements?: ElementHandleChannel[], elements?: ElementHandleChannel[],
options?: { options?: {
valueOrLabel?: string,
value?: string, value?: string,
label?: string, label?: string,
index?: number, index?: number,
@ -2663,6 +2664,7 @@ export type FrameSelectOptionOptions = {
strict?: boolean, strict?: boolean,
elements?: ElementHandleChannel[], elements?: ElementHandleChannel[],
options?: { options?: {
valueOrLabel?: string,
value?: string, value?: string,
label?: string, label?: string,
index?: number, index?: number,
@ -3275,6 +3277,7 @@ export type ElementHandleScrollIntoViewIfNeededResult = void;
export type ElementHandleSelectOptionParams = { export type ElementHandleSelectOptionParams = {
elements?: ElementHandleChannel[], elements?: ElementHandleChannel[],
options?: { options?: {
valueOrLabel?: string,
value?: string, value?: string,
label?: string, label?: string,
index?: number, index?: number,
@ -3286,6 +3289,7 @@ export type ElementHandleSelectOptionParams = {
export type ElementHandleSelectOptionOptions = { export type ElementHandleSelectOptionOptions = {
elements?: ElementHandleChannel[], elements?: ElementHandleChannel[],
options?: { options?: {
valueOrLabel?: string,
value?: string, value?: string,
label?: string, label?: string,
index?: number, index?: number,

View file

@ -1962,6 +1962,7 @@ Frame:
items: items:
type: object type: object
properties: properties:
valueOrLabel: string?
value: string? value: string?
label: string? label: string?
index: number? index: number?
@ -2512,6 +2513,7 @@ ElementHandle:
items: items:
type: object type: object
properties: properties:
valueOrLabel: string?
value: string? value: string?
label: string? label: string?
index: number? index: number?

View file

@ -36,6 +36,13 @@ it('should select single option by value', async ({ page, server }) => {
expect(await page.evaluate(() => window['result'].onChange)).toEqual(['blue']); expect(await page.evaluate(() => window['result'].onChange)).toEqual(['blue']);
}); });
it('should fall back to selecting by label', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/select.html');
await page.selectOption('select', 'Blue');
expect(await page.evaluate(() => window['result'].onInput)).toEqual(['blue']);
expect(await page.evaluate(() => window['result'].onChange)).toEqual(['blue']);
});
it('should select single option by label', async ({ page, server }) => { it('should select single option by label', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/select.html'); await page.goto(server.PREFIX + '/input/select.html');
await page.selectOption('select', { label: 'Indigo' }); await page.selectOption('select', { label: 'Indigo' });