mobile-wdio-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +54 -0
- package/bin/cli.mjs +120 -0
- package/lib/create.mjs +128 -0
- package/lib/doctor-cli.mjs +38 -0
- package/lib/doctor.mjs +324 -0
- package/package.json +44 -0
- package/template/.cursor/mcp.json +13 -0
- package/template/.cursor/rules/wdio-mcp-mobile.mdc +52 -0
- package/template/.env.example +33 -0
- package/template/LICENSE +15 -0
- package/template/README.md +158 -0
- package/template/THIRD_PARTY.md +39 -0
- package/template/apps/.gitkeep +1 -0
- package/template/configs/wdio.cloud.android.conf.ts +31 -0
- package/template/configs/wdio.cloud.ios.conf.ts +31 -0
- package/template/configs/wdio.local.android.conf.ts +23 -0
- package/template/configs/wdio.local.ios.conf.ts +23 -0
- package/template/configs/wdio.shared.ts +36 -0
- package/template/package.json +61 -0
- package/template/patches/@wdio+mcp+3.2.2.patch +87 -0
- package/template/scripts/android-env.sh +102 -0
- package/template/scripts/doctor-cli.mjs +38 -0
- package/template/scripts/doctor-runner.mjs +7 -0
- package/template/scripts/download-demo-android.mjs +47 -0
- package/template/scripts/ensure-appium.mjs +71 -0
- package/template/scripts/mcp-with-appium.sh +30 -0
- package/template/scripts/mobile-wdio-doctor-core.mjs +324 -0
- package/template/scripts/ping-appium.mjs +24 -0
- package/template/scripts/run-android-local.sh +11 -0
- package/template/scripts/run-appium-local.sh +11 -0
- package/template/scripts/run-mcp-android-smoke.sh +47 -0
- package/template/src/env/buildEnv.test.ts +126 -0
- package/template/src/env/buildEnv.ts +81 -0
- package/template/src/env.ts +13 -0
- package/template/src/lib/safeFilePart.test.ts +17 -0
- package/template/src/lib/safeFilePart.ts +4 -0
- package/template/src/locators/locators.test.ts +35 -0
- package/template/src/locators/login.locators.ts +18 -0
- package/template/src/locators/nativeAlert.locators.ts +13 -0
- package/template/src/locators/tabBar.locators.ts +12 -0
- package/template/src/pages/Login.page.test.ts +73 -0
- package/template/src/pages/Login.page.ts +37 -0
- package/template/src/pages/NativeAlert.page.test.ts +91 -0
- package/template/src/pages/NativeAlert.page.ts +35 -0
- package/template/src/pages/TabBar.page.test.ts +35 -0
- package/template/src/pages/TabBar.page.ts +19 -0
- package/template/src/specs/app.login.spec.ts +20 -0
- package/template/src/test-utils/wdioTestGlobals.ts +82 -0
- package/template/tsconfig.json +22 -0
- package/template/vitest.config.ts +25 -0
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mobile-wdio-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a production-ready WebdriverIO + Appium mobile framework with environment checks (doctor).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"author": "Manoj Kumar Salugu",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Assign4/mobile-agentic-wdio-poc.git",
|
|
11
|
+
"directory": "packages/mobile-wdio-kit"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Assign4/mobile-agentic-wdio-poc/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/Assign4/mobile-agentic-wdio-poc#readme",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"webdriverio",
|
|
19
|
+
"appium",
|
|
20
|
+
"mobile",
|
|
21
|
+
"testing",
|
|
22
|
+
"android",
|
|
23
|
+
"ios",
|
|
24
|
+
"cursor",
|
|
25
|
+
"mcp"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"mobile-wdio-kit": "bin/cli.mjs",
|
|
32
|
+
"mwk": "bin/cli.mjs"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin",
|
|
36
|
+
"lib",
|
|
37
|
+
"template",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"prepack": "node ./scripts/verify-template.mjs"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"wdio-mcp": {
|
|
4
|
+
"command": "sh",
|
|
5
|
+
"args": ["scripts/mcp-with-appium.sh"],
|
|
6
|
+
"cwd": "${workspaceFolder}",
|
|
7
|
+
"env": {
|
|
8
|
+
"ANDROID_HOME": "${env:HOME}/Library/Android/sdk",
|
|
9
|
+
"ANDROID_SDK_ROOT": "${env:HOME}/Library/Android/sdk"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Local Android/iOS automation with wdio-mcp — session start, natural phrasing, demo auto-login
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wdio-mcp (local mobile)
|
|
7
|
+
|
|
8
|
+
The MCP server exposes tools with **fixed names** (for example `start_session`). You cannot rename the tool in Cursor; treat the user’s chat as **natural language** that you map to those tools.
|
|
9
|
+
|
|
10
|
+
This repo **patches `@wdio/mcp`** (`patches/@wdio+mcp+*.patch`, applied on `npm install`): for the **WebdriverIO demo APK** (`android.wdio.native.app` in `appPath`) and **iOS demo** (`ios-demo` in `appPath`), **`start_session` alone** completes login using `MOBILE_USERNAME` / `MOBILE_PASSWORD` (loaded via `scripts/mcp-with-appium.sh` sourcing `.env`). The session response text mentions demo auto-login when it succeeded.
|
|
11
|
+
|
|
12
|
+
## Map user intent → `start_session`
|
|
13
|
+
|
|
14
|
+
Call **`start_session`** before any other wdio-mcp automation tool (`click_element`, `tap_element`, `get_elements`, etc.). There is no active driver until `start_session` succeeds.
|
|
15
|
+
|
|
16
|
+
When the user says any of the following (or similar), they mean **start a mobile session** — i.e. call **`start_session`** with the right platform and **absolute `appPath`**:
|
|
17
|
+
|
|
18
|
+
- “Start a session”, “start session”, “open a session”
|
|
19
|
+
- “Launch the app”, “open the app”, “start the app”, “run the app”
|
|
20
|
+
- “Open Android / the APK / the emulator app”
|
|
21
|
+
- “Launch iOS”, “open the **.app**”, “start on simulator”, “run the iOS build”
|
|
22
|
+
|
|
23
|
+
If they do not specify platform, prefer **Android** when the conversation is about this repo’s default demo APK; use **iOS** when they mention simulator, `.app`, or iOS explicitly.
|
|
24
|
+
|
|
25
|
+
### Android (`platform: "android"`)
|
|
26
|
+
|
|
27
|
+
- `automationName`: `"UiAutomator2"`
|
|
28
|
+
- **`appPath`**: absolute path from repo root — read `ANDROID_APP_PATH` from `.env` and `resolve(projectRoot, ANDROID_APP_PATH)`, or default `apps/android.wdio.native.app.v2.0.0.apk`
|
|
29
|
+
- **`deviceName`**: match `adb devices` (e.g. `emulator-5554`) or `ANDROID_DEVICE_NAME` from `.env`
|
|
30
|
+
- **`appiumConfig`**: `{ "host": "<APPIUM_HOST>", "port": <APPIUM_PORT>, "path": "/" }` (defaults `127.0.0.1`, `4723`)
|
|
31
|
+
- `autoGrantPermissions`: `true` when launching the demo app (matches project smoke script)
|
|
32
|
+
|
|
33
|
+
### iOS (`platform: "ios"`)
|
|
34
|
+
|
|
35
|
+
- `automationName`: `"XCUITest"`
|
|
36
|
+
- **`appPath`**: absolute path to the **`.app` bundle** — read `IOS_APP_PATH` from `.env` (e.g. `./apps/ios-demo.app`) and `resolve(projectRoot, IOS_APP_PATH)`. The user saying “launch the app” on iOS means this bundle path, not an APK.
|
|
37
|
+
- **`deviceName`**: `IOS_DEVICE_NAME` from `.env` or the simulator name the user specifies
|
|
38
|
+
- **`appiumConfig`**: same host/port/path pattern as Android when using local Appium
|
|
39
|
+
|
|
40
|
+
## After `start_session` on the demo app
|
|
41
|
+
|
|
42
|
+
**Do not** run a separate login tool sequence unless the user opts out of built-in auto-login or the response says auto-login failed.
|
|
43
|
+
|
|
44
|
+
**Opt-out** (launch only, stay logged out): set `WDIO_MCP_DEMO_AUTO_LOGIN=0` (or `false`) in `.cursor/mcp.json` under `wdio-mcp` → `env`, reload the window / restart MCP, then `start_session` again. Or use a non-demo `appPath` (no built-in login). If the user says “only launch” or “don’t log in”, tell them to use that env flag (or a different app); do not assume you must drive the login UI yourself after a successful demo `start_session`.
|
|
45
|
+
|
|
46
|
+
For **non-demo** apps, there is no built-in login; drive the UI with `click_element` / `set_value` as needed.
|
|
47
|
+
|
|
48
|
+
Use `get_elements` when you need to see the current hierarchy.
|
|
49
|
+
|
|
50
|
+
## Teardown
|
|
51
|
+
|
|
52
|
+
Call **`close_session`** when the user is done or asks to tear down.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
ANDROID_APP_PATH=./apps/android.wdio.native.app.v2.0.0.apk
|
|
2
|
+
ANDROID_DEVICE_NAME="Android Emulator"
|
|
3
|
+
ANDROID_UDID=
|
|
4
|
+
|
|
5
|
+
IOS_APP_PATH=./apps/ios-demo.app
|
|
6
|
+
IOS_DEVICE_NAME="iPhone 15"
|
|
7
|
+
IOS_BUNDLE_ID=com.example.demo
|
|
8
|
+
|
|
9
|
+
APPIUM_HOST=127.0.0.1
|
|
10
|
+
APPIUM_PORT=4723
|
|
11
|
+
|
|
12
|
+
MOBILE_USERNAME=test@webdriver.io
|
|
13
|
+
MOBILE_PASSWORD=Test1234!
|
|
14
|
+
|
|
15
|
+
# Patched wdio-mcp (see patches/): set to 0 or false to skip built-in demo-app login after start_session.
|
|
16
|
+
# WDIO_MCP_DEMO_AUTO_LOGIN=0
|
|
17
|
+
|
|
18
|
+
ARTIFACTS_DIR=./artifacts
|
|
19
|
+
|
|
20
|
+
# LambdaTest-style cloud (optional)
|
|
21
|
+
CLOUD_USERNAME=
|
|
22
|
+
CLOUD_ACCESS_KEY=
|
|
23
|
+
CLOUD_HOSTNAME=mobile-hub.lambdatest.com
|
|
24
|
+
CLOUD_PATH=/wd/hub
|
|
25
|
+
BUILD_NAME=wdio-mobile
|
|
26
|
+
|
|
27
|
+
ANDROID_CLOUD_DEVICE="Galaxy S24"
|
|
28
|
+
ANDROID_CLOUD_PLATFORM_VERSION=14
|
|
29
|
+
ANDROID_CLOUD_APP=
|
|
30
|
+
|
|
31
|
+
IOS_CLOUD_DEVICE="iPhone 15"
|
|
32
|
+
IOS_CLOUD_PLATFORM_VERSION=17
|
|
33
|
+
IOS_CLOUD_APP=
|
package/template/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Mobile WDIO (minimal)
|
|
2
|
+
|
|
3
|
+
WebdriverIO **Mocha** specs for native **Android / iOS**, plus official **`@wdio/mcp`** so Cursor can drive the app from chat. No Cucumber and no custom MCP server—configs, page objects, locators, env, Vitest unit tests, and a small **`patch-package`** patch on `@wdio/mcp` so `start_session` can complete demo login in one step.
|
|
4
|
+
|
|
5
|
+
**Open source:** [LICENSE](./LICENSE) (ISC). **Trademarks, demo APK, npm deps, patches:** read [THIRD_PARTY.md](./THIRD_PARTY.md) before you publish or redistribute. **Publishing the CLI:** [RELEASING.md](./RELEASING.md).
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
configs/ wdio.local.*.conf.ts, wdio.cloud.*.conf.ts, wdio.shared.ts
|
|
11
|
+
src/env.ts dotenv-backed settings
|
|
12
|
+
src/specs/ *.spec.ts (Mocha)
|
|
13
|
+
src/pages/ small screen helpers
|
|
14
|
+
src/locators/ Android / iOS selectors
|
|
15
|
+
scripts/ android-env.sh, ensure-appium.mjs, mcp-with-appium.sh (Cursor MCP entry), ping-appium.mjs, run-mcp-android-smoke.sh
|
|
16
|
+
patches/ patch-package diff for @wdio/mcp (demo auto-login after start_session)
|
|
17
|
+
.cursor/rules/ wdio-mcp-mobile.mdc (agent: session + demo auto-login, Android/iOS app paths)
|
|
18
|
+
.cursor/mcp.json → `sh scripts/mcp-with-appium.sh` (do **not** use `npm run mcp:server` as the MCP command: npm prints to stdout and breaks JSON-RPC on stdio)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
1. Copy `.env.example` → `.env`.
|
|
24
|
+
2. Put apps under `apps/` or set `ANDROID_APP_PATH` / `IOS_APP_PATH`.
|
|
25
|
+
3. Install drivers: `npm run appium:driver:android` (and iOS if needed).
|
|
26
|
+
|
|
27
|
+
## Scaffold & environment check (`mobile-wdio-kit`)
|
|
28
|
+
|
|
29
|
+
The **`mobile-wdio-kit`** package (`packages/mobile-wdio-kit`) copies an embedded **template** of this repo, then runs **`npm install`**, **`patch-package`**, copies **`.env.example` → `.env`**, and downloads the **WebdriverIO demo Android APK** (APKs are not shipped inside the npm tarball).
|
|
30
|
+
|
|
31
|
+
**Where projects are created:** `create <path>` resolves the directory with Node’s `path.resolve()` from your **shell’s current working directory**—not from the global npm install location. So `mobile-wdio-kit create ~/Desktop/foo` creates on the Desktop regardless of whether you ran the command from `~/Projects` or elsewhere (and `~/...` works even inside quotes thanks to CLI tilde expansion).
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx mobile-wdio-kit@latest create ./my-mobile-tests
|
|
35
|
+
cd my-mobile-tests
|
|
36
|
+
npm run doctor
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g mobile-wdio-kit # after publish
|
|
41
|
+
mobile-wdio-kit create ./my-mobile-tests
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
From a **git clone** of this repo (path must include `mobile-agentic-wdio-poc`):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd mobile-agentic-wdio-poc
|
|
48
|
+
npm install -g ./packages/mobile-wdio-kit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Doctor** checks Node, Android SDK / `adb`, demo APK, `.env`, Appium drivers, Xcode/simctl (macOS), and Appium `/status`. Here: `npm run doctor` or `npm run setup:demo-android` (APK only). Generated apps use a **vendored** doctor so `npm install` does not require this package on the registry.
|
|
52
|
+
|
|
53
|
+
Maintainers: `npm run kit:sync`, then publish—see [RELEASING.md](./RELEASING.md) and [packages/mobile-wdio-kit/README.md](./packages/mobile-wdio-kit/README.md).
|
|
54
|
+
|
|
55
|
+
## CLI tests
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm run test:android # ensures emulator if none online, then WDIO + Appium service
|
|
59
|
+
npm run test:ios # local Appium; you provide simulator/device
|
|
60
|
+
npm run test:cloud:android
|
|
61
|
+
npm run test:cloud:ios
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Demo credentials: `test@webdriver.io` / `Test1234!` (override with `MOBILE_USERNAME` / `MOBILE_PASSWORD`).
|
|
65
|
+
|
|
66
|
+
## Cursor / MCP (prompt-driven testing)
|
|
67
|
+
|
|
68
|
+
With the default `.cursor/mcp.json`, **Cursor starts the MCP server via `scripts/mcp-with-appium.sh`**. That script starts **Appium in the background** if nothing is listening on `APPIUM_HOST` / `APPIUM_PORT` (logs under `artifacts/appium-mcp.log`), then runs `wdio-mcp`. If no device is online, it **starts an AVD in the background** but does not wait for a cold boot (Cursor’s MCP client times out if the launcher blocks for minutes). Prefer `adb devices` showing `device` before you enable the MCP server, or wait for the emulator to finish booting before calling `start_session`.
|
|
69
|
+
|
|
70
|
+
**Do not** set the MCP command to `npm run mcp:server`: npm echoes script lines to stdout, which corrupts the MCP protocol and produces errors like `Unexpected token '>'` / `is not valid JSON`.
|
|
71
|
+
|
|
72
|
+
To run Appium yourself (e.g. debugging), use `npm run appium:start` or `npx appium --address 127.0.0.1 --port 4723` as before.
|
|
73
|
+
|
|
74
|
+
### 1. Emulator or device (when not using Cursor MCP)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
adb devices # should list one device/emulator as "device"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The MCP launcher runs the same check and can boot an AVD if none is online (see `scripts/android-env.sh`).
|
|
81
|
+
|
|
82
|
+
### 2. Confirm Appium is ready (optional)
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm run mcp:ping-appium
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
You should see `Appium ready at http://127.0.0.1:4723/status`.
|
|
89
|
+
|
|
90
|
+
### 3. Cursor
|
|
91
|
+
|
|
92
|
+
1. Open **this repo** as the workspace root (so `.cursor/mcp.json` `cwd` resolves correctly; if you use a multi-root workspace, set `cwd` in `.cursor/mcp.json` to the full path of this project).
|
|
93
|
+
2. **Cursor Settings → MCP**: ensure **wdio-mcp** is enabled (toggle on). Reload the window after changing MCP config so the new launcher runs.
|
|
94
|
+
3. Start an **Agent** chat with MCP allowed. The workspace rule **`.cursor/rules/wdio-mcp-mobile.mdc`** explains behavior. For this repo’s **WebdriverIO demo** Android APK / iOS `.app`, a **patched** `wdio-mcp` runs the login flow **inside** `start_session` (credentials from `.env`: `MOBILE_USERNAME` / `MOBILE_PASSWORD`; the launcher script sources `.env`). One prompt like “start session” is enough to be **logged in and past the success dialog**, ready to explore. To **skip** that behavior, set `WDIO_MCP_DEMO_AUTO_LOGIN=0` under `env` in `.cursor/mcp.json` and reload.
|
|
95
|
+
|
|
96
|
+
**Short example prompts**
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
Start an Android session (use .env for app path, device, Appium).
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
Launch the demo app on the emulator — I want to explore after login.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
Start an iOS session with the .app under apps/ (resolve IOS_APP_PATH from .env), then close_session when done.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Explicit prompt (Android demo app)**
|
|
111
|
+
|
|
112
|
+
Use this when you want every capability spelled out. Replace `<ABSOLUTE_PATH_TO_REPO>` with the real path (e.g. from `pwd` in the project root). Match `deviceName` to `adb devices` (often `emulator-5554`).
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
Use wdio-mcp tools only. Call start_session for Android with:
|
|
116
|
+
- appPath: <ABSOLUTE_PATH_TO_REPO>/apps/android.wdio.native.app.v2.0.0.apk
|
|
117
|
+
- deviceName: emulator-5554
|
|
118
|
+
- automationName: UiAutomator2
|
|
119
|
+
- autoGrantPermissions: true
|
|
120
|
+
- appiumConfig: host 127.0.0.1, port 4723, path /
|
|
121
|
+
|
|
122
|
+
Demo login runs inside start_session; use get_elements to explore. close_session when done.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Explicit prompt (iOS simulator, `.app` bundle)**
|
|
126
|
+
|
|
127
|
+
Point `appPath` at the **directory** ending in `.app` (from `IOS_APP_PATH`, resolved to an absolute path). Set `platform: ios`, `automationName: XCUITest`, and `deviceName` to your simulator (e.g. from `IOS_DEVICE_NAME` in `.env`).
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
Use wdio-mcp only. start_session: platform ios, automationName XCUITest, appPath <ABSOLUTE_PATH_TO_REPO>/apps/ios-demo.app, deviceName iPhone 15, appiumConfig host 127.0.0.1 port 4723 path /. Demo login runs inside start_session; close_session when done.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 4. Test the MCP stack without Cursor
|
|
134
|
+
|
|
135
|
+
This starts Appium, runs the official MCP client over stdio, drives the same login flow, then stops Appium:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm run test:mcp:android
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Manual server (debugging)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm run mcp:server:with-appium # same as Cursor: emulator + Appium + MCP
|
|
145
|
+
npm run mcp:server # MCP only; Appium must already be running
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Use these if you need to run the MCP server outside Cursor’s managed process.
|
|
149
|
+
|
|
150
|
+
## Validate (no device)
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm run validate
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Failure artifacts
|
|
157
|
+
|
|
158
|
+
On a failed spec, WDIO saves `artifacts/screenshots/<test-title>.png` (see `configs/wdio.shared.ts`).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Third-party software, trademarks, and demo assets
|
|
2
|
+
|
|
3
|
+
This document is for **attribution and transparency**. It is **not legal advice**. If you ship a product or publish a package, have your own counsel review licenses and trademark use.
|
|
4
|
+
|
|
5
|
+
## Trademarks
|
|
6
|
+
|
|
7
|
+
- **WebdriverIO**, **Appium**, and related marks belong to their respective owners (e.g. OpenJS Foundation / project communities).
|
|
8
|
+
- **Android** is a trademark of Google LLC.
|
|
9
|
+
- **Apple**, **iOS**, **Xcode**, and **Simulator** are trademarks of Apple Inc.
|
|
10
|
+
- **Cursor** is a trademark of Anysphere, Inc.
|
|
11
|
+
- **LambdaTest** is a trademark of LambdaTest Inc.
|
|
12
|
+
|
|
13
|
+
This project is **not affiliated with, endorsed by, or sponsored by** those organizations unless explicitly stated. Use their marks only in ways that comply with their brand guidelines.
|
|
14
|
+
|
|
15
|
+
## Demo Android application (APK)
|
|
16
|
+
|
|
17
|
+
The optional setup script downloads the **WebdriverIO native demo app** binary from the upstream project (default URL is defined in `scripts/download-demo-android.mjs`). That app and its branding are subject to **their** license and terms—not this repo’s `LICENSE` file.
|
|
18
|
+
|
|
19
|
+
- Source / releases: [webdriverio/native-demo-app](https://github.com/webdriverio/native-demo-app)
|
|
20
|
+
|
|
21
|
+
Verify the license in that repository before redistributing the APK or using it in commercial contexts.
|
|
22
|
+
|
|
23
|
+
## npm dependencies
|
|
24
|
+
|
|
25
|
+
Runtime and development dependencies (WebdriverIO, Appium, drivers, Vitest, `patch-package`, etc.) are each licensed under **their own** terms (MIT, Apache-2.0, and others). After `npm install`, see each package’s `LICENSE` or `package.json` `license` field, or run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx license-checker --summary
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
(Install `license-checker` globally or use another SBOM tool you prefer.)
|
|
32
|
+
|
|
33
|
+
## Patch applied to `@wdio/mcp`
|
|
34
|
+
|
|
35
|
+
This repo includes a **`patch-package`** patch under `patches/` that modifies `@wdio/mcp` in `node_modules` after install. The **original `@wdio/mcp` code** remains under its upstream license; the patch is a derivative change applied on your machine at install time. Keep the patch file and this notice if you redistribute a project that relies on it.
|
|
36
|
+
|
|
37
|
+
## `mobile-wdio-kit` npm package
|
|
38
|
+
|
|
39
|
+
The `packages/mobile-wdio-kit` package **bundles a template** copied from this repository (configs, scripts, tests, etc.) and **vendored doctor scripts**. Publishing that tarball does not transfer ownership of third-party code; attribution and upstream licenses still apply as above.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Capabilities, Options } from "@wdio/types";
|
|
2
|
+
import { env, requireCloudCredentials } from "../src/env.ts";
|
|
3
|
+
import { sharedMobileConfig } from "./wdio.shared.ts";
|
|
4
|
+
|
|
5
|
+
const { user, key } = requireCloudCredentials();
|
|
6
|
+
|
|
7
|
+
export const config: Options.Testrunner &
|
|
8
|
+
Capabilities.WithRequestedTestrunnerCapabilities = {
|
|
9
|
+
...sharedMobileConfig,
|
|
10
|
+
user,
|
|
11
|
+
key,
|
|
12
|
+
protocol: "https",
|
|
13
|
+
hostname: env.cloudHostname,
|
|
14
|
+
port: 443,
|
|
15
|
+
path: env.cloudPath,
|
|
16
|
+
services: [],
|
|
17
|
+
capabilities: [
|
|
18
|
+
{
|
|
19
|
+
platformName: "Android",
|
|
20
|
+
"appium:automationName": "UiAutomator2",
|
|
21
|
+
"appium:deviceName": env.android.cloudDevice,
|
|
22
|
+
"appium:platformVersion": env.android.cloudPlatformVersion,
|
|
23
|
+
"appium:app": env.android.cloudApp,
|
|
24
|
+
"lt:options": {
|
|
25
|
+
build: `${env.buildName} — Android`,
|
|
26
|
+
name: "Mobile login",
|
|
27
|
+
isRealMobile: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Capabilities, Options } from "@wdio/types";
|
|
2
|
+
import { env, requireCloudCredentials } from "../src/env.ts";
|
|
3
|
+
import { sharedMobileConfig } from "./wdio.shared.ts";
|
|
4
|
+
|
|
5
|
+
const { user, key } = requireCloudCredentials();
|
|
6
|
+
|
|
7
|
+
export const config: Options.Testrunner &
|
|
8
|
+
Capabilities.WithRequestedTestrunnerCapabilities = {
|
|
9
|
+
...sharedMobileConfig,
|
|
10
|
+
user,
|
|
11
|
+
key,
|
|
12
|
+
protocol: "https",
|
|
13
|
+
hostname: env.cloudHostname,
|
|
14
|
+
port: 443,
|
|
15
|
+
path: env.cloudPath,
|
|
16
|
+
services: [],
|
|
17
|
+
capabilities: [
|
|
18
|
+
{
|
|
19
|
+
platformName: "iOS",
|
|
20
|
+
"appium:automationName": "XCUITest",
|
|
21
|
+
"appium:deviceName": env.ios.cloudDevice,
|
|
22
|
+
"appium:platformVersion": env.ios.cloudPlatformVersion,
|
|
23
|
+
"appium:app": env.ios.cloudApp,
|
|
24
|
+
"lt:options": {
|
|
25
|
+
build: `${env.buildName} — iOS`,
|
|
26
|
+
name: "Mobile login",
|
|
27
|
+
isRealMobile: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Capabilities, Options } from "@wdio/types";
|
|
2
|
+
import { env } from "../src/env.ts";
|
|
3
|
+
import { sharedMobileConfig } from "./wdio.shared.ts";
|
|
4
|
+
|
|
5
|
+
export const config: Options.Testrunner &
|
|
6
|
+
Capabilities.WithRequestedTestrunnerCapabilities = {
|
|
7
|
+
...sharedMobileConfig,
|
|
8
|
+
hostname: env.appiumHost,
|
|
9
|
+
port: env.appiumPort,
|
|
10
|
+
path: "/",
|
|
11
|
+
services: ["appium"],
|
|
12
|
+
capabilities: [
|
|
13
|
+
{
|
|
14
|
+
platformName: "Android",
|
|
15
|
+
"appium:automationName": "UiAutomator2",
|
|
16
|
+
"appium:deviceName": env.android.deviceName,
|
|
17
|
+
...(env.android.udid ? { "appium:udid": env.android.udid } : {}),
|
|
18
|
+
"appium:app": env.android.appPath,
|
|
19
|
+
"appium:autoGrantPermissions": true,
|
|
20
|
+
"appium:noReset": false,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Capabilities, Options } from "@wdio/types";
|
|
2
|
+
import { env } from "../src/env.ts";
|
|
3
|
+
import { sharedMobileConfig } from "./wdio.shared.ts";
|
|
4
|
+
|
|
5
|
+
export const config: Options.Testrunner &
|
|
6
|
+
Capabilities.WithRequestedTestrunnerCapabilities = {
|
|
7
|
+
...sharedMobileConfig,
|
|
8
|
+
hostname: env.appiumHost,
|
|
9
|
+
port: env.appiumPort,
|
|
10
|
+
path: "/",
|
|
11
|
+
services: ["appium"],
|
|
12
|
+
capabilities: [
|
|
13
|
+
{
|
|
14
|
+
platformName: "iOS",
|
|
15
|
+
"appium:automationName": "XCUITest",
|
|
16
|
+
"appium:deviceName": env.ios.deviceName,
|
|
17
|
+
"appium:app": env.ios.appPath,
|
|
18
|
+
"appium:bundleId": env.ios.bundleId,
|
|
19
|
+
"appium:autoAcceptAlerts": true,
|
|
20
|
+
"appium:noReset": false,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { Options } from "@wdio/types";
|
|
5
|
+
import { env } from "../src/env.ts";
|
|
6
|
+
import { safeFilePart } from "../src/lib/safeFilePart.ts";
|
|
7
|
+
|
|
8
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const specGlob = join(root, "src/specs/**/*.spec.ts");
|
|
10
|
+
|
|
11
|
+
/** Shared WDIO options for all mobile configs (Mocha + plain specs). */
|
|
12
|
+
export const sharedMobileConfig: Partial<Options.Testrunner> = {
|
|
13
|
+
runner: "local",
|
|
14
|
+
specs: [specGlob],
|
|
15
|
+
maxInstances: 1,
|
|
16
|
+
logLevel: "info",
|
|
17
|
+
waitforTimeout: 15_000,
|
|
18
|
+
connectionRetryTimeout: 120_000,
|
|
19
|
+
connectionRetryCount: 1,
|
|
20
|
+
framework: "mocha",
|
|
21
|
+
mochaOpts: { timeout: 90_000 },
|
|
22
|
+
reporters: ["spec"],
|
|
23
|
+
before: () => {
|
|
24
|
+
mkdirSync(join(env.artifactsDir, "screenshots"), { recursive: true });
|
|
25
|
+
},
|
|
26
|
+
afterTest: async (test, _ctx, { passed }) => {
|
|
27
|
+
if (passed) return;
|
|
28
|
+
const name = safeFilePart(test.title || "failed");
|
|
29
|
+
const path = join(env.artifactsDir, "screenshots", `${name}.png`);
|
|
30
|
+
try {
|
|
31
|
+
await browser.saveScreenshot(path);
|
|
32
|
+
} catch {
|
|
33
|
+
// Session or UiAutomator2 may already be dead (e.g. adb device offline).
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mobile-agentic-wdio-poc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "WebdriverIO + Appium mobile tests, Cursor MCP, Vitest (scaffolded via mobile-wdio-kit).",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"postinstall": "patch-package",
|
|
9
|
+
"test:android": "sh scripts/run-android-local.sh",
|
|
10
|
+
"test:ios": "wdio run configs/wdio.local.ios.conf.ts",
|
|
11
|
+
"test:cloud:android": "wdio run configs/wdio.cloud.android.conf.ts",
|
|
12
|
+
"test:cloud:ios": "wdio run configs/wdio.cloud.ios.conf.ts",
|
|
13
|
+
"test:mcp:android": "sh scripts/run-mcp-android-smoke.sh",
|
|
14
|
+
"mcp:server": "wdio-mcp",
|
|
15
|
+
"mcp:server:with-appium": "MCP_BLOCK_UNTIL_EMULATOR_READY=1 sh scripts/mcp-with-appium.sh",
|
|
16
|
+
"mcp:ping-appium": "node scripts/ping-appium.mjs",
|
|
17
|
+
"appium:start": "sh scripts/run-appium-local.sh",
|
|
18
|
+
"appium:driver:android": "appium driver install --source=npm appium-uiautomator2-driver@4.2.9",
|
|
19
|
+
"appium:driver:ios": "appium driver install xcuitest",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test:unit": "vitest run",
|
|
22
|
+
"test:unit:watch": "vitest",
|
|
23
|
+
"test:unit:coverage": "vitest run --coverage",
|
|
24
|
+
"validate": "npm run typecheck && npm run test:unit:coverage",
|
|
25
|
+
"setup:demo-android": "node scripts/download-demo-android.mjs",
|
|
26
|
+
"doctor": "node scripts/doctor-runner.mjs --cwd ."
|
|
27
|
+
},
|
|
28
|
+
"keywords": [],
|
|
29
|
+
"author": "",
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/Assign4/mobile-agentic-wdio-poc.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/Assign4/mobile-agentic-wdio-poc/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/Assign4/mobile-agentic-wdio-poc#readme",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
41
|
+
"@types/mocha": "^10.0.10",
|
|
42
|
+
"@types/node": "^25.5.2",
|
|
43
|
+
"@wdio/appium-service": "^9.27.0",
|
|
44
|
+
"@wdio/cli": "^9.27.0",
|
|
45
|
+
"@wdio/local-runner": "^9.27.0",
|
|
46
|
+
"@wdio/mcp": "^3.2.2",
|
|
47
|
+
"@wdio/mocha-framework": "^9.27.0",
|
|
48
|
+
"@wdio/spec-reporter": "^9.27.0",
|
|
49
|
+
"appium": "^2.19.0",
|
|
50
|
+
"appium-uiautomator2-driver": "^4.2.9",
|
|
51
|
+
"dotenv": "^17.4.0",
|
|
52
|
+
"expect-webdriverio": "^5.6.5",
|
|
53
|
+
"node-addon-api": "^8.7.0",
|
|
54
|
+
"node-gyp": "^12.2.0",
|
|
55
|
+
"patch-package": "^8.0.1",
|
|
56
|
+
"typescript": "^6.0.2",
|
|
57
|
+
"vitest": "^3.2.4",
|
|
58
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
59
|
+
"webdriverio": "^9.27.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
diff --git a/node_modules/@wdio/mcp/lib/server.js b/node_modules/@wdio/mcp/lib/server.js
|
|
2
|
+
index 718f1b4..e9e2a00 100755
|
|
3
|
+
--- a/node_modules/@wdio/mcp/lib/server.js
|
|
4
|
+
+++ b/node_modules/@wdio/mcp/lib/server.js
|
|
5
|
+
@@ -3607,6 +3607,65 @@ Note: Unable to set window size (${windowWidth}x${windowHeight}). ${e}`;
|
|
6
|
+
}]
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
+async function wdioMcpAppendDemoAutoLoginNote(browser, platform2, appPath) {
|
|
10
|
+
+ if (process.env.WDIO_MCP_DEMO_AUTO_LOGIN === "0" || process.env.WDIO_MCP_DEMO_AUTO_LOGIN === "false") {
|
|
11
|
+
+ return "";
|
|
12
|
+
+ }
|
|
13
|
+
+ if (!appPath || typeof appPath !== "string") {
|
|
14
|
+
+ return "";
|
|
15
|
+
+ }
|
|
16
|
+
+ const isAndroidDemo = platform2 === "android" && /android\.wdio\.native\.app/i.test(appPath);
|
|
17
|
+
+ const isIosDemo = platform2 === "ios" && /ios-demo/i.test(appPath);
|
|
18
|
+
+ if (!isAndroidDemo && !isIosDemo) {
|
|
19
|
+
+ return "";
|
|
20
|
+
+ }
|
|
21
|
+
+ const waitMs = 25e3;
|
|
22
|
+
+ const user = process.env.MOBILE_USERNAME || "test@webdriver.io";
|
|
23
|
+
+ const pass = process.env.MOBILE_PASSWORD || "Test1234!";
|
|
24
|
+
+ const clickSel = async (selector) => {
|
|
25
|
+
+ await browser.waitUntil(browser.$(selector).isExisting, { timeout: waitMs });
|
|
26
|
+
+ await browser.$(selector).scrollIntoView({ block: "center", inline: "center" });
|
|
27
|
+
+ await browser.$(selector).click();
|
|
28
|
+
+ };
|
|
29
|
+
+ const setSel = async (selector, value) => {
|
|
30
|
+
+ await browser.waitUntil(browser.$(selector).isExisting, { timeout: waitMs });
|
|
31
|
+
+ await browser.$(selector).scrollIntoView({ block: "center", inline: "center" });
|
|
32
|
+
+ await browser.$(selector).clearValue();
|
|
33
|
+
+ await browser.$(selector).setValue(value);
|
|
34
|
+
+ };
|
|
35
|
+
+ try {
|
|
36
|
+
+ await clickSel("~Login");
|
|
37
|
+
+ await clickSel("~button-login-container");
|
|
38
|
+
+ await setSel("~input-email", user);
|
|
39
|
+
+ await setSel("~input-password", pass);
|
|
40
|
+
+ try {
|
|
41
|
+
+ await clickSel("~Login-screen");
|
|
42
|
+
+ } catch {
|
|
43
|
+
+ }
|
|
44
|
+
+ await clickSel("~button-LOGIN");
|
|
45
|
+
+ const deadline = Date.now() + 2e4;
|
|
46
|
+
+ let src = "";
|
|
47
|
+
+ while (Date.now() < deadline) {
|
|
48
|
+
+ src = await browser.getPageSource();
|
|
49
|
+
+ if (/success/i.test(src)) {
|
|
50
|
+
+ break;
|
|
51
|
+
+ }
|
|
52
|
+
+ await new Promise((r) => setTimeout(r, 400));
|
|
53
|
+
+ }
|
|
54
|
+
+ if (!/success/i.test(src)) {
|
|
55
|
+
+ throw new Error("success state not found after tapping LOGIN");
|
|
56
|
+
+ }
|
|
57
|
+
+ if (isAndroidDemo) {
|
|
58
|
+
+ await clickSel('//android.widget.Button[@text="OK"]');
|
|
59
|
+
+ } else {
|
|
60
|
+
+ await clickSel("~OK");
|
|
61
|
+
+ }
|
|
62
|
+
+ return "\n\nDemo auto-login: finished (credentials from MOBILE_USERNAME / MOBILE_PASSWORD). Ready for exploration.";
|
|
63
|
+
+ } catch (e) {
|
|
64
|
+
+ return `
|
|
65
|
+
+Demo auto-login failed: ${e}. Session is still active; complete login manually if needed.`;
|
|
66
|
+
+ }
|
|
67
|
+
+}
|
|
68
|
+
async function startMobileSession(args) {
|
|
69
|
+
const { platform: platform2, appPath, app, deviceName, noReset } = args;
|
|
70
|
+
if (!appPath && !app && noReset !== true) {
|
|
71
|
+
@@ -3640,6 +3699,7 @@ async function startMobileSession(args) {
|
|
72
|
+
appiumConfig: { hostname: serverConfig.hostname, port: serverConfig.port, path: serverConfig.path },
|
|
73
|
+
steps: []
|
|
74
|
+
});
|
|
75
|
+
+ const demoLoginNote = await wdioMcpAppendDemoAutoLoginNote(browser, platform2, appPath);
|
|
76
|
+
const appInfo = appPath ? `
|
|
77
|
+
App: ${appPath}` : "\nApp: (connected to running app)";
|
|
78
|
+
const detachNote = shouldAutoDetach ? "\n\n(Auto-detach enabled: session will be preserved on close. Use close_session({ detach: false }) to force terminate.)" : "";
|
|
79
|
+
@@ -3649,7 +3709,7 @@ App: ${appPath}` : "\nApp: (connected to running app)";
|
|
80
|
+
type: "text",
|
|
81
|
+
text: `${platform2} app session started with sessionId: ${sessionId}
|
|
82
|
+
Device: ${deviceName}${appInfo}
|
|
83
|
+
-Appium Server: ${serverConfig.hostname}:${serverConfig.port}${serverConfig.path}${detachNote}`
|
|
84
|
+
+Appium Server: ${serverConfig.hostname}:${serverConfig.port}${serverConfig.path}${detachNote}${demoLoginNote}`
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
};
|