diff --git a/src/server/chromium/crDragDrop.ts b/src/server/chromium/crDragDrop.ts new file mode 100644 index 0000000000000..46956be2cbfb8 --- /dev/null +++ b/src/server/chromium/crDragDrop.ts @@ -0,0 +1,301 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. +*/ +import { CRPage } from './crPage'; +import * as types from '../types'; +import * as dragScriptSource from '../../generated/dragScriptSource'; +import { assert } from '../../utils/utils'; + +export class DragManager { + private _page: CRPage; + private _setup = false; + private _dragState: { + data: DataTransferJSON, + x: number, + y: number + } | null = null; + constructor(page: CRPage) { + this._page = page; + } + + async _setupIfNeeded() { + if (this._setup) + return false; + const page = this._page; + await page._page.exposeBinding('dragStarted', false, (source, data: DataTransferJSON) => { + this._dragState = { + x: NaN, + y: NaN, + data, + }; + }, 'utility'); + await page.evaluateOnNewDocument(dragScriptSource.source, 'utility'); + await Promise.all(page._page.frames().map(frame => frame._evaluateExpression(dragScriptSource.source, false, {}, 'utility').catch(e => {}))); + } + + async cancel() { + if (!this._dragState) + return false; + await this._dispatchDragEvent('dragend'); + this._dragState = null; + return true; + } + + async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set, moveCallback: () => Promise): Promise { + if (this._dragState) { + await this._moveDrag(x, y); + return; + } + if (button !== 'left') + return moveCallback(); + + await this._setupIfNeeded(); + await moveCallback(); + // thread the renderer to wait for the drag callback + await this._page._page.mainFrame()._evaluateExpression('', false, null); + if (this._dragState) { + this._dragState!.x = x; + this._dragState!.y = y; + } + } + async _moveDrag(x: number, y: number) { + assert(this._dragState, 'missing drag state'); + if (x === this._dragState.x && y === this._dragState.y) + return; + this._dragState.x = x; + this._dragState.y = y; + await this._dispatchDragEvent('dragover'); + } + + isDragging() { + return !!this._dragState; + } + + async down(x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set): Promise { + return !!this._dragState; + } + + async up(x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set) { + // await this._moveDrag(x, y); + assert(this._dragState, 'missing drag state'); + this._dragState.x = x; + this._dragState.y = y; + await this._dispatchDragEvent('drop'); + this._dragState = null; + } + + async _dispatchDragEvent(type: 'dragover'|'drop'|'dragend') { + assert(this._dragState, 'missing drag state'); + const {backendNodeId, frameId} = await this._page._mainFrameSession._client.send('DOM.getNodeForLocation', { + x: Math.round(this._dragState.x), + y: Math.round(this._dragState.y), + ignorePointerEventsNone: false, + includeUserAgentShadowDOM: false + }); + const frame = this._page._page.frames().find(x => x._id === frameId)!; + + const context = await frame._utilityContext(); + const elementHandle = await this._page._mainFrameSession._adoptBackendNodeId(backendNodeId, context); + // console.log(elementHandle); + return await elementHandle.evaluate(dispatchDragEvent, { + json: type === 'dragend' ? null : this._dragState.data, + type, + x: this._dragState.x, + y: this._dragState.y, + }); + } +} + + +async function dispatchDragEvent(element: Node, {type, x, y, json}: {type: 'dragover'|'drop'|'dragend', x: number, y: number, json: DataTransferJSON|null}) { + const node = element; // document.elementFromPoint(x, y); + if (!node) + throw new Error(`could not find node at (${x},${y})`); + const dataTransfer = jsonToDataTransfer(json); + const lastDragNode = (window as any).__lastDragNode as Node; + if (lastDragNode !== node) { + if (node) { + node.dispatchEvent(new DragEvent('dragenter', { + dataTransfer, + bubbles: true, + cancelable: false + })); + } + if (lastDragNode) { + lastDragNode.dispatchEvent(new DragEvent('dragleave', { + dataTransfer, + bubbles: true, + cancelable: false + })); + } + (window as any).__lastDragNode = node; + } + const dragOverEvent = new DragEvent('dragover', { + dataTransfer, + bubbles: true, + cancelable: true + }); + + node.dispatchEvent(dragOverEvent); + if (type === 'dragend') { + endDrag(); + return; + } + if (type === 'dragover') return; + + // TODO(einbinder) This should check if the effect is allowed + // by the DataTransfer, but currently the user can't set the effect. + if (dragOverEvent.defaultPrevented) { + const dropEvent = new DragEvent('drop', { + dataTransfer, + bubbles: true, + cancelable: true + }); + node.dispatchEvent(dropEvent); + if (dropEvent.defaultPrevented) { + endDrag(); + return; + } + } + doDefaultDrop(); + endDrag(); + + function endDrag() { + if ((window as any).__draggingElement) { + const draggingElement: Element = (window as any).__draggingElement; + delete (window as any).__draggingElement; + draggingElement.dispatchEvent(new DragEvent('dragend', { + dataTransfer: new DataTransfer(), + bubbles: true, + cancelable: false + })); + } + } + + function doDefaultDrop() { + const htmlItem = json!.items.find(x => x.type === 'text/html'); + const textItem = json!.items.find(x => x.type.startsWith('text/')); + if (!htmlItem && !textItem) + return; + // const html = document.createElement('') + const editableTarget = editableAncestor(node); + if (!editableTarget) + return; + editableTarget.focus(); + if (htmlItem) + document.execCommand('insertHTML', false, itemToString(htmlItem)); + else if (textItem) + document.execCommand('insertText', false, itemToString(textItem)); + } + + function jsonToDataTransfer(json: DataTransferJSON|null): DataTransfer { + const transfer = new DataTransfer(); + // Chromium doesn't allow setting these properties on a DataTransfer that wasn't + // specifically created for drag and drop. Redefining them lets us set them, + // but its mostly futile because this doesn't effect the main execution context. + Object.defineProperty(transfer, 'effectAllowed', {value: transfer.effectAllowed, writable: true}); + Object.defineProperty(transfer, 'dropEffect', {value: transfer.dropEffect, writable: true}); + if (json) { + for (const {data, type} of json.items) { + if (typeof data === 'string') + transfer.items.add(data, type); + else + transfer.items.add(jsonToFile(data)); + } + transfer.effectAllowed = json.effectAllowed; + transfer.dropEffect = json.dropEffect; + } + return transfer; + } + + + function binaryDataURLToString(url: string) { + return atob(url.slice(url.indexOf(',') + 1)); + } + + function jsonToFile(json: FileJSON): File { + const data = atob(binaryDataURLToString(json.dataURL)); + const file = new File([data], json.name, { + lastModified: json.lastModified, + type: json.type, + }); + return file; + } + + function itemToString({data}: ItemJSON) { + if (typeof data === 'string') + return data; + return binaryDataURLToString(data.dataURL); + } + + function editableAncestor(node: Node|null): HTMLElement|null { + if (!node) + return null; + if (isEditable(node)) + return node as HTMLElement; + return editableAncestor(node.parentElement); + } + + function isEditable(node: Node) { + if (node.nodeType !== Node.ELEMENT_NODE) + return false; + const element = node as Element; + if (element.nodeName.toLowerCase() === 'input') { + const input = element as HTMLInputElement; + const type = (input.getAttribute('type') || '').toLowerCase(); + const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local']); + const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']); + if (!kTextInputTypes.has(type) && !kDateTypes.has(type)) + return false; + if (input.disabled) + return false; + if (input.readOnly) + return false; + return true; + } else if (element.nodeName.toLowerCase() === 'textarea') { + const textarea = element as HTMLTextAreaElement; + if (textarea.disabled) + return false; + if (textarea.readOnly) + return false; + } else if (!(element as HTMLElement).isContentEditable) { + return false; + } + return true; + } + +} + +type DataTransferJSON = { + dropEffect: string; + effectAllowed: string; + items: ItemJSON[]; +}; + +type FileJSON = { + lastModified: number; + name: string; + dataURL: string; + type: string; + // only exists for electron + path?: string; +}; + +type ItemJSON = { + data: string|FileJSON; + kind: string; + type: string; +}; + diff --git a/src/server/chromium/crInput.ts b/src/server/chromium/crInput.ts index 44d39bb8e6523..1fa2e5c2d6c1d 100644 --- a/src/server/chromium/crInput.ts +++ b/src/server/chromium/crInput.ts @@ -20,6 +20,8 @@ import * as types from '../types'; import { CRSession } from './crConnection'; import { macEditingCommands } from '../macEditingCommands'; import { isString } from '../../utils/utils'; +import { DragManager } from './crDragDrop'; +import { CRPage } from './crPage'; function toModifiersMask(modifiers: Set): number { let mask = 0; @@ -38,6 +40,7 @@ export class RawKeyboardImpl implements input.RawKeyboard { constructor( private _client: CRSession, private _isMac: boolean, + private _dragManger: DragManager, ) { } _commandsForCode(code: string, modifiers: Set) { @@ -60,6 +63,8 @@ export class RawKeyboardImpl implements input.RawKeyboard { } async keydown(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise { + if (code === 'Escape' && await this._dragManger.cancel()) + return; const commands = this._commandsForCode(code, modifiers); await this._client.send('Input.dispatchKeyEvent', { type: text ? 'keyDown' : 'rawKeyDown', @@ -94,22 +99,30 @@ export class RawKeyboardImpl implements input.RawKeyboard { export class RawMouseImpl implements input.RawMouse { private _client: CRSession; + private _page: CRPage; + private _dragManager: DragManager; - constructor(client: CRSession) { + constructor(page: CRPage, client: CRSession, dragManager: DragManager) { + this._page = page; this._client = client; + this._dragManager = dragManager; } async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set, modifiers: Set): Promise { - await this._client.send('Input.dispatchMouseEvent', { - type: 'mouseMoved', - button, - x, - y, - modifiers: toModifiersMask(modifiers) + await this._dragManager.move(x, y, button, buttons, modifiers, async () => { + await this._client.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + button, + x, + y, + modifiers: toModifiersMask(modifiers) + }); }); } async down(x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise { + if (this._dragManager.isDragging()) + return; await this._client.send('Input.dispatchMouseEvent', { type: 'mousePressed', button, @@ -121,6 +134,10 @@ export class RawMouseImpl implements input.RawMouse { } async up(x: number, y: number, button: types.MouseButton, buttons: Set, modifiers: Set, clickCount: number): Promise { + if (this._dragManager.isDragging()) { + await this._dragManager.up(x, y, button, buttons, modifiers); + return; + } await this._client.send('Input.dispatchMouseEvent', { type: 'mouseReleased', button, diff --git a/src/server/chromium/crPage.ts b/src/server/chromium/crPage.ts index aabadb188a657..ae750cd4c2a80 100644 --- a/src/server/chromium/crPage.ts +++ b/src/server/chromium/crPage.ts @@ -39,6 +39,7 @@ import * as sourceMap from '../../utils/sourceMap'; import { rewriteErrorMessage } from '../../utils/stackTrace'; import { assert, headersArrayToObject, createGuid } from '../../utils/utils'; import { VideoRecorder } from './videoRecorder'; +import { DragManager } from './crDragDrop'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -68,8 +69,9 @@ export class CRPage implements PageDelegate { constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, hasUIWindow: boolean) { this._targetId = targetId; this._opener = opener; - this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._isMac); - this.rawMouse = new RawMouseImpl(client); + const dragManager = new DragManager(this); + this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._isMac, dragManager); + this.rawMouse = new RawMouseImpl(this, client, dragManager); this.rawTouchscreen = new RawTouchscreenImpl(client); this._pdf = new CRPDF(client); this._coverage = new CRCoverage(client); @@ -128,7 +130,7 @@ export class CRPage implements PageDelegate { async exposeBinding(binding: PageBinding) { await this._forAllFrameSessions(frame => frame._initBinding(binding)); - await Promise.all(this._page.frames().map(frame => frame._evaluateExpression(binding.source, false, {}).catch(e => {}))); + await Promise.all(this._page.frames().map(frame => frame._evaluateExpression(binding.source, false, {}, binding.world).catch(e => {}))); } async updateExtraHTTPHeaders(): Promise { diff --git a/src/server/injected/dragScript.ts b/src/server/injected/dragScript.ts new file mode 100644 index 0000000000000..c605483853123 --- /dev/null +++ b/src/server/injected/dragScript.ts @@ -0,0 +1,114 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + +type DataTransferJSON = { + dropEffect: string; + effectAllowed: string; + items: ItemJSON[]; +}; + +type FileJSON = { + lastModified: number; + name: string; + dataURL: string; + type: string; + // only exists for electron + path?: string; +}; + +type ItemJSON = { + data: string|FileJSON; + kind: string; + type: string; +}; +// This is a binding added by Playwright. See crDragDrop.ts +declare function dragStarted(json: DataTransferJSON): Promise; + +window.addEventListener('dragstart', async originalDrag => { + if (originalDrag.defaultPrevented || !originalDrag.isTrusted) + return; + // cancel this drag, and create our own in order to track it + originalDrag.preventDefault(); + originalDrag.stopImmediatePropagation(); + const drag = new DragEvent(originalDrag.type, { + altKey: originalDrag.altKey, + bubbles: originalDrag.bubbles, + button: originalDrag.button, + buttons: originalDrag.buttons, + cancelable: originalDrag.cancelable, + clientX: originalDrag.clientX, + clientY: originalDrag.clientY, + composed: originalDrag.composed, + ctrlKey: originalDrag.ctrlKey, + dataTransfer: originalDrag.dataTransfer, + detail: originalDrag.detail, + metaKey: originalDrag.metaKey, + modifierAltGraph: originalDrag.getModifierState('AltGraph'), + modifierCapsLock: originalDrag.getModifierState('CapsLock'), + movementX: originalDrag.movementX, + movementY: originalDrag.movementY + }); + const path = originalDrag.composedPath(); + if (!path.length) + return; + path[0].dispatchEvent(drag); + if (!drag.dataTransfer || drag.defaultPrevented) + return; + const json = await dataTransferToJSON(drag.dataTransfer); + (window as any).__draggingElement = path[0]; + dragStarted(json); +}, true); + + +async function dataTransferToJSON(dataTransfer: DataTransfer): Promise { + // const files = await Promise.all([...dataTransfer.files].map(fileToJSON)); + const items = await Promise.all([...dataTransfer.items].map(async item => { + let data: string|FileJSON; + // store the item data before it disapears next tick + const {type, kind} = item; + if (kind === 'file') + data = await fileToJSON(item.getAsFile()!); + else + data = await new Promise(x => item.getAsString(x)); + return { + kind, + type, + data, + } as ItemJSON; + })); + return { + dropEffect: dataTransfer.dropEffect, + effectAllowed: dataTransfer.effectAllowed, + // files, + items, + // types: dataTransfer.types, + }; +} + +async function fileToJSON(file: File): Promise { + const buffer = await file.arrayBuffer(); + btoa(new Uint8Array(buffer).toString()); + const reader = new FileReader(); + const promise = new Promise(x => reader.onload = x); + reader.readAsDataURL(file); + await promise; + return { + lastModified: file.lastModified, + name: file.name, + type: file.type, + dataURL: reader.result as string + }; +} diff --git a/src/server/injected/dragScript.webpack.config.js b/src/server/injected/dragScript.webpack.config.js new file mode 100644 index 0000000000000..85a8fd5975385 --- /dev/null +++ b/src/server/injected/dragScript.webpack.config.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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 path = require('path'); +const InlineSource = require('./webpack-inline-source-plugin.js'); + +module.exports = { + entry: path.join(__dirname, 'dragScript.ts'), + devtool: 'source-map', + module: { + rules: [ + { + test: /\.tsx?$/, + loader: 'ts-loader', + options: { + transpileOnly: true + }, + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: [ '.tsx', '.ts', '.js' ] + }, + output: { + libraryTarget: 'var', + filename: 'dragScriptSource.js', + path: path.resolve(__dirname, '../../../lib/server/injected/packed') + }, + plugins: [ + new InlineSource(path.join(__dirname, '..', '..', 'generated', 'dragScriptSource.ts')), + ] +}; diff --git a/src/server/page.ts b/src/server/page.ts index a63f8c87f204d..6163838a71182 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -257,13 +257,13 @@ export class Page extends EventEmitter { this._timeoutSettings.setDefaultTimeout(timeout); } - async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource) { - const identifier = PageBinding.identifier(name, 'main'); + async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource, world: types.World = 'main') { + const identifier = PageBinding.identifier(name, world); if (this._pageBindings.has(identifier)) throw new Error(`Function "${name}" has been already registered`); if (this._browserContext._pageBindings.has(identifier)) throw new Error(`Function "${name}" has been already registered in the browser context`); - const binding = new PageBinding(name, playwrightBinding, needsHandle, 'main'); + const binding = new PageBinding(name, playwrightBinding, needsHandle, world); this._pageBindings.set(identifier, binding); await this._delegate.exposeBinding(binding); } diff --git a/test/drag.spec.ts b/test/drag.spec.ts index 6c665c3b33923..01130e761ab32 100644 --- a/test/drag.spec.ts +++ b/test/drag.spec.ts @@ -17,8 +17,8 @@ import { ElementHandle } from '..'; import { it, expect, describe } from './fixtures'; import { attachFrame } from './utils'; -describe('Drag and drop', test => { - test.fixme(); +describe('Drag and drop', (test, {browserName}) => { + test.fixme(browserName !== 'chromium'); }, () => { it('should work', async ({server, page, context}) => { await page.goto(server.PREFIX + '/drag-n-drop.html'); @@ -127,8 +127,8 @@ describe('Drag and drop', test => { 'mousedown', isFirefox ? 'dragstart' : 'mousemove', isFirefox ? 'mousemove' : 'dragstart', - 'dragenter', - 'dragover', + // 'dragenter', + // 'dragover', ]); await page.reload(); @@ -141,7 +141,157 @@ describe('Drag and drop', test => { expect(await afterNavigationEvents.jsonValue()).toEqual([ 'dragenter', 'dragover', + 'dragover', + 'drop', + ]); + }); + + it('should work even if the page tries to stop us', async ({page, server}) => { + await page.goto(server.PREFIX + '/drag-n-drop.html'); + await page.evaluate(() => { + window.DragEvent = null; + }); + await page.hover('#source'); + await page.mouse.down(); + await page.hover('#target'); + await page.mouse.up(); + expect(await page.$eval('#target', target => target.contains(document.querySelector('#source')))).toBe(true); // could not find source in target + }); + + it('should drag text into a textarea', async ({page}) => { + await page.setContent(` +
ThisIsTheText
+ + `); + await page.click('div', { + clickCount: 3 + }); + await page.mouse.down(); + await page.hover('textarea'); + await page.mouse.up(); + expect(await page.$eval('textarea', t => t.value.trim())).toBe('ThisIsTheText'); + }); + + it('should not drop when the dragover is ignored', async ({page}) => { + await page.setContent(` +
drag target
+ this is the drop target + `); + await page.evaluate(() => { + const events = window['events'] = []; + document.querySelector('div').addEventListener('dragstart', event => { + events.push('dragstart'); + event.dataTransfer.effectAllowed = 'copy'; + event.dataTransfer.setData('text/plain', 'drag data'); + }); + document.querySelector('drop-target').addEventListener('dragover', event => { + events.push('dragover'); + }); + document.querySelector('drop-target').addEventListener('drop', event => { + events.push('drop'); + }); + document.querySelector('div').addEventListener('dragend', event => { + events.push('dragend'); + }); + }); + await page.hover('div'); + await page.mouse.down(); + await page.hover('drop-target'); + await page.mouse.up(); + expect(await page.evaluate('events')).toEqual([ + 'dragstart', + 'dragover', + 'dragend' + ]); + }); + it('should respect the drop effect', (test, {browserName}) => { + test.fixme(browserName === 'chromium', 'Chromium doesn\'t let users set dropEffect on our fake data transfer'); + }, async ({page}) => { + page.on('console', console.log); + expect(await testIfDropped('copy', 'copy')).toBe(true); + expect(await testIfDropped('copy', 'move')).toBe(false); + expect(await testIfDropped('all', 'link')).toBe(true); + expect(await testIfDropped('all', 'none')).toBe(false); + + expect(await testIfDropped('copyMove', 'copy')).toBe(true); + expect(await testIfDropped('copyLink', 'copy')).toBe(true); + expect(await testIfDropped('linkMove', 'copy')).toBe(false); + + expect(await testIfDropped('copyMove', 'link')).toBe(false); + expect(await testIfDropped('copyLink', 'link')).toBe(true); + expect(await testIfDropped('linkMove', 'link')).toBe(true); + + expect(await testIfDropped('copyMove', 'move')).toBe(true); + expect(await testIfDropped('copyLink', 'move')).toBe(false); + expect(await testIfDropped('linkMove', 'move')).toBe(true); + + expect(await testIfDropped('uninitialized', 'copy')).toBe(true); + + async function testIfDropped(effectAllowed: string, dropEffect: string) { + await page.setContent(` +
drag target
+ this is the drop target + `); + await page.evaluate(({effectAllowed, dropEffect}) => { + window['dropped'] = false; + + document.querySelector('div').addEventListener('dragstart', event => { + event.dataTransfer.effectAllowed = effectAllowed; + event.dataTransfer.setData('text/plain', 'drag data'); + }); + + const dropTarget: HTMLElement = document.querySelector('drop-target'); + dropTarget.addEventListener('dragover', event => { + const before = event.dataTransfer.dropEffect + ':'; + event.dataTransfer.dropEffect = dropEffect; + console.log('set drop effect',before, dropEffect, event.dataTransfer.dropEffect); + event.preventDefault(); + }); + dropTarget.addEventListener('drop', event => { + window['dropped'] = true; + }); + }, {effectAllowed, dropEffect}); + await page.hover('div'); + await page.mouse.down(); + await page.hover('drop-target'); + await page.mouse.up(); + return await page.evaluate('dropped'); + } + }); + it('should drop when it has all drop effects', async ({page}) => { + await page.setContent(` +
drag target
+ this is the drop target + `); + await page.evaluate(() => { + const events = window['events'] = []; + const dropTarget: HTMLElement = document.querySelector('drop-target'); + document.querySelector('div').addEventListener('dragstart', event => { + events.push('dragstart'); + event.dataTransfer.effectAllowed = 'all'; + event.dataTransfer.setData('text/plain', 'drag data'); + }); + dropTarget.addEventListener('dragover', event => { + events.push('dragover'); + event.dataTransfer.dropEffect = 'copy'; + event.preventDefault(); + }); + dropTarget.addEventListener('drop', event => { + events.push('drop'); + }); + document.querySelector('div').addEventListener('dragend', event => { + events.push('dragend'); + }); + }); + await page.hover('div'); + await page.mouse.down(); + await page.hover('drop-target'); + await page.mouse.up(); + expect(await page.evaluate('events')).toEqual([ + 'dragstart', + 'dragover', 'drop', + 'dragend' ]); }); diff --git a/utils/runWebpack.js b/utils/runWebpack.js index 01f6de5760c20..1aa736be65629 100644 --- a/utils/runWebpack.js +++ b/utils/runWebpack.js @@ -20,6 +20,7 @@ const path = require('path'); const files = [ path.join('src', 'server', 'injected', 'injectedScript.webpack.config.js'), path.join('src', 'server', 'injected', 'utilityScript.webpack.config.js'), + path.join('src', 'server', 'injected', 'dragScript.webpack.config.js'), path.join('src', 'debug', 'injected', 'debugScript.webpack.config.js'), ];