diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 3af647e082..67852c541f 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -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) { diff --git a/tests/page/page-fill.spec.ts b/tests/page/page-fill.spec.ts index 1e1585bea3..9e90d94ecd 100644 --- a/tests/page/page-fill.spec.ts +++ b/tests/page/page-fill.spec.ts @@ -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(``); + 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'); diff --git a/tests/page/page-select-option.spec.ts b/tests/page/page-select-option.spec.ts index 43778b766a..522c6270ff 100644 --- a/tests/page/page-select-option.spec.ts +++ b/tests/page/page-select-option.spec.ts @@ -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(``); + 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']); +}); diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index 19869e9139..966459a2c9 100644 --- a/tests/page/page-set-input-files.spec.ts +++ b/tests/page/page-set-input-files.spec.ts @@ -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(``); + 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(``); const [fileChooser] = await Promise.all([