diff --git a/packages/playwright-core/src/server/injected/recorder.ts b/packages/playwright-core/src/server/injected/recorder.ts index 334e8569a5919..0e0ebbc8d88f7 100644 --- a/packages/playwright-core/src/server/injected/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder.ts @@ -39,6 +39,7 @@ interface RecorderTool { disable?(): void; onClick?(event: MouseEvent): void; onDragStart?(event: DragEvent): void; + onDrop?(event: DragEvent): void; onInput?(event: Event): void; onKeyDown?(event: KeyboardEvent): void; onKeyUp?(event: KeyboardEvent): void; @@ -142,6 +143,7 @@ class RecordActionTool implements RecorderTool { private _hoveredElement: HTMLElement | null = null; private _activeModel: HighlightModel | null = null; private _expectProgrammaticKeyUp = false; + private _dragSourceElement: HTMLElement | null = null; constructor(private _recorder: Recorder) { } @@ -329,6 +331,27 @@ class RecordActionTool implements RecorderTool { this._recorder.updateHighlight(null, false); } + onDragStart(event: DragEvent): void { + this._dragSourceElement = event.target as HTMLElement; + } + + onDrop(event: DragEvent): void { + if (this._actionInProgress(event)) + return; + const targetElement = this._recorder.deepEventTarget(event); + const sourceElement = this._dragSourceElement; + this._dragSourceElement = null; + if (!sourceElement) + return; + consumeEvent(event); + this._performAction({ + name: 'dragAndDrop', + signals: [], + source: generateSelector(this._recorder.injectedScript, sourceElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }).selector, + target: generateSelector(this._recorder.injectedScript, targetElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }).selector, + }); + } + private _onFocus(userGesture: boolean) { const activeElement = deepActiveElement(this._recorder.document); // Firefox dispatches "focus" event to body when clicking on a backgrounded headed browser window. @@ -349,6 +372,8 @@ class RecordActionTool implements RecorderTool { return true; if (nodeName === 'INPUT' && ['date'].includes((target as HTMLInputElement).type)) return true; + if (target.draggable && nodeName !== 'A') + return true; return false; } @@ -834,6 +859,7 @@ export class Recorder { addEventListener(this.document, 'click', event => this._onClick(event as MouseEvent), true), addEventListener(this.document, 'auxclick', event => this._onClick(event as MouseEvent), true), addEventListener(this.document, 'dragstart', event => this._onDragStart(event as DragEvent), true), + addEventListener(this.document, 'drop', event => this._onDrop(event as DragEvent), true), addEventListener(this.document, 'input', event => this._onInput(event), true), addEventListener(this.document, 'keydown', event => this._onKeyDown(event as KeyboardEvent), true), addEventListener(this.document, 'keyup', event => this._onKeyUp(event as KeyboardEvent), true), @@ -911,6 +937,14 @@ export class Recorder { this._currentTool.onDragStart?.(event); } + private _onDrop(event: DragEvent) { + if (!event.isTrusted) + return; + if (this._ignoreOverlayEvent(event)) + return; + this._currentTool.onDrop?.(event); + } + private _onPointerDown(event: PointerEvent) { if (!event.isTrusted) return; diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 8f46e2995b231..7914157848554 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -652,6 +652,8 @@ class ContextRecorder extends EventEmitter { const values = action.options.map(value => ({ value })); await perform('selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true })); } + if (action.name === 'dragAndDrop') + await perform('dragAndDrop', { source: action.source, target: action.target }, callMetadata => frame.dragAndDrop(callMetadata, action.source, action.target, { timeout: kActionTimeout, strict: true })); } private async _recordAction(frame: Frame, action: actions.Action) { diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/recorder/csharp.ts index 709b9df49d8e0..bbf5df82bc80b 100644 --- a/packages/playwright-core/src/server/recorder/csharp.ts +++ b/packages/playwright-core/src/server/recorder/csharp.ts @@ -154,6 +154,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator { return `await ${subject}.GotoAsync(${quote(action.url)});`; case 'select': return `await ${subject}.${this._asLocator(action.selector)}.SelectOptionAsync(${formatObject(action.options)});`; + case 'dragAndDrop': + return `await ${subject}.${this._asLocator(action.source)}.DragToAsync(${subject}.${this._asLocator(action.target)});`; case 'assertText': return `await Expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'ToContainTextAsync' : 'ToHaveTextAsync'}(${quote(action.text)});`; case 'assertChecked': diff --git a/packages/playwright-core/src/server/recorder/java.ts b/packages/playwright-core/src/server/recorder/java.ts index c15241a98ef22..c17c7962e7056 100644 --- a/packages/playwright-core/src/server/recorder/java.ts +++ b/packages/playwright-core/src/server/recorder/java.ts @@ -122,6 +122,8 @@ export class JavaLanguageGenerator implements LanguageGenerator { return `${subject}.navigate(${quote(action.url)});`; case 'select': return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.selectOption(${formatSelectOption(action.options.length > 1 ? action.options : action.options[0])});`; + case 'dragAndDrop': + return `${subject}.${this._asLocator(action.source, inFrameLocator)}.dragTo(${subject}.${this._asLocator(action.target, inFrameLocator)});`; case 'assertText': return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${action.substring ? 'containsText' : 'hasText'}(${quote(action.text)});`; case 'assertChecked': diff --git a/packages/playwright-core/src/server/recorder/javascript.ts b/packages/playwright-core/src/server/recorder/javascript.ts index 6ffbb00dcf282..dc405e42a1584 100644 --- a/packages/playwright-core/src/server/recorder/javascript.ts +++ b/packages/playwright-core/src/server/recorder/javascript.ts @@ -125,6 +125,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { return `await ${subject}.goto(${quote(action.url)});`; case 'select': return `await ${subject}.${this._asLocator(action.selector)}.selectOption(${formatObject(action.options.length > 1 ? action.options : action.options[0])});`; + case 'dragAndDrop': + return `await ${subject}.${this._asLocator(action.source)}.dragTo(${subject}.${this._asLocator(action.target)});`; case 'assertText': return `await expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'toContainText' : 'toHaveText'}(${quote(action.text)});`; case 'assertChecked': diff --git a/packages/playwright-core/src/server/recorder/python.ts b/packages/playwright-core/src/server/recorder/python.ts index a0e60e32be2ad..07e89ed149fb6 100644 --- a/packages/playwright-core/src/server/recorder/python.ts +++ b/packages/playwright-core/src/server/recorder/python.ts @@ -134,6 +134,8 @@ export class PythonLanguageGenerator implements LanguageGenerator { return `${subject}.goto(${quote(action.url)})`; case 'select': return `${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`; + case 'dragAndDrop': + return `${subject}.${this._asLocator(action.source)}.drag_to(${subject}.${this._asLocator(action.target)})`; case 'assertText': return `expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'to_contain_text' : 'to_have_text'}(${quote(action.text)})`; case 'assertChecked': diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts index 5be43e9ea911c..2f7d76fe6a5d4 100644 --- a/packages/playwright-core/src/server/recorder/recorderActions.ts +++ b/packages/playwright-core/src/server/recorder/recorderActions.ts @@ -27,6 +27,7 @@ export type ActionName = 'select' | 'uncheck' | 'setInputFiles' | + 'dragAndDrop' | 'assertText' | 'assertValue' | 'assertChecked'; @@ -94,6 +95,12 @@ export type SetInputFilesAction = ActionBase & { files: string[], }; +export type DragAndDropAction = ActionBase & { + name: 'dragAndDrop', + source: string, + target: string, +}; + export type AssertTextAction = ActionBase & { name: 'assertText', selector: string, @@ -113,7 +120,7 @@ export type AssertCheckedAction = ActionBase & { checked: boolean, }; -export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction; +export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | DragAndDropAction | AssertTextAction | AssertValueAction | AssertCheckedAction; // Signals. diff --git a/tests/assets/drag-n-drop.html b/tests/assets/drag-n-drop.html index f40b1bfc5de8d..05066634a99ec 100644 --- a/tests/assets/drag-n-drop.html +++ b/tests/assets/drag-n-drop.html @@ -50,8 +50,8 @@
+
Select this element, drag it to the Drop Zone and then release the selection to move the element.