diff --git a/packages/web-integration/src/puppeteer/base-page.ts b/packages/web-integration/src/puppeteer/base-page.ts index f12b4c6ffc..f36cb47921 100644 --- a/packages/web-integration/src/puppeteer/base-page.ts +++ b/packages/web-integration/src/puppeteer/base-page.ts @@ -78,6 +78,11 @@ type ScreencastCdpSession = { ): void; }; +type InputCdpSession = { + send(method: string, params?: Record): Promise; + detach(): Promise; +}; + function isClosedPageError(error: unknown) { if (!(error instanceof Error)) { return false; @@ -798,36 +803,54 @@ export class Page< }; } + private async createInputCdpSession(): Promise { + if (this.interfaceType === 'puppeteer') { + const page = this.underlyingPage as PuppeteerPage; + return (await (typeof page.createCDPSession === 'function' + ? page.createCDPSession() + : page.target().createCDPSession())) as unknown as InputCdpSession; + } + + const page = this.underlyingPage as PlaywrightPage; + return (await page + .context() + .newCDPSession(page)) as unknown as InputCdpSession; + } + + private async selectAllByCdp(): Promise { + const client = await this.createInputCdpSession(); + try { + await client.send('Input.dispatchKeyEvent', { + type: 'rawKeyDown', + + commands: ['selectAll'], + }); + await client.send('Input.dispatchKeyEvent', { + type: 'keyUp', + }); + } finally { + await client.detach().catch(() => undefined); + } + } + async clearInput(element?: ElementInfo): Promise { const backspace = async () => { await sleep(100); await this.keyboard.press([{ key: 'Backspace' }]); }; - const isMac = process.platform === 'darwin'; debugPage('clearInput begin'); - if (isMac) { - if (this.interfaceType === 'puppeteer') { - // https://github.com/segment-boneyard/nightmare/issues/810#issuecomment-452669866 - element && - (await this.mouse.click(element.center[0], element.center[1], { - count: 3, - })); - await backspace(); - } - element && (await this.mouse.click(element.center[0], element.center[1])); - await this.underlyingPage.keyboard.down('Meta'); - await this.underlyingPage.keyboard.press('a'); - await this.underlyingPage.keyboard.up('Meta'); - await backspace(); - } else { - element && (await this.mouse.click(element.center[0], element.center[1])); - await this.underlyingPage.keyboard.down('Control'); - await this.underlyingPage.keyboard.press('a'); - await this.underlyingPage.keyboard.up('Control'); + element && (await this.mouse.click(element.center[0], element.center[1])); + try { + await this.selectAllByCdp(); await backspace(); + debugPage('clearInput end'); + return; + } catch (error) { + debugPage('clearInput cdp selectAll failed, fallback to shortcut', error); } + debugPage('clearInput end'); } diff --git a/packages/web-integration/tests/unit-test/base-page-clear-input-cdp.test.ts b/packages/web-integration/tests/unit-test/base-page-clear-input-cdp.test.ts new file mode 100644 index 0000000000..a8cd10ab1e --- /dev/null +++ b/packages/web-integration/tests/unit-test/base-page-clear-input-cdp.test.ts @@ -0,0 +1,107 @@ +import { WebPage as PlaywrightWebPage } from '@/playwright/page'; +import { PuppeteerWebPage } from '@/puppeteer/page'; +import { type Browser as PlaywrightBrowser, chromium } from 'playwright'; +import puppeteer, { type Browser as PuppeteerBrowser } from 'puppeteer'; +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; + +const TEST_TIMEOUT_MS = 120_000; + +const PAGE_HTML = ` + + + + + + +`; + +async function puppeteerInputCenter(page: any): Promise<[number, number]> { + return page.$eval('#target', (el: HTMLInputElement) => { + const rect = el.getBoundingClientRect(); + return [rect.left + rect.width / 2, rect.top + rect.height / 2]; + }); +} + +async function playwrightInputCenter(page: any): Promise<[number, number]> { + return page.locator('#target').evaluate((el: HTMLInputElement) => { + const rect = el.getBoundingClientRect(); + return [rect.left + rect.width / 2, rect.top + rect.height / 2]; + }); +} + +describe('BasePage clearInput CDP selectAll', () => { + describe('Puppeteer', () => { + let browser: PuppeteerBrowser; + + beforeAll(async () => { + browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + }, TEST_TIMEOUT_MS); + + afterAll(async () => { + await browser?.close(); + }, TEST_TIMEOUT_MS); + + test( + 'clears the focused input', + async () => { + const page = await browser.newPage(); + await page.setContent(PAGE_HTML); + + const webPage = new PuppeteerWebPage(page); + const center = await puppeteerInputCenter(page); + + await webPage.clearInput({ center } as any); + + const value = await page.$eval( + '#target', + (el) => (el as HTMLInputElement).value, + ); + await page.close(); + + expect(value).toBe(''); + }, + TEST_TIMEOUT_MS, + ); + }); + + describe('Playwright', () => { + let browser: PlaywrightBrowser; + + beforeAll(async () => { + browser = await chromium.launch({ + headless: true, + // CI installs Puppeteer's Chrome cache, but not Playwright's browser + // bundle because dependencies are installed with --ignore-scripts. + executablePath: puppeteer.executablePath(), + }); + }, TEST_TIMEOUT_MS); + + afterAll(async () => { + await browser?.close(); + }, TEST_TIMEOUT_MS); + + test( + 'clears the focused input', + async () => { + const page = await browser.newPage(); + await page.setContent(PAGE_HTML); + + const webPage = new PlaywrightWebPage(page); + const center = await playwrightInputCenter(page); + + await webPage.clearInput({ center } as any); + + const value = await page.locator('#target').evaluate((el) => { + return (el as HTMLInputElement).value; + }); + await page.close(); + + expect(value).toBe(''); + }, + TEST_TIMEOUT_MS, + ); + }); +});