Skip to content

Commit

Permalink
feat(codegen): basic drag'n drop support
Browse files Browse the repository at this point in the history
  • Loading branch information
mxschmitt committed Nov 6, 2023
1 parent ffd2e02 commit 3b09717
Show file tree
Hide file tree
Showing 9 changed files with 82 additions and 3 deletions.
34 changes: 34 additions & 0 deletions packages/playwright-core/src/server/injected/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
}
Expand Down Expand Up @@ -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.
Expand All @@ -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;
}

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/recorder/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/recorder/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/recorder/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/recorder/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type ActionName =
'select' |
'uncheck' |
'setInputFiles' |
'dragAndDrop' |
'assertText' |
'assertValue' |
'assertChecked';
Expand Down Expand Up @@ -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,
Expand All @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions tests/assets/drag-n-drop.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@

<body>
<div>
<p id="source" ondragstart="dragstart_handler(event);" draggable="true">
<p id="source" ondragstart="dragstart_handler(event);" draggable="true" data-testid="drag-source">
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 id="target" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" data-testid="drag-target">Drop Zone</div>
</body>
28 changes: 28 additions & 0 deletions tests/library/inspector/cli-codegen-1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,34 @@ test.describe('cli codegen', () => {
expect(message.text()).toBe('click 250 250');
});

test('should be able to generate drag and drop action', async ({ page, openRecorder, server }) => {
const recorder = await openRecorder();

await page.goto(server.PREFIX + '/drag-n-drop.html');
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('JavaScript', 'dragTo'),
page.locator('#source').dragTo(page.locator('#target')),
]);

expect(sources.get('JavaScript')!.text).toContain(`
await page.getByTestId('drag-source').dragTo(page.getByTestId('drag-target'));`);

expect(sources.get('Python')!.text).toContain(`
page.get_by_test_id("drag-source").drag_to(page.get_by_test_id("drag-target"))`);

expect(sources.get('Python Async')!.text).toContain(`
await page.get_by_test_id("drag-source").drag_to(page.get_by_test_id("drag-target"))`);

expect(sources.get('Java')!.text).toContain(`
page.getByTestId("drag-source").dragTo(page.getByTestId("drag-target"));`);

expect(sources.get('C#')!.text).toContain(`
await page.GetByTestId("drag-source").DragToAsync(page.GetByTestId("drag-target"));`);

expect(message.text()).toBe('Drop');
});

test('should work with TrustedTypes', async ({ page, openRecorder }) => {
const recorder = await openRecorder();

Expand Down

0 comments on commit 3b09717

Please sign in to comment.