From 4d06b6e229ccb36fe9fc2f93487912153d6b9196 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 19 Dec 2023 16:48:17 -0800 Subject: [PATCH 1/6] fix: selectOption should fire composed input Fixes https://github.com/microsoft/playwright/issues/28726 --- .../src/server/injected/injectedScript.ts | 2 +- tests/page/page-select-option.spec.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 3af647e082953..8df667d75192d 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -720,7 +720,7 @@ 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('input', { 'bubbles': true, 'composed': true })); select.dispatchEvent(new Event('change', { 'bubbles': true })); return selectedOptions.map(option => option.value); } diff --git a/tests/page/page-select-option.spec.ts b/tests/page/page-select-option.spec.ts index 43778b766a23d..3c541064dfb18 100644 --- a/tests/page/page-select-option.spec.ts +++ b/tests/page/page-select-option.spec.ts @@ -287,3 +287,18 @@ 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', async ({ page, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28726' }); + await page.goto(server.PREFIX + '/input/select.html'); + 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']); +}); From 5976330292d7bc85245ccfb93148a01e6c57ea68 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 20 Dec 2023 17:48:24 -0800 Subject: [PATCH 2/6] Update test to check shadow dom boundary --- tests/page/page-select-option.spec.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/page/page-select-option.spec.ts b/tests/page/page-select-option.spec.ts index 3c541064dfb18..522c6270ff857 100644 --- a/tests/page/page-select-option.spec.ts +++ b/tests/page/page-select-option.spec.ts @@ -288,9 +288,27 @@ it('should wait for multiple options to be present', async ({ page, server }) => expect(items).toStrictEqual(['green', 'scarlet']); }); -it('input event.composed should be true', async ({ page, server }) => { +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.PREFIX + '/input/select.html'); + 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']) { @@ -301,4 +319,5 @@ it('input event.composed should be true', async ({ page, server }) => { }); 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']); }); From 5bd75ebf3b87e4e033fd1449f290708350484342 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 20 Dec 2023 18:19:27 -0800 Subject: [PATCH 3/6] fill events --- .../src/server/injected/injectedScript.ts | 4 +- tests/page/page-fill.spec.ts | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 8df667d75192d..d77b6fc7ba0f0 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -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,7 +749,7 @@ 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('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. } diff --git a/tests/page/page-fill.spec.ts b/tests/page/page-fill.spec.ts index 1e1585bea314b..3586bc9820b6a 100644 --- a/tests/page/page-fill.spec.ts +++ b/tests/page/page-fill.spec.ts @@ -89,6 +89,48 @@ 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 }) => { + 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 => { + console.log(e.type, e.composed); + (window as any).firedEvents.push(e.type + ':' + e.composed); + }, false); + } + }); + await page.locator('input').fill(value); + expect(await page.evaluate(() => window['firedEvents'])).toEqual(['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'); From a0f5b75c14dd5693ca273746cbdb9f2765ef56e7 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 20 Dec 2023 18:25:26 -0800 Subject: [PATCH 4/6] setInputFiles events --- .../src/server/injected/injectedScript.ts | 12 +++---- tests/page/page-set-input-files.spec.ts | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index d77b6fc7ba0f0..67852c541f8e3 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, 'composed': 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); } @@ -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, 'composed': 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-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index 19869e9139160..966459a2c9207 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([ From deb1f8e7d4005d56ee328e9d7a51303a4c310233 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 21 Dec 2023 13:56:11 -0800 Subject: [PATCH 5/6] Update epxectations for month, week pickers --- tests/page/page-fill.spec.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/page/page-fill.spec.ts b/tests/page/page-fill.spec.ts index 3586bc9820b6a..50d6dfa747628 100644 --- a/tests/page/page-fill.spec.ts +++ b/tests/page/page-fill.spec.ts @@ -98,7 +98,7 @@ for (const [type, value] of Object.entries({ 'range': '42', 'week': '2020-W50' })) { - it(`input event.composed should be true and cross shadow dom boundary - ${type}`, async ({ page, server }) => { + 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(`