diff --git a/docs/api.md b/docs/api.md index c7e80ab3ee1d8..7dfd6fc7744d8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -59,7 +59,6 @@ + [page.mouse](#pagemouse) + [page.pdf(options)](#pagepdfoptions) + [page.plainText()](#pageplaintext) - + [page.press(key[, options])](#pagepresskey-options) + [page.reload(options)](#pagereloadoptions) + [page.screenshot([options])](#pagescreenshotoptions) + [page.select(selector, ...values)](#pageselectselector-values) @@ -74,7 +73,7 @@ + [page.title()](#pagetitle) + [page.touchscreen](#pagetouchscreen) + [page.tracing](#pagetracing) - + [page.type(text, options)](#pagetypetext-options) + + [page.type(selector, text[, options])](#pagetypeselector-text-options) + [page.url()](#pageurl) + [page.viewport()](#pageviewport) + [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) @@ -83,7 +82,9 @@ + [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) * [class: Keyboard](#class-keyboard) + [keyboard.down(key[, options])](#keyboarddownkey-options) + + [keyboard.press(key[, options])](#keyboardpresskey-options) + [keyboard.sendCharacter(char)](#keyboardsendcharacterchar) + + [keyboard.type(text, options)](#keyboardtypetext-options) + [keyboard.up(key)](#keyboardupkey) * [class: Mouse](#class-mouse) + [mouse.click(x, y, [options])](#mouseclickx-y-options) @@ -139,12 +140,15 @@ + [elementHandle.click([options])](#elementhandleclickoptions) + [elementHandle.dispose()](#elementhandledispose) + [elementHandle.executionContext()](#elementhandleexecutioncontext) + + [elementHandle.focus()](#elementhandlefocus) + [elementHandle.getProperties()](#elementhandlegetproperties) + [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname) + [elementHandle.hover()](#elementhandlehover) + [elementHandle.jsonValue()](#elementhandlejsonvalue) + + [elementHandle.press(key[, options])](#elementhandlepresskey-options) + [elementHandle.tap()](#elementhandletap) + [elementHandle.toString()](#elementhandletostring) + + [elementHandle.type(text[, options])](#elementhandletypetext-options) + [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths) * [class: Request](#class-request) + [request.abort()](#requestabort) @@ -759,15 +763,6 @@ The `format` options are: #### page.plainText() - returns: <[Promise]<[string]>> Returns page's inner text. -#### page.press(key[, options]) -- `key` <[string]> Name of key to press, such as `ArrowLeft`. See [KeyboardEvent.key](https://www.w3.org/TR/uievents-key/) -- `options` <[Object]> - - `text` <[string]> If specified, generates an input event with this text. - - `delay` <[number]> Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0. -- returns: <[Promise]> - -Shortcut for [`keyboard.down`](#keyboarddownkey-options) and [`keyboard.up`](#keyboardupkey). - #### page.reload(options) - `options` <[Object]> Navigation parameters which might have the following properties: - `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. @@ -896,7 +891,8 @@ Shortcut for [page.mainFrame().title()](#frametitle). #### page.tracing - returns: <[Tracing]> -#### page.type(text, options) +#### page.type(selector, text[, options]) +- `selector` <[string]> A [selector] of an element to type into. If there are multiple elements satisfying the selector, the first will be used. - `text` <[string]> A text to type into a focused element. - `options` <[Object]> - `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0. @@ -904,11 +900,11 @@ Shortcut for [page.mainFrame().title()](#frametitle). Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. -To press a special key, use [`page.press`](#pagepresskey-options). +To press a special key, like `Control` or `ArrowDown`, use [`keyboard.press`](#pagekeyboardpresskey-options). ```js -page.type('Hello'); // Types instantly -page.type('World', {delay: 100}); // Types slower, like a user +page.type('#mytextarea', 'Hello'); // Types instantly +page.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a user ``` #### page.url() @@ -1003,21 +999,21 @@ Shortcut for [page.mainFrame().waitForSelector(selector[, options])](#framewaitf ### class: Keyboard -Keyboard provides an api for managing a virtual keyboard. The high level api is [`page.type`](#pagetypetext-options), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page. +Keyboard provides an api for managing a virtual keyboard. The high level api is [`keyboard.type`](#keyboardtypetext-options), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page. For finer control, you can use [`keyboard.down`](#keyboarddownkey-options), [`keyboard.up`](#keyboardupkey), and [`keyboard.sendCharacter`](#keyboardsendcharacterchar) to manually fire events as if they were generated from a real keyboard. An example of holding down `Shift` in order to select and delete some text: ```js -page.type('Hello World!'); -page.press('ArrowLeft'); +page.keyboard.type('Hello World!'); +page.keyboard.press('ArrowLeft'); page.keyboard.down('Shift'); for (let i = 0; i < ' World'.length; i++) - page.press('ArrowLeft'); + page.keyboard.press('ArrowLeft'); page.keyboard.up('Shift'); -page.press('Backspace'); +page.keyboard.press('Backspace'); // Result text will end up saying 'Hello!' ``` @@ -1035,6 +1031,15 @@ If `key` is a modifier key, `Shift`, `Meta`, `Control`, or `Alt`, subsequent key After the key is pressed once, subsequent calls to [`keyboard.down`](#keyboarddownkey-options) will have [repeat](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat) set to true. To release the key, use [`keyboard.up`](#keyboardupkey). +#### keyboard.press(key[, options]) +- `key` <[string]> Name of key to press, such as `ArrowLeft`. See [KeyboardEvent.key](https://www.w3.org/TR/uievents-key/) +- `options` <[Object]> + - `text` <[string]> If specified, generates an input event with this text. + - `delay` <[number]> Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0. +- returns: <[Promise]> + +Shortcut for [`keyboard.down`](#keyboarddownkey-options) and [`keyboard.up`](#keyboardupkey). + #### keyboard.sendCharacter(char) - `char` <[string]> Character to send into the page. - returns: <[Promise]> @@ -1045,6 +1050,21 @@ Dispatches a `keypress` and `input` event. This does not send a `keydown` or `ke page.keyboard.sendCharacter('嗨'); ``` +#### keyboard.type(text, options) +- `text` <[string]> A text to type into a focused element. +- `options` <[Object]> + - `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0. +- returns: <[Promise]> + +Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. + +To press a special key, like `Control` or `ArrowDown`, use [`keyboard.press`](#keyboardpresskey-options). + +```js +page.keyboard.type('Hello'); // Types instantly +page.keyboard.type('World', {delay: 100}); // Types slower, like a user +``` + #### keyboard.up(key) - `key` <[string]> Name of key to release, such as `ArrowLeft`. See [KeyboardEvent.key](https://www.w3.org/TR/uievents-key/) - returns: <[Promise]> @@ -1396,7 +1416,7 @@ const twoHandle = await executionContext.evaluateHandle(() => 2); const result = await executionContext.evaluate((a, b) => a + b, oneHandle, twoHandle); await oneHandle.dispose(); await twoHandle.dispose(); -console.log(result); // prints '3'. +console.log(result); // prints '3'. ``` #### executionContext.evaluateHandle(pageFunction, ...args) @@ -1527,6 +1547,11 @@ The `elementHandle.dispose` method stops referencing the element handle. #### elementHandle.executionContext() - returns: [ExecutionContext] +#### elementHandle.focus() +- returns: <[Promise]> + +Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element. + #### elementHandle.getProperties() - returns: <[Promise]<[Map]<[string], [JSHandle]>>> @@ -1563,6 +1588,15 @@ Returns a JSON representation of the object. The JSON is generated by running [` > **NOTE** The method will throw if the referenced object is not stringifiable. +#### elementHandle.press(key[, options]) +- `key` <[string]> Name of key to press, such as `ArrowLeft`. See [KeyboardEvent.key](https://www.w3.org/TR/uievents-key/) +- `options` <[Object]> + - `text` <[string]> If specified, generates an input event with this text. + - `delay` <[number]> Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0. +- returns: <[Promise]> + +Focuses the element, and then uses [`keyboard.down`](#keyboarddownkey-options) and [`keyboard.up`](#keyboardupkey). + #### elementHandle.tap() - returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM. @@ -1572,6 +1606,21 @@ If the element is detached from DOM, the method throws an error. #### elementHandle.toString() - returns: <[string]> +#### elementHandle.type(text[, options]) +- `text` <[string]> A text to type into a focused element. +- `options` <[Object]> + - `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0. +- returns: <[Promise]> + +Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. + +To press a special key, like `Control` or `ArrowDown`, use [`elementHandle.press`](#elementhandlepresskey-options). + +```js +elementHandle.type('Hello'); // Types instantly +elementHandle.type('World', {delay: 100}); // Types slower, like a user +``` + #### elementHandle.uploadFile(...filePaths) - `...filePaths` <...[string]> Sets the value of the file input these paths. If some of the `filePaths` are relative paths, then they are resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). - returns: <[Promise]> diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js index 4239e8abb482d..9ce2f5ccad3e2 100644 --- a/lib/ElementHandle.js +++ b/lib/ElementHandle.js @@ -22,13 +22,14 @@ class ElementHandle extends JSHandle { * @param {!ExecutionContext} context * @param {!Session} client * @param {!Object} remoteObject - * @param {!Mouse} mouse - * @param {!Touchscreen} touchscreen; + * @param {!Page} page */ - constructor(context, client, remoteObject, mouse, touchscreen) { + constructor(context, client, remoteObject, page) { super(context, client, remoteObject); - this._mouse = mouse; - this._touchscreen = touchscreen; + this._client = client; + this._remoteObject = remoteObject; + this._page = page; + this._disposed = false; } /** @@ -63,7 +64,7 @@ class ElementHandle extends JSHandle { async hover() { const {x, y} = await this._visibleCenter(); - await this._mouse.move(x, y); + await this._page.mouse.move(x, y); } /** @@ -71,7 +72,7 @@ class ElementHandle extends JSHandle { */ async click(options) { const {x, y} = await this._visibleCenter(); - await this._mouse.click(x, y, options); + await this._page.mouse.click(x, y, options); } /** @@ -86,7 +87,29 @@ class ElementHandle extends JSHandle { async tap() { const {x, y} = await this._visibleCenter(); - await this._touchscreen.tap(x, y); + await this._page.touchscreen.tap(x, y); + } + + async focus() { + await this.executionContext().evaluate(element => element.focus(), this); + } + + /** + * @param {string} text + * @param {{delay: (number|undefined)}=} options + */ + async type(text, options) { + await this.focus(); + await this._page.keyboard.type(text, options); + } + + /** + * @param {string} key + * @param {!Object=} options + */ + async press(key, options) { + await this.focus(); + await this._page.keyboard.press(key, options); } } diff --git a/lib/FrameManager.js b/lib/FrameManager.js index afbb940e7516a..8e6cf6c85a743 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -23,15 +23,12 @@ const ElementHandle = require('./ElementHandle'); class FrameManager extends EventEmitter { /** * @param {!Session} client - * @param {!Object} frameTree - * @param {!Mouse} mouse - * @param {!Touchscreen} touchscreen + * @param {!Page} page */ - constructor(client, mouse, touchscreen) { + constructor(client, page) { super(); this._client = client; - this._mouse = mouse; - this._touchscreen = touchscreen; + this._page = page; /** @type {!Map} */ this._frames = new Map(); /** @type {!Map} */ @@ -67,7 +64,7 @@ class FrameManager extends EventEmitter { return; console.assert(parentFrameId); const parentFrame = this._frames.get(parentFrameId); - const frame = new Frame(this._client, this._mouse, this._touchscreen, parentFrame, frameId); + const frame = new Frame(this._client, this._page, parentFrame, frameId); this._frames.set(frame._id, frame); this.emit(FrameManager.Events.FrameAttached, frame); } @@ -94,7 +91,7 @@ class FrameManager extends EventEmitter { frame._id = framePayload.id; } else { // Initial main frame navigation. - frame = new Frame(this._client, this._mouse, this._touchscreen, null, framePayload.id); + frame = new Frame(this._client, this._page, null, framePayload.id); } this._frames.set(framePayload.id, frame); this._mainFrame = frame; @@ -141,7 +138,7 @@ class FrameManager extends EventEmitter { const context = this._contextIdToContext.get(contextId); console.assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId); if (remoteObject.subtype === 'node') - return new ElementHandle(context, this._client, remoteObject, this._mouse, this._touchscreen); + return new ElementHandle(context, this._client, remoteObject, this._page); return new JSHandle(context, this._client, remoteObject); } @@ -178,15 +175,12 @@ FrameManager.Events = { class Frame { /** * @param {!Session} client - * @param {!Mouse} mouse - * @param {!Touchscreen} touchscreen * @param {?Frame} parentFrame * @param {string} frameId */ - constructor(client, mouse, touchscreen, parentFrame, frameId) { + constructor(client, page, parentFrame, frameId) { this._client = client; - this._mouse = mouse; - this._touchscreen = touchscreen; + this._page = page; this._parentFrame = parentFrame; this._url = ''; this._id = frameId; diff --git a/lib/Input.js b/lib/Input.js index 9a2dbe95c03e7..a6968167eeeb5 100644 --- a/lib/Input.js +++ b/lib/Input.js @@ -88,6 +88,32 @@ class Keyboard { unmodifiedText: char }); } + + /** + * @param {string} text + * @param {{delay: (number|undefined)}=} options + */ + async type(text, options) { + let delay = 0; + if (options && options.delay) + delay = options.delay; + for (const char of text) { + await this.press(char, {text: char, delay}); + if (delay) + await new Promise(f => setTimeout(f, delay)); + } + } + + /** + * @param {string} key + * @param {!Object=} options + */ + async press(key, options) { + await this.down(key, options); + if (options && options.delay) + await new Promise(f => setTimeout(f, options.delay)); + await this.up(key); + } } class Mouse { diff --git a/lib/Page.js b/lib/Page.js index dd0da5b3f8343..0f079a9f104dc 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -63,7 +63,7 @@ class Page extends EventEmitter { this._keyboard = new Keyboard(client); this._mouse = new Mouse(client, this._keyboard); this._touchscreen = new Touchscreen(client, this._keyboard); - this._frameManager = new FrameManager(client, this._mouse, this._touchscreen); + this._frameManager = new FrameManager(client, this); this._networkManager = new NetworkManager(client); this._emulationManager = new EmulationManager(client); this._tracing = new Tracing(client); @@ -711,7 +711,7 @@ class Page extends EventEmitter { async focus(selector) { const handle = await this.$(selector); console.assert(handle, 'No node found for selector: ' + selector); - await this.evaluate(element => element.focus(), handle); + await handle.focus(); await handle.dispose(); } @@ -739,31 +739,14 @@ class Page extends EventEmitter { } /** + * @param {string} selector * @param {string} text * @param {{delay: (number|undefined)}=} options */ - async type(text, options) { - let delay = 0; - if (options && options.delay) - delay = options.delay; - let last; - for (const char of text) { - last = this.press(char, {text: char, delay}); - if (delay) - await new Promise(f => setTimeout(f, delay)); - } - await last; - } - - /** - * @param {string} text - * @param {!Object=} options - */ - async press(key, options) { - this._keyboard.down(key, options); - if (options && options.delay) - await new Promise(f => setTimeout(f, options.delay)); - await this._keyboard.up(key); + async type(selector, text, options) { + const handle = await this.$(selector); + await handle.type(text, options); + await handle.dispose(); } /** diff --git a/test/test.js b/test/test.js index 0596552ef0241..fd992e84ea4e1 100644 --- a/test/test.js +++ b/test/test.js @@ -1426,8 +1426,9 @@ describe('Page', function() { })); it('should type into the textarea', SX(async function() { await page.goto(PREFIX + '/input/textarea.html'); - await page.focus('textarea'); - await page.type('Type in this text!'); + + const textarea = await page.$('textarea'); + await textarea.type('Type in this text!'); expect(await page.evaluate(() => result)).toBe('Type in this text!'); })); it('should click the button after navigation ', SX(async function() { @@ -1452,29 +1453,28 @@ describe('Page', function() { })); it('should move with the arrow keys', SX(async function(){ await page.goto(PREFIX + '/input/textarea.html'); - await page.focus('textarea'); - await page.type('Hello World!'); + await page.type('textarea', 'Hello World!'); expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello World!'); for (let i = 0; i < 'World!'.length; i++) - page.press('ArrowLeft'); - await page.type('inserted '); + page.keyboard.press('ArrowLeft'); + await page.keyboard.type('inserted '); expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello inserted World!'); page.keyboard.down('Shift'); for (let i = 0; i < 'inserted '.length; i++) - page.press('ArrowLeft'); + page.keyboard.press('ArrowLeft'); page.keyboard.up('Shift'); - await page.press('Backspace'); + await page.keyboard.press('Backspace'); expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello World!'); })); - it('should send a character with Page.press', SX(async function() { + it('should send a character with ElementHandle.press', SX(async function() { await page.goto(PREFIX + '/input/textarea.html'); - await page.focus('textarea'); - await page.press('a', {text: 'f'}); + const textarea = await page.$('textarea'); + await textarea.press('a', {text: 'f'}); expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('f'); await page.evaluate(() => window.addEventListener('keydown', e => e.preventDefault(), true)); - await page.press('a', {text: 'y'}); + await textarea.press('a', {text: 'y'}); expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('f'); })); it('should send a character with sendCharacter', SX(async function() { @@ -1519,12 +1519,12 @@ describe('Page', function() { })); it('should send proper codes while typing', SX(async function(){ await page.goto(PREFIX + '/input/keyboard.html'); - await page.type('!'); + await page.keyboard.type('!'); expect(await page.evaluate(() => getResult())).toBe( [ 'Keydown: ! 49 []', 'Keypress: ! 33 33 33 []', 'Keyup: ! 49 []'].join('\n')); - await page.type('^'); + await page.keyboard.type('^'); expect(await page.evaluate(() => getResult())).toBe( [ 'Keydown: ^ 54 []', 'Keypress: ^ 94 94 94 []', @@ -1534,7 +1534,7 @@ describe('Page', function() { await page.goto(PREFIX + '/input/keyboard.html'); const keyboard = page.keyboard; await keyboard.down('Shift'); - await page.type('~'); + await page.keyboard.type('~'); expect(await page.evaluate(() => getResult())).toBe( [ 'Keydown: Shift 16 [Shift]', 'Keydown: ~ 192 [Shift]', // 192 is ` keyCode @@ -1555,7 +1555,7 @@ describe('Page', function() { Promise.resolve().then(() => event.preventDefault()); }, false); }); - await page.type('Hello World!'); + await page.keyboard.type('Hello World!'); expect(await page.evaluate(() => textarea.value)).toBe('He Wrd!'); })); it('keyboard.modifiers()', SX(async function(){ @@ -1603,7 +1603,7 @@ describe('Page', function() { await page.goto(PREFIX + '/input/textarea.html'); await page.focus('textarea'); const text = 'This is the text that we are going to try to select. Let\'s see how it goes.'; - await page.type(text); + await page.keyboard.type(text); await page.evaluate(() => document.querySelector('textarea').scrollTop = 0); const {x, y} = await page.evaluate(dimensions); await page.mouse.move(x + 2,y + 2); @@ -1616,7 +1616,7 @@ describe('Page', function() { await page.goto(PREFIX + '/input/textarea.html'); await page.focus('textarea'); const text = 'This is the text that we are going to try to select. Let\'s see how it goes.'; - await page.type(text); + await page.keyboard.type(text); await page.click('textarea'); await page.click('textarea', {clickCount: 2}); await page.click('textarea', {clickCount: 3}); @@ -1659,7 +1659,7 @@ describe('Page', function() { await page.evaluate(() => document.querySelector('textarea').addEventListener('keydown', e => window.lastEvent = e, true)); await page.keyboard.down('a', {text: 'a'}); expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false); - await page.press('a'); + await page.keyboard.press('a'); expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(true); })); // @see https://github.com/GoogleChrome/puppeteer/issues/206