Skip to content

Commit

Permalink
feat: add longpress logic (#13888)
Browse files Browse the repository at this point in the history
* feat: add longpress logic

- add docs
- add ut's

* fixes based on feedback
  • Loading branch information
wswebcreation authored Nov 20, 2024
1 parent 0812795 commit be4fea7
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 11 deletions.
44 changes: 36 additions & 8 deletions packages/webdriverio/src/commands/element/click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const log = logger('webdriver')
* give added capabilities like passing button type, coordinates etc. By default, when using options a release action
* command is send after performing the click action, pass `option.skipRelease=true` to skip this action.
*
* Note: If you have fixed-position elements (such as a fixed header or footer) that cover up the
* :::info
*
* If you have fixed-position elements (such as a fixed header or footer) that cover up the
* selected element after it is scrolled within the viewport, the click will be issued at the given coordinates, but will
* be received by your fixed (overlaying) element. In these cased the following error is thrown:
*
Expand All @@ -26,6 +28,15 @@ const log = logger('webdriver')
* the click. You also can try to scroll to the element yourself using `scroll` with an offset appropriate for your
* scenario.
*
* :::
*
* :::info
*
* The click command can also be used to simulate a long press on a mobile device. This is done by setting the `duration`.
* See the example below for more information.
*
* :::
*
* <example>
:example.html
<button id="myButton" onclick="document.getElementById('someText').innerHTML='I was clicked'">Click me</button>
Expand Down Expand Up @@ -75,14 +86,25 @@ const log = logger('webdriver')
})
* </example>
*
* <example>
:longpress.example.js
it('should be able to open the contacts menu on iOS by executing a longPress', async () => {
const contacts = await $('~Contacts')
// opens the Contacts menu on iOS where you can quickly create
// a new contact, edit your home screen, or remove the app
await contacts.click({ duration: 2000 })
})
* </example>
*
* @alias element.click
* @uses protocol/element, protocol/elementIdClick, protocol/performActions, protocol/positionClick
* @type action
* @param {ClickOptions=} options click options (optional)
* @param {string= | number=} options.button can be one of [0, "left", 1, "middle", 2, "right"] (optional)
* @param {number=} options.x Number (optional)
* @param {number=} options.y Number (optional)
* @param {boolean=} options.skipRelease Boolean (optional)
* @param {ClickOptions=} options Click options (optional)
* @param {string= | number=} options.button Can be one of `[0, "left", 1, "middle", 2, "right"]` <br /><strong>WEB-ONLY</strong> (Desktop/Mobile)
* @param {number=} options.x Clicks X horizontal pixels away from location of the element (from center point of element)<br /><strong>WEB and Native</strong> (Desktop/Mobile)
* @param {number=} options.y Clicks Y vertical pixels away from location of the element (from center point of element)<br /><strong>WEB and Native support</strong> (Desktop/Mobile)
* @param {boolean=} options.skipRelease Boolean (optional) <br /><strong>WEB-ONLY</strong> (Desktop/Mobile)
* @param {number=} options.duration Duration of the click, aka "LongPress" <br /><strong>MOBILE-NATIVE-APP-ONLY</strong> (Mobile)
*/
export function click(
this: WebdriverIO.Element,
Expand Down Expand Up @@ -111,6 +133,10 @@ async function elementClick(element: WebdriverIO.Element) {
try {
return await element.elementClick(element.elementId)
} catch (error) {
// We will never reach this code in the case of a mobile app, the implicitWait will wait for the element but will throw
// an error with the following message:
// `Error: Can't call click on element with selector "<selector>" because element wasn't found at implicitWait`
// This means that he workaround is not reachable in the case of a mobile app and thus will not automatically scroll the element into view
let err = error as Error
if (typeof error === 'string') {
err = new Error(error)
Expand All @@ -131,9 +157,10 @@ async function actionClick(element: WebdriverIO.Element, options: Partial<ClickO
x: 0,
y: 0,
skipRelease: false,
duration: 0
}

const { button, x, y, skipRelease }: ClickOptions = { ...defaultOptions, ...options }
const { button, x, y, skipRelease, duration }: ClickOptions = { ...defaultOptions, ...options }

if (
typeof x !== 'number'
Expand All @@ -160,10 +187,11 @@ async function actionClick(element: WebdriverIO.Element, options: Partial<ClickO
}
const clickNested = async () => {
await browser.action('pointer', {
parameters: { pointerType: 'mouse' }
parameters: { pointerType: browser.isMobile ? 'touch' : 'mouse' }
})
.move({ origin: element, x, y })
.down({ button })
.pause(duration)
.up({ button })
.perform(skipRelease)
}
Expand Down
3 changes: 2 additions & 1 deletion packages/webdriverio/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,8 @@ export type ClickOptions = {
button: Button | ButtonNames,
x: number,
y: number,
skipRelease:boolean
skipRelease: boolean
duration: number
}

export type WaitForOptions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ exports[`click test > should allow to left click on an element by passing a butt
"button": 0,
"type": "pointerDown",
},
{
"duration": 0,
"type": "pause",
},
{
"button": 0,
"type": "pointerUp",
Expand Down Expand Up @@ -47,6 +51,10 @@ exports[`click test > should allow to left click on an element by passing a butt
"button": 0,
"type": "pointerDown",
},
{
"duration": 0,
"type": "pause",
},
{
"button": 0,
"type": "pointerUp",
Expand Down Expand Up @@ -78,6 +86,10 @@ exports[`click test > should allow to left click on an element with an offset wi
"button": 0,
"type": "pointerDown",
},
{
"duration": 0,
"type": "pause",
},
{
"button": 0,
"type": "pointerUp",
Expand Down Expand Up @@ -109,6 +121,10 @@ exports[`click test > should allow to middle click on an element 1`] = `
"button": 1,
"type": "pointerDown",
},
{
"duration": 0,
"type": "pause",
},
{
"button": 1,
"type": "pointerUp",
Expand Down Expand Up @@ -139,6 +155,10 @@ exports[`click test > should allow to middle click on an element 2`] = `
"button": 1,
"type": "pointerDown",
},
{
"duration": 0,
"type": "pause",
},
{
"button": 1,
"type": "pointerUp",
Expand Down Expand Up @@ -169,6 +189,10 @@ exports[`click test > should allow to right click on an element 1`] = `
"button": 2,
"type": "pointerDown",
},
{
"duration": 0,
"type": "pause",
},
{
"button": 2,
"type": "pointerUp",
Expand Down Expand Up @@ -199,6 +223,10 @@ exports[`click test > should allow to right click on an element 2`] = `
"button": 2,
"type": "pointerDown",
},
{
"duration": 0,
"type": "pause",
},
{
"button": 2,
"type": "pointerUp",
Expand All @@ -218,3 +246,37 @@ exports[`click test > should allow to right click on an element with an offset a
"type": "pointerDown",
}
`;

exports[`click test > should to send a duration to execute a longPress on a mobile device 1`] = `
{
"actions": [
{
"button": 0,
"duration": 100,
"origin": {
"element-6066-11e4-a52e-4f735466cecf": "some-elem-123",
},
"type": "pointerMove",
"x": 0,
"y": 0,
},
{
"button": 0,
"type": "pointerDown",
},
{
"duration": 1000,
"type": "pause",
},
{
"button": 0,
"type": "pointerUp",
},
],
"id": "action9",
"parameters": {
"pointerType": "touch",
},
"type": "pointer",
}
`;
26 changes: 24 additions & 2 deletions packages/webdriverio/tests/commands/element/click.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,33 @@ describe('click test', () => {
.toBe(20)
expect(JSON.parse(vi.mocked(fetch).mock.calls[3][1]?.body as any).actions[0].actions[0].y)
.toBe(15)
expect(JSON.parse(vi.mocked(fetch).mock.calls[3][1]?.body as any).actions[0].actions[2])
.toStrictEqual({ type: 'pause', duration: 0 })
expect(JSON.parse(vi.mocked(fetch).mock.calls[3][1]?.body as any).actions[0].actions[1])
.toMatchSnapshot()
expect(JSON.parse(vi.mocked(fetch).mock.calls[3][1]?.body as any).actions[0].actions[2])
expect(JSON.parse(vi.mocked(fetch).mock.calls[3][1]?.body as any).actions[0].actions[3])
.toStrictEqual({ type: 'pointerUp', button: 2 })
})

it('should to send a duration to execute a longPress on a mobile device', async () => {
const browser = await remote({
baseUrl: 'http://foobar.com',
capabilities: {
browserName: 'foobar',
mobileMode: true,
} as any
})
const elem = await browser.$('#foo')
await elem.click({ duration: 1000 })
// @ts-expect-error mock implementation
expect(vi.mocked(fetch).mock.calls[3][0].pathname)
.toBe('/session/foobar-123/actions')
expect(JSON.parse(vi.mocked(fetch).mock.calls[3][1]?.body as any).actions[0])
.toMatchSnapshot()
expect(JSON.parse(vi.mocked(fetch).mock.calls[3][1]?.body as any).actions[0].actions[2])
.toStrictEqual({ type: 'pause', duration: 1000 })
})

it('should throw an error if the passed argument is not an options object', async () => {
const browser = await remote({
baseUrl: 'http://foobar.com',
Expand Down Expand Up @@ -169,14 +190,15 @@ describe('click test', () => {
})
const elem = await browser.$('#foo')

// @ts-expect-error invalid param
expect(elem.click({ button: 'not-supported' })).rejects.toThrow('Button type not supported.')
})

it('should not call releaseAction when skipRelease is true', async () => {
const browser = await remote({
baseUrl: 'http://foobar.com',
capabilities: {
browserName: 'foobar'
browserName: 'foobar',
}
})
const elem = await browser.$('#foo')
Expand Down

0 comments on commit be4fea7

Please sign in to comment.