Skip to content

Commit aaf278e

Browse files
added support to download wda.app from appium releases for simulators
Co-authored-by: SrinivasanTarget <[email protected]>
1 parent 57a70ea commit aaf278e

File tree

5 files changed

+458
-11
lines changed

5 files changed

+458
-11
lines changed

src/tests/test-setup-wda.ts

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/**
2+
* Test file for setup-wda tool
3+
* Run with: npx tsx src/tests/test-setup-wda.ts
4+
*/
5+
import path from 'path';
6+
import fs from 'fs';
7+
import os from 'os';
8+
import { createRequire } from 'module';
9+
import https from 'https';
10+
import { exec } from 'child_process';
11+
import { promisify } from 'util';
12+
13+
const execAsync = promisify(exec);
14+
15+
// Mock server object to test the tool
16+
class MockServer {
17+
private tools: Map<string, any> = new Map();
18+
19+
addTool(tool: any) {
20+
this.tools.set(tool.name, tool);
21+
console.log(`✅ Tool registered: ${tool.name}`);
22+
}
23+
24+
getTool(name: string) {
25+
return this.tools.get(name);
26+
}
27+
}
28+
29+
async function downloadFile(url: string, destPath: string): Promise<void> {
30+
return new Promise((resolve, reject) => {
31+
const file = fs.createWriteStream(destPath);
32+
33+
https
34+
.get(url, response => {
35+
// Handle redirects
36+
if (response.statusCode === 302 || response.statusCode === 301) {
37+
file.close();
38+
fs.unlinkSync(destPath);
39+
return downloadFile(response.headers.location!, destPath)
40+
.then(resolve)
41+
.catch(reject);
42+
}
43+
44+
if (response.statusCode !== 200) {
45+
file.close();
46+
fs.unlinkSync(destPath);
47+
return reject(
48+
new Error(`Failed to download: ${response.statusCode}`)
49+
);
50+
}
51+
52+
const totalSize = parseInt(
53+
response.headers['content-length'] || '0',
54+
10
55+
);
56+
let downloadedSize = 0;
57+
58+
response.on('data', chunk => {
59+
downloadedSize += chunk.length;
60+
const percent = ((downloadedSize / totalSize) * 100).toFixed(1);
61+
process.stdout.write(
62+
`\r Downloading... ${percent}% (${Math.round(downloadedSize / 1024 / 1024)}MB / ${Math.round(totalSize / 1024 / 1024)}MB)`
63+
);
64+
});
65+
66+
response.pipe(file);
67+
68+
file.on('finish', () => {
69+
file.close();
70+
process.stdout.write('\n');
71+
resolve();
72+
});
73+
})
74+
.on('error', err => {
75+
file.close();
76+
fs.unlinkSync(destPath);
77+
reject(err);
78+
});
79+
});
80+
}
81+
82+
async function unzipFile(zipPath: string, destDir: string): Promise<void> {
83+
console.log(` Extracting to: ${destDir}`);
84+
await execAsync(`unzip -q "${zipPath}" -d "${destDir}"`);
85+
console.log(' ✅ Extraction complete');
86+
}
87+
88+
function cachePath(folder: string): string {
89+
return path.join(os.homedir(), '.cache', 'appium-mcp', folder);
90+
}
91+
92+
async function getLatestWDAVersion(): Promise<string> {
93+
return new Promise((resolve, reject) => {
94+
const options = {
95+
hostname: 'api.github.com',
96+
path: '/repos/appium/WebDriverAgent/releases/latest',
97+
method: 'GET',
98+
headers: {
99+
'User-Agent': 'mcp-appium-test',
100+
Accept: 'application/vnd.github.v3+json',
101+
},
102+
};
103+
104+
https
105+
.get(options, response => {
106+
let data = '';
107+
108+
response.on('data', chunk => {
109+
data += chunk;
110+
});
111+
112+
response.on('end', () => {
113+
try {
114+
const release = JSON.parse(data);
115+
if (release.tag_name) {
116+
// Remove 'v' prefix if present
117+
const version = release.tag_name.replace(/^v/, '');
118+
resolve(version);
119+
} else {
120+
reject(new Error('No tag_name found in release data'));
121+
}
122+
} catch (error) {
123+
reject(error);
124+
}
125+
});
126+
})
127+
.on('error', err => {
128+
reject(err);
129+
});
130+
});
131+
}
132+
133+
async function main() {
134+
console.log('🧪 Testing WDA Download and Setup\n');
135+
console.log('='.repeat(60));
136+
137+
try {
138+
// Fetch latest WDA version from GitHub releases API
139+
console.log('\n🔍 Fetching latest WDA version from GitHub...');
140+
const wdaVersion = await getLatestWDAVersion();
141+
console.log(`✅ Latest WDA Version: v${wdaVersion}`);
142+
console.log(
143+
' Source: https://github.com/appium/WebDriverAgent/releases/latest'
144+
);
145+
146+
// Create cache directory structure using home directory
147+
const versionCacheDir = cachePath(`wda/${wdaVersion}`);
148+
const extractDir = path.join(versionCacheDir, 'extracted');
149+
const zipPath = path.join(
150+
versionCacheDir,
151+
'WebDriverAgentRunner-Runner.zip'
152+
);
153+
const appPath = path.join(extractDir, 'WebDriverAgentRunner-Runner.app');
154+
155+
console.log('\n📁 Cache Directory:', versionCacheDir);
156+
console.log(` (~/.cache/appium-device-farm/wda/${wdaVersion})`);
157+
158+
// Check if this version is already cached
159+
if (fs.existsSync(appPath)) {
160+
console.log('\n✅ WDA version already cached! Skipping download.');
161+
console.log(` Location: ${appPath}`);
162+
163+
// Show app contents
164+
const appContents = fs.readdirSync(appPath);
165+
console.log('\n📋 Cached App bundle contents:');
166+
appContents.forEach(item => {
167+
console.log(` - ${item}`);
168+
});
169+
170+
console.log('\n' + '='.repeat(60));
171+
console.log('🎉 Using cached WDA!');
172+
console.log(`💡 Cache location: ${versionCacheDir}`);
173+
return;
174+
}
175+
176+
// Version not cached, proceed with download
177+
console.log('\n⚠️ Version not found in cache. Downloading...');
178+
179+
// Create cache directories
180+
fs.mkdirSync(versionCacheDir, { recursive: true });
181+
fs.mkdirSync(extractDir, { recursive: true });
182+
183+
// Download URL
184+
const downloadUrl = `https://github.com/appium/WebDriverAgent/releases/download/v${wdaVersion}/WebDriverAgentRunner-Runner.zip`;
185+
186+
console.log('\n⬇️ Downloading WDA from GitHub releases...');
187+
console.log(` URL: ${downloadUrl}`);
188+
189+
const startTime = Date.now();
190+
await downloadFile(downloadUrl, zipPath);
191+
const downloadTime = ((Date.now() - startTime) / 1000).toFixed(1);
192+
193+
console.log(` ✅ Download complete (${downloadTime}s)`);
194+
195+
// Check file size
196+
const stats = fs.statSync(zipPath);
197+
const fileSizeMB = (stats.size / 1024 / 1024).toFixed(2);
198+
console.log(` 📦 File size: ${fileSizeMB} MB`);
199+
200+
// Unzip the file
201+
console.log('\n📂 Extracting WDA bundle...');
202+
await unzipFile(zipPath, extractDir);
203+
204+
// List contents
205+
console.log('\n📋 Extracted contents:');
206+
const contents = fs.readdirSync(extractDir);
207+
contents.forEach(item => {
208+
const itemPath = path.join(extractDir, item);
209+
const isDir = fs.statSync(itemPath).isDirectory();
210+
console.log(` ${isDir ? '📁' : '📄'} ${item}`);
211+
});
212+
213+
// Check if WebDriverAgentRunner-Runner.app exists
214+
if (fs.existsSync(appPath)) {
215+
console.log('\n✅ WebDriverAgentRunner-Runner.app found!');
216+
console.log(` Location: ${appPath}`);
217+
218+
// Show app contents
219+
const appContents = fs.readdirSync(appPath);
220+
console.log('\n App bundle contents:');
221+
appContents.forEach(item => {
222+
console.log(` - ${item}`);
223+
});
224+
} else {
225+
console.log('\n⚠️ WebDriverAgentRunner-Runner.app not found');
226+
}
227+
228+
console.log('\n' + '='.repeat(60));
229+
console.log('🎉 Download and setup completed successfully!');
230+
console.log(`\n💡 Cached location: ${versionCacheDir}`);
231+
console.log(
232+
' This version will be reused on subsequent runs (no re-download needed)'
233+
);
234+
} catch (error: any) {
235+
console.error('\n❌ Test failed:', error.message);
236+
if (error.stack) {
237+
console.error('\nStack trace:', error.stack);
238+
}
239+
process.exit(1);
240+
}
241+
}
242+
243+
// Run main function
244+
main().catch(error => {
245+
console.error('\n💥 Unexpected error:', error);
246+
process.exit(1);
247+
});

src/tools/boot-simulator.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,27 +54,20 @@ export default function bootSimulator(server: any): void {
5454
};
5555
}
5656

57-
console.log(`Booting iOS simulator: ${simulator.name} (${udid})...`);
58-
5957
const simctl = new Simctl();
6058
simctl.udid = udid;
6159

62-
// Boot the device
60+
// Boot the device and measure time
61+
const bootStartTime = Date.now();
6362
await simctl.bootDevice();
64-
console.log(
65-
'Simulator boot initiated, waiting for boot to complete...'
66-
);
67-
68-
// Wait for boot to complete with 2 minute timeout
6963
await simctl.startBootMonitor({ timeout: 120000 });
70-
71-
console.log(`Simulator "${simulator.name}" booted successfully!`);
64+
const bootDuration = ((Date.now() - bootStartTime) / 1000).toFixed(1);
7265

7366
return {
7467
content: [
7568
{
7669
type: 'text',
77-
text: `✅ Simulator booted successfully!\n\nDevice: ${simulator.name}\nUDID: ${udid}\niOS Version: ${simulator.platform || 'Unknown'}\n\n🚀 The simulator is now ready for session creation. You can now use the create_session tool to start an Appium session.`,
70+
text: `✅ Simulator booted successfully!\n\nDevice: ${simulator.name}\nUDID: ${udid}\niOS Version: ${simulator.platform || 'Unknown'}\nBoot Time: ${bootDuration} seconds\n\n🚀 The simulator is now ready for session creation. You can use the create_session tool to start an Appium session.`,
7871
},
7972
],
8073
};

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import generateLocators from './locators.js';
88
import selectPlatform from './select-platform.js';
99
import selectDevice from './select-device.js';
1010
import bootSimulator from './boot-simulator.js';
11+
import setupWDA from './setup-wda.js';
1112
import generateTest from './generate-tests.js';
1213
import scroll from './scroll.js';
1314
import scrollToElement from './scroll-to-element.js';
@@ -26,6 +27,7 @@ export default function registerTools(server: FastMCP): void {
2627
selectPlatform(server);
2728
selectDevice(server);
2829
bootSimulator(server);
30+
setupWDA(server);
2931
createSession(server);
3032
deleteSession(server);
3133
createCloudSession(server);

0 commit comments

Comments
 (0)