From accf2b6ca84c93bc700277b4e3382d894fb45a76 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:10:46 +0100 Subject: [PATCH] feat: implement ElementHandle.uploadFile for WebDriver BiDi (#11963) --- docs/webdriver-bidi.md | 2 +- .../puppeteer-core/src/bidi/ElementHandle.ts | 29 +++++++++- packages/puppeteer-core/src/bidi/Frame.ts | 10 ++++ .../src/bidi/core/BrowsingContext.ts | 15 +++++ .../src/bidi/core/Connection.ts | 4 ++ test/TestExpectations.json | 18 ++---- test/src/input.spec.ts | 58 ++++++++++++++----- 7 files changed, 105 insertions(+), 31 deletions(-) diff --git a/docs/webdriver-bidi.md b/docs/webdriver-bidi.md index ad906e1edfa..266bf018c69 100644 --- a/docs/webdriver-bidi.md +++ b/docs/webdriver-bidi.md @@ -70,6 +70,7 @@ This is an exciting step towards a more unified and efficient cross-browser auto - Input + - ElementHandle.uploadFile - ElementHandle.click - Keyboard.down - Keyboard.press @@ -141,7 +142,6 @@ This is an exciting step towards a more unified and efficient cross-browser auto - Other methods: - Browser.userAgent() - - ElementHandle.uploadFile() - Frame.isOOPFrame() - Frame.waitForDevicePrompt() - HTTPResponse.buffer() diff --git a/packages/puppeteer-core/src/bidi/ElementHandle.ts b/packages/puppeteer-core/src/bidi/ElementHandle.ts index 1e73428ef70..42636976719 100644 --- a/packages/puppeteer-core/src/bidi/ElementHandle.ts +++ b/packages/puppeteer-core/src/bidi/ElementHandle.ts @@ -7,7 +7,6 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import {ElementHandle, type AutofillData} from '../api/ElementHandle.js'; -import {UnsupportedOperation} from '../common/Errors.js'; import {throwIfDisposed} from '../util/decorators.js'; import type {BidiFrame} from './Frame.js'; @@ -90,7 +89,31 @@ export class BidiElementHandle< return null; } - override uploadFile(this: ElementHandle): never { - throw new UnsupportedOperation(); + override async uploadFile( + this: BidiElementHandle, + ...files: string[] + ): Promise { + // Locate all files and confirm that they exist. + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + let path: typeof import('path'); + try { + path = await import('path'); + } catch (error) { + if (error instanceof TypeError) { + throw new Error( + `JSHandle#uploadFile can only be used in Node-like environments.` + ); + } + throw error; + } + + files = files.map(file => { + if (path.win32.isAbsolute(file) || path.posix.isAbsolute(file)) { + return file; + } else { + return path.resolve(file); + } + }); + await this.frame.setFiles(this, files); } } diff --git a/packages/puppeteer-core/src/bidi/Frame.ts b/packages/puppeteer-core/src/bidi/Frame.ts index f08972f28d4..2cc259cff69 100644 --- a/packages/puppeteer-core/src/bidi/Frame.ts +++ b/packages/puppeteer-core/src/bidi/Frame.ts @@ -42,6 +42,7 @@ import {BidiCdpSession} from './CDPSession.js'; import type {BrowsingContext} from './core/BrowsingContext.js'; import {BidiDeserializer} from './Deserializer.js'; import {BidiDialog} from './Dialog.js'; +import type {BidiElementHandle} from './ElementHandle.js'; import {ExposeableFunction} from './ExposedFunction.js'; import {BidiHTTPRequest, requests} from './HTTPRequest.js'; import type {BidiHTTPResponse} from './HTTPResponse.js'; @@ -519,6 +520,15 @@ export class BidiFrame extends Frame { concurrency, }); } + + @throwIfDetached + async setFiles(element: BidiElementHandle, files: string[]): Promise { + await this.browsingContext.setFiles( + // SAFETY: ElementHandles are always remote references. + element.remoteValue() as Bidi.Script.SharedReference, + files + ); + } } function isConsoleLogEntry( diff --git a/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts b/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts index 711018bc307..6ba600c0e56 100644 --- a/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts +++ b/packages/puppeteer-core/src/bidi/core/BrowsingContext.ts @@ -524,6 +524,21 @@ export class BrowsingContext extends EventEmitter<{ }); } + @throwIfDisposed(context => { + // SAFETY: Disposal implies this exists. + return context.#reason!; + }) + async setFiles( + element: Bidi.Script.SharedReference, + files: string[] + ): Promise { + await this.#session.send('input.setFiles', { + context: this.id, + element, + files, + }); + } + [disposeSymbol](): void { this.#reason ??= 'Browsing context already closed, probably because the user context closed.'; diff --git a/packages/puppeteer-core/src/bidi/core/Connection.ts b/packages/puppeteer-core/src/bidi/core/Connection.ts index 528409d5e69..90aa16144db 100644 --- a/packages/puppeteer-core/src/bidi/core/Connection.ts +++ b/packages/puppeteer-core/src/bidi/core/Connection.ts @@ -106,6 +106,10 @@ export interface Commands { params: Bidi.Input.ReleaseActionsParameters; returnType: Bidi.EmptyResult; }; + 'input.setFiles': { + params: Bidi.Input.SetFilesParameters; + returnType: Bidi.EmptyResult; + }; 'permissions.setPermission': { params: Bidi.Permissions.SetPermissionParameters; diff --git a/test/TestExpectations.json b/test/TestExpectations.json index b5d171cd2c5..4f2ef2676a9 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -395,6 +395,12 @@ "parameters": ["cdp", "firefox"], "expectations": ["SKIP"] }, + { + "testIdPattern": "[input.spec] input tests ElementHandle.uploadFile *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, { "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not throw for circular objects", "platforms": ["darwin", "linux", "win32"], @@ -1728,18 +1734,6 @@ "parameters": ["firefox", "webDriverBiDi"], "expectations": ["FAIL"] }, - { - "testIdPattern": "[input.spec] input tests input should upload the file", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["FAIL"] - }, - { - "testIdPattern": "[input.spec] input tests input should upload the file", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL"] - }, { "testIdPattern": "[input.spec] input tests Page.waitForFileChooser should prioritize exact timeout over default timeout", "platforms": ["darwin", "linux", "win32"], diff --git a/test/src/input.spec.ts b/test/src/input.spec.ts index 7e4cae6709c..47064528d3e 100644 --- a/test/src/input.spec.ts +++ b/test/src/input.spec.ts @@ -17,14 +17,13 @@ const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt'); describe('input tests', function () { setupTestBrowserHooks(); - describe('input', function () { + describe('ElementHandle.uploadFile', function () { it('should upload the file', async () => { const {page, server} = await getTestState(); await page.goto(server.PREFIX + '/input/fileupload.html'); - const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD); using input = (await page.$('input'))!; - await page.evaluate((e: HTMLElement) => { + await input.evaluate(e => { (globalThis as any)._inputEvents = []; e.addEventListener('change', ev => { return (globalThis as any)._inputEvents.push(ev.type); @@ -32,34 +31,63 @@ describe('input tests', function () { e.addEventListener('input', ev => { return (globalThis as any)._inputEvents.push(ev.type); }); - }, input); - await input.uploadFile(filePath); + }); + + const file = path.relative(process.cwd(), FILE_TO_UPLOAD); + await input.uploadFile(file); + expect( - await page.evaluate((e: HTMLInputElement) => { - return e.files![0]!.name; - }, input) + await input.evaluate(e => { + return e.files?.[0]?.name; + }) ).toBe('file-to-upload.txt'); expect( - await page.evaluate((e: HTMLInputElement) => { - return e.files![0]!.type; - }, input) + await input.evaluate(e => { + return e.files?.[0]?.type; + }) ).toBe('text/plain'); expect( await page.evaluate(() => { return (globalThis as any)._inputEvents; }) ).toEqual(['input', 'change']); + }); + + it('should read the file', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/fileupload.html'); + using input = (await page.$('input'))!; + await input.evaluate(e => { + (globalThis as any)._inputEvents = []; + e.addEventListener('change', ev => { + return (globalThis as any)._inputEvents.push(ev.type); + }); + e.addEventListener('input', ev => { + return (globalThis as any)._inputEvents.push(ev.type); + }); + }); + + const file = path.relative(process.cwd(), FILE_TO_UPLOAD); + await input.uploadFile(file); + expect( - await page.evaluate((e: HTMLInputElement) => { + await input.evaluate(e => { + const file = e.files?.[0]; + if (!file) { + throw new Error('No file found'); + } + const reader = new FileReader(); const promise = new Promise(fulfill => { - return (reader.onload = fulfill); + reader.addEventListener('load', fulfill); }); - reader.readAsText(e.files![0]!); + reader.readAsText(file); + return promise.then(() => { return reader.result; }); - }, input) + }) ).toBe('contents of the file'); }); });