-
-
Notifications
You must be signed in to change notification settings - Fork 25
feat(navigation): add swipe tool for horizontal and vertical swiping #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
SrinivasanTarget
merged 1 commit into
appium:main
from
Mochxd:feat/navigation-add-swipe-tool
Dec 1, 2025
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,357 @@ | ||
| import { z } from 'zod'; | ||
| import { getDriver, getPlatformName } from '../../session-store.js'; | ||
| import log from '../../logger.js'; | ||
| import { elementUUIDScheme } from '../../schema.js'; | ||
|
|
||
| function calculateSwipeCoordinates( | ||
| direction: 'left' | 'right' | 'up' | 'down', | ||
| width: number, | ||
| height: number | ||
| ): { startX: number; startY: number; endX: number; endY: number } { | ||
| const centerX = Math.floor(width / 2); | ||
| const centerY = Math.floor(height / 2); | ||
|
|
||
| switch (direction) { | ||
| case 'left': | ||
| return { | ||
| startX: Math.floor(width * 0.8), | ||
| startY: centerY, | ||
| endX: Math.floor(width * 0.2), | ||
| endY: centerY, | ||
| }; | ||
| case 'right': | ||
| return { | ||
| startX: Math.floor(width * 0.2), | ||
| startY: centerY, | ||
| endX: Math.floor(width * 0.8), | ||
| endY: centerY, | ||
| }; | ||
| case 'up': | ||
| return { | ||
| startX: centerX, | ||
| startY: Math.floor(height * 0.8), | ||
| endX: centerX, | ||
| endY: Math.floor(height * 0.2), | ||
| }; | ||
| case 'down': | ||
| return { | ||
| startX: centerX, | ||
| startY: Math.floor(height * 0.2), | ||
| endX: centerX, | ||
| endY: Math.floor(height * 0.8), | ||
| }; | ||
| default: | ||
| throw new Error(`Invalid direction: ${direction}`); | ||
| } | ||
| } | ||
|
|
||
| async function performAndroidSwipe( | ||
| driver: any, | ||
| startX: number, | ||
| startY: number, | ||
| endX: number, | ||
| endY: number, | ||
| duration: number | ||
| ): Promise<void> { | ||
| await driver.performActions([ | ||
| { | ||
| type: 'pointer', | ||
| id: 'finger1', | ||
| parameters: { pointerType: 'touch' }, | ||
| actions: [ | ||
| { type: 'pointerMove', duration: 0, x: startX, y: startY }, | ||
| { type: 'pointerDown', button: 0 }, | ||
| { type: 'pause', duration: 250 }, | ||
| { type: 'pointerMove', duration: duration, x: endX, y: endY }, | ||
| { type: 'pointerUp', button: 0 }, | ||
| ], | ||
| }, | ||
| ]); | ||
| } | ||
|
|
||
| async function performiOSSwipe( | ||
| driver: any, | ||
| startX: number, | ||
| startY: number, | ||
| endX: number, | ||
| endY: number, | ||
| duration: number | ||
| ): Promise<void> { | ||
| try { | ||
| await driver.execute('mobile: dragFromToForDuration', { | ||
| fromX: startX, | ||
| fromY: startY, | ||
| toX: endX, | ||
| toY: endY, | ||
| duration: duration / 1000, | ||
| }); | ||
| log.info('iOS swipe completed using mobile: dragFromToForDuration'); | ||
| } catch (dragError) { | ||
| log.info('mobile: dragFromToForDuration failed, trying performActions'); | ||
| await driver.performActions([ | ||
| { | ||
| type: 'pointer', | ||
| id: 'finger1', | ||
| parameters: { pointerType: 'touch' }, | ||
| actions: [ | ||
| { type: 'pointerMove', duration: 0, x: startX, y: startY }, | ||
| { type: 'pointerDown', button: 0 }, | ||
| { type: 'pause', duration: 200 }, | ||
| { type: 'pointerMove', duration: duration, x: endX, y: endY }, | ||
| { type: 'pause', duration: 50 }, | ||
| { type: 'pointerUp', button: 0 }, | ||
| ], | ||
| }, | ||
| ]); | ||
| log.info('iOS swipe completed using performActions'); | ||
| } | ||
| } | ||
|
|
||
| export default function swipe(server: any): void { | ||
| server.addTool({ | ||
| name: 'appium_swipe', | ||
| description: `Swipe on the current screen in a specified direction or between custom coordinates. | ||
| Supports four directions: left, right, up, down. | ||
| Can also perform custom coordinate-based swipes for precise control. | ||
| This is useful for navigating carousels, switching tabs, dismissing elements, or navigating between screens.`, | ||
| parameters: z.object({ | ||
| direction: z | ||
| .enum(['left', 'right', 'up', 'down']) | ||
| .optional() | ||
| .describe( | ||
| 'Direction to swipe. If provided, coordinates will be calculated automatically based on screen size or, when elementUUID is set, relative to that element. Either direction OR custom coordinates must be provided.' | ||
| ), | ||
| elementUUID: elementUUIDScheme | ||
| .optional() | ||
| .describe( | ||
| 'Optional element to base the swipe on. When provided with direction, the swipe is calculated relative to this element instead of the whole screen.' | ||
| ), | ||
| startX: z | ||
| .number() | ||
| .int() | ||
| .min(0) | ||
| .optional() | ||
| .describe( | ||
| 'Starting X coordinate for custom swipe. Required if direction is not provided.' | ||
| ), | ||
| startY: z | ||
| .number() | ||
| .int() | ||
| .min(0) | ||
| .optional() | ||
| .describe( | ||
| 'Starting Y coordinate for custom swipe. Required if direction is not provided.' | ||
| ), | ||
| endX: z | ||
| .number() | ||
| .int() | ||
| .min(0) | ||
| .optional() | ||
| .describe( | ||
| 'Ending X coordinate for custom swipe. Required if direction is not provided.' | ||
| ), | ||
| endY: z | ||
| .number() | ||
| .int() | ||
| .min(0) | ||
| .optional() | ||
| .describe( | ||
| 'Ending Y coordinate for custom swipe. Required if direction is not provided.' | ||
| ), | ||
| duration: z | ||
| .number() | ||
| .int() | ||
| .min(0) | ||
| .max(5000) | ||
| .default(600) | ||
| .optional() | ||
| .describe( | ||
| 'Duration of the swipe gesture in milliseconds. Default is 600ms. Higher values create slower swipes.' | ||
| ), | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: false, | ||
| openWorldHint: false, | ||
| }, | ||
| execute: async (args: any, context: any): Promise<any> => { | ||
| const driver = getDriver(); | ||
| if (!driver) { | ||
| throw new Error( | ||
| 'No active driver session. Please create a session first.' | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| const platform = getPlatformName(driver); | ||
| let startX: number, startY: number, endX: number, endY: number; | ||
|
|
||
| if (args.direction) { | ||
| if (args.elementUUID) { | ||
| const rect = await (driver as any).getElementRect(args.elementUUID); | ||
| const elementCenterX = Math.floor(rect.x + rect.width / 2); | ||
| const elementCenterY = Math.floor(rect.y + rect.height / 2); | ||
|
|
||
| switch (args.direction) { | ||
| case 'left': | ||
| startX = Math.floor(rect.x + rect.width * 0.8); | ||
| startY = elementCenterY; | ||
| endX = Math.floor(rect.x + rect.width * 0.2); | ||
| endY = elementCenterY; | ||
| break; | ||
| case 'right': | ||
| startX = Math.floor(rect.x + rect.width * 0.2); | ||
| startY = elementCenterY; | ||
| endX = Math.floor(rect.x + rect.width * 0.8); | ||
| endY = elementCenterY; | ||
| break; | ||
| case 'up': | ||
| startX = elementCenterX; | ||
| startY = Math.floor(rect.y + rect.height * 0.8); | ||
| endX = elementCenterX; | ||
| endY = Math.floor(rect.y + rect.height * 0.2); | ||
| break; | ||
| case 'down': | ||
| startX = elementCenterX; | ||
| startY = Math.floor(rect.y + rect.height * 0.2); | ||
| endX = elementCenterX; | ||
| endY = Math.floor(rect.y + rect.height * 0.8); | ||
| break; | ||
| default: | ||
| throw new Error(`Invalid direction: ${args.direction}`); | ||
| } | ||
| log.info('Calculated element-based swipe coordinates:', { | ||
| elementUUID: args.elementUUID, | ||
| startX, | ||
| startY, | ||
| endX, | ||
| endY, | ||
| }); | ||
| } else { | ||
| const { width, height } = await driver.getWindowSize(); | ||
| log.info('Device screen size:', { width, height }); | ||
| const coords = calculateSwipeCoordinates( | ||
| args.direction, | ||
| width, | ||
| height | ||
| ); | ||
| startX = coords.startX; | ||
| startY = coords.startY; | ||
| endX = coords.endX; | ||
| endY = coords.endY; | ||
| } | ||
| } else if ( | ||
| args.startX !== undefined && | ||
| args.startY !== undefined && | ||
| args.endX !== undefined && | ||
| args.endY !== undefined | ||
| ) { | ||
| startX = args.startX; | ||
| startY = args.startY; | ||
| endX = args.endX; | ||
| endY = args.endY; | ||
| } else { | ||
| throw new Error( | ||
| 'Either direction or all custom coordinates (startX, startY, endX, endY) must be provided.' | ||
| ); | ||
| } | ||
|
|
||
| const duration = args.duration || 600; | ||
|
|
||
| log.info('Swipe coordinates:', { | ||
| startX, | ||
| startY, | ||
| endX, | ||
| endY, | ||
| duration, | ||
| }); | ||
|
|
||
| if (platform === 'Android') { | ||
| if (startX !== endX && Math.abs(startY - endY) < 50) { | ||
| const swipeDuration = Math.min(duration, 400); | ||
| await driver.performActions([ | ||
| { | ||
| type: 'pointer', | ||
| id: 'finger1', | ||
| parameters: { pointerType: 'touch' }, | ||
| actions: [ | ||
| { type: 'pointerMove', duration: 0, x: startX, y: startY }, | ||
| { type: 'pointerDown', button: 0 }, | ||
| { type: 'pause', duration: 200 }, | ||
| { | ||
| type: 'pointerMove', | ||
| duration: swipeDuration, | ||
| x: endX, | ||
| y: endY, | ||
| }, | ||
| { type: 'pause', duration: 50 }, | ||
| { type: 'pointerUp', button: 0 }, | ||
| ], | ||
| }, | ||
| ]); | ||
| log.info('Android horizontal swipe completed'); | ||
| } else { | ||
| await performAndroidSwipe( | ||
| driver, | ||
| startX, | ||
| startY, | ||
| endX, | ||
| endY, | ||
| duration | ||
| ); | ||
| } | ||
| log.info('Android swipe action completed successfully.'); | ||
| } else if (platform === 'iOS') { | ||
| if (args.direction) { | ||
| try { | ||
| await driver.execute('mobile: swipe', { | ||
| direction: args.direction, | ||
| }); | ||
| log.info( | ||
| `iOS swipe completed using mobile: swipe (${args.direction})` | ||
| ); | ||
| } catch (swipeError) { | ||
| log.info('mobile: swipe failed, trying dragFromToForDuration'); | ||
| await performiOSSwipe( | ||
| driver, | ||
| startX, | ||
| startY, | ||
| endX, | ||
| endY, | ||
| duration | ||
| ); | ||
| } | ||
| } else { | ||
| await performiOSSwipe(driver, startX, startY, endX, endY, duration); | ||
| } | ||
| log.info('iOS swipe action completed successfully.'); | ||
| } else { | ||
| throw new Error( | ||
| `Unsupported platform: ${platform}. Only Android and iOS are supported.` | ||
| ); | ||
| } | ||
|
|
||
| const directionText = args.direction | ||
| ? ` ${args.direction}` | ||
| : ` from (${startX}, ${startY}) to (${endX}, ${endY})`; | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: `Swiped${directionText} successfully.`, | ||
| }, | ||
| ], | ||
| }; | ||
| } catch (err: any) { | ||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: `Failed to perform swipe. Error: ${err.toString()}`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| }, | ||
| }); | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Mochxd firstly thanks for contribution. I would prefer to have swipe tool based on element rather than only on the basis of screen size. Can you enhance to support both ways?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SrinivasanTarget Thank you for the feedback and the kind words, I’ve updated the implementation to support element based swipes in addition to screen based swipes, had to do a little git time travel with a force-push to keep the history clean. Promise I used my powers responsibly. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SrinivasanTarget hello Any update for the review?