From cf9c4d153aa5ec359c704f09d01ca22aa00f0afd Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 27 Nov 2019 13:50:24 -0800 Subject: [PATCH] feat(webkit): implement file chooser interception (frontend) (#98) --- package.json | 2 +- src/webkit/JSHandle.ts | 4 ++- src/webkit/Page.ts | 58 +++++++++++++++++++++++++++++++++++++++--- test/input.spec.js | 8 +++--- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index f02cf60ef0..5649d01fc7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "playwright": { "chromium_revision": "719491", "firefox_revision": "1004", - "webkit_revision": "1001" + "webkit_revision": "1002" }, "scripts": { "unit": "node test/test.js", diff --git a/src/webkit/JSHandle.ts b/src/webkit/JSHandle.ts index f35c9689f1..de26bd3072 100644 --- a/src/webkit/JSHandle.ts +++ b/src/webkit/JSHandle.ts @@ -183,7 +183,9 @@ export class ElementHandle extends js.JSHandle { async setInputFiles(...files: (string|input.FilePayload)[]) { const multiple = await this.evaluate((element: HTMLInputElement) => !!element.multiple); assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!'); - await this.evaluate(input.setFileInputFunction, await input.loadFiles(files)); + const filePayloads = await input.loadFiles(files); + const objectId = this._remoteObject.objectId; + await this._client.send('DOM.setInputFiles', { objectId, files: filePayloads }); } async focus() { diff --git a/src/webkit/Page.ts b/src/webkit/Page.ts index 1f3a0229d1..25fc1710a1 100644 --- a/src/webkit/Page.ts +++ b/src/webkit/Page.ts @@ -60,6 +60,7 @@ export class Page extends EventEmitter { private _disconnectPromise: Promise | undefined; private _sessionListeners: RegisteredListener[] = []; private _emulatedMediaType: string | undefined; + private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); static async create(session: TargetSession, target: Target, defaultViewport: Viewport | null, screenshotTaskQueue: TaskQueue): Promise { const page = new Page(session, target, screenshotTaskQueue); @@ -97,6 +98,7 @@ export class Page extends EventEmitter { this._frameManager.initialize(), this._session.send('Console.enable'), this._session.send('Dialog.enable'), + this._session.send('Page.setInterceptFileChooserDialog', { enabled: true }), ]); } @@ -108,7 +110,8 @@ export class Page extends EventEmitter { helper.addEventListener(this._session, 'Page.loadEventFired', event => this.emit(Events.Page.Load)), helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)), helper.addEventListener(this._session, 'Page.domContentEventFired', event => this.emit(Events.Page.DOMContentLoaded)), - helper.addEventListener(this._session, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)) + helper.addEventListener(this._session, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)), + helper.addEventListener(this._session, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)) ]; } @@ -438,6 +441,32 @@ export class Page extends EventEmitter { return this._closed; } + async waitForFileChooser(options: { timeout?: number; } = {}): Promise { + const { + timeout = this._timeoutSettings.timeout(), + } = options; + let callback; + const promise = new Promise(x => callback = x); + this._fileChooserInterceptors.add(callback); + return helper.waitWithTimeout(promise, 'waiting for file chooser', timeout).catch(e => { + this._fileChooserInterceptors.delete(callback); + throw e; + }); + } + + async _onFileChooserOpened(event: {frameId: Protocol.Network.FrameId, element: Protocol.Runtime.RemoteObject}) { + if (!this._fileChooserInterceptors.size) + return; + const context = await this._frameManager.frame(event.frameId)._utilityContext(); + const handle = createJSHandle(context, event.element) as ElementHandle; + const interceptors = Array.from(this._fileChooserInterceptors); + this._fileChooserInterceptors.clear(); + const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple); + const fileChooser = new FileChooser(handle, multiple); + for (const interceptor of interceptors) + interceptor.call(null, fileChooser); + } + get mouse(): input.Mouse { return this._mouse; } @@ -557,7 +586,28 @@ export class ConsoleMessage { } } -type MediaFeature = { - name: string, - value: string +export class FileChooser { + private _element: ElementHandle; + private _multiple: boolean; + private _handled = false; + + constructor(element: ElementHandle, multiple: boolean) { + this._element = element; + this._multiple = multiple; + } + + isMultiple(): boolean { + return this._multiple; + } + + async accept(filePaths: string[]): Promise { + assert(!this._handled, 'Cannot accept FileChooser which is already handled!'); + this._handled = true; + await this._element.setInputFiles(...filePaths); + } + + async cancel(): Promise { + assert(!this._handled, 'Cannot cancel FileChooser which is already handled!'); + this._handled = true; + } } diff --git a/test/input.spec.js b/test/input.spec.js index 0810a2f761..d36548dfd9 100644 --- a/test/input.spec.js +++ b/test/input.spec.js @@ -38,7 +38,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME }); }); - describe.skip(WEBKIT)('Page.waitForFileChooser', function() { + describe('Page.waitForFileChooser', function() { it('should work when file input is attached to DOM', async({page, server}) => { await page.setContent(``); const [chooser] = await Promise.all([ @@ -97,7 +97,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME }); }); - describe.skip(WEBKIT)('FileChooser.accept', function() { + describe('FileChooser.accept', function() { it('should accept single file', async({page, server}) => { await page.setContent(``); const [chooser] = await Promise.all([ @@ -161,7 +161,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME }); }); - describe.skip(WEBKIT)('FileChooser.cancel', function() { + describe('FileChooser.cancel', function() { it('should cancel dialog', async({page, server}) => { // Consider file chooser canceled if we can summon another one. // There's no reliable way in WebPlatform to see that FileChooser was @@ -191,7 +191,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME }); }); - describe.skip(WEBKIT)('FileChooser.isMultiple', () => { + describe('FileChooser.isMultiple', () => { it('should work for single file pick', async({page, server}) => { await page.setContent(``); const [chooser] = await Promise.all([