feat(firefox): implemented *.fill (#63)

This commit is contained in:
Pavel Feldman 2019-11-22 16:55:35 -08:00 committed by Dmitry Gozman
parent c4c8d498bd
commit 3190044c00
7 changed files with 98 additions and 103 deletions

View file

@ -17,7 +17,7 @@
import * as path from 'path'; import * as path from 'path';
import { assert, debugError, helper } from '../helper'; import { assert, debugError, helper } from '../helper';
import { ClickOptions, Modifier, MultiClickOptions, PointerActionOptions, SelectOption, selectFunction } from '../input'; import { ClickOptions, Modifier, MultiClickOptions, PointerActionOptions, SelectOption, selectFunction, fillFunction } from '../input';
import { CDPSession } from './Connection'; import { CDPSession } from './Connection';
import { ExecutionContext } from './ExecutionContext'; import { ExecutionContext } from './ExecutionContext';
import { Frame } from './Frame'; import { Frame } from './Frame';
@ -321,34 +321,7 @@ export class ElementHandle extends JSHandle {
async fill(value: string): Promise<void> { async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"'); assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this.evaluate((element: HTMLElement) => { const error = await this.evaluate(fillFunction);
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
if (element.nodeName.toLowerCase() === 'input') {
const input = element as HTMLInputElement;
const type = input.getAttribute('type') || '';
const kTextInputTypes = new Set(['', 'password', 'search', 'tel', 'text', 'url']);
if (!kTextInputTypes.has(type.toLowerCase()))
return 'Cannot fill input of type "' + type + '".';
input.selectionStart = 0;
input.selectionEnd = input.value.length;
} else if (element.nodeName.toLowerCase() === 'textarea') {
const textarea = element as HTMLTextAreaElement;
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.value.length;
} else if (element.isContentEditable) {
if (!element.ownerDocument || !element.ownerDocument.defaultView)
return 'Element does not belong to a window';
const range = element.ownerDocument.createRange();
range.selectNodeContents(element);
const selection = element.ownerDocument.defaultView.getSelection();
selection.removeAllRanges();
selection.addRange(range);
} else {
return 'Element is not an <input>, <textarea> or [contenteditable] element.';
}
return false;
});
if (error) if (error)
throw new Error(error); throw new Error(error);
await this.focus(); await this.focus();

View file

@ -281,6 +281,13 @@ export class Frame {
return result; return result;
} }
async fill(selector: string, value: string) {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.fill(value);
await handle.dispose();
}
async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) {
const handle = await this.$(selector); const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector); assert(handle, 'No node found for selector: ' + selector);

View file

@ -15,15 +15,15 @@
* limitations under the License. * limitations under the License.
*/ */
import {assert, debugError, helper} from '../helper';
import * as path from 'path'; import * as path from 'path';
import {ExecutionContext} from './ExecutionContext'; import { assert, debugError, helper } from '../helper';
import {Frame} from './FrameManager'; import { ClickOptions, fillFunction, MultiClickOptions, selectFunction, SelectOption } from '../input';
import { JugglerSession } from './Connection'; import { JugglerSession } from './Connection';
import { MultiClickOptions, ClickOptions, selectFunction, SelectOption } from '../input';
import Injected from '../injected/injected'; import Injected from '../injected/injected';
type SelectorRoot = Element | ShadowRoot | Document; type SelectorRoot = Element | ShadowRoot | Document;
import { ExecutionContext } from './ExecutionContext';
import { Frame } from './FrameManager';
export class JSHandle { export class JSHandle {
_context: ExecutionContext; _context: ExecutionContext;
@ -361,6 +361,15 @@ export class ElementHandle extends JSHandle {
return this.evaluate(selectFunction, ...options); return this.evaluate(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);
if (error)
throw new Error(error);
await this.focus();
await this._frame._page.keyboard.sendCharacter(value);
}
async _clickablePoint(): Promise<{ x: number; y: number; }> { async _clickablePoint(): Promise<{ x: number; y: number; }> {
const result = await this._session.send('Page.getContentQuads', { const result = await this._session.send('Page.getContentQuads', {
frameId: this._frameId, frameId: this._frameId,

View file

@ -460,88 +460,92 @@ export class Page extends EventEmitter {
} }
} }
async evaluate(pageFunction, ...args) { evaluate(pageFunction, ...args) {
return await this.mainFrame().evaluate(pageFunction, ...args); return this.mainFrame().evaluate(pageFunction, ...args);
} }
async addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise<ElementHandle> { addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise<ElementHandle> {
return await this.mainFrame().addScriptTag(options); return this.mainFrame().addScriptTag(options);
} }
async addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise<ElementHandle> { addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise<ElementHandle> {
return await this.mainFrame().addStyleTag(options); return this.mainFrame().addStyleTag(options);
} }
async click(selector: string, options?: ClickOptions) { click(selector: string, options?: ClickOptions) {
return await this.mainFrame().click(selector, options); return this.mainFrame().click(selector, options);
} }
async dblclick(selector: string, options?: MultiClickOptions) { dblclick(selector: string, options?: MultiClickOptions) {
return this.mainFrame().dblclick(selector, options); return this.mainFrame().dblclick(selector, options);
} }
async tripleclick(selector: string, options?: MultiClickOptions) { tripleclick(selector: string, options?: MultiClickOptions) {
return this.mainFrame().tripleclick(selector, options); return this.mainFrame().tripleclick(selector, options);
} }
async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { fill(selector: string, value: string) {
return await this._frameManager.mainFrame().type(selector, text, options); return this.mainFrame().fill(selector, value);
} }
async focus(selector: string) { select(selector: string, ...values: Array<string>): Promise<Array<string>> {
return await this._frameManager.mainFrame().focus(selector); return this._frameManager.mainFrame().select(selector, ...values);
} }
async hover(selector: string) { type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) {
return await this._frameManager.mainFrame().hover(selector); return this._frameManager.mainFrame().type(selector, text, options);
} }
async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: { polling?: string | number; timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}, ...args: Array<any>): Promise<JSHandle> { focus(selector: string) {
return await this._frameManager.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args); return this._frameManager.mainFrame().focus(selector);
} }
async waitForFunction(pageFunction: Function | string, options: { polling?: string | number; timeout?: number; } | undefined = {}, ...args): Promise<JSHandle> { hover(selector: string) {
return await this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args); return this._frameManager.mainFrame().hover(selector);
} }
async waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<ElementHandle> { waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: { polling?: string | number; timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}, ...args: Array<any>): Promise<JSHandle> {
return await this._frameManager.mainFrame().waitForSelector(selector, options); return this._frameManager.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
} }
async waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<ElementHandle> { waitForFunction(pageFunction: Function | string, options: { polling?: string | number; timeout?: number; } | undefined = {}, ...args): Promise<JSHandle> {
return await this._frameManager.mainFrame().waitForXPath(xpath, options); return this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args);
} }
async title(): Promise<string> { waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<ElementHandle> {
return await this._frameManager.mainFrame().title(); return this._frameManager.mainFrame().waitForSelector(selector, options);
} }
async $(selector: string): Promise<ElementHandle | null> { waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<ElementHandle> {
return await this._frameManager.mainFrame().$(selector); return this._frameManager.mainFrame().waitForXPath(xpath, options);
} }
async $$(selector: string): Promise<Array<ElementHandle>> { title(): Promise<string> {
return await this._frameManager.mainFrame().$$(selector); return this._frameManager.mainFrame().title();
} }
async $eval(selector: string, pageFunction: Function | string, ...args: Array<any>): Promise<(object | undefined)> { $(selector: string): Promise<ElementHandle | null> {
return await this._frameManager.mainFrame().$eval(selector, pageFunction, ...args); return this._frameManager.mainFrame().$(selector);
} }
async $$eval(selector: string, pageFunction: Function | string, ...args: Array<any>): Promise<(object | undefined)> { $$(selector: string): Promise<Array<ElementHandle>> {
return await this._frameManager.mainFrame().$$eval(selector, pageFunction, ...args); return this._frameManager.mainFrame().$$(selector);
} }
async $x(expression: string): Promise<Array<ElementHandle>> { $eval(selector: string, pageFunction: Function | string, ...args: Array<any>): Promise<(object | undefined)> {
return await this._frameManager.mainFrame().$x(expression); return this._frameManager.mainFrame().$eval(selector, pageFunction, ...args);
} }
async evaluateHandle(pageFunction, ...args) { $$eval(selector: string, pageFunction: Function | string, ...args: Array<any>): Promise<(object | undefined)> {
return await this._frameManager.mainFrame().evaluateHandle(pageFunction, ...args); return this._frameManager.mainFrame().$$eval(selector, pageFunction, ...args);
} }
async select(selector: string, ...values: Array<string>): Promise<Array<string>> { $x(expression: string): Promise<Array<ElementHandle>> {
return await this._frameManager.mainFrame().select(selector, ...values); return this._frameManager.mainFrame().$x(expression);
}
evaluateHandle(pageFunction, ...args) {
return this._frameManager.mainFrame().evaluateHandle(pageFunction, ...args);
} }
async close(options: any = {}) { async close(options: any = {}) {

View file

@ -140,3 +140,32 @@ export const selectFunction = (element: HTMLSelectElement, ...optionsToSelect: (
element.dispatchEvent(new Event('change', { 'bubbles': true })); element.dispatchEvent(new Event('change', { 'bubbles': true }));
return options.filter(option => option.selected).map(option => option.value); return options.filter(option => option.selected).map(option => option.value);
}; };
export const fillFunction = (element: HTMLElement) => {
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
if (element.nodeName.toLowerCase() === 'input') {
const input = element as HTMLInputElement;
const type = input.getAttribute('type') || '';
const kTextInputTypes = new Set(['', 'password', 'search', 'tel', 'text', 'url']);
if (!kTextInputTypes.has(type.toLowerCase()))
return 'Cannot fill input of type "' + type + '".';
input.selectionStart = 0;
input.selectionEnd = input.value.length;
} else if (element.nodeName.toLowerCase() === 'textarea') {
const textarea = element as HTMLTextAreaElement;
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.value.length;
} else if (element.isContentEditable) {
if (!element.ownerDocument || !element.ownerDocument.defaultView)
return 'Element does not belong to a window';
const range = element.ownerDocument.createRange();
range.selectNodeContents(element);
const selection = element.ownerDocument.defaultView.getSelection();
selection.removeAllRanges();
selection.addRange(range);
} else {
return 'Element is not an <input>, <textarea> or [contenteditable] element.';
}
return false;
};

View file

@ -16,7 +16,7 @@
*/ */
import * as fs from 'fs'; import * as fs from 'fs';
import { assert, debugError, helper } from '../helper'; import { assert, debugError, helper } from '../helper';
import { ClickOptions, MultiClickOptions, selectFunction, SelectOption } from '../input'; import { ClickOptions, MultiClickOptions, selectFunction, SelectOption, fillFunction } from '../input';
import { TargetSession } from './Connection'; import { TargetSession } from './Connection';
import { ExecutionContext } from './ExecutionContext'; import { ExecutionContext } from './ExecutionContext';
import { FrameManager } from './FrameManager'; import { FrameManager } from './FrameManager';
@ -250,34 +250,7 @@ export class ElementHandle extends JSHandle {
async fill(value: string): Promise<void> { async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"'); assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this.evaluate((element: HTMLElement) => { const error = await this.evaluate(fillFunction);
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
if (element.nodeName.toLowerCase() === 'input') {
const input = element as HTMLInputElement;
const type = input.getAttribute('type') || '';
const kTextInputTypes = new Set(['', 'password', 'search', 'tel', 'text', 'url']);
if (!kTextInputTypes.has(type.toLowerCase()))
return 'Cannot fill input of type "' + type + '".';
input.selectionStart = 0;
input.selectionEnd = input.value.length;
} else if (element.nodeName.toLowerCase() === 'textarea') {
const textarea = element as HTMLTextAreaElement;
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.value.length;
} else if (element.isContentEditable) {
if (!element.ownerDocument || !element.ownerDocument.defaultView)
return 'Element does not belong to a window';
const range = element.ownerDocument.createRange();
range.selectNodeContents(element);
const selection = element.ownerDocument.defaultView.getSelection();
selection.removeAllRanges();
selection.addRange(range);
} else {
return 'Element is not an <input>, <textarea> or [contenteditable] element.';
}
return false;
});
if (error) if (error)
throw new Error(error); throw new Error(error);
await this.focus(); await this.focus();

View file

@ -1046,7 +1046,7 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
}); });
}); });
describe.skip(FFOX)('Page.fill', function() { describe('Page.fill', function() {
it('should fill textarea', async({page, server}) => { it('should fill textarea', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html'); await page.goto(server.PREFIX + '/input/textarea.html');
await page.fill('textarea', 'some value'); await page.fill('textarea', 'some value');