From 59418aa6f368c97a78d74b15cf8b46bc05e0b600 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 14 Nov 2022 15:16:25 -0800 Subject: [PATCH] chore: ignore untrusted clicks when recording (#18796) Fixes https://github.com/microsoft/playwright/issues/18776 --- .../src/server/injected/recorder.ts | 20 ++++++++- tests/library/inspector/cli-codegen-1.spec.ts | 44 +++++++++++++------ tests/library/inspector/cli-codegen-3.spec.ts | 14 +++--- tests/library/inspector/inspectorTest.ts | 18 +++++++- 4 files changed, 72 insertions(+), 24 deletions(-) diff --git a/packages/playwright-core/src/server/injected/recorder.ts b/packages/playwright-core/src/server/injected/recorder.ts index 924fbf73ff..f700bacc69 100644 --- a/packages/playwright-core/src/server/injected/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder.ts @@ -75,8 +75,10 @@ class Recorder { addEventListener(document, 'mouseup', event => this._onMouseUp(event as MouseEvent), true), addEventListener(document, 'mousemove', event => this._onMouseMove(event as MouseEvent), true), addEventListener(document, 'mouseleave', event => this._onMouseLeave(event as MouseEvent), true), - addEventListener(document, 'focus', () => this._onFocus(true), true), - addEventListener(document, 'scroll', () => { + addEventListener(document, 'focus', event => event.isTrusted && this._onFocus(true), true), + addEventListener(document, 'scroll', event => { + if (!event.isTrusted) + return; this._hoveredModel = null; this._highlight.hideActionPoint(); this._updateHighlight(); @@ -156,6 +158,8 @@ class Recorder { } private _onClick(event: MouseEvent) { + if (!event.isTrusted) + return; if (this._mode === 'inspecting') globalThis.__pw_recorderSetSelector(this._hoveredModel ? this._hoveredModel.selector : ''); if (this._shouldIgnoreMouseEvent(event)) @@ -204,6 +208,8 @@ class Recorder { } private _onMouseDown(event: MouseEvent) { + if (!event.isTrusted) + return; if (this._shouldIgnoreMouseEvent(event)) return; if (!this._performingAction) @@ -212,6 +218,8 @@ class Recorder { } private _onMouseUp(event: MouseEvent) { + if (!event.isTrusted) + return; if (this._shouldIgnoreMouseEvent(event)) return; if (!this._performingAction) @@ -219,6 +227,8 @@ class Recorder { } private _onMouseMove(event: MouseEvent) { + if (!event.isTrusted) + return; if (this._mode === 'none') return; const target = this._deepEventTarget(event); @@ -229,6 +239,8 @@ class Recorder { } private _onMouseLeave(event: MouseEvent) { + if (!event.isTrusted) + return; // Leaving iframe. if (window.top !== window && this._deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) { this._hoveredElement = null; @@ -339,6 +351,8 @@ class Recorder { } private _onKeyDown(event: KeyboardEvent) { + if (!event.isTrusted) + return; if (this._mode === 'inspecting') { consumeEvent(event); return; @@ -376,6 +390,8 @@ class Recorder { } private _onKeyUp(event: KeyboardEvent) { + if (!event.isTrusted) + return; if (this._mode === 'none') return; if (!this._shouldGenerateKeyPressFor(event)) diff --git a/tests/library/inspector/cli-codegen-1.spec.ts b/tests/library/inspector/cli-codegen-1.spec.ts index 340306949a..f8c6a42960 100644 --- a/tests/library/inspector/cli-codegen-1.spec.ts +++ b/tests/library/inspector/cli-codegen-1.spec.ts @@ -31,7 +31,7 @@ test.describe('cli codegen', () => { const [message, sources] = await Promise.all([ page.waitForEvent('console', msg => msg.type() !== 'error'), recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('button', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect.soft(sources.get('JavaScript').text).toContain(` @@ -52,6 +52,27 @@ test.describe('cli codegen', () => { expect(message.text()).toBe('click'); }); + + test('should ignore programmatic events', async ({ page, openRecorder }) => { + const recorder = await openRecorder(); + + await recorder.setContentAndWait(``); + + const locator = await recorder.hoverOverElement('button'); + expect(locator).toBe(`getByRole('button', { name: 'Submit' })`); + + await page.dispatchEvent('button', 'click', { detail: 1 }); + + await Promise.all([ + page.waitForEvent('console', msg => msg.type() !== 'error'), + recorder.waitForOutput('JavaScript', 'click'), + recorder.trustedClick() + ]); + + const clicks = recorder.sources().get('Playwright Test').actions.filter(l => l.includes('Submit')); + expect(clicks.length).toBe(1); + }); + test('should click after same-document navigation', async ({ page, openRecorder, server }) => { const recorder = await openRecorder(); @@ -74,7 +95,7 @@ test.describe('cli codegen', () => { const [message, sources] = await Promise.all([ page.waitForEvent('console', msg => msg.type() !== 'error'), recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('button', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect(sources.get('JavaScript').text).toContain(` @@ -95,16 +116,14 @@ test.describe('cli codegen', () => { `); - const locator = await recorder.waitForHighlight(() => recorder.page.hover('canvas', { + const locator = await recorder.hoverOverElement('canvas', { position: { x: 250, y: 250 }, - })); + }); expect(locator).toBe(`locator('canvas')`); const [message, sources] = await Promise.all([ page.waitForEvent('console', msg => msg.type() !== 'error'), recorder.waitForOutput('JavaScript', 'click'), - recorder.page.click('canvas', { - position: { x: 250, y: 250 }, - }) + recorder.trustedClick(), ]); expect(sources.get('JavaScript').text).toContain(` @@ -154,7 +173,7 @@ test.describe('cli codegen', () => { const [message, sources] = await Promise.all([ page.waitForEvent('console', msg => msg.type() !== 'error'), recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('button', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect.soft(sources.get('JavaScript').text).toContain(` @@ -182,13 +201,12 @@ test.describe('cli codegen', () => { // Force highlight. await recorder.hoverOverElement('span'); - // Append text after highlight. await page.evaluate(() => { const div = document.createElement('div'); div.setAttribute('onclick', "console.log('click')"); div.textContent = ' Some long text here '; - document.documentElement.appendChild(div); + document.body.appendChild(div); }); const locator = await recorder.hoverOverElement('div'); @@ -200,7 +218,7 @@ test.describe('cli codegen', () => { const [message, sources] = await Promise.all([ page.waitForEvent('console', msg => msg.type() !== 'error'), recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('div', 'click', { detail: 1 }) + recorder.trustedMove('div').then(() => recorder.trustedClick()), ]); expect(sources.get('JavaScript').text).toContain(` await page.getByText('Some long text here').click();`); @@ -585,7 +603,7 @@ test.describe('cli codegen', () => { const [popup, sources] = await Promise.all([ page.context().waitForEvent('page'), recorder.waitForOutput('JavaScript', 'waitForEvent'), - page.dispatchEvent('a', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect.soft(sources.get('JavaScript').text).toContain(` @@ -628,7 +646,7 @@ test.describe('cli codegen', () => { const [, sources] = await Promise.all([ page.waitForNavigation(), recorder.waitForOutput('JavaScript', '.click()'), - page.dispatchEvent('a', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect.soft(sources.get('JavaScript').text).toContain(` diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 4546708a13..21c635ff15 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -33,7 +33,7 @@ test.describe('cli codegen', () => { const [message, sources] = await Promise.all([ page.waitForEvent('console', msg => msg.type() !== 'error'), recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('button', 'click', { detail: 1 }) + recorder.trustedClick() ]); expect.soft(sources.get('JavaScript').text).toContain(` @@ -68,7 +68,7 @@ test.describe('cli codegen', () => { const [message, sources] = await Promise.all([ page.waitForEvent('console', msg => msg.type() !== 'error'), recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('button', 'click', { detail: 1 }) + recorder.trustedClick() ]); expect.soft(sources.get('JavaScript').text).toContain(` @@ -240,7 +240,7 @@ test.describe('cli codegen', () => { const [message, sources] = await Promise.all([ page.waitForEvent('console', msg => msg.type() !== 'error'), recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('div', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect.soft(sources.get('JavaScript').text).toContain(` @@ -271,7 +271,7 @@ test.describe('cli codegen', () => { const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('input', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect.soft(sources.get('JavaScript').text).toContain(` @@ -300,7 +300,7 @@ test.describe('cli codegen', () => { const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('input', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect.soft(sources.get('JavaScript').text).toContain(` @@ -329,7 +329,7 @@ test.describe('cli codegen', () => { const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('input', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect.soft(sources.get('JavaScript').text).toContain(` @@ -358,7 +358,7 @@ test.describe('cli codegen', () => { const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'click'), - page.dispatchEvent('input', 'click', { detail: 1 }) + recorder.trustedClick(), ]); expect.soft(sources.get('JavaScript').text).toContain(` diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index d6577b1594..62cd6b51dd 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -171,8 +171,22 @@ class Recorder { return new Promise(f => callback = f); } - async hoverOverElement(selector: string): Promise { - return this.waitForHighlight(() => this.page.dispatchEvent(selector, 'mousemove', { detail: 1 })); + async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }}): Promise { + return this.waitForHighlight(async () => { + const box = await this.page.locator(selector).first().boundingBox(); + const offset = options?.position || { x: box.width / 2, y: box.height / 2 }; + await this.page.mouse.move(box.x + offset.x, box.y + offset.y); + }); + } + + async trustedMove(selector: string) { + const box = await this.page.locator(selector).first().boundingBox(); + await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + } + + async trustedClick() { + await this.page.mouse.down(); + await this.page.mouse.up(); } async focusElement(selector: string): Promise {