diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index d3456c693d52a..44f46bcb2a3ac 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -374,6 +374,20 @@ Optional event-specific initialization properties. ### option: Frame.dispatchEvent.timeout = %%-input-timeout-%% +## async method: Frame.dragAndDrop + +### param: Frame.dragAndDrop.source = %%-input-source-%% + +### param: Frame.dragAndDrop.target = %%-input-target-%% + +### option: Frame.dragAndDrop.force = %%-input-force-%% + +### option: Frame.dragAndDrop.noWaitAfter = %%-input-no-wait-after-%% + +### option: Frame.dragAndDrop.timeout = %%-input-timeout-%% + +### option: Frame.dragAndDrop.trial = %%-input-trial-%% + ## async method: Frame.evalOnSelector * langs: - alias-python: eval_on_selector diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index d20b8ff4ed1d9..a993da69ca930 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -811,6 +811,20 @@ Optional event-specific initialization properties. ### option: Page.dispatchEvent.timeout = %%-input-timeout-%% +## async method: Page.dragAndDrop + +### param: Page.dragAndDrop.source = %%-input-source-%% + +### param: Page.dragAndDrop.target = %%-input-target-%% + +### option: Page.dragAndDrop.force = %%-input-force-%% + +### option: Page.dragAndDrop.noWaitAfter = %%-input-no-wait-after-%% + +### option: Page.dragAndDrop.timeout = %%-input-timeout-%% + +### option: Page.dragAndDrop.trial = %%-input-trial-%% + ## async method: Page.emulateMedia This method changes the `CSS media type` through the `media` argument, and/or the `'prefers-colors-scheme'` media feature, using the `colorScheme` argument. diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 131efabe4692f..f7fd21e5b138f 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -44,7 +44,17 @@ Whether to bypass the [actionability](./actionability.md) checks. Defaults to `f ## input-selector - `selector` <[string]> -A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See +A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See +[working with selectors](./selectors.md) for more details. + +## input-source +- `source` <[string]> +A selector to search for an element to drag. If there are multiple elements satisfying the selector, the first will be used. See +[working with selectors](./selectors.md) for more details. + +## input-target +- `target` <[string]> +A selector to search for an element to drop onto. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. ## input-position diff --git a/src/client/frame.ts b/src/client/frame.ts index 8886961d0a954..ce722c7696704 100644 --- a/src/client/frame.ts +++ b/src/client/frame.ts @@ -298,6 +298,12 @@ export class Frame extends ChannelOwner { + return await channel.dragAndDrop({ source, target, ...options }); + }); + } + async tap(selector: string, options: channels.FrameTapOptions = {}) { return this._wrapApiCall(async (channel: channels.FrameChannel) => { return await channel.tap({ selector, ...options }); diff --git a/src/client/page.ts b/src/client/page.ts index 76024265f71ba..d49c294bac06b 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -505,6 +505,10 @@ export class Page extends ChannelOwner { + return await this._frame.dragAndDrop(metadata, params.source, params.target, params); + } + async tap(params: channels.FrameTapParams, metadata: CallMetadata): Promise { return await this._frame.tap(metadata, params.selector, params); } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 0fe8296ca5355..0957519591d9d 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -1310,6 +1310,7 @@ export interface FrameChannel extends Channel { check(params: FrameCheckParams, metadata?: Metadata): Promise; click(params: FrameClickParams, metadata?: Metadata): Promise; content(params?: FrameContentParams, metadata?: Metadata): Promise; + dragAndDrop(params: FrameDragAndDropParams, metadata?: Metadata): Promise; dblclick(params: FrameDblclickParams, metadata?: Metadata): Promise; dispatchEvent(params: FrameDispatchEventParams, metadata?: Metadata): Promise; evaluateExpression(params: FrameEvaluateExpressionParams, metadata?: Metadata): Promise; @@ -1448,6 +1449,21 @@ export type FrameContentOptions = {}; export type FrameContentResult = { value: string, }; +export type FrameDragAndDropParams = { + source: string, + target: string, + force?: boolean, + noWaitAfter?: boolean, + timeout?: number, + trial?: boolean, +}; +export type FrameDragAndDropOptions = { + force?: boolean, + noWaitAfter?: boolean, + timeout?: number, + trial?: boolean, +}; +export type FrameDragAndDropResult = void; export type FrameDblclickParams = { selector: string, force?: boolean, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 812b97e8eee75..06f0f3ca0e545 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -1177,6 +1177,15 @@ Frame: returns: value: string + dragAndDrop: + parameters: + source: string + target: string + force: boolean? + noWaitAfter: boolean? + timeout: number? + trial: boolean? + dblclick: parameters: selector: string diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index a4355a13f0eec..a36c08a7252fe 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -587,6 +587,14 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { trial: tOptional(tBoolean), }); scheme.FrameContentParams = tOptional(tObject({})); + scheme.FrameDragAndDropParams = tObject({ + source: tString, + target: tString, + force: tOptional(tBoolean), + noWaitAfter: tOptional(tBoolean), + timeout: tOptional(tNumber), + trial: tOptional(tBoolean), + }); scheme.FrameDblclickParams = tObject({ selector: tString, force: tOptional(tBoolean), diff --git a/src/server/frames.ts b/src/server/frames.ts index 3c514828bfef4..08502310a3379 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -982,6 +982,30 @@ export class Frame extends SdkObject { }, this._page._timeoutSettings.timeout(options)); } + async dragAndDrop(metadata: CallMetadata, source: string, target: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + const controller = new ProgressController(metadata, this); + await controller.run(async progress => { + await dom.assertDone(await this._retryWithProgressIfNotConnected(progress, source, async handle => { + return handle._retryPointerAction(progress, 'move and down', false, async point => { + await this._page.mouse.move(point.x, point.y); + await this._page.mouse.down(); + }, { + ...options, + timeout: progress.timeUntilDeadline(), + }); + })); + await dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, async handle => { + return handle._retryPointerAction(progress, 'move and up', false, async point => { + await this._page.mouse.move(point.x, point.y); + await this._page.mouse.up(); + }, { + ...options, + timeout: progress.timeUntilDeadline(), + }); + })); + }, this._page._timeoutSettings.timeout(options)); + } + async tap(metadata: CallMetadata, selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { const controller = new ProgressController(metadata, this); return controller.run(async progress => { diff --git a/tests/page/page-drag.spec.ts b/tests/page/page-drag.spec.ts index d8b70bd5363dc..0f13e3d4a255d 100644 --- a/tests/page/page-drag.spec.ts +++ b/tests/page/page-drag.spec.ts @@ -228,6 +228,12 @@ it.describe('Drag and drop', () => { expect(await page.$eval('#target', target => target.contains(document.querySelector('#source')))).toBe(true); // could not find source in target }); + it('should work with the helper method', async ({page, server}) => { + await page.goto(server.PREFIX + '/drag-n-drop.html'); + await page.dragAndDrop('#source', '#target'); + expect(await page.$eval('#target', target => target.contains(document.querySelector('#source')))).toBe(true); // could not find source in target + }); + async function trackEvents(target: ElementHandle) { const eventsHandle = await target.evaluateHandle(target => { const events: string[] = []; diff --git a/types/types.d.ts b/types/types.d.ts index b560ed5ee4666..428ca1c6b24f8 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1125,7 +1125,7 @@ export interface Page { * zero timeout disables this. * * Shortcut for main frame's [frame.check(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-check). - * @param selector A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. * @param options */ check(selector: string, options?: { @@ -1180,7 +1180,7 @@ export interface Page { * zero timeout disables this. * * Shortcut for main frame's [frame.click(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-click). - * @param selector A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. * @param options */ click(selector: string, options?: { @@ -1295,7 +1295,7 @@ export interface Page { * * Shortcut for main frame's * [frame.dblclick(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-dblclick). - * @param selector A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. * @param options */ dblclick(selector: string, options?: { @@ -1381,7 +1381,7 @@ export interface Page { * await page.dispatchEvent('#source', 'dragstart', { dataTransfer }); * ``` * - * @param selector A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. * @param type DOM event type: `"click"`, `"dragstart"`, etc. * @param eventInit Optional event-specific initialization properties. * @param options @@ -1396,6 +1396,39 @@ export interface Page { timeout?: number; }): Promise; + /** + * @param source + * @param target + * @param options + */ + dragAndDrop(source: string, target: string, options?: { + /** + * Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`. + */ + force?: boolean; + + /** + * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can + * opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to + * inaccessible pages. Defaults to `false`. + */ + noWaitAfter?: boolean; + + /** + * 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)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number; + + /** + * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults to + * `false`. Useful to wait until the element is ready for the action without performing it. + */ + trial?: boolean; + }): Promise; + /** * This method changes the `CSS media type` through the `media` argument, and/or the `'prefers-colors-scheme'` media * feature, using the `colorScheme` argument. @@ -1508,7 +1541,7 @@ export interface Page { * * Shortcut for main frame's * [frame.fill(selector, value[, options])](https://playwright.dev/docs/api/class-frame#frame-fill). - * @param selector A selector to search for element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. + * @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details. * @param value Value to fill for the ``, `