Skip to content

Commit 45682bf

Browse files
committed
feat(codegen): basic drag'n drop support
1 parent ffd2e02 commit 45682bf

File tree

9 files changed

+82
-3
lines changed

9 files changed

+82
-3
lines changed

packages/playwright-core/src/server/injected/recorder.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface RecorderTool {
3939
disable?(): void;
4040
onClick?(event: MouseEvent): void;
4141
onDragStart?(event: DragEvent): void;
42+
onDrop?(event: DragEvent): void;
4243
onInput?(event: Event): void;
4344
onKeyDown?(event: KeyboardEvent): void;
4445
onKeyUp?(event: KeyboardEvent): void;
@@ -142,6 +143,7 @@ class RecordActionTool implements RecorderTool {
142143
private _hoveredElement: HTMLElement | null = null;
143144
private _activeModel: HighlightModel | null = null;
144145
private _expectProgrammaticKeyUp = false;
146+
private _dragSourceElement: HTMLElement | null = null;
145147

146148
constructor(private _recorder: Recorder) {
147149
}
@@ -329,6 +331,27 @@ class RecordActionTool implements RecorderTool {
329331
this._recorder.updateHighlight(null, false);
330332
}
331333

334+
onDragStart(event: DragEvent): void {
335+
this._dragSourceElement = event.target as HTMLElement;
336+
}
337+
338+
onDrop(event: DragEvent): void {
339+
if (this._actionInProgress(event))
340+
return;
341+
const targetElement = this._recorder.deepEventTarget(event);
342+
const sourceElement = this._dragSourceElement;
343+
this._dragSourceElement = null;
344+
if (!sourceElement)
345+
return;
346+
consumeEvent(event);
347+
this._performAction({
348+
name: 'dragAndDrop',
349+
signals: [],
350+
source: generateSelector(this._recorder.injectedScript, sourceElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }).selector,
351+
target: generateSelector(this._recorder.injectedScript, targetElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }).selector,
352+
});
353+
}
354+
332355
private _onFocus(userGesture: boolean) {
333356
const activeElement = deepActiveElement(this._recorder.document);
334357
// Firefox dispatches "focus" event to body when clicking on a backgrounded headed browser window.
@@ -349,6 +372,8 @@ class RecordActionTool implements RecorderTool {
349372
return true;
350373
if (nodeName === 'INPUT' && ['date'].includes((target as HTMLInputElement).type))
351374
return true;
375+
if (target.draggable)
376+
return true;
352377
return false;
353378
}
354379

@@ -834,6 +859,7 @@ export class Recorder {
834859
addEventListener(this.document, 'click', event => this._onClick(event as MouseEvent), true),
835860
addEventListener(this.document, 'auxclick', event => this._onClick(event as MouseEvent), true),
836861
addEventListener(this.document, 'dragstart', event => this._onDragStart(event as DragEvent), true),
862+
addEventListener(this.document, 'drop', event => this._onDrop(event as DragEvent), true),
837863
addEventListener(this.document, 'input', event => this._onInput(event), true),
838864
addEventListener(this.document, 'keydown', event => this._onKeyDown(event as KeyboardEvent), true),
839865
addEventListener(this.document, 'keyup', event => this._onKeyUp(event as KeyboardEvent), true),
@@ -911,6 +937,14 @@ export class Recorder {
911937
this._currentTool.onDragStart?.(event);
912938
}
913939

940+
private _onDrop(event: DragEvent) {
941+
if (!event.isTrusted)
942+
return;
943+
if (this._ignoreOverlayEvent(event))
944+
return;
945+
this._currentTool.onDrop?.(event);
946+
}
947+
914948
private _onPointerDown(event: PointerEvent) {
915949
if (!event.isTrusted)
916950
return;

packages/playwright-core/src/server/recorder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,8 @@ class ContextRecorder extends EventEmitter {
652652
const values = action.options.map(value => ({ value }));
653653
await perform('selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true }));
654654
}
655+
if (action.name === 'dragAndDrop')
656+
await perform('dragAndDrop', { source: action.source, target: action.target }, callMetadata => frame.dragAndDrop(callMetadata, action.source, action.target, { timeout: kActionTimeout, strict: true }));
655657
}
656658

657659
private async _recordAction(frame: Frame, action: actions.Action) {

packages/playwright-core/src/server/recorder/csharp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
154154
return `await ${subject}.GotoAsync(${quote(action.url)});`;
155155
case 'select':
156156
return `await ${subject}.${this._asLocator(action.selector)}.SelectOptionAsync(${formatObject(action.options)});`;
157+
case 'dragAndDrop':
158+
return `await ${subject}.${this._asLocator(action.source)}.DragToAsync(${subject}.${this._asLocator(action.target)});`;
157159
case 'assertText':
158160
return `await Expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'ToContainTextAsync' : 'ToHaveTextAsync'}(${quote(action.text)});`;
159161
case 'assertChecked':

packages/playwright-core/src/server/recorder/java.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
122122
return `${subject}.navigate(${quote(action.url)});`;
123123
case 'select':
124124
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.selectOption(${formatSelectOption(action.options.length > 1 ? action.options : action.options[0])});`;
125+
case 'dragAndDrop':
126+
return `${subject}.${this._asLocator(action.source, inFrameLocator)}.dragTo(${subject}.${this._asLocator(action.target, inFrameLocator)});`;
125127
case 'assertText':
126128
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${action.substring ? 'containsText' : 'hasText'}(${quote(action.text)});`;
127129
case 'assertChecked':

packages/playwright-core/src/server/recorder/javascript.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
125125
return `await ${subject}.goto(${quote(action.url)});`;
126126
case 'select':
127127
return `await ${subject}.${this._asLocator(action.selector)}.selectOption(${formatObject(action.options.length > 1 ? action.options : action.options[0])});`;
128+
case 'dragAndDrop':
129+
return `await ${subject}.${this._asLocator(action.source)}.dragTo(${subject}.${this._asLocator(action.target)});`;
128130
case 'assertText':
129131
return `await expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'toContainText' : 'toHaveText'}(${quote(action.text)});`;
130132
case 'assertChecked':

packages/playwright-core/src/server/recorder/python.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ export class PythonLanguageGenerator implements LanguageGenerator {
134134
return `${subject}.goto(${quote(action.url)})`;
135135
case 'select':
136136
return `${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
137+
case 'dragAndDrop':
138+
return `${subject}.${this._asLocator(action.source)}.drag_to(${subject}.${this._asLocator(action.target)})`;
137139
case 'assertText':
138140
return `expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'to_contain_text' : 'to_have_text'}(${quote(action.text)})`;
139141
case 'assertChecked':

packages/playwright-core/src/server/recorder/recorderActions.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type ActionName =
2727
'select' |
2828
'uncheck' |
2929
'setInputFiles' |
30+
'dragAndDrop' |
3031
'assertText' |
3132
'assertValue' |
3233
'assertChecked';
@@ -94,6 +95,12 @@ export type SetInputFilesAction = ActionBase & {
9495
files: string[],
9596
};
9697

98+
export type DragAndDropAction = ActionBase & {
99+
name: 'dragAndDrop',
100+
source: string,
101+
target: string,
102+
};
103+
97104
export type AssertTextAction = ActionBase & {
98105
name: 'assertText',
99106
selector: string,
@@ -113,7 +120,7 @@ export type AssertCheckedAction = ActionBase & {
113120
checked: boolean,
114121
};
115122

116-
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction;
123+
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | DragAndDropAction | AssertTextAction | AssertValueAction | AssertCheckedAction;
117124

118125
// Signals.
119126

tests/assets/drag-n-drop.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050

5151
<body>
5252
<div>
53-
<p id="source" ondragstart="dragstart_handler(event);" draggable="true">
53+
<p id="source" ondragstart="dragstart_handler(event);" draggable="true" data-testid="drag-source">
5454
Select this element, drag it to the Drop Zone and then release the selection to move the element.</p>
5555
</div>
56-
<div id="target" ondrop="drop_handler(event);" ondragover="dragover_handler(event);">Drop Zone</div>
56+
<div id="target" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" data-testid="drag-target">Drop Zone</div>
5757
</body>

tests/library/inspector/cli-codegen-1.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,34 @@ test.describe('cli codegen', () => {
155155
expect(message.text()).toBe('click 250 250');
156156
});
157157

158+
test('should be able to generate drag and drop action', async ({ page, openRecorder, server }) => {
159+
const recorder = await openRecorder();
160+
161+
await page.goto(server.PREFIX + '/drag-n-drop.html');
162+
const [message, sources] = await Promise.all([
163+
page.waitForEvent('console', msg => msg.type() !== 'error'),
164+
recorder.waitForOutput('JavaScript', 'dragTo'),
165+
page.locator('#source').dragTo(page.locator('#target')),
166+
]);
167+
168+
expect(sources.get('JavaScript')!.text).toContain(`
169+
await page.getByTestId('drag-source').dragTo(page.getByTestId('drag-target'));`);
170+
171+
expect(sources.get('Python')!.text).toContain(`
172+
page.get_by_test_id("drag-source").drag_to(page.get_by_test_id("drag-target"))`);
173+
174+
expect(sources.get('Python Async')!.text).toContain(`
175+
await page.get_by_test_id("drag-source").drag_to(page.get_by_test_id("drag-target"))`);
176+
177+
expect(sources.get('Java')!.text).toContain(`
178+
page.getByTestId("drag-source").dragTo(page.getByTestId("drag-target"));`);
179+
180+
expect(sources.get('C#')!.text).toContain(`
181+
await page.GetByTestId("drag-source").DragToAsync(page.GetByTestId("drag-target"));`);
182+
183+
expect(message.text()).toBe('Drop');
184+
});
185+
158186
test('should work with TrustedTypes', async ({ page, openRecorder }) => {
159187
const recorder = await openRecorder();
160188

0 commit comments

Comments
 (0)