chrome(filechooser): align file chooser implementations (#88)

This commit is contained in:
Pavel Feldman 2019-11-26 14:29:21 -08:00 committed by GitHub
parent 1c40eb0b28
commit 64d3e83ddf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 119 additions and 108 deletions

View File

@ -270,10 +270,10 @@
* [elementHandle.press(key[, options])](#elementhandlepresskey-options)
* [elementHandle.screenshot([options])](#elementhandlescreenshotoptions)
* [elementHandle.select(...values)](#elementhandleselectvalues)
* [elementHandle.setInputFiles(...files)](#elementhandlesetinputfilesfiles)
* [elementHandle.toString()](#elementhandletostring)
* [elementHandle.tripleclick([options])](#elementhandletripleclickoptions)
* [elementHandle.type(text[, options])](#elementhandletypetext-options)
* [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths)
- [class: Request](#class-request)
* [request.failure()](#requestfailure)
* [request.frame()](#requestframe)
@ -3541,6 +3541,15 @@ handle.select('red', 'green', 'blue');
handle.select({ value: 'blue' }, { index: 2 }, 'red');
```
#### elementHandle.setInputFiles(...files)
- `...files` <...[string]|[Object]> Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd).
- `name` <[string]> <[File]> name
- `type` <[string]> <[File]> type
- `data` <[string]> Base64-encoded data
- returns: <[Promise]>
This method expects `elementHandle` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
#### elementHandle.toString()
- returns: <[string]>
@ -3583,12 +3592,6 @@ await elementHandle.type('some text');
await elementHandle.press('Enter');
```
#### elementHandle.uploadFile(...filePaths)
- `...filePaths` <...[string]> Sets the value of the file input to these paths. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd).
- returns: <[Promise]>
This method expects `elementHandle` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
### class: Request
Whenever the page sends a request, such as for a network resource, the following events are emitted by playwright's page:
@ -3883,6 +3886,7 @@ TimeoutError is emitted whenever certain operations are terminated due to timeou
[Element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element"
[Error]: https://nodejs.org/api/errors.html#errors_class_error "Error"
[ExecutionContext]: #class-executioncontext "ExecutionContext"
[File]: #class-file "https://developer.mozilla.org/en-US/docs/Web/API/File"
[FileChooser]: #class-filechooser "FileChooser"
[Frame]: #class-frame "Frame"
[JSHandle]: #class-jshandle "JSHandle"

View File

@ -151,17 +151,21 @@ export class ExecutionContext implements types.EvaluationContext<JSHandle> {
}
}
async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId) {
const {object} = await this._client.send('DOM.resolveNode', {
backendNodeId,
executionContextId: this._contextId,
});
return createJSHandle(this, object) as ElementHandle;
}
async _adoptElementHandle(elementHandle: ElementHandle): Promise<ElementHandle> {
assert(elementHandle.executionContext() !== this, 'Cannot adopt handle that already belongs to this execution context');
assert(this._frame, 'Cannot adopt handle without a Frame');
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: elementHandle._remoteObject.objectId,
});
const {object} = await this._client.send('DOM.resolveNode', {
backendNodeId: nodeInfo.node.backendNodeId,
executionContextId: this._contextId,
});
return createJSHandle(this, object) as ElementHandle;
return this._adoptBackendNodeId(nodeInfo.node.backendNodeId);
}
_injected(): Promise<JSHandle> {

View File

@ -15,10 +15,10 @@
* limitations under the License.
*/
import * as path from 'path';
import * as types from '../types';
import { assert, debugError, helper } from '../helper';
import { ClickOptions, Modifier, MultiClickOptions, PointerActionOptions, SelectOption, selectFunction, fillFunction } from '../input';
import Injected from '../injected/injected';
import * as input from '../input';
import * as types from '../types';
import { CDPSession } from './Connection';
import { ExecutionContext } from './ExecutionContext';
import { Frame } from './Frame';
@ -26,7 +26,6 @@ import { FrameManager } from './FrameManager';
import { Page } from './Page';
import { Protocol } from './protocol';
import { releaseObject, valueFromRemoteObject } from './protocolHelper';
import Injected from '../injected/injected';
type SelectorRoot = Element | ShadowRoot | Document;
@ -236,7 +235,7 @@ export class ElementHandle extends JSHandle {
return { point, scrollX, scrollY };
}
async _performPointerAction(action: (point: Point) => Promise<void>, options?: PointerActionOptions): Promise<void> {
async _performPointerAction(action: (point: Point) => Promise<void>, options?: input.PointerActionOptions): Promise<void> {
await this._scrollIntoViewIfNeeded();
let point: Point;
if (options && options.relativePoint) {
@ -259,7 +258,7 @@ export class ElementHandle extends JSHandle {
await this._scrollIntoViewIfNeeded();
point = await this._clickablePoint();
}
let restoreModifiers: Modifier[] | undefined;
let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers)
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
await action(point);
@ -289,23 +288,23 @@ export class ElementHandle extends JSHandle {
}));
}
hover(options?: PointerActionOptions): Promise<void> {
hover(options?: input.PointerActionOptions): Promise<void> {
return this._performPointerAction(point => this._page.mouse.move(point.x, point.y), options);
}
click(options?: ClickOptions): Promise<void> {
click(options?: input.ClickOptions): Promise<void> {
return this._performPointerAction(point => this._page.mouse.click(point.x, point.y, options), options);
}
dblclick(options?: MultiClickOptions): Promise<void> {
dblclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._page.mouse.dblclick(point.x, point.y, options), options);
}
tripleclick(options?: MultiClickOptions): Promise<void> {
tripleclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._page.mouse.tripleclick(point.x, point.y, options), options);
}
async select(...values: (string | ElementHandle | SelectOption)[]): Promise<string[]> {
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
const options = values.map(value => typeof value === 'object' ? value : { value });
for (const option of options) {
if (option instanceof ElementHandle)
@ -317,22 +316,22 @@ export class ElementHandle extends JSHandle {
if (option.index !== undefined)
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
}
return this.evaluate(selectFunction, ...options);
return this.evaluate(input.selectFunction, ...options);
}
async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this.evaluate(fillFunction);
const error = await this.evaluate(input.fillFunction);
if (error)
throw new Error(error);
await this.focus();
await this._page.keyboard.sendCharacters(value);
}
async uploadFile(...filePaths: string[]) {
const files = filePaths.map(filePath => path.resolve(filePath));
const objectId = this._remoteObject.objectId;
await this._client.send('DOM.setFileInputFiles', { objectId, files });
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));
}
async focus() {

View File

@ -16,16 +16,15 @@
*/
import * as fs from 'fs';
import * as path from 'path';
import * as types from '../types';
import { assert, debugError, helper } from '../helper';
import { ClickOptions, fillFunction, MultiClickOptions, selectFunction, SelectOption } from '../input';
import { JugglerSession } from './Connection';
import Injected from '../injected/injected';
type SelectorRoot = Element | ShadowRoot | Document;
import * as input from '../input';
import * as types from '../types';
import { JugglerSession } from './Connection';
import { ExecutionContext } from './ExecutionContext';
import { Frame } from './FrameManager';
type SelectorRoot = Element | ShadowRoot | Document;
const readFileAsync = helper.promisify(fs.readFile);
export class JSHandle {
@ -294,47 +293,28 @@ export class ElementHandle extends JSHandle {
throw new Error(error);
}
async click(options?: ClickOptions) {
async click(options?: input.ClickOptions) {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._frame._page.mouse.click(x, y, options);
}
async dblclick(options?: MultiClickOptions): Promise<void> {
async dblclick(options?: input.MultiClickOptions): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._frame._page.mouse.dblclick(x, y, options);
}
async tripleclick(options?: MultiClickOptions): Promise<void> {
async tripleclick(options?: input.MultiClickOptions): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._frame._page.mouse.tripleclick(x, y, options);
}
async uploadFile(...files: Array<string>) {
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!');
const blobs = await Promise.all(files.map(path => readFileAsync(path)));
const payloads: FilePayload[] = [];
for (let i = 0; i < files.length; ++i) {
payloads.push({
name: path.basename(files[i]),
mimeType: 'application/octet-stream',
data: blobs[i].toString('base64')
});
}
await this.evaluate(async (element: HTMLInputElement, payloads: FilePayload[]) => {
const files = await Promise.all(payloads.map(async (file: FilePayload) => {
const result = await fetch(`data:${file.mimeType};base64,${file.data}`);
return new File([await result.blob()], file.name);
}));
const dt = new DataTransfer();
for (const file of files)
dt.items.add(file);
element.files = dt.files;
element.dispatchEvent(new Event('input', { 'bubbles': true }));
}, payloads);
await this.evaluate(input.setFileInputFunction, await input.loadFiles(files));
}
async hover() {
@ -357,7 +337,7 @@ export class ElementHandle extends JSHandle {
await this._frame._page.keyboard.press(key, options);
}
async select(...values: (string | ElementHandle | SelectOption)[]): Promise<string[]> {
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
const options = values.map(value => typeof value === 'object' ? value : { value });
for (const option of options) {
if (option instanceof ElementHandle)
@ -369,12 +349,12 @@ export class ElementHandle extends JSHandle {
if (option.index !== undefined)
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
}
return this.evaluate(selectFunction, ...options);
return this.evaluate(input.selectFunction, ...options);
}
async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this.evaluate(fillFunction);
const error = await this.evaluate(input.fillFunction);
if (error)
throw new Error(error);
await this.focus();

View File

@ -558,7 +558,7 @@ export class Page extends EventEmitter {
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);
const fileChooser = new FileChooser(this, this._session, handle, multiple);
const fileChooser = new FileChooser(handle, multiple);
for (const interceptor of interceptors)
interceptor.call(null, fileChooser);
}
@ -623,21 +623,12 @@ export type Viewport = {
hasTouch?: boolean;
}
type MediaFeature = {
name: string,
value: string
};
export class FileChooser {
private _page; Page;
private _client: JugglerSession;
private _element: ElementHandle;
private _multiple: boolean;
private _handled = false;
constructor(page: Page, client: JugglerSession, element: ElementHandle, multiple: boolean) {
this._page = page;
this._client = client;
constructor(element: ElementHandle, multiple: boolean) {
this._element = element;
this._multiple = multiple;
}
@ -649,7 +640,7 @@ export class FileChooser {
async accept(filePaths: string[]): Promise<any> {
assert(!this._handled, 'Cannot accept FileChooser which is already handled!');
this._handled = true;
await this._element.uploadFile(...filePaths);
await this._element.setInputFiles(...filePaths);
}
async cancel(): Promise<any> {

View File

@ -1,8 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { assert } from './helper';
import * as fs from 'fs';
import * as path from 'path';
import { assert, helper } from './helper';
import * as keyboardLayout from './USKeyboardLayout';
const readFileAsync = helper.promisify(fs.readFile);
export type Modifier = 'Alt' | 'Control' | 'Meta' | 'Shift';
export type Button = 'left' | 'right' | 'middle';
@ -344,5 +347,38 @@ export const fillFunction = (element: HTMLElement) => {
return false;
};
export const loadFiles = async (items: (string|FilePayload)[]): Promise<FilePayload[]> => {
return Promise.all(items.map(async item => {
if (typeof item === 'string') {
const file: FilePayload = {
name: path.basename(item),
type: 'application/octet-stream',
data: (await readFileAsync(item)).toString('base64')
};
return file;
} else {
return item as FilePayload;
}
}));
}
export const setFileInputFunction = async (element: HTMLInputElement, payloads: FilePayload[]) => {
const files = await Promise.all(payloads.map(async (file: FilePayload) => {
const result = await fetch(`data:${file.type};base64,${file.data}`);
return new File([await result.blob()], file.name);
}));
const dt = new DataTransfer();
for (const file of files)
dt.items.add(file);
element.files = dt.files;
element.dispatchEvent(new Event('input', { 'bubbles': true }));
};
export type FilePayload = {
name: string,
type: string,
data: string
};
export const mediaTypes = new Set(['screen', 'print']);
export const mediaColorSchemes = new Set(['dark', 'light', 'no-preference']);

View File

@ -17,7 +17,7 @@
import * as fs from 'fs';
import { assert, debugError, helper } from '../helper';
import { ClickOptions, MultiClickOptions, selectFunction, SelectOption, fillFunction } from '../input';
import * as input from '../input';
import { TargetSession } from './Connection';
import { ExecutionContext } from './ExecutionContext';
import { FrameManager } from './FrameManager';
@ -217,25 +217,25 @@ export class ElementHandle extends JSHandle {
await this._page.mouse.move(x, y);
}
async click(options?: ClickOptions): Promise<void> {
async click(options?: input.ClickOptions): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._page.mouse.click(x, y, options);
}
async dblclick(options?: MultiClickOptions): Promise<void> {
async dblclick(options?: input.MultiClickOptions): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._page.mouse.dblclick(x, y, options);
}
async tripleclick(options?: MultiClickOptions): Promise<void> {
async tripleclick(options?: input.MultiClickOptions): Promise<void> {
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint();
await this._page.mouse.tripleclick(x, y, options);
}
async select(...values: (string | ElementHandle | SelectOption)[]): Promise<string[]> {
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
const options = values.map(value => typeof value === 'object' ? value : { value });
for (const option of options) {
if (option instanceof ElementHandle)
@ -247,18 +247,24 @@ export class ElementHandle extends JSHandle {
if (option.index !== undefined)
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
}
return this.evaluate(selectFunction, ...options);
return this.evaluate(input.selectFunction, ...options);
}
async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this.evaluate(fillFunction);
const error = await this.evaluate(input.fillFunction);
if (error)
throw new Error(error);
await this.focus();
await this._page.keyboard.sendCharacters(value);
}
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));
}
async focus() {
await this.evaluate(element => element.focus());
}

View File

@ -19,7 +19,7 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe.skip(FFOX)('DefaultBrowserContext', function() {
describe('DefaultBrowserContext', function() {
beforeEach(async state => {
state.browser = await playwright.launch(defaultBrowserOptions);
state.page = await state.browser.newPage();
@ -34,7 +34,7 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p
await page.evaluate(() => {
document.cookie = 'username=John Doe';
});
expect(await page.cookies()).toEqual([{
expect(await page.browserContext().cookies()).toEqual([{
name: 'username',
value: 'John Doe',
domain: 'localhost',
@ -47,14 +47,15 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p
sameSite: 'None',
}]);
});
it.skip(WEBKIT)('page.setCookie() should work', async({page, server}) => {
it.skip(WEBKIT)('context.setCookies() should work', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
await page.browserContext().setCookies([{
url: server.EMPTY_PAGE,
name: 'username',
value: 'John Doe'
});
}]);
expect(await page.evaluate(() => document.cookie)).toBe('username=John Doe');
expect(await page.cookies()).toEqual([{
expect(await page.browserContext().cookies()).toEqual([{
name: 'username',
value: 'John Doe',
domain: 'localhost',
@ -67,30 +68,20 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p
sameSite: 'None',
}]);
});
it.skip(WEBKIT)('page.deleteCookie() should work', async({page, server}) => {
it.skip(WEBKIT)('context.clearCookies() should work', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
await page.setCookie({
await page.browserContext().setCookies([{
url: server.EMPTY_PAGE,
name: 'cookie1',
value: '1'
}, {
url: server.EMPTY_PAGE,
name: 'cookie2',
value: '2'
});
expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2');
await page.deleteCookie({name: 'cookie2'});
expect(await page.evaluate('document.cookie')).toBe('cookie1=1');
expect(await page.cookies()).toEqual([{
name: 'cookie1',
value: '1',
domain: 'localhost',
path: '/',
expires: -1,
size: 8,
httpOnly: false,
secure: false,
session: true,
sameSite: 'None',
}]);
expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2');
await page.browserContext().clearCookies();
expect(await page.evaluate('document.cookie')).toBe('');
});
});
};

View File

@ -27,7 +27,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
await page.goto(server.PREFIX + '/input/fileupload.html');
const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD);
const input = await page.$('input');
await input.uploadFile(filePath);
await input.setInputFiles(filePath);
expect(await page.evaluate(e => e.files[0].name, input)).toBe('file-to-upload.txt');
expect(await page.evaluate(e => {
const reader = new FileReader();

View File

@ -102,10 +102,10 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
expect(requests[1].url()).toContain('/one-style.css');
expect(requests[1].headers().referer).toContain('/one-style.html');
});
it('should properly return navigation response when URL has cookies', async({page, server}) => {
it('should properly return navigation response when URL has cookies', async({context, page, server}) => {
// Setup cookie.
await page.goto(server.EMPTY_PAGE);
await page.setCookie({ name: 'foo', value: 'bar'});
await context.setCookies([{ url: server.EMPTY_PAGE, name: 'foo', value: 'bar'}]);
// Setup request interception.
await page.interception.enable();