diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index adeb960576c44..a5aa991bb8f66 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -635,9 +635,10 @@ export class ElementHandle< avoid paying the cost unnecessarily. */ const path = await import('path'); + const mime = await import('mime'); const fs = await helper.importFSModule(); // Locate all files and confirm that they exist. - const files = await Promise.all( + const resolvedFilePaths = await Promise.all( filePaths.map(async (filePath) => { const resolvedPath: string = path.resolve(filePath); try { @@ -650,29 +651,33 @@ export class ElementHandle< return resolvedPath; }) ); - const { objectId } = this._remoteObject; - const { node } = await this._client.send('DOM.describeNode', { objectId }); - const { backendNodeId } = node; - /* The zero-length array is a special case, it seems that - DOM.setFileInputFiles does not actually update the files in that case, - so the solution is to eval the element value to a new FileList directly. - */ - if (files.length === 0) { - await (this as ElementHandle).evaluate((element) => { - element.files = new DataTransfer().files; + const filePayloads = await Promise.all( + resolvedFilePaths.map(async (filePath) => { + const buffer = await fs.promises.readFile(filePath); + const name = path.basename(filePath); + const mimeType = mime.getType(name) || 'application/octet-stream'; - // Dispatch events for this case because it should behave akin to a user action. + return { name, mimeType, content: buffer.toString('base64') }; + }) + ); + + await (this as ElementHandle).evaluate( + (element, files) => { + const dt = new DataTransfer(); + for (const item of files) { + const bytes = Uint8Array.from(atob(item.content), (c) => + c.charCodeAt(0) + ); + const file = new File([bytes], item.name); + dt.items.add(file); + } + element.files = dt.files; element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); - }); - } else { - await this._client.send('DOM.setFileInputFiles', { - objectId, - files, - backendNodeId, - }); - } + }, + filePayloads + ); } /** diff --git a/test/input.spec.ts b/test/input.spec.ts index 9244356333a34..fc497610227c6 100644 --- a/test/input.spec.ts +++ b/test/input.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import fs from 'fs'; import path from 'path'; import expect from 'expect'; import { @@ -65,6 +66,34 @@ describe('input tests', function () { }, input) ).toBe('contents of the file'); }); + + it('uploadFile should work with CSP', async () => { + const { page, server } = getTestState(); + + server.setCSP('/empty.html', 'default-src "none"'); + await page.goto(server.EMPTY_PAGE); + await page.setContent(``); + const input = await page.$('input'); + await input.uploadFile( + path.join(__dirname, '/assets/file-to-upload.txt') + ); + + const numFiles = await input.evaluate( + (e: HTMLInputElement) => e.files.length + ); + expect(numFiles).toBe(1); + const filename = await input.evaluate( + (e: HTMLInputElement) => e.files[0].name + ); + expect(filename).toBe('file-to-upload.txt'); + const content = await input.evaluate((e: HTMLInputElement) => + e.files[0].text() + ); + const expectedContent = await fs.promises.readFile( + path.join(__dirname, '/assets/file-to-upload.txt') + ); + expect(content).toBe(expectedContent.toString()); + }); }); describeFailsFirefox('Page.waitForFileChooser', function () {