fix: input events triggered by playwright actions should have composed=true (#28730)

Fixes https://github.com/microsoft/playwright/issues/28726
This commit is contained in:
Yury Semikhatsky 2023-12-21 15:25:16 -08:00 committed by GitHub
parent 207585ef63
commit ff99aa33b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 7 deletions

View File

@ -720,8 +720,8 @@ export class InjectedScript {
select.value = undefined as any;
selectedOptions.forEach(option => option.selected = true);
progress.log(' selected specified option(s)');
select.dispatchEvent(new Event('input', { 'bubbles': true }));
select.dispatchEvent(new Event('change', { 'bubbles': true }));
select.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
select.dispatchEvent(new Event('change', { bubbles: true }));
return selectedOptions.map(option => option.value);
}
@ -732,7 +732,7 @@ export class InjectedScript {
if (element.nodeName.toLowerCase() === 'input') {
const input = element as HTMLInputElement;
const type = input.type.toLowerCase();
const kInputTypesToSetValue = new Set(['color', 'date', 'time', 'datetime', 'datetime-local', 'month', 'range', 'week']);
const kInputTypesToSetValue = new Set(['color', 'date', 'time', 'datetime-local', 'month', 'range', 'week']);
const kInputTypesToTypeInto = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']);
if (!kInputTypesToTypeInto.has(type) && !kInputTypesToSetValue.has(type)) {
progress.log(` input of type "${type}" cannot be filled`);
@ -749,8 +749,8 @@ export class InjectedScript {
input.value = value;
if (input.value !== value)
throw this.createStacklessError('Malformed value');
element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
return 'done'; // We have already changed the value, no need to input it.
}
} else if (element.nodeName.toLowerCase() === 'textarea') {
@ -852,8 +852,8 @@ export class InjectedScript {
for (const file of files)
dt.items.add(file);
input.files = dt.files;
input.dispatchEvent(new Event('input', { 'bubbles': true }));
input.dispatchEvent(new Event('change', { 'bubbles': true }));
input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
expectHitTarget(hitPoint: { x: number, y: number }, targetElement: Element) {

View File

@ -89,6 +89,52 @@ it('should fill date input after clicking', async ({ page, server }) => {
expect(await page.$eval('input', input => input.value)).toBe('2020-03-02');
});
for (const [type, value] of Object.entries({
'color': '#aaaaaa',
'date': '2020-03-02',
'time': '13:15',
'datetime-local': '2020-03-02T13:15:30',
'month': '2020-03',
'range': '42',
'week': '2020-W50'
})) {
it(`input event.composed should be true and cross shadow dom boundary - ${type}`, async ({ page, server, browserName }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28726' });
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<body><script>
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<input type=${type}></input>';
document.body.appendChild(div);
</script></body>`);
await page.locator('body').evaluate(select => {
(window as any).firedBodyEvents = [];
for (const event of ['input', 'change']) {
select.addEventListener(event, e => {
(window as any).firedBodyEvents.push(e.type + ':' + e.composed);
}, false);
}
});
await page.locator('input').evaluate(select => {
(window as any).firedEvents = [];
for (const event of ['input', 'change']) {
select.addEventListener(event, e => {
(window as any).firedEvents.push(e.type + ':' + e.composed);
}, false);
}
});
await page.locator('input').fill(value);
expect(await page.evaluate(() => window['firedEvents'])).toEqual(
(browserName !== 'chromium' && (type === 'month' || type === 'week')) ?
['input:true'] :
['input:true', 'change:false']
);
expect(await page.evaluate(() => window['firedBodyEvents'])).toEqual(['input:true']);
});
}
it('should throw on incorrect date', async ({ page, browserName }) => {
it.skip(browserName === 'webkit', 'WebKit does not support date inputs');

View File

@ -287,3 +287,37 @@ it('should wait for multiple options to be present', async ({ page, server }) =>
const items = await selectPromise;
expect(items).toStrictEqual(['green', 'scarlet']);
});
it('input event.composed should be true and cross shadow dom boundary', async ({ page, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28726' });
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<body><script>
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'open'});
shadowRoot.innerHTML = \`<select>
<option value="black">Black</option>
<option value="blue">Blue</option>
</select>\`;
document.body.appendChild(div);
</script></body>`);
await page.locator('body').evaluate(select => {
(window as any).firedBodyEvents = [];
for (const event of ['input', 'change']) {
select.addEventListener(event, e => {
(window as any).firedBodyEvents.push(e.type + ':' + e.composed);
}, false);
}
});
await page.locator('select').evaluate(select => {
(window as any).firedEvents = [];
for (const event of ['input', 'change']) {
select.addEventListener(event, e => {
(window as any).firedEvents.push(e.type + ':' + e.composed);
}, false);
}
});
await page.selectOption('select', 'blue');
expect(await page.evaluate(() => window['firedEvents'])).toEqual(['input:true', 'change:false']);
expect(await page.evaluate(() => window['firedBodyEvents'])).toEqual(['input:true']);
});

View File

@ -533,6 +533,41 @@ it('should emit input and change events', async ({ page, asset }) => {
expect(events[1].type).toBe('change');
});
it('input event.composed should be true and cross shadow dom boundary', async ({ page, server, asset }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28726' });
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<body><script>
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<input type=file></input>';
document.body.appendChild(div);
</script></body>`);
await page.locator('body').evaluate(select => {
(window as any).firedBodyEvents = [];
for (const event of ['input', 'change']) {
select.addEventListener(event, e => {
(window as any).firedBodyEvents.push(e.type + ':' + e.composed);
}, false);
}
});
await page.locator('input').evaluate(select => {
(window as any).firedEvents = [];
for (const event of ['input', 'change']) {
select.addEventListener(event, e => {
(window as any).firedEvents.push(e.type + ':' + e.composed);
}, false);
}
});
await page.locator('input').setInputFiles({
name: 'test.txt',
mimeType: 'text/plain',
buffer: Buffer.from('this is a test')
});
expect(await page.evaluate(() => window['firedEvents'])).toEqual(['input:true', 'change:false']);
expect(await page.evaluate(() => window['firedBodyEvents'])).toEqual(['input:true']);
});
it('should work for single file pick', async ({ page, server }) => {
await page.setContent(`<input type=file>`);
const [fileChooser] = await Promise.all([