feat: implement ElementHandle.uploadFile for WebDriver BiDi (#11963)

This commit is contained in:
jrandolf 2024-02-26 15:10:46 +01:00 committed by GitHub
parent 414f43388b
commit accf2b6ca8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 105 additions and 31 deletions

View File

@ -70,6 +70,7 @@ This is an exciting step towards a more unified and efficient cross-browser auto
- Input - Input
- ElementHandle.uploadFile
- ElementHandle.click - ElementHandle.click
- Keyboard.down - Keyboard.down
- Keyboard.press - Keyboard.press
@ -141,7 +142,6 @@ This is an exciting step towards a more unified and efficient cross-browser auto
- Other methods: - Other methods:
- Browser.userAgent() - Browser.userAgent()
- ElementHandle.uploadFile()
- Frame.isOOPFrame() - Frame.isOOPFrame()
- Frame.waitForDevicePrompt() - Frame.waitForDevicePrompt()
- HTTPResponse.buffer() - HTTPResponse.buffer()

View File

@ -7,7 +7,6 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {ElementHandle, type AutofillData} from '../api/ElementHandle.js'; import {ElementHandle, type AutofillData} from '../api/ElementHandle.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {throwIfDisposed} from '../util/decorators.js'; import {throwIfDisposed} from '../util/decorators.js';
import type {BidiFrame} from './Frame.js'; import type {BidiFrame} from './Frame.js';
@ -90,7 +89,31 @@ export class BidiElementHandle<
return null; return null;
} }
override uploadFile(this: ElementHandle<HTMLInputElement>): never { override async uploadFile(
throw new UnsupportedOperation(); this: BidiElementHandle<HTMLInputElement>,
...files: string[]
): Promise<void> {
// 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);
} }
} }

View File

@ -42,6 +42,7 @@ import {BidiCdpSession} from './CDPSession.js';
import type {BrowsingContext} from './core/BrowsingContext.js'; import type {BrowsingContext} from './core/BrowsingContext.js';
import {BidiDeserializer} from './Deserializer.js'; import {BidiDeserializer} from './Deserializer.js';
import {BidiDialog} from './Dialog.js'; import {BidiDialog} from './Dialog.js';
import type {BidiElementHandle} from './ElementHandle.js';
import {ExposeableFunction} from './ExposedFunction.js'; import {ExposeableFunction} from './ExposedFunction.js';
import {BidiHTTPRequest, requests} from './HTTPRequest.js'; import {BidiHTTPRequest, requests} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js'; import type {BidiHTTPResponse} from './HTTPResponse.js';
@ -519,6 +520,15 @@ export class BidiFrame extends Frame {
concurrency, concurrency,
}); });
} }
@throwIfDetached
async setFiles(element: BidiElementHandle, files: string[]): Promise<void> {
await this.browsingContext.setFiles(
// SAFETY: ElementHandles are always remote references.
element.remoteValue() as Bidi.Script.SharedReference,
files
);
}
} }
function isConsoleLogEntry( function isConsoleLogEntry(

View File

@ -524,6 +524,21 @@ export class BrowsingContext extends EventEmitter<{
}); });
} }
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setFiles(
element: Bidi.Script.SharedReference,
files: string[]
): Promise<void> {
await this.#session.send('input.setFiles', {
context: this.id,
element,
files,
});
}
[disposeSymbol](): void { [disposeSymbol](): void {
this.#reason ??= this.#reason ??=
'Browsing context already closed, probably because the user context closed.'; 'Browsing context already closed, probably because the user context closed.';

View File

@ -106,6 +106,10 @@ export interface Commands {
params: Bidi.Input.ReleaseActionsParameters; params: Bidi.Input.ReleaseActionsParameters;
returnType: Bidi.EmptyResult; returnType: Bidi.EmptyResult;
}; };
'input.setFiles': {
params: Bidi.Input.SetFilesParameters;
returnType: Bidi.EmptyResult;
};
'permissions.setPermission': { 'permissions.setPermission': {
params: Bidi.Permissions.SetPermissionParameters; params: Bidi.Permissions.SetPermissionParameters;

View File

@ -395,6 +395,12 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["SKIP"] "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", "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not throw for circular objects",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -1728,18 +1734,6 @@
"parameters": ["firefox", "webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"] "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", "testIdPattern": "[input.spec] input tests Page.waitForFileChooser should prioritize exact timeout over default timeout",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],

View File

@ -17,14 +17,13 @@ const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt');
describe('input tests', function () { describe('input tests', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
describe('input', function () { describe('ElementHandle.uploadFile', function () {
it('should upload the file', async () => { it('should upload the file', async () => {
const {page, server} = await getTestState(); const {page, server} = await getTestState();
await page.goto(server.PREFIX + '/input/fileupload.html'); await page.goto(server.PREFIX + '/input/fileupload.html');
const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD);
using input = (await page.$('input'))!; using input = (await page.$('input'))!;
await page.evaluate((e: HTMLElement) => { await input.evaluate(e => {
(globalThis as any)._inputEvents = []; (globalThis as any)._inputEvents = [];
e.addEventListener('change', ev => { e.addEventListener('change', ev => {
return (globalThis as any)._inputEvents.push(ev.type); return (globalThis as any)._inputEvents.push(ev.type);
@ -32,34 +31,63 @@ describe('input tests', function () {
e.addEventListener('input', ev => { e.addEventListener('input', ev => {
return (globalThis as any)._inputEvents.push(ev.type); 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( expect(
await page.evaluate((e: HTMLInputElement) => { await input.evaluate(e => {
return e.files![0]!.name; return e.files?.[0]?.name;
}, input) })
).toBe('file-to-upload.txt'); ).toBe('file-to-upload.txt');
expect( expect(
await page.evaluate((e: HTMLInputElement) => { await input.evaluate(e => {
return e.files![0]!.type; return e.files?.[0]?.type;
}, input) })
).toBe('text/plain'); ).toBe('text/plain');
expect( expect(
await page.evaluate(() => { await page.evaluate(() => {
return (globalThis as any)._inputEvents; return (globalThis as any)._inputEvents;
}) })
).toEqual(['input', 'change']); ).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( 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 reader = new FileReader();
const promise = new Promise(fulfill => { 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 promise.then(() => {
return reader.result; return reader.result;
}); });
}, input) })
).toBe('contents of the file'); ).toBe('contents of the file');
}); });
}); });