Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement ElementHandle.uploadFile for WebDriver BiDi #11963

Merged
merged 1 commit into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/webdriver-bidi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
29 changes: 26 additions & 3 deletions packages/puppeteer-core/src/bidi/ElementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -90,7 +89,31 @@ export class BidiElementHandle<
return null;
}

override uploadFile(this: ElementHandle<HTMLInputElement>): never {
throw new UnsupportedOperation();
override async uploadFile(
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);
}
}
10 changes: 10 additions & 0 deletions packages/puppeteer-core/src/bidi/Frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -519,6 +520,15 @@ export class BidiFrame extends Frame {
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(
Expand Down
15 changes: 15 additions & 0 deletions packages/puppeteer-core/src/bidi/core/BrowsingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
this.#reason ??=
'Browsing context already closed, probably because the user context closed.';
Expand Down
4 changes: 4 additions & 0 deletions packages/puppeteer-core/src/bidi/core/Connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 6 additions & 12 deletions test/TestExpectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down
58 changes: 43 additions & 15 deletions test/src/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,77 @@ 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);
});
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');
});
});
Expand Down