feat: support multi-select/combo box with toHaveValue (#14555)

This commit is contained in:
Ross Wollman 2022-06-02 12:10:28 -04:00 committed by GitHub
parent d5bfd786b9
commit e0a87e52d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 214 additions and 12 deletions

View File

@ -293,9 +293,9 @@ Whether to use `element.innerText` instead of `element.textContent` when retriev
The opposite of [`method: LocatorAssertions.toHaveValue`]. The opposite of [`method: LocatorAssertions.toHaveValue`].
### param: LocatorAssertions.NotToHaveValue.value ### param: LocatorAssertions.NotToHaveValue.value
- `value` <[string]|[RegExp]> - `value` <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
Expected value. Expected value. A list of expected values can be used if the Locator is a `select` element with the `multiple` attribute.
### option: LocatorAssertions.NotToHaveValue.timeout = %%-js-assertions-timeout-%% ### option: LocatorAssertions.NotToHaveValue.timeout = %%-js-assertions-timeout-%%
### option: LocatorAssertions.NotToHaveValue.timeout = %%-csharp-java-python-assertions-timeout-%% ### option: LocatorAssertions.NotToHaveValue.timeout = %%-csharp-java-python-assertions-timeout-%%
@ -1201,9 +1201,9 @@ await Expect(locator).ToHaveValueAsync(new Regex("[0-9]"));
``` ```
### param: LocatorAssertions.toHaveValue.value ### param: LocatorAssertions.toHaveValue.value
- `value` <[string]|[RegExp]> - `value` <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
Expected value. Expected value. A list of expected values can be used if the Locator is a `select` element with the `multiple` attribute.
### option: LocatorAssertions.toHaveValue.timeout = %%-js-assertions-timeout-%% ### option: LocatorAssertions.toHaveValue.timeout = %%-js-assertions-timeout-%%
### option: LocatorAssertions.toHaveValue.timeout = %%-csharp-java-python-assertions-timeout-%% ### option: LocatorAssertions.toHaveValue.timeout = %%-csharp-java-python-assertions-timeout-%%

View File

@ -1051,6 +1051,20 @@ export class InjectedScript {
} }
} }
// Multi-Select/Combobox
{
if (expression === 'to.have.value' && options.expectedText?.length && options.expectedText.length >= 2) {
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)
return { received, matches: false };
return { received, matches: received.map((r, i) => new ExpectedTextMatcher(options.expectedText![i]).matches(r)).every(Boolean) };
}
}
{ {
// Single text value. // Single text value.
let received: string | undefined; let received: string | undefined;
@ -1072,7 +1086,7 @@ export class InjectedScript {
element = this.retarget(element, 'follow-label')!; element = this.retarget(element, 'follow-label')!;
if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT') if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')
throw this.createStacklessError('Not an input element'); throw this.createStacklessError('Not an input element');
received = (element as any).value; received = (element as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement).value;
} }
if (received !== undefined && options.expectedText) { if (received !== undefined && options.expectedText) {

View File

@ -234,13 +234,20 @@ export function toHaveText(
export function toHaveValue( export function toHaveValue(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: LocatorEx, locator: LocatorEx,
expected: string | RegExp, expected: string | RegExp | (string | RegExp)[],
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout, customStackTrace) => { if (Array.isArray(expected)) {
const expectedText = toExpectedTextValues([expected]); return toEqual.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return await locator._expect(customStackTrace, 'to.have.value', { expectedText, isNot, timeout }); const expectedText = toExpectedTextValues(expected);
}, expected, options); 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);
}
} }
export function toHaveTitle( export function toHaveTitle(

View File

@ -3528,10 +3528,10 @@ interface LocatorAssertions {
* await expect(locator).toHaveValue(/[0-9]/); * await expect(locator).toHaveValue(/[0-9]/);
* ``` * ```
* *
* @param value Expected value. * @param value Expected value. A list of expected values can be used if the Locator is a `select` element with the `multiple` attribute.
* @param options * @param options
*/ */
toHaveValue(value: string|RegExp, options?: { toHaveValue(value: string|RegExp|Array<string|RegExp>, options?: {
/** /**
* Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`.
*/ */

View File

@ -412,6 +412,187 @@ test('should support toHaveValue failing', async ({ runInlineTest }) => {
expect(result.output).toContain('"Text content"'); expect(result.output).toContain('"Text content"');
}); });
test.describe('should support toHaveValue with multi-select', () => {
test('works with text', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent(\`
<select multiple>
<option value="R">Red</option>
<option value="G">Green</option>
<option value="B">Blue</option>
</select>
\`);
const locator = page.locator('select');
await locator.selectOption(['R', 'G']);
await expect(locator).toHaveValue(['R', 'G']);
});
`,
}, { workers: 1 });
expect(result.passed).toBe(1);
expect(result.exitCode).toBe(0);
});
test('follows labels', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent(\`
<label for="colors">Pick a Color</label>
<select id="colors" multiple>
<option value="R">Red</option>
<option value="G">Green</option>
<option value="B">Blue</option>
</select>
\`);
const locator = page.locator('text=Pick a Color');
await locator.selectOption(['R', 'G']);
await expect(locator).toHaveValue(['R', 'G']);
});
`,
}, { workers: 1 });
expect(result.passed).toBe(1);
expect(result.exitCode).toBe(0);
});
test('exact match with text', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent(\`
<select multiple>
<option value="RR">Red</option>
<option value="GG">Green</option>
</select>
\`);
const locator = page.locator('select');
await locator.selectOption(['RR', 'GG']);
await expect(locator).toHaveValue(['R', 'G']);
});
`,
}, { workers: 1 });
expect(result.passed).toBe(0);
expect(result.exitCode).toBe(1);
expect(stripAnsi(result.output)).toContain(`
- Expected - 2
+ Received + 2
Array [
- "R",
- "G",
+ "RR",
+ "GG",
]
`);
});
test('works with regex', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent(\`
<select multiple>
<option value="R">Red</option>
<option value="G">Green</option>
<option value="B">Blue</option>
</select>
\`);
const locator = page.locator('select');
await locator.selectOption(['R', 'G']);
await expect(locator).toHaveValue([/R/, /G/]);
});
`,
}, { workers: 1 });
expect(result.passed).toBe(1);
expect(result.exitCode).toBe(0);
});
test('fails when items not selected', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent(\`
<select multiple>
<option value="R">Red</option>
<option value="G">Green</option>
<option value="B">Blue</option>
</select>
\`);
const locator = page.locator('select');
await locator.selectOption(['B']);
await expect(locator).toHaveValue([/R/, /G/]);
});
`,
}, { workers: 1 });
expect(result.passed).toBe(0);
expect(result.exitCode).toBe(1);
expect(stripAnsi(result.output)).toContain(`
- Expected - 2
+ Received + 1
Array [
- /R/,
- /G/,
+ "B",
]
`);
});
test('fails when multiple not specified', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent(\`
<select>
<option value="R">Red</option>
<option value="G">Green</option>
<option value="B">Blue</option>
</select>
\`);
const locator = page.locator('select');
await locator.selectOption(['B']);
await expect(locator).toHaveValue([/R/, /G/]);
});
`,
}, { workers: 1 });
expect(result.passed).toBe(0);
expect(result.exitCode).toBe(1);
expect(result.output).toContain('Not a select element with a multiple attribute');
});
test('fails when not a select element', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent(\`
<input value="foo" />
\`);
const locator = page.locator('input');
await expect(locator).toHaveValue([/R/, /G/]);
});
`,
}, { workers: 1 });
expect(result.passed).toBe(0);
expect(result.exitCode).toBe(1);
expect(result.output).toContain('Not a select element with a multiple attribute');
});
});
test('should print expected/received before timeout', async ({ runInlineTest }) => { test('should print expected/received before timeout', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'a.test.ts': ` 'a.test.ts': `