diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md
index fdf918af12..5b5e4c7fd6 100644
--- a/docs/src/api/class-locatorassertions.md
+++ b/docs/src/api/class-locatorassertions.md
@@ -293,13 +293,25 @@ Whether to use `element.innerText` instead of `element.textContent` when retriev
The opposite of [`method: LocatorAssertions.toHaveValue`].
### param: LocatorAssertions.NotToHaveValue.value
-- `value` <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
+- `value` <[string]|[RegExp]>
-Expected value. A list of expected values can be used if the Locator is a `select` element with the `multiple` attribute.
+Expected value.
### option: LocatorAssertions.NotToHaveValue.timeout = %%-js-assertions-timeout-%%
### option: LocatorAssertions.NotToHaveValue.timeout = %%-csharp-java-python-assertions-timeout-%%
+## async method: LocatorAssertions.NotToHaveValues
+* langs: python
+
+The opposite of [`method: LocatorAssertions.toHaveValues`].
+
+### param: LocatorAssertions.NotToHaveValues.values
+- `values` <[Array]<[string]|[RegExp]>>
+
+Expected options currently selected.
+
+### option: LocatorAssertions.NotToHaveValues.timeout = %%-js-assertions-timeout-%%
+### option: LocatorAssertions.NotToHaveValues.timeout = %%-csharp-java-python-assertions-timeout-%%
## async method: LocatorAssertions.toBeChecked
* langs:
@@ -1201,9 +1213,68 @@ await Expect(locator).ToHaveValueAsync(new Regex("[0-9]"));
```
### param: LocatorAssertions.toHaveValue.value
-- `value` <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
+- `value` <[string]|[RegExp]>
-Expected value. A list of expected values can be used if the Locator is a `select` element with the `multiple` attribute.
+Expected value.
### option: LocatorAssertions.toHaveValue.timeout = %%-js-assertions-timeout-%%
### option: LocatorAssertions.toHaveValue.timeout = %%-csharp-java-python-assertions-timeout-%%
+
+## async method: LocatorAssertions.toHaveValues
+* langs:
+ - alias-java: hasValues
+
+Ensures the [Locator] points to multi-select/combobox (i.e. a `select` with the `multiple` attribute) and the specified values are selected.
+
+For example, given the following element:
+
+```html
+
+```
+
+```js
+const locator = page.locator("id=favorite-colors");
+await locator.selectOption(["R", "G"]);
+await expect(locator).toHaveValues([/R/, /G/]);
+```
+
+```java
+page.locator("id=favorite-colors").selectOption(["R", "G"]);
+assertThat(page.locator("id=favorite-colors")).hasValues(new Pattern[] { Pattern.compile("R"), Pattern.compile("G") });
+```
+
+```python async
+import re
+from playwright.async_api import expect
+
+locator = page.locator("id=favorite-colors")
+await locator.select_option(["R", "G"])
+await expect(locator).to_have_values([re.compile(r"R"), re.compile(r"G")])
+```
+
+```python sync
+import re
+from playwright.sync_api import expect
+
+locator = page.locator("id=favorite-colors")
+locator.select_option(["R", "G"])
+expect(locator).to_have_values([re.compile(r"R"), re.compile(r"G")])
+```
+
+```csharp
+var locator = Page.Locator("id=favorite-colors");
+await locator.SelectOptionAsync(new string[] { "R", "G" })
+await Expect(locator).ToHaveValuesAsync(new Regex[] { new Regex("R"), new Regex("G") });
+```
+
+### param: LocatorAssertions.toHaveValues.values
+- `values` <[Array]<[string]|[RegExp]>>
+
+Expected options currently selected.
+
+### option: LocatorAssertions.toHaveValues.timeout = %%-js-assertions-timeout-%%
+### option: LocatorAssertions.toHaveValues.timeout = %%-csharp-java-python-assertions-timeout-%%
diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts
index a6f6f95ea4..f2a8af0440 100644
--- a/packages/playwright-core/src/server/injected/injectedScript.ts
+++ b/packages/playwright-core/src/server/injected/injectedScript.ts
@@ -1053,13 +1053,13 @@ export class InjectedScript {
// Multi-Select/Combobox
{
- if (expression === 'to.have.value' && options.expectedText?.length && options.expectedText.length >= 2) {
+ if (expression === 'to.have.values') {
element = this.retarget(element, 'follow-label')!;
if (element.nodeName !== 'SELECT' || !(element as HTMLSelectElement).multiple)
throw this.createStacklessError('Not a select element with a multiple attribute');
const received = [...(element as HTMLSelectElement).selectedOptions].map(o => o.value);
- if (received.length !== options.expectedText.length)
+ if (received.length !== options.expectedText!.length)
return { received, matches: false };
return { received, matches: received.map((r, i) => new ExpectedTextMatcher(options.expectedText![i]).matches(r)).every(Boolean) };
}
diff --git a/packages/playwright-test/src/expect.ts b/packages/playwright-test/src/expect.ts
index 77d1b88f9c..eb17f3b7d5 100644
--- a/packages/playwright-test/src/expect.ts
+++ b/packages/playwright-test/src/expect.ts
@@ -36,7 +36,8 @@ import {
toHaveText,
toHaveTitle,
toHaveURL,
- toHaveValue
+ toHaveValue,
+ toHaveValues,
} from './matchers/matchers';
import { toMatchSnapshot, toHaveScreenshot } from './matchers/toMatchSnapshot';
import type { Expect } from './types';
@@ -141,6 +142,7 @@ const customMatchers = {
toHaveTitle,
toHaveURL,
toHaveValue,
+ toHaveValues,
toMatchSnapshot,
toHaveScreenshot,
};
diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts
index 81f9ebd63a..c45f4b4ad8 100644
--- a/packages/playwright-test/src/matchers/matchers.ts
+++ b/packages/playwright-test/src/matchers/matchers.ts
@@ -234,20 +234,25 @@ export function toHaveText(
export function toHaveValue(
this: ReturnType,
locator: LocatorEx,
- expected: string | RegExp | (string | RegExp)[],
+ expected: string | RegExp,
options?: { timeout?: number },
) {
- if (Array.isArray(expected)) {
- return toEqual.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
- const expectedText = toExpectedTextValues(expected);
- return await locator._expect(customStackTrace, 'to.have.value', { expectedText, isNot, timeout });
- }, expected, options);
- } else {
- return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
- const expectedText = toExpectedTextValues([expected]);
- return await locator._expect(customStackTrace, 'to.have.value', { expectedText, isNot, timeout });
- }, expected, options);
- }
+ return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
+ const expectedText = toExpectedTextValues([expected]);
+ return await locator._expect(customStackTrace, 'to.have.value', { expectedText, isNot, timeout });
+ }, expected, options);
+}
+
+export function toHaveValues(
+ this: ReturnType,
+ locator: LocatorEx,
+ expected: (string | RegExp)[],
+ options?: { timeout?: number },
+) {
+ return toEqual.call(this, 'toHaveValues', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
+ const expectedText = toExpectedTextValues(expected);
+ return await locator._expect(customStackTrace, 'to.have.values', { expectedText, isNot, timeout });
+ }, expected, options);
}
export function toHaveTitle(
diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts
index 62861ec1cc..95401720f0 100644
--- a/packages/playwright-test/types/test.d.ts
+++ b/packages/playwright-test/types/test.d.ts
@@ -3528,10 +3528,40 @@ interface LocatorAssertions {
* await expect(locator).toHaveValue(/[0-9]/);
* ```
*
- * @param value Expected value. A list of expected values can be used if the Locator is a `select` element with the `multiple` attribute.
+ * @param value Expected value.
* @param options
*/
- toHaveValue(value: string|RegExp|Array, options?: {
+ toHaveValue(value: string|RegExp, options?: {
+ /**
+ * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`.
+ */
+ timeout?: number;
+ }): Promise;
+
+ /**
+ * Ensures the [Locator] points to multi-select/combobox (i.e. a `select` with the `multiple` attribute) and the specified
+ * values are selected.
+ *
+ * For example, given the following element:
+ *
+ * ```html
+ *
+ * ```
+ *
+ * ```js
+ * const locator = page.locator("id=favorite-colors");
+ * await locator.selectOption(["R", "G"]);
+ * await expect(locator).toHaveValues([/R/, /G/]);
+ * ```
+ *
+ * @param values Expected options currently selected.
+ * @param options
+ */
+ toHaveValues(values: Array, options?: {
/**
* Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`.
*/
diff --git a/tests/playwright-test/playwright.expect.text.spec.ts b/tests/playwright-test/playwright.expect.text.spec.ts
index 75e6118163..ef205cae0a 100644
--- a/tests/playwright-test/playwright.expect.text.spec.ts
+++ b/tests/playwright-test/playwright.expect.text.spec.ts
@@ -412,7 +412,7 @@ test('should support toHaveValue failing', async ({ runInlineTest }) => {
expect(result.output).toContain('"Text content"');
});
-test.describe('should support toHaveValue with multi-select', () => {
+test.describe('should support toHaveValues with multi-select', () => {
test('works with text', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
@@ -428,7 +428,7 @@ test.describe('should support toHaveValue with multi-select', () => {
\`);
const locator = page.locator('select');
await locator.selectOption(['R', 'G']);
- await expect(locator).toHaveValue(['R', 'G']);
+ await expect(locator).toHaveValues(['R', 'G']);
});
`,
}, { workers: 1 });
@@ -452,7 +452,7 @@ test.describe('should support toHaveValue with multi-select', () => {
\`);
const locator = page.locator('text=Pick a Color');
await locator.selectOption(['R', 'G']);
- await expect(locator).toHaveValue(['R', 'G']);
+ await expect(locator).toHaveValues(['R', 'G']);
});
`,
}, { workers: 1 });
@@ -474,7 +474,7 @@ test.describe('should support toHaveValue with multi-select', () => {
\`);
const locator = page.locator('select');
await locator.selectOption(['RR', 'GG']);
- await expect(locator).toHaveValue(['R', 'G']);
+ await expect(locator).toHaveValues(['R', 'G']);
});
`,
}, { workers: 1 });
@@ -508,7 +508,7 @@ test.describe('should support toHaveValue with multi-select', () => {
\`);
const locator = page.locator('select');
await locator.selectOption(['R', 'G']);
- await expect(locator).toHaveValue([/R/, /G/]);
+ await expect(locator).toHaveValues([/R/, /G/]);
});
`,
}, { workers: 1 });
@@ -531,7 +531,7 @@ test.describe('should support toHaveValue with multi-select', () => {
\`);
const locator = page.locator('select');
await locator.selectOption(['B']);
- await expect(locator).toHaveValue([/R/, /G/]);
+ await expect(locator).toHaveValues([/R/, /G/]);
});
`,
}, { workers: 1 });
@@ -564,7 +564,7 @@ test.describe('should support toHaveValue with multi-select', () => {
\`);
const locator = page.locator('select');
await locator.selectOption(['B']);
- await expect(locator).toHaveValue([/R/, /G/]);
+ await expect(locator).toHaveValues([/R/, /G/]);
});
`,
}, { workers: 1 });
@@ -583,7 +583,7 @@ test.describe('should support toHaveValue with multi-select', () => {
\`);
const locator = page.locator('input');
- await expect(locator).toHaveValue([/R/, /G/]);
+ await expect(locator).toHaveValues([/R/, /G/]);
});
`,
}, { workers: 1 });