├── .azure-template
└── bootstrap.yml
├── .eslintignore
├── .eslintrc.js
├── .github
├── dependabot.yml
└── workflows
│ └── npm-publish.yml
├── .gitignore
├── .npmignore
├── .prettierrc.json
├── README.md
├── assets
└── GesturesPlugin.jpg
├── azure-pipelines.yml
├── package-lock.json
├── package.json
├── src
├── element.js
├── gestures
│ ├── doubleTap.js
│ ├── dragAndDrop.js
│ ├── longPress.js
│ └── swipe.js
├── index.js
├── logger.js
├── plugin.js
└── sessionInfo.js
├── test
└── e2e
│ └── android.spec.js
└── tsconfig.json
/.azure-template/bootstrap.yml:
--------------------------------------------------------------------------------
1 | steps:
2 | - task: NodeTool@0
3 | inputs:
4 | versionSpec: '$(NODE_VERSION)'
5 | - script: |
6 | npm config delete prefix
7 | npm config set prefix $NVM_DIR/versions/node/`node --version`
8 | node --version
9 | npm install -g appium
10 | npm install -g wait-on
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules/*
2 | /lib/*
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'eslint:recommended',
4 | 'plugin:@typescript-eslint/eslint-recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'prettier',
7 | ],
8 | env: {
9 | node: true,
10 | es6: true,
11 | mocha: true,
12 | },
13 | parser: '@typescript-eslint/parser',
14 | parserOptions: {
15 | ecmaVersion: 2016,
16 | sourceType: 'module',
17 | babelOptions: {
18 | plugins: ['@babel/plugin-proposal-class-properties'],
19 | },
20 | },
21 | plugins: ['prettier', '@typescript-eslint'],
22 | rules: {
23 | 'prettier/prettier': ['error', { singleQuote: true }],
24 | quotes: ['error', 'single'],
25 | '@typescript-eslint/no-explicit-any': 'off',
26 | '@typescript-eslint/no-empty-function': 'off',
27 | },
28 | settings: {
29 | 'import/resolver': {
30 | node: {
31 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
32 | },
33 | },
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | branches:
5 | - main
6 | tags:
7 | - '*.*.*'
8 | pull_request:
9 | types:
10 | - labeled
11 |
12 | jobs:
13 | release:
14 | if: github.event.action != 'labeled'
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Post bumpr status comment
19 | uses: haya14busa/action-bumpr@v1
20 |
21 | # Get tag name.
22 | - id: tag
23 | uses: haya14busa/action-cond@v1
24 | with:
25 | cond: "${{ startsWith(github.ref, 'refs/tags/') }}"
26 | if_true: ${{ github.ref }}
27 | if_false: ${{ steps.bumpr.outputs.next_version }}
28 |
29 | - name: Release
30 | uses: justincy/github-action-npm-release@2.0.2
31 | id: release
32 | - uses: actions/setup-node@v3
33 | if: steps.release.outputs.released == 'true'
34 | with:
35 | registry-url: 'https://registry.npmjs.org'
36 | node-version: 16
37 | - name: Publish
38 | if: steps.release.outputs.released == 'true'
39 | run: echo "//registry.npmjs.org/:_authToken=${{secrets.npm_token}}" > .npmrc && npm ci && npm publish
40 | env:
41 | NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
42 |
43 | release-check:
44 | if: github.event.action == 'labeled'
45 | runs-on: ubuntu-latest
46 | steps:
47 | - uses: actions/checkout@v2
48 | - name: Post bumpr status comment
49 | uses: haya14busa/action-bumpr@v1
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
3 | lib/
4 | .DS_Store
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | docs/*
2 | test/*
3 | .eslintrc
4 | .vscode
5 | .github
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | # appium-gestures-plugin [](https://badge.fury.io/js/appium-gestures-plugin)
10 |
11 | This is an Appium plugin designed to perform basic gestures using W3C Actions.
12 |
13 | ## Prerequisite
14 |
15 | Appium version 2.0
16 |
17 | ## Installation - Server
18 |
19 | Install the plugin using Appium's plugin CLI, either as a named plugin or via NPM:
20 |
21 | ```shell
22 | appium plugin install --source=npm appium-gestures-plugin
23 | ```
24 |
25 | ## Activation
26 |
27 | The plugin will not be active unless turned on when invoking the Appium server:
28 |
29 | ```shell
30 | appium --use-plugins=gestures
31 | ```
32 |
33 | # Usage
34 |
35 | Sample app used to demonstrate below gesture is available [here](https://github.com/webdriverio/native-demo-app/releases)
36 |
37 | # Swipe Left
38 |
39 | ```java
40 | RemoteWebElement carousel = (RemoteWebElement) wait.until(presenceOfElementLocated(AppiumBy.accessibilityId("Carousel")));
41 |
42 | driver.executeScript("gesture: swipe", Map.of("elementId", carousel.getId(), "percentage", 50, "direction", "left"));
43 | ```
44 |
45 | # Swipe Right
46 |
47 | ```java
48 | RemoteWebElement carousel = (RemoteWebElement) wait.until(presenceOfElementLocated(AppiumBy.accessibilityId("Carousel")));
49 |
50 | driver.executeScript("gesture: swipe", Map.of("elementId", carousel.getId(), "percentage", 50, "direction", "right"));
51 | ```
52 |
53 | # Swipe Up
54 |
55 | ```java
56 | RemoteWebElement scrollView = (RemoteWebElement) wait.until(presenceOfElementLocated(AppiumBy.accessibilityId("Swipe-screen")));
57 |
58 | driver.executeScript("gesture: swipe", Map.of("elementId", scrollView.getId(),
59 | "percentage", 50,
60 | "direction", "up"));
61 | ```
62 |
63 | # Swipe Down
64 |
65 | ```java
66 | RemoteWebElement scrollView = (RemoteWebElement) wait.until(presenceOfElementLocated(AppiumBy.accessibilityId("Swipe-screen")));
67 |
68 | driver.executeScript("gesture: swipe", Map.of("elementId", scrollView.getId(),
69 | "percentage", 50,
70 | "direction", "down"));
71 | ```
72 |
73 | # scrollElementIntoView
74 |
75 | **JAVA**
76 | ```java
77 | RemoteWebElement scrollView = (RemoteWebElement) wait.until(presenceOfElementLocated(AppiumBy.accessibilityId("Swipe-screen")));
78 |
79 | driver.executeScript("gesture: scrollElementIntoView", Map.of("scrollableView", scrollView.getId(),
80 | "strategy", "accessibility id",
81 | "selector", "WebdriverIO logo",
82 | "percentage", 50,
83 | "direction", "up",
84 | "maxCount", 3));
85 |
86 | ```
87 | **PYTHON**
88 | ```python
89 | list_view = driver.find_element(by=AppiumBy.ID, value='android:id/list')
90 | driver.execute_script('gesture: scrollElementIntoView',
91 | {'scrollableView': list_view.id, 'strategy': 'accessibility id', 'selector': 'Picker',
92 | 'percentage': 50, 'direction': 'up', 'maxCount': 3})
93 | ```
94 |
95 | Sample app used to demonstrate below gesture is available [here](https://github.com/AppiumTestDistribution/appium-demo/blob/main/VodQA.apk)
96 |
97 | # Drag and Drop
98 |
99 | **JAVA**
100 | ```java
101 | RemoteWebElement source = (RemoteWebElement) wait.until(elementToBeClickable(AppiumBy.accessibilityId("dragMe")));
102 | RemoteWebElement destination = (RemoteWebElement) wait.until(elementToBeClickable(AppiumBy.accessibilityId("dropzone")));
103 |
104 | driver.executeScript("gesture: dragAndDrop", Map.of("sourceId", source.getId(), "destinationId", destination.getId()));
105 | ```
106 | **PYTHON**
107 | ```python
108 | el1 = driver.find_element(by=AppiumBy.ID, value='io.appium.android.apis:id/drag_dot_1')
109 | el2 = driver.find_element(by=AppiumBy.ID, value='io.appium.android.apis:id/drag_dot_2')
110 |
111 | driver.execute_script('gesture: dragAndDrop', {
112 | 'sourceId': el1.id,
113 | 'destinationId': el2.id,
114 | })
115 | ```
116 |
117 | # Double Tap
118 |
119 | ```java
120 | RemoteWebElement doubleTapMe = (RemoteWebElement) driver.findElement(AppiumBy.accessibilityId("doubleTapMe"));
121 |
122 | driver.executeScript("gesture: doubleTap", Map.of("elementId", doubleTapMe.getId()));
123 | ```
124 |
125 | # Long Press
126 |
127 | Pressure has to be between 0 and 1.
128 |
129 | ```java
130 | RemoteWebElement longPress = (RemoteWebElement) driver.findElement(AppiumBy.accessibilityId("longpress"));
131 |
132 | driver.executeScript("gesture: longPress", Map.of("elementId", longPress.getId(), "pressure", 0.5, "duration", 800));
133 |
134 | ```
135 |
136 | # WDIO
137 |
138 | ```js
139 | await driver.execute('gesture: dragAndDrop', { sourceId, destinationId });
140 | ```
141 |
142 | ## Supported
143 |
144 | - Swipe Left, right, up and down
145 | - scrollElementIntoView
146 | - Drag and Drop
147 | - Double Tap
148 | - Long Press
149 |
150 | ### TODO
151 |
152 | - zoom
153 | - multi finger swipe
154 |
--------------------------------------------------------------------------------
/assets/GesturesPlugin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AppiumTestDistribution/appium-gestures-plugin/ffe7b9668ce70c28e597b94ce6ca32a193f27868/assets/GesturesPlugin.jpg
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # Gradle
2 | # Build your Java project and run tests with Gradle using a Gradle wrapper script.
3 | # Add steps that analyze code, save build artifacts, deploy, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/java
5 | trigger:
6 | - main
7 |
8 | variables:
9 | ANDROID_EMU_NAME: test
10 | ANDROID_EMU_ABI: x86
11 | ANDROID_EMU_TARGET: android-28
12 | ANDROID_EMU_TAG: default
13 | XCODE_VERSION: 14.2
14 | IOS_PLATFORM_VERSION: 16.2
15 | IOS_DEVICE_NAME: iPhone 12
16 | NODE_VERSION: 18.x
17 | JDK_VERSION: 1.8
18 |
19 | jobs:
20 | - job: Android_E2E_Tests
21 | pool:
22 | vmImage: 'macOS-latest'
23 | steps:
24 | - template: .azure-template/bootstrap.yml
25 | - script: $NVM_DIR/versions/node/`node --version`/bin/appium driver install uiautomator2
26 | displayName: Install UIA2 driver
27 | - script: |
28 | echo "y" | $ANDROID_HOME/tools/bin/sdkmanager --install 'system-images;$(ANDROID_EMU_TARGET);$(ANDROID_EMU_TAG);$(ANDROID_EMU_ABI)'
29 | echo "no" | $ANDROID_HOME/tools/bin/avdmanager create avd -n "$(ANDROID_EMU_NAME)" -k 'system-images;$(ANDROID_EMU_TARGET);$(ANDROID_EMU_TAG);$(ANDROID_EMU_ABI)' --force
30 | echo $ANDROID_HOME/emulator/emulator -list-avds
31 |
32 | echo "Starting emulator"
33 | nohup $ANDROID_HOME/emulator/emulator -avd "$(ANDROID_EMU_NAME)" -no-snapshot -delay-adb > /dev/null 2>&1 &
34 | $ANDROID_HOME/platform-tools/adb wait-for-device
35 | $ANDROID_HOME/platform-tools/adb devices -l
36 | echo "Emulator started"
37 | displayName: Emulator configuration
38 | - script: |
39 | npm ci
40 | $NVM_DIR/versions/node/$(node --version)/bin/appium plugin install --source=local .
41 | nohup $NVM_DIR/versions/node/$(node --version)/bin/appium server -ka 800 --use-plugins=gestures -pa /wd/hub &
42 | $NVM_DIR/versions/node/$(node --version)/bin/wait-on http://127.0.0.1:4723/wd/hub/status
43 | PLATFORM=android npm run test-e2e
44 | displayName: Android E2E Test
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "appium-gestures-plugin",
3 | "version": "4.0.1",
4 | "description": "This is an Appium plugin designed to perform gestures using W3C Actions.",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "build": "npx tsc",
8 | "prepublish": "npx tsc",
9 | "lint": "eslint '**/*.js' --fix",
10 | "test-e2e": "mocha --require ts-node/register -p test/e2e/android.spec.js --timeout 260000",
11 | "prettier": "prettier '**/*.js' --write --single-quote",
12 | "appium-home": "rm -rf /tmp/some-temp-dir && export APPIUM_HOME=/tmp/some-temp-dir",
13 | "install-plugin": "npm run build && appium plugin install --source=local $(pwd)",
14 | "clear-cache": "rm -rf $HOME/.cache/appium-gestures-plugin",
15 | "install-driver": "export APPIUM_HOME=/tmp/some-temp-dir && appium driver install uiautomator2",
16 | "install-wait-plugin": "export APPIUM_HOME=/tmp/some-temp-dir && appium plugin install --source=npm appium-wait-plugin",
17 | "reinstall-plugin": "export APPIUM_HOME=/tmp/some-temp-dir && npm run appium-home && (appium plugin uninstall gestures || exit 0) && npm run install-plugin",
18 | "run-server": "export APPIUM_HOME=/tmp/some-temp-dir && appium server -ka 800 --use-plugins=gestures -pa /wd/hub "
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/AppiumTestDistribution/appium-gestures-plugin.git"
23 | },
24 | "contributors": [
25 | {
26 | "name": "Saikrishna",
27 | "email": "saikrishna321@yahoo.com"
28 | },
29 | {
30 | "name": "Srinivasan Sekar",
31 | "email": "srinivasan.sekar1990@gmail.com"
32 | }
33 | ],
34 | "license": "ISC",
35 | "bugs": {
36 | "url": "https://github.com/AppiumTestDistribution/appium-gestures-plugin/issues"
37 | },
38 | "homepage": "https://github.com/AppiumTestDistribution/appium-gestures-plugin#readme",
39 | "devDependencies": {
40 | "@babel/core": "^7.3.4",
41 | "@babel/plugin-proposal-class-properties": "^7.13.0",
42 | "@types/chai": "^4.2.16",
43 | "@types/lodash": "^4.14.170",
44 | "@types/mocha": "^8.2.2",
45 | "@types/node": "^15.6.1",
46 | "babel-eslint": "^10.0.1",
47 | "chai": "^4.1.0",
48 | "eslint": "^8.42.0",
49 | "eslint-config-prettier": "^8.1.0",
50 | "eslint-plugin-import": "^2.8.0",
51 | "eslint-plugin-prettier": "^3.0.1",
52 | "husky": "^6.0.0",
53 | "lint-staged": "^11.0.0",
54 | "mocha": "^10.2.0",
55 | "prettier": "^2.0.5",
56 | "sinon": "^15.1.0",
57 | "ts-node": "^10.0.0",
58 | "typescript": "^5.0.4",
59 | "webdriverio": "8.27.0"
60 | },
61 | "dependencies": {
62 | "@appium/base-plugin": "^2.2.26"
63 | },
64 | "peerDependencies": {
65 | "appium": "^2.3.0"
66 | },
67 | "appium": {
68 | "pluginName": "gestures",
69 | "mainClass": "GesturesPlugin"
70 | },
71 | "engines": {
72 | "node": "^14.17.0 || ^16.13.0 || >=18.0.0",
73 | "npm": ">=8"
74 | },
75 | "lint-staged": {
76 | "src/*.{js,jsx,ts,tsx,json,css,scss,md}": [
77 | "npm run prettier",
78 | "git add"
79 | ]
80 | },
81 | "husky": {
82 | "hooks": {
83 | "pre-commit": "lint-staged",
84 | "pre-push": "npm run test"
85 | }
86 | },
87 | "files": [
88 | "lib"
89 | ]
90 | }
91 |
--------------------------------------------------------------------------------
/src/element.js:
--------------------------------------------------------------------------------
1 | export function getCenter(value) {
2 | return {
3 | x: value.x + value.width / 2,
4 | y: value.y + value.height / 2,
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/src/gestures/doubleTap.js:
--------------------------------------------------------------------------------
1 | import * as Element from '../element';
2 |
3 | export default async function doubleTap(elementId, driver) {
4 | {
5 | const value = await driver.getElementRect(elementId);
6 | const { x, y } = await Element.getCenter(value);
7 |
8 | const androidPauseAction = {
9 | duration: 0,
10 | type: 'pause',
11 | };
12 |
13 | const actionsData = [
14 | {
15 | id: 'finger1',
16 | type: 'pointer',
17 | parameters: { pointerType: 'touch' },
18 | actions: [
19 | { duration: 0, x, y, type: 'pointerMove', origin: 'viewport' },
20 | { button: 0, type: 'pointerDown' },
21 | { duration: 200, type: 'pause' },
22 | { button: 0, type: 'pointerUp' },
23 | { duration: 40, type: 'pause' },
24 | { button: 0, type: 'pointerDown' },
25 | { duration: 200, type: 'pause' },
26 | { button: 0, type: 'pointerUp' },
27 | ],
28 | },
29 | ];
30 |
31 | if (driver.caps.automationName !== 'XCuiTest') {
32 | actionsData[0].actions.unshift(androidPauseAction);
33 | }
34 |
35 | await driver.performActions(actionsData);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/gestures/dragAndDrop.js:
--------------------------------------------------------------------------------
1 | import * as Element from '../element';
2 | import log from '../logger';
3 |
4 | export default async function dragAndDrop(sourceId, destinationId, driver) {
5 | const [source, destination] = await Promise.all([
6 | driver.getElementRect(sourceId),
7 | driver.getElementRect(destinationId),
8 | ]);
9 |
10 | const [{ x: sourceX, y: sourceY }, { x: destinationX, y: destinationY }] = await Promise.all([
11 | Element.getCenter(source),
12 | Element.getCenter(destination),
13 | ]);
14 |
15 | const androidPauseAction = {
16 | duration: 0,
17 | type: 'pause',
18 | };
19 |
20 | const actionsData = [
21 | {
22 | id: 'finger',
23 | type: 'pointer',
24 | parameters: { pointerType: 'touch' },
25 | actions: [
26 | { duration: 0, x: sourceX, y: sourceY, type: 'pointerMove', origin: 'viewport' },
27 | { button: 1, type: 'pointerDown' },
28 | { duration: 600, type: 'pause' },
29 | {
30 | duration: 600,
31 | x: destinationX,
32 | y: destinationY,
33 | type: 'pointerMove',
34 | origin: 'viewport',
35 | },
36 | { button: 1, type: 'pointerUp' },
37 | ],
38 | },
39 | ];
40 |
41 | if (driver.caps.automationName === 'XCuiTest') {
42 | await driver.performActions(actionsData);
43 | } else {
44 | log.info('Drag and Drop for android');
45 | const androidActions = actionsData;
46 | androidActions[0].actions.unshift(androidPauseAction);
47 | await driver.performActions(actionsData);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/gestures/longPress.js:
--------------------------------------------------------------------------------
1 | import * as Element from '../element';
2 | import log from '../logger';
3 |
4 | export default async function longPress(elementId, pressure, duration, driver) {
5 | const elementRect = await driver.getElementRect(elementId);
6 | log.info(
7 | `Performing a long press on element ${elementId} with pressure ${pressure}% and duration ${duration}ms`
8 | );
9 | const { x, y } = Element.getCenter(elementRect);
10 |
11 | const actionsData = {
12 | id: 'finger',
13 | type: 'pointer',
14 | parameters: { pointerType: 'touch' },
15 | actions: [
16 | { duration: 0, x, y, type: 'pointerMove', origin: 'viewport' },
17 | { button: 1, pressure, type: 'pointerDown' },
18 | { duration, type: 'pause' },
19 | { button: 1, type: 'pointerUp' },
20 | ],
21 | };
22 |
23 | if (driver.caps.automationName !== 'XCuiTest') {
24 | actionsData.actions.unshift({ duration: 0, type: 'pause' });
25 | }
26 |
27 | await driver.performActions([actionsData]);
28 | }
29 |
--------------------------------------------------------------------------------
/src/gestures/swipe.js:
--------------------------------------------------------------------------------
1 | import * as Element from '../element';
2 | import log from '../logger';
3 |
4 | const MAGIC_NUMBER = 49;
5 |
6 | export async function swipe(elementId, percentage, direction, driver) {
7 | const swipeActions = [];
8 | const value = await driver.getElementRect(elementId);
9 | log.info(`Swiping ${direction} at ${percentage}% of the element ${elementId}`);
10 | const pointer = getDirectionActions(direction, value, percentage);
11 | const actionsData = getActionsData(elementId, pointer, driver);
12 | swipeActions.push(actionsData);
13 | await driver.performActions(swipeActions);
14 | }
15 |
16 | export async function scrollElementIntoView(config) {
17 | const {
18 | scrollableView,
19 | strategy,
20 | selector,
21 | percentage,
22 | direction,
23 | maxCount = 5,
24 | driver,
25 | } = config;
26 |
27 | for (
28 | let count = 0;
29 | count < maxCount && !(await isElementFound(driver, strategy, selector));
30 | count++
31 | ) {
32 | log.info('Swiping now...');
33 | await swipe(scrollableView, percentage, direction, driver);
34 | }
35 | }
36 |
37 | function getDirectionActions(direction, value, percentage) {
38 | const { x, y } = Element.getCenter(value);
39 | const directionActions = {
40 | left: {
41 | sourceX: x + (value.width * MAGIC_NUMBER) / 100,
42 | sourceY: y,
43 | destinationX: (x + (value.width * MAGIC_NUMBER) / 100) * (percentage / 100),
44 | destinationY: y,
45 | },
46 | right: {
47 | sourceX: (x + (value.width * MAGIC_NUMBER) / 100) * (percentage / 100),
48 | sourceY: y,
49 | destinationX: x + (value.width * MAGIC_NUMBER) / 100,
50 | destinationY: y,
51 | },
52 | up: {
53 | sourceX: x,
54 | sourceY: y + (value.height * MAGIC_NUMBER) / 100,
55 | destinationX: x,
56 | destinationY: y - percentage / 100,
57 | },
58 | down: {
59 | sourceX: x,
60 | sourceY: y - percentage / 100,
61 | destinationX: x,
62 | destinationY: y + (value.height * MAGIC_NUMBER) / 100,
63 | },
64 | };
65 | return directionActions[direction];
66 | }
67 |
68 | function getActionsData(elementId, pointer, driver) {
69 | const actionsData = {
70 | id: `${elementId}`,
71 | type: 'pointer',
72 | parameters: { pointerType: 'touch' },
73 | actions: [
74 | {
75 | duration: 0,
76 | x: pointer.sourceX,
77 | y: pointer.sourceY,
78 | type: 'pointerMove',
79 | origin: 'viewport',
80 | },
81 | { button: 1, type: 'pointerDown' },
82 | { duration: 600, type: 'pause' },
83 | {
84 | duration: 600,
85 | x: pointer.destinationX,
86 | y: pointer.destinationY,
87 | type: 'pointerMove',
88 | origin: 'viewport',
89 | },
90 | { button: 1, type: 'pointerUp' },
91 | ],
92 | };
93 |
94 | if (driver.caps.automationName !== 'XCuiTest') {
95 | actionsData.actions.unshift({ duration: 0, type: 'pause' });
96 | }
97 |
98 | return actionsData;
99 | }
100 |
101 | async function isElementFound(driver, strategy, selector) {
102 | try {
103 | log.info(`Checking if ${strategy} element '${selector}' is present`);
104 | await driver.findElement(strategy, selector);
105 | log.info(`Element '${selector}' is found`);
106 | return true;
107 | } catch (e) {
108 | log.info(`Element '${selector}' is not found`);
109 | return false;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import GesturesPlugin from './plugin';
2 | export default GesturesPlugin;
3 | export { GesturesPlugin };
4 |
--------------------------------------------------------------------------------
/src/logger.js:
--------------------------------------------------------------------------------
1 | import { logger } from '@appium/support';
2 | const log = logger.getLogger('gestures');
3 | module.exports = log;
4 |
--------------------------------------------------------------------------------
/src/plugin.js:
--------------------------------------------------------------------------------
1 | import BasePlugin from '@appium/base-plugin';
2 | import dragAndDrop from './gestures/dragAndDrop';
3 | import { swipe, scrollElementIntoView } from './gestures/swipe';
4 | import doubleTap from './gestures/doubleTap';
5 | import longPress from './gestures/longPress';
6 |
7 | export default class GesturesPlugin extends BasePlugin {
8 | static executeMethodMap = {
9 | 'gesture: dragAndDrop': {
10 | command: 'dragAndDrop',
11 | params: { required: ['sourceId', 'destinationId'] },
12 | },
13 | 'gesture: swipe': {
14 | command: 'swipe',
15 | params: {
16 | required: ['elementId', 'percentage', 'direction'],
17 | },
18 | },
19 | 'gesture: scrollElementIntoView': {
20 | command: 'scrollElementIntoView',
21 | params: {
22 | required: ['scrollableView', 'strategy', 'selector', 'percentage', 'direction', 'maxCount'],
23 | },
24 | },
25 | 'gesture: doubleTap': {
26 | command: 'doubleTap',
27 | params: { required: ['elementId'] },
28 | },
29 | 'gesture: longPress': {
30 | command: 'longPress',
31 | params: { required: ['elementId', 'pressure', 'duration'] },
32 | },
33 | };
34 |
35 | constructor(pluginName) {
36 | super(pluginName);
37 | }
38 |
39 | async execute(next, driver, script, args) {
40 | return await this.executeMethod(next, driver, script, args);
41 | }
42 |
43 | async swipe(next, driver, elementId, percentage, direction) {
44 | await swipe(elementId, percentage, direction, driver);
45 | }
46 |
47 | async scrollElementIntoView(
48 | next,
49 | driver,
50 | scrollableView,
51 | strategy,
52 | selector,
53 | percentage,
54 | direction,
55 | maxCount
56 | ) {
57 | await scrollElementIntoView({
58 | scrollableView,
59 | strategy,
60 | selector,
61 | percentage,
62 | direction,
63 | maxCount,
64 | driver,
65 | });
66 | }
67 |
68 | async dragAndDrop(next, driver, sourceId, destinationId) {
69 | await dragAndDrop(sourceId, destinationId, driver);
70 | }
71 |
72 | async doubleTap(next, driver, elementId) {
73 | await doubleTap(elementId, driver);
74 | }
75 |
76 | async longPress(next, driver, elementId, pressure, duration) {
77 | await longPress(elementId, pressure, duration, driver);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/sessionInfo.js:
--------------------------------------------------------------------------------
1 | export default function sessionInfo(driver) {
2 | const automationName = driver.caps.automationName;
3 |
4 | if (automationName === 'XCuiTest') {
5 | const baseUrl = `${driver.wda.wdaBaseUrl}:${driver.wda.wdaRemotePort}`;
6 | const jwProxySessionId = driver.wda.jwproxy.sessionId;
7 | return {
8 | baseUrl,
9 | jwProxySessionId,
10 | automationName,
11 | driverUrl: `${baseUrl}/session/${jwProxySessionId}`,
12 | };
13 | } else {
14 | const baseUrl = `http://${driver.uiautomator2.host}:${driver.uiautomator2.systemPort}`;
15 | const jwProxySessionId = driver.uiautomator2.jwproxy.sessionId;
16 | return {
17 | baseUrl,
18 | jwProxySessionId,
19 | automationName,
20 | driverUrl: `${baseUrl}/session/${jwProxySessionId}`,
21 | };
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/test/e2e/android.spec.js:
--------------------------------------------------------------------------------
1 | import { remote } from 'webdriverio';
2 |
3 | const APPIUM_HOST = '127.0.0.1';
4 | const APPIUM_PORT = 4723;
5 | const WDIO_PARAMS = {
6 | connectionRetryCount: 0,
7 | hostname: APPIUM_HOST,
8 | port: APPIUM_PORT,
9 | path: '/wd/hub/',
10 | logLevel: 'info',
11 | waitforTimeout: 10000,
12 | mochaOpts: {
13 | timeout: 20000,
14 | },
15 | };
16 | const capabilities = {
17 | platformName: 'Android',
18 | 'appium:automationName': 'UIAutomator2',
19 | 'appium:deviceName': 'emulator-5555',
20 | 'appium:app':
21 | 'https://github.com/AppiumTestDistribution/appium-demo/blob/main/VodQA.apk?raw=true',
22 | };
23 | let driver;
24 | describe('Plugin Test', () => {
25 | beforeEach(async () => {
26 | driver = await remote({ ...WDIO_PARAMS, capabilities });
27 | });
28 |
29 | it('Horizontal swipe test', async () => {
30 | await driver.$('~login').click();
31 | await driver.$('~slider1').click();
32 | const slider = await driver.$('~slider');
33 | await slider.waitForDisplayed({ timeout: 10000 });
34 | await driver.executeScript('gesture: swipe', [
35 | {
36 | elementId: slider.elementId,
37 | percentage: 50,
38 | direction: 'right',
39 | },
40 | ]);
41 | });
42 |
43 | it('Drag & Drop test', async () => {
44 | await driver.$('~login').click();
45 | await driver.$('~dragAndDrop').click();
46 | const dragMe = await driver.$('~dragMe');
47 | await dragMe.waitForDisplayed({ timeout: 10000 });
48 | const dropzone = await driver.$('~dropzone');
49 | await dropzone.waitForDisplayed({ timeout: 10000 });
50 | await driver.executeScript('gesture: dragAndDrop', [
51 | {
52 | sourceId: dragMe.elementId,
53 | destinationId: dropzone.elementId,
54 | },
55 | ]);
56 | });
57 |
58 | afterEach(async () => await driver.deleteSession());
59 | });
60 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | "incremental": true /* Enable incremental compilation */,
7 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | "allowJs": true /* Allow javascript files to be compiled. */,
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "./lib" /* Redirect output structure to the directory. */,
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true /* Enable all strict type-checking options. */,
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 |
43 | /* Module Resolution Options */
44 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48 | // "typeRoots": [
49 | // "./typings",
50 | // "./node_modules/@types"
51 | // ],
52 | "types": ["node"] /* Type declaration files to be included in compilation. */,
53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
54 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
57 |
58 | /* Source Map Options */
59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
61 | "inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */,
62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
63 |
64 | /* Experimental Options */
65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
67 |
68 | /* Advanced Options */
69 | "skipLibCheck": true /* Skip type checking of declaration files. */,
70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
71 | },
72 | "include": ["./src/*", "src/index.js"],
73 | "exclude": ["lib", "node_modules", "test", "typings"]
74 | }
75 |
--------------------------------------------------------------------------------