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 });