mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: do not select a disabled option (#36418)
This commit is contained in:
parent
1c3488aa5f
commit
86c2ee6f3c
@ -748,7 +748,7 @@ export class InjectedScript {
|
||||
throw this.createStacklessError(`Unexpected element state "${state}"`);
|
||||
}
|
||||
|
||||
selectOptions(node: Node, optionsToSelect: (Node | { valueOrLabel?: string, value?: string, label?: string, index?: number })[]): string[] | 'error:notconnected' | 'error:optionsnotfound' {
|
||||
selectOptions(node: Node, optionsToSelect: (Node | { valueOrLabel?: string, value?: string, label?: string, index?: number })[]): string[] | 'error:notconnected' | 'error:optionsnotfound' | 'error:optionnotenabled' {
|
||||
const element = this.retarget(node, 'follow-label');
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
@ -776,6 +776,8 @@ export class InjectedScript {
|
||||
};
|
||||
if (!remainingOptionsToSelect.some(filter))
|
||||
continue;
|
||||
if (!this.elementState(option, 'enabled').matches)
|
||||
return 'error:optionnotenabled';
|
||||
selectedOptions.push(option);
|
||||
if (select.multiple) {
|
||||
remainingOptionsToSelect = remainingOptionsToSelect.filter(o => !filter(o));
|
||||
|
@ -1060,8 +1060,12 @@ export function getAriaDisabled(element: Element): boolean {
|
||||
|
||||
function isNativelyDisabled(element: Element) {
|
||||
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
|
||||
const isNativeFormControl = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'].includes(element.tagName);
|
||||
return isNativeFormControl && (element.hasAttribute('disabled') || belongsToDisabledFieldSet(element));
|
||||
const isNativeFormControl = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'].includes(elementSafeTagName(element));
|
||||
return isNativeFormControl && (element.hasAttribute('disabled') || belongsToDisabledOptGroup(element) || belongsToDisabledFieldSet(element));
|
||||
}
|
||||
|
||||
function belongsToDisabledOptGroup(element: Element): boolean {
|
||||
return elementSafeTagName(element) === 'OPTION' && !!element.closest('OPTGROUP[DISABLED]');
|
||||
}
|
||||
|
||||
function belongsToDisabledFieldSet(element: Element): boolean {
|
||||
|
@ -38,7 +38,7 @@ export type InputFilesItems = {
|
||||
};
|
||||
|
||||
type ActionName = 'click' | 'hover' | 'dblclick' | 'tap' | 'move and up' | 'move and down';
|
||||
type PerformActionResult = 'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:optionsnotfound' | { missingState: ElementState } | { hitTargetDescription: string } | 'done';
|
||||
type PerformActionResult = 'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:optionsnotfound' | 'error:optionnotenabled' | { missingState: ElementState } | { hitTargetDescription: string } | 'done';
|
||||
|
||||
export class NonRecoverableDOMError extends Error {
|
||||
}
|
||||
@ -336,6 +336,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
progress.log(' did not find some options');
|
||||
continue;
|
||||
}
|
||||
if (result === 'error:optionnotenabled') {
|
||||
progress.log(' option being selected is not enabled');
|
||||
continue;
|
||||
}
|
||||
if (typeof result === 'object' && 'hitTargetDescription' in result) {
|
||||
progress.log(` ${result.hitTargetDescription} intercepts pointer events`);
|
||||
continue;
|
||||
|
@ -320,7 +320,7 @@ it('input event.composed should be true and cross shadow dom boundary', async ({
|
||||
expect(await page.evaluate(() => window['firedBodyEvents'])).toEqual(['input:true']);
|
||||
});
|
||||
|
||||
it('should wait for option to be enabled', async ({ page }) => {
|
||||
it('should wait for select to be enabled', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<select disabled>
|
||||
<option>one</option>
|
||||
@ -346,6 +346,68 @@ it('should wait for option to be enabled', async ({ page }) => {
|
||||
await expect(page.locator('select')).toHaveValue('two');
|
||||
});
|
||||
|
||||
it('should wait for option to be enabled', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<select>
|
||||
<option>one</option>
|
||||
<option disabled id=myoption>two</option>
|
||||
</select>
|
||||
|
||||
<script>
|
||||
function hydrate() {
|
||||
const option = document.querySelector('#myoption');
|
||||
option.removeAttribute('disabled');
|
||||
const select = document.querySelector('select');
|
||||
select.addEventListener('change', () => {
|
||||
window['result'] = select.value;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
`);
|
||||
|
||||
const error = await page.locator('select').selectOption('two', { timeout: 1000 }).catch(e => e);
|
||||
expect(error.message).toContain('option being selected is not enabled');
|
||||
|
||||
const selectPromise = page.locator('select').selectOption('two');
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
await page.evaluate(() => (window as any).hydrate());
|
||||
await selectPromise;
|
||||
expect(await page.evaluate(() => window['result'])).toEqual('two');
|
||||
await expect(page.locator('select')).toHaveValue('two');
|
||||
});
|
||||
|
||||
it('should wait for optgroup to be enabled', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<select>
|
||||
<option>one</option>
|
||||
<optgroup label="Group" disabled id=mygroup>
|
||||
<option>two</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<script>
|
||||
function hydrate() {
|
||||
const group = document.querySelector('#mygroup');
|
||||
group.removeAttribute('disabled');
|
||||
const select = document.querySelector('select');
|
||||
select.addEventListener('change', () => {
|
||||
window['result'] = select.value;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
`);
|
||||
|
||||
const error = await page.locator('select').selectOption('two', { timeout: 1000 }).catch(e => e);
|
||||
expect(error.message).toContain('option being selected is not enabled');
|
||||
|
||||
const selectPromise = page.locator('select').selectOption('two');
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
await page.evaluate(() => (window as any).hydrate());
|
||||
await selectPromise;
|
||||
expect(await page.evaluate(() => window['result'])).toEqual('two');
|
||||
await expect(page.locator('select')).toHaveValue('two');
|
||||
});
|
||||
|
||||
it('should wait for select to be swapped', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<select disabled>
|
||||
|
@ -217,6 +217,15 @@ test('should support disabled', async ({ page }) => {
|
||||
<fieldset disabled>
|
||||
<button>Yay</button>
|
||||
</fieldset>
|
||||
<select>
|
||||
<optgroup disabled>
|
||||
<option>one</option>
|
||||
</optgroup>
|
||||
<optgroup>
|
||||
<option>two</option>
|
||||
</optgroup>
|
||||
<option disabled>three</option>
|
||||
</select>
|
||||
`);
|
||||
expect(await page.locator(`role=button[disabled]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button disabled="">Bye</button>`,
|
||||
@ -241,6 +250,18 @@ test('should support disabled', async ({ page }) => {
|
||||
`<button>Hi</button>`,
|
||||
`<button aria-disabled="false">Oh</button>`,
|
||||
]);
|
||||
expect(await page.getByRole('option', { disabled: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<option>one</option>`,
|
||||
`<option disabled="">three</option>`,
|
||||
]);
|
||||
expect(await page.getByRole('option', { disabled: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<option>two</option>`,
|
||||
]);
|
||||
expect(await page.getByRole('option').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<option>one</option>`,
|
||||
`<option>two</option>`,
|
||||
`<option disabled="">three</option>`,
|
||||
]);
|
||||
});
|
||||
|
||||
test('should inherit disabled from the ancestor', async ({ page }) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user