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}"`);
|
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');
|
const element = this.retarget(node, 'follow-label');
|
||||||
if (!element)
|
if (!element)
|
||||||
return 'error:notconnected';
|
return 'error:notconnected';
|
||||||
@ -776,6 +776,8 @@ export class InjectedScript {
|
|||||||
};
|
};
|
||||||
if (!remainingOptionsToSelect.some(filter))
|
if (!remainingOptionsToSelect.some(filter))
|
||||||
continue;
|
continue;
|
||||||
|
if (!this.elementState(option, 'enabled').matches)
|
||||||
|
return 'error:optionnotenabled';
|
||||||
selectedOptions.push(option);
|
selectedOptions.push(option);
|
||||||
if (select.multiple) {
|
if (select.multiple) {
|
||||||
remainingOptionsToSelect = remainingOptionsToSelect.filter(o => !filter(o));
|
remainingOptionsToSelect = remainingOptionsToSelect.filter(o => !filter(o));
|
||||||
|
@ -1060,8 +1060,12 @@ export function getAriaDisabled(element: Element): boolean {
|
|||||||
|
|
||||||
function isNativelyDisabled(element: Element) {
|
function isNativelyDisabled(element: Element) {
|
||||||
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
|
// 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);
|
const isNativeFormControl = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'].includes(elementSafeTagName(element));
|
||||||
return isNativeFormControl && (element.hasAttribute('disabled') || belongsToDisabledFieldSet(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 {
|
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 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 {
|
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');
|
progress.log(' did not find some options');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (result === 'error:optionnotenabled') {
|
||||||
|
progress.log(' option being selected is not enabled');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (typeof result === 'object' && 'hitTargetDescription' in result) {
|
if (typeof result === 'object' && 'hitTargetDescription' in result) {
|
||||||
progress.log(` ${result.hitTargetDescription} intercepts pointer events`);
|
progress.log(` ${result.hitTargetDescription} intercepts pointer events`);
|
||||||
continue;
|
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']);
|
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(`
|
await page.setContent(`
|
||||||
<select disabled>
|
<select disabled>
|
||||||
<option>one</option>
|
<option>one</option>
|
||||||
@ -346,6 +346,68 @@ it('should wait for option to be enabled', async ({ page }) => {
|
|||||||
await expect(page.locator('select')).toHaveValue('two');
|
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 }) => {
|
it('should wait for select to be swapped', async ({ page }) => {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<select disabled>
|
<select disabled>
|
||||||
|
@ -217,6 +217,15 @@ test('should support disabled', async ({ page }) => {
|
|||||||
<fieldset disabled>
|
<fieldset disabled>
|
||||||
<button>Yay</button>
|
<button>Yay</button>
|
||||||
</fieldset>
|
</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([
|
expect(await page.locator(`role=button[disabled]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||||
`<button disabled="">Bye</button>`,
|
`<button disabled="">Bye</button>`,
|
||||||
@ -241,6 +250,18 @@ test('should support disabled', async ({ page }) => {
|
|||||||
`<button>Hi</button>`,
|
`<button>Hi</button>`,
|
||||||
`<button aria-disabled="false">Oh</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 }) => {
|
test('should inherit disabled from the ancestor', async ({ page }) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user