api(dispatchEvent): page, frame and handle versions added (#1932)

This commit is contained in:
Pavel Feldman 2020-04-23 14:58:37 -07:00 committed by GitHub
parent 671cfa0a54
commit c1c0237d4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 332 additions and 37 deletions

View File

@ -674,6 +674,7 @@ page.removeListener('request', logRequest);
- [page.context()](#pagecontext)
- [page.coverage](#pagecoverage)
- [page.dblclick(selector[, options])](#pagedblclickselector-options)
- [page.dispatchEvent(selector, type[, eventInit, options])](#pagedispatcheventselector-type-eventinit-options)
- [page.emulateMedia(options)](#pageemulatemediaoptions)
- [page.evaluate(pageFunction[, arg])](#pageevaluatepagefunction-arg)
- [page.evaluateHandle(pageFunction[, arg])](#pageevaluatehandlepagefunction-arg)
@ -1040,6 +1041,40 @@ Bear in mind that if the first click of the `dblclick()` triggers a navigation e
Shortcut for [page.mainFrame().dblclick(selector[, options])](#framedblclickselector-options).
#### page.dispatchEvent(selector, type[, eventInit, options])
- `selector` <[string]> A selector to search for element to use. If there are multiple elements satisfying the selector, the first will be used.
- `type` <[string]> DOM event type: `"click"`, `"dragstart"`, etc.
- `eventInit` <[Object]> event-specific initialization properties.
- `options` <[Object]>
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]>
The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the elment, `click` is dispatched. This is equivalend to calling [`element.click()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click).
```js
await page.dispatchEvent('button#submit', 'click');
```
Under the hood, it creates an instance of an event based on the given `type`, initializes it with `eventInit` properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default.
Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties:
- [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent)
- [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent)
- [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent)
- [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent)
- [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent)
- [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent)
- [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event)
You can also specify `JSHandle` as the property value if you want live objects to be passed into the event:
```js
// Note you can only create DataTransfer in Chromium and Firefox
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
await page.dispatchEvent('#source', 'dragstart', { dataTransfer });
```
#### page.emulateMedia(options)
- `options` <[Object]>
- `media` <"screen"|"print"> Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` disables CSS media emulation.
@ -1894,6 +1929,7 @@ An example of getting text from an iframe element:
- [frame.click(selector[, options])](#frameclickselector-options)
- [frame.content()](#framecontent)
- [frame.dblclick(selector[, options])](#framedblclickselector-options)
- [frame.dispatchEvent(selector, type[, eventInit, options])](#framedispatcheventselector-type-eventinit-options)
- [frame.evaluate(pageFunction[, arg])](#frameevaluatepagefunction-arg)
- [frame.evaluateHandle(pageFunction[, arg])](#frameevaluatehandlepagefunction-arg)
- [frame.fill(selector, value[, options])](#framefillselector-value-options)
@ -2052,6 +2088,39 @@ Bear in mind that if the first click of the `dblclick()` triggers a navigation e
> **NOTE** `frame.dblclick()` dispatches two `click` events and a single `dblclick` event.
#### frame.dispatchEvent(selector, type[, eventInit, options])
- `selector` <[string]> A selector to search for element to double click. If there are multiple elements satisfying the selector, the first will be double clicked.
- `type` <[string]> DOM event type: `"click"`, `"dragstart"`, etc.
- `eventInit` <[Object]> event-specific initialization properties.
- `options` <[Object]>
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]>
The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the elment, `click` is dispatched. This is equivalend to calling [`element.click()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click).
```js
await frame.dispatchEvent('button#submit', 'click');
```
Under the hood, it creates an instance of an event based on the given `type`, initializes it with `eventInit` properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default.
Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties:
- [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent)
- [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent)
- [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent)
- [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent)
- [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent)
- [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent)
- [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event)
You can also specify `JSHandle` as the property value if you want live objects to be passed into the event:
```js
// Note you can only create DataTransfer in Chromium and Firefox
const dataTransfer = await frame.evaluateHandle(() => new DataTransfer());
await frame.dispatchEvent('#source', 'dragstart', { dataTransfer });
```
#### frame.evaluate(pageFunction[, arg])
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
- `arg` <[Serializable]|[JSHandle]> Optional argument to pass to `pageFunction`
@ -2477,6 +2546,7 @@ ElementHandle instances can be used as an argument in [`page.$eval()`](#pageeval
- [elementHandle.click([options])](#elementhandleclickoptions)
- [elementHandle.contentFrame()](#elementhandlecontentframe)
- [elementHandle.dblclick([options])](#elementhandledblclickoptions)
- [elementHandle.dispatchEvent(type[, eventInit])](#elementhandledispatcheventtype-eventinit)
- [elementHandle.fill(value[, options])](#elementhandlefillvalue-options)
- [elementHandle.focus()](#elementhandlefocus)
- [elementHandle.getAttribute(name)](#elementhandlegetattributename)
@ -2626,6 +2696,36 @@ Bear in mind that if the first click of the `dblclick()` triggers a navigation e
> **NOTE** `elementHandle.dblclick()` dispatches two `click` events and a single `dblclick` event.
#### elementHandle.dispatchEvent(type[, eventInit])
- `type` <[string]> DOM event type: `"click"`, `"dragstart"`, etc.
- `eventInit` <[Object]> event-specific initialization properties.
- returns: <[Promise]>
The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the elment, `click` is dispatched. This is equivalend to calling [`element.click()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click).
```js
await elementHandle.dispatchEvent('click');
```
Under the hood, it creates an instance of an event based on the given `type`, initializes it with `eventInit` properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default.
Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties:
- [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent)
- [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent)
- [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent)
- [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent)
- [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent)
- [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent)
- [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event)
You can also specify `JSHandle` as the property value if you want live objects to be passed into the event:
```js
// Note you can only create DataTransfer in Chromium and Firefox
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
await elementHandle.dispatchEvent('dragstart', { dataTransfer });
```
#### elementHandle.fill(value[, options])
- `value` <[string]> Value to set for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `options` <[Object]>

View File

@ -94,6 +94,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this;
}
async _evaluateInMain<R, Arg>(pageFunction: types.FuncOn<{ injected: Injected, node: T }, Arg, R>, arg: Arg): Promise<R> {
const main = await this._context.frame._mainContext();
return main._doEvaluateInternal(true /* returnByValue */, true /* waitForNavigations */, pageFunction, { injected: await main._injected(), node: this }, arg);
}
async _evaluateInUtility<R, Arg>(pageFunction: types.FuncOn<{ injected: Injected, node: T }, Arg, R>, arg: Arg): Promise<R> {
const utility = await this._context.frame._utilityContext();
return utility._doEvaluateInternal(true /* returnByValue */, true /* waitForNavigations */, pageFunction, { injected: await utility._injected(), node: this }, arg);
@ -152,6 +157,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, {});
}
async dispatchEvent(type: string, eventInit: Object = {}) {
await this._evaluateInMain(({ injected, node }, { type, eventInit }) =>
injected.dispatchEvent(node, type, eventInit), { type, eventInit });
}
async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise<void> {
this._page._log(inputLog, 'scrolling into view if needed...');
await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect);

View File

@ -454,6 +454,13 @@ export class Frame {
return handle;
}
async dispatchEvent(selector: string, type: string, eventInit?: Object, options?: types.TimeoutOptions): Promise<void> {
const deadline = this._page._timeoutSettings.computeDeadline(options);
const task = selectors._dispatchEventTask(selector, type, eventInit || {}, deadline);
const result = await this._scheduleRerunnableTask(task, 'main', deadline, `selector "${selectorToString(selector, 'attached')}"`);
result.dispose();
}
async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>;
async $eval<R>(selector: string, pageFunction: types.FuncOn<Element, void, R>, arg?: any): Promise<R>;
async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R> {

View File

@ -346,6 +346,21 @@ export class Injected {
return { status: result ? 'success' : 'timeout' };
}
dispatchEvent(node: Node, type: string, eventInit: Object) {
let event;
eventInit = { bubbles: true, cancelable: true, composed: true, ...eventInit };
switch (eventType.get(type)) {
case 'mouse': event = new MouseEvent(type, eventInit); break;
case 'keyboard': event = new KeyboardEvent(type, eventInit); break;
case 'touch': event = new TouchEvent(type, eventInit); break;
case 'pointer': event = new PointerEvent(type, eventInit); break;
case 'focus': event = new FocusEvent(type, eventInit); break;
case 'drag': event = new DragEvent(type, eventInit); break;
default: event = new Event(type, eventInit); break;
}
node.dispatchEvent(event);
}
private _parentElementOrShadowHost(element: Element): Element | undefined {
if (element.parentElement)
return element.parentElement;
@ -368,3 +383,51 @@ export class Injected {
return element;
}
}
const eventType = new Map<string, 'mouse'|'keyboard'|'touch'|'pointer'|'focus'|'drag'>([
['auxclick', 'mouse'],
['click', 'mouse'],
['dblclick', 'mouse'],
['mousedown','mouse'],
['mouseeenter', 'mouse'],
['mouseleave', 'mouse'],
['mousemove', 'mouse'],
['mouseout', 'mouse'],
['mouseover', 'mouse'],
['mouseup', 'mouse'],
['mouseleave', 'mouse'],
['mousewheel', 'mouse'],
['keydown', 'keyboard'],
['keyup', 'keyboard'],
['keypress', 'keyboard'],
['textInput', 'keyboard'],
['touchstart', 'touch'],
['touchmove', 'touch'],
['touchend', 'touch'],
['touchcancel', 'touch'],
['pointerover', 'pointer'],
['pointerout', 'pointer'],
['pointerenter', 'pointer'],
['pointerleave', 'pointer'],
['pointerdown', 'pointer'],
['pointerup', 'pointer'],
['pointermove', 'pointer'],
['pointercancel', 'pointer'],
['gotpointercapture', 'pointer'],
['lostpointercapture', 'pointer'],
['focus', 'focus'],
['blur', 'focus'],
['drag', 'drag'],
['dragstart', 'drag'],
['dragend', 'drag'],
['dragover', 'drag'],
['dragenter', 'drag'],
['dragleave', 'drag'],
['dragexit', 'drag'],
['drop', 'drag'],
]);

View File

@ -220,6 +220,10 @@ export class Page extends ExtendedEventEmitter implements InnerLogger {
return this.mainFrame().waitForSelector(selector, options);
}
async dispatchEvent(selector: string, type: string, eventInit?: Object, options?: types.TimeoutOptions): Promise<void> {
return this.mainFrame().dispatchEvent(selector, type, eventInit, options);
}
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>;
async evaluateHandle<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<types.SmartHandle<R>>;
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>> {

View File

@ -163,6 +163,19 @@ export class Selectors {
return { world: this._needsMainContext(parsed) ? 'main' : 'utility', task };
}
_dispatchEventTask(selector: string, type: string, eventInit: Object, deadline: number): (context: dom.FrameExecutionContext) => Promise<js.JSHandle> {
const parsed = this._parseSelector(selector);
const task = async (context: dom.FrameExecutionContext) => context.evaluateHandleInternal(({ evaluator, parsed, type, eventInit, timeout }) => {
return evaluator.injected.poll('mutation', timeout, () => {
const element = evaluator.querySelector(parsed, document);
if (element)
evaluator.injected.dispatchEvent(element, type, eventInit);
return element || false;
});
}, { evaluator: await this._prepareEvaluator(context), parsed, type, eventInit, timeout: helper.timeUntilDeadline(deadline) });
return task;
}
async _createSelector(name: string, handle: dom.ElementHandle<Element>): Promise<string | undefined> {
const mainContext = await handle._page.mainFrame()._mainContext();
return mainContext.evaluateInternal(({ evaluator, target, name }) => {

View File

@ -1,51 +1,41 @@
<!DOCTYPE html>
<html lang=en>
<title>Examples of DataTransfer's setData(), getData() and clearData()</title>
<meta content="width=device-width">
<style>
div:not(.mouse-helper) {
margin: 0em;
padding: 2em;
}
#source {
color: blue;
border: 1px solid black;
}
#target {
border: 1px solid black;
}
div:not(.mouse-helper) {
margin: 0em;
padding: 2em;
}
#source {
color: blue;
border: 1px solid black;
}
#target {
border: 1px solid black;
}
</style>
<script>
function dragstart_handler(ev) {
console.log("dragStart");
// Change the source element's background color to signify drag has started
ev.currentTarget.style.border = "dashed";
// Set the drag's format and data. Use the event target's id for the data
ev.dataTransfer.setData("text/plain", ev.target.id);
ev.currentTarget.style.border = "dashed";
ev.dataTransfer.setData("text/plain", ev.target.id);
}
function dragover_handler(ev) {
console.log("dragOver");
ev.preventDefault();
ev.preventDefault();
}
function drop_handler(ev) {
console.log("Drop");
ev.preventDefault();
// Get the data, which is the id of the drop target
var data = ev.dataTransfer.getData("text");
ev.target.appendChild(document.getElementById(data));
// Clear the drag data cache (for all formats/types)
ev.dataTransfer.clearData();
console.log("Drop");
ev.preventDefault();
var data = ev.dataTransfer.getData("text");
ev.target.appendChild(document.getElementById(data));
ev.dataTransfer.clearData();
}
</script>
<body>
<script src="input/mouse-helper.js"></script>
<h1>Examples of <code>DataTransfer</code>: <code>setData()</code>, <code>getData()</code>, <code>clearData()</code></h1>
<div>
<p id="source" ondragstart="dragstart_handler(event);" draggable="true">
Select this element, drag it to the Drop Zone and then release the selection to move the element.</p>
</div>
<div id="target" ondrop="drop_handler(event);" ondragover="dragover_handler(event);">Drop Zone</div>
<div>
<p id="source" ondragstart="dragstart_handler(event);" draggable="true">
Select this element, drag it to the Drop Zone and then release the selection to move the element.</p>
</div>
<div id="target" ondrop="drop_handler(event);" ondragover="dragover_handler(event);">Drop Zone</div>
</body>
</html>

View File

@ -15,6 +15,7 @@
window.shiftKey = undefined;
window.pageX = undefined;
window.pageY = undefined;
window.bubbles = undefined;
document.querySelector('button').addEventListener('click', e => {
result = 'Clicked';
offsetX = e.offsetX;
@ -22,6 +23,9 @@
pageX = e.pageX;
pageY = e.pageY;
shiftKey = e.shiftKey;
bubbles = e.bubbles;
cancelable = e.cancelable;
composed = e.composed;
}, false);
</script>
</body>

103
test/dispatchevent.spec.js Normal file
View File

@ -0,0 +1,103 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const utils = require('./utils');
const {FFOX, CHROMIUM, WEBKIT, WIN} = utils.testOptions(browserType);
describe('Page.dispatchEvent(click)', function() {
it('should dispatch click event', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.dispatchEvent('button', 'click');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should dispatch click event properties', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.dispatchEvent('button', 'click');
expect(await page.evaluate(() => bubbles)).toBeTruthy();
expect(await page.evaluate(() => cancelable)).toBeTruthy();
expect(await page.evaluate(() => composed)).toBeTruthy();
});
it('should dispatch click svg', async({page, server}) => {
await page.setContent(`
<svg height="100" width="100">
<circle onclick="javascript:window.__CLICKED=42" cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
</svg>
`);
await page.dispatchEvent('circle', 'click');
expect(await page.evaluate(() => window.__CLICKED)).toBe(42);
});
it('should dispatch click on a span with an inline element inside', async({page, server}) => {
await page.setContent(`
<style>
span::before {
content: 'q';
}
</style>
<span onclick='javascript:window.CLICKED=42'></span>
`);
await page.dispatchEvent('span', 'click');
expect(await page.evaluate(() => window.CLICKED)).toBe(42);
});
it('should dispatch click after navigation ', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.dispatchEvent('button', 'click');
await page.goto(server.PREFIX + '/input/button.html');
await page.dispatchEvent('button', 'click');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should dispatch click after a cross origin navigation ', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.dispatchEvent('button', 'click');
await page.goto(server.CROSS_PROCESS_PREFIX + '/input/button.html');
await page.dispatchEvent('button', 'click');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should not fail when element is blocked on hover', async({page, server}) => {
await page.setContent(`<style>
container { display: block; position: relative; width: 200px; height: 50px; }
div, button { position: absolute; left: 0; top: 0; bottom: 0; right: 0; }
div { pointer-events: none; }
container:hover div { pointer-events: auto; background: red; }
</style>
<container>
<button onclick="window.clicked=true">Click me</button>
<div></div>
</container>`);
await page.dispatchEvent('button', 'click');
expect(await page.evaluate(() => window.clicked)).toBeTruthy();
});
});
describe('Page.dispatchEvent(drag)', function() {
it.fail(WEBKIT)('should dispatch drag drop events', async({page, server}) => {
await page.goto(server.PREFIX + '/drag-n-drop.html');
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
await page.dispatchEvent('#source', 'dragstart', { dataTransfer });
await page.dispatchEvent('#target', 'drop', { dataTransfer });
expect(await page.evaluate(() => {
return source.parentElement === target;
})).toBeTruthy();
});
});
describe('ElementHandle.dispatchEvent(click)', function() {
it('should dispatch click event', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await button.dispatchEvent('click');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
});

View File

@ -158,6 +158,7 @@ module.exports = {
'./click.spec.js',
'./cookies.spec.js',
'./dialog.spec.js',
'./dispatchevent.spec.js',
'./download.spec.js',
'./elementhandle.spec.js',
'./emulation.spec.js',