Skip to content

Commit

Permalink
api(dispatchEvent): page, frame and handle versions added (#1932)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Apr 23, 2020
1 parent 671cfa0 commit c1c0237
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 37 deletions.
100 changes: 100 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]>
Expand Down
10 changes: 10 additions & 0 deletions src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions src/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
63 changes: 63 additions & 0 deletions src/injected/injected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'],
]);
4 changes: 4 additions & 0 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>> {
Expand Down
13 changes: 13 additions & 0 deletions src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
64 changes: 27 additions & 37 deletions test/assets/drag-n-drop.html
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 4 additions & 0 deletions test/assets/input/button.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
window.shiftKey = undefined;
window.pageX = undefined;
window.pageY = undefined;
window.bubbles = undefined;
document.querySelector('button').addEventListener('click', e => {
result = 'Clicked';
offsetX = e.offsetX;
offsetY = e.offsetY;
pageX = e.pageX;
pageY = e.pageY;
shiftKey = e.shiftKey;
bubbles = e.bubbles;
cancelable = e.cancelable;
composed = e.composed;
}, false);
</script>
</body>
Expand Down
Loading

0 comments on commit c1c0237

Please sign in to comment.