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
- 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.
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**
```html
<select multiple>
<option value="red">Red</div>
<option value="green">Green</div>
<option value="blue">Blue</div>
</select>
```
```js
// single selection matching the value
// single selection matching the value or label
element.selectOption('blue');
// single selection matching the label
element.selectOption({ label: 'Blue' });
// multiple selection
// multiple selection for red, green and blue options
element.selectOption(['red', 'green', 'blue']);
```
```java
// single selection matching the value
// single selection matching the value or label
element.selectOption("blue");
// single selection matching the label
element.selectOption(new SelectOption().setLabel("Blue"));
// multiple selection
// multiple selection for blue, red and second option
element.selectOption(new String[] {"red", "green", "blue"});
```
```python async
# single selection matching the value
# single selection matching the value or label
await element.select_option("blue")
# single selection matching the label
await element.select_option(label="blue")
# multiple selection
# multiple selection for blue, red and second option
await element.select_option(value=["red", "green", "blue"])
```
```python sync
# single selection matching the value
# single selection matching the value or label
element.select_option("blue")
# single selection matching both the label
# single selection matching the label
element.select_option(label="blue")
# multiple selection
# multiple selection for blue, red and second option
element.select_option(value=["red", "green", "blue"])
```
```csharp
// single selection matching the value
// single selection matching the value or label
await element.SelectOptionAsync(new[] { "blue" });
// single selection matching the label
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
await element.SelectOptionAsync(new[] {
new SelectOptionValue() { Label = "blue" },
new SelectOptionValue() { Index = 2 },
new SelectOptionValue() { Value = "red" }});
await element.SelectOptionAsync(new[] { "red", "green", "blue" });
```
### 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.
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.
## wait-for-navigation-url

View file

@ -252,7 +252,7 @@ export function convertSelectOptionValues(values: string | api.ElementHandle | S
if (values[0] instanceof ElementHandle)
return { elements: (values as ElementHandle[]).map((v: ElementHandle) => v._elementChannel) };
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[] };
}

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 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 FilePayload = { name: string, mimeType: string, buffer: Buffer };
export type StorageState = {

View file

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

View file

@ -621,8 +621,9 @@ export class InjectedScript {
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 {
const element = this.retarget(node, 'follow-label');
if (!element)
return 'error:notconnected';
@ -634,10 +635,12 @@ export class InjectedScript {
let remainingOptionsToSelect = optionsToSelect.slice();
for (let index = 0; index < options.length; 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)
return option === optionToSelect;
let matches = true;
if (optionToSelect.valueOrLabel !== undefined)
matches = matches && (optionToSelect.valueOrLabel === option.value || optionToSelect.valueOrLabel === option.label);
if (optionToSelect.value !== undefined)
matches = matches && optionToSelect.value === option.value;
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
* used.
* @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
* `{value:'string'}`. Option is considered matching if all specified properties match.
* only the 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.
* @param options
*/
selectOption(selector: string, values: null|string|ElementHandle|Array<string>|{
@ -6389,8 +6389,8 @@ export interface Frame {
*
* @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
* only the first option matching one of the passed options is selected. String values are equivalent to
* `{value:'string'}`. Option is considered matching if all specified properties match.
* only the 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.
* @param options
*/
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
* only the first option matching one of the passed options is selected. String values are equivalent to
* `{value:'string'}`. Option is considered matching if all specified properties match.
* only the 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.
* @param options
*/
selectOption(values: null|string|ElementHandle|Array<string>|{
@ -10923,6 +10923,10 @@ export interface Locator {
}): 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
* the `<select>` element and selects these options.
*
@ -10937,20 +10941,28 @@ export interface Locator {
*
* **Usage**
*
* ```html
* <select multiple>
* <option value="red">Red</div>
* <option value="green">Green</div>
* <option value="blue">Blue</div>
* </select>
* ```
*
* ```js
* // single selection matching the value
* // single selection matching the value or label
* element.selectOption('blue');
*
* // single selection matching the label
* element.selectOption({ label: 'Blue' });
*
* // multiple selection
* // multiple selection for blue, red and second option
* element.selectOption(['red', 'green', 'blue']);
* ```
*
* @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
* `{value:'string'}`. Option is considered matching if all specified properties match.
* only the 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.
* @param options
*/
selectOption(values: null|string|ElementHandle|Array<string>|{

View file

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

View file

@ -1962,6 +1962,7 @@ Frame:
items:
type: object
properties:
valueOrLabel: string?
value: string?
label: string?
index: number?
@ -2512,6 +2513,7 @@ ElementHandle:
items:
type: object
properties:
valueOrLabel: string?
value: string?
label: string?
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']);
});
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 }) => {
await page.goto(server.PREFIX + '/input/select.html');
await page.selectOption('select', { label: 'Indigo' });