Skip to content

Commit 04c190b

Browse files
committed
Add unit tests for appautomate utilities
1 parent e0138bf commit 04c190b

File tree

2 files changed

+190
-59
lines changed

2 files changed

+190
-59
lines changed

src/tools/appautomate.ts

Lines changed: 54 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import logger from "../logger";
55
import config from "../config";
66
import { trackMCP } from "../lib/instrumentation";
7+
78
import {
89
getDevicesAndBrowsers,
910
BrowserStackProducts,
@@ -56,70 +57,65 @@ async function takeAppScreenshot(args: {
5657
appPath: string;
5758
desiredPhone: string;
5859
}): Promise<CallToolResult> {
59-
validateArgs(args);
60-
const { desiredPlatform, desiredPhone, appPath } = args;
61-
let { desiredPlatformVersion } = args;
62-
63-
logger.info("Fetching available platform devices from cache...");
64-
const platforms = (
65-
await getDevicesAndBrowsers(BrowserStackProducts.APP_AUTOMATE)
66-
).mobile as PlatformDevices[];
67-
logger.info(platforms);
68-
logger.info(`Found ${platforms.length} platforms in device cache.`);
69-
70-
const platformData = platforms.find(
71-
(p) => p.os === desiredPlatform.toLowerCase(),
72-
);
73-
if (!platformData) {
74-
throw new Error(`Platform ${desiredPlatform} not found in device cache.`);
75-
}
60+
let driver;
61+
try {
62+
validateArgs(args);
63+
const { desiredPlatform, desiredPhone, appPath } = args;
64+
let { desiredPlatformVersion } = args;
7665

77-
const matchingDevices = findMatchingDevice(
78-
platformData.devices,
79-
desiredPhone,
80-
);
81-
const availableVersions = getDeviceVersions(matchingDevices);
66+
const platforms = (
67+
await getDevicesAndBrowsers(BrowserStackProducts.APP_AUTOMATE)
68+
).mobile as PlatformDevices[];
8269

83-
desiredPlatformVersion = resolveVersion(
84-
availableVersions,
85-
desiredPlatformVersion,
86-
);
87-
const selectedDevice = matchingDevices.find(
88-
(d) => d.os_version === desiredPlatformVersion,
89-
);
70+
const platformData = platforms.find(
71+
(p) => p.os === desiredPlatform.toLowerCase(),
72+
);
9073

91-
if (!selectedDevice) {
92-
throw new Error(
93-
`Device "${desiredPhone}" with version ${desiredPlatformVersion} not found.`,
74+
if (!platformData) {
75+
throw new Error(`Platform ${desiredPlatform} not found in device cache.`);
76+
}
77+
78+
const matchingDevices = findMatchingDevice(
79+
platformData.devices,
80+
desiredPhone,
9481
);
95-
}
9682

97-
logger.info(
98-
`Selected device: ${selectedDevice.device} with version ${selectedDevice.os_version}`,
99-
);
83+
const availableVersions = getDeviceVersions(matchingDevices);
84+
desiredPlatformVersion = resolveVersion(
85+
availableVersions,
86+
desiredPlatformVersion,
87+
);
10088

101-
const automationName =
102-
desiredPlatform === Platform.ANDROID ? "uiautomator2" : "xcuitest";
103-
104-
const app_url = await uploadApp(appPath);
105-
logger.info(`App uploaded. URL: ${app_url}`);
106-
107-
const capabilities = {
108-
platformName: desiredPlatform,
109-
"appium:platformVersion": selectedDevice.os_version,
110-
"appium:deviceName": selectedDevice.device,
111-
"appium:app": app_url,
112-
"appium:automationName": automationName,
113-
"appium:autoGrantPermissions": true,
114-
"bstack:options": {
115-
userName: config.browserstackUsername,
116-
accessKey: config.browserstackAccessKey,
117-
appiumVersion: "2.0.1",
118-
},
119-
};
89+
const selectedDevice = matchingDevices.find(
90+
(d) => d.os_version === desiredPlatformVersion,
91+
);
92+
93+
if (!selectedDevice) {
94+
throw new Error(
95+
`Device "${desiredPhone}" with version ${desiredPlatformVersion} not found.`,
96+
);
97+
}
98+
99+
const automationName =
100+
desiredPlatform === Platform.ANDROID ? "uiautomator2" : "xcuitest";
101+
102+
const app_url = await uploadApp(appPath);
103+
logger.info(`App uploaded. URL: ${app_url}`);
104+
105+
const capabilities = {
106+
platformName: desiredPlatform,
107+
"appium:platformVersion": selectedDevice.os_version,
108+
"appium:deviceName": selectedDevice.device,
109+
"appium:app": app_url,
110+
"appium:automationName": automationName,
111+
"appium:autoGrantPermissions": true,
112+
"bstack:options": {
113+
userName: config.browserstackUsername,
114+
accessKey: config.browserstackAccessKey,
115+
appiumVersion: "2.0.1",
116+
},
117+
};
120118

121-
let driver;
122-
try {
123119
logger.info("Starting WebDriver session on BrowserStack...");
124120
driver = await wdio.remote({
125121
protocol: "https",
@@ -128,8 +124,7 @@ async function takeAppScreenshot(args: {
128124
path: "/wd/hub",
129125
capabilities,
130126
});
131-
logger.info(driver);
132-
logger.info("Taking screenshot of the app...");
127+
133128
const screenshotBase64 = await driver.takeScreenshot();
134129

135130
return {

tests/tools/appautomate.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import {
2+
findMatchingDevice,
3+
getDeviceVersions,
4+
resolveVersion,
5+
validateArgs,
6+
} from '../../src/tools/appautomate-utils/appautomate';
7+
8+
// Mock only the external dependencies
9+
jest.mock('../../src/config', () => ({
10+
__esModule: true,
11+
default: {
12+
browserstackUsername: 'fake-user',
13+
browserstackAccessKey: 'fake-key',
14+
},
15+
}));
16+
17+
jest.mock('fs');
18+
jest.mock('../../src/logger', () => ({
19+
error: jest.fn(),
20+
info: jest.fn(),
21+
}));
22+
23+
jest.mock('../../src/lib/instrumentation', () => ({
24+
trackMCP: jest.fn(),
25+
}));
26+
27+
describe('appautomate utils', () => {
28+
const validAndroidArgs = {
29+
desiredPlatform: 'android',
30+
desiredPlatformVersion: '12.0',
31+
appPath: '/path/to/app.apk',
32+
desiredPhone: 'Samsung Galaxy S20',
33+
};
34+
35+
const validIOSArgs = {
36+
desiredPlatform: 'ios',
37+
desiredPlatformVersion: '16.0',
38+
appPath: '/path/to/app.ipa',
39+
desiredPhone: 'iPhone 12 Pro',
40+
};
41+
42+
beforeEach(() => {
43+
jest.clearAllMocks();
44+
});
45+
46+
describe('validateArgs', () => {
47+
it('should validate Android args successfully', () => {
48+
expect(() => validateArgs(validAndroidArgs)).not.toThrow();
49+
});
50+
51+
it('should validate iOS args successfully', () => {
52+
expect(() => validateArgs(validIOSArgs)).not.toThrow();
53+
});
54+
55+
it('should fail if platform is not provided', () => {
56+
const args = { ...validAndroidArgs, desiredPlatform: '' };
57+
expect(() => validateArgs(args)).toThrow('Missing required arguments');
58+
});
59+
60+
it('should fail if app path is not provided', () => {
61+
const args = { ...validAndroidArgs, appPath: '' };
62+
expect(() => validateArgs(args)).toThrow('You must provide an appPath');
63+
});
64+
65+
it('should fail if phone is not provided', () => {
66+
const args = { ...validAndroidArgs, desiredPhone: '' };
67+
expect(() => validateArgs(args)).toThrow('Missing required arguments');
68+
});
69+
70+
it('should fail if Android app path does not end with .apk', () => {
71+
const args = { ...validAndroidArgs, appPath: '/path/to/app.ipa' };
72+
expect(() => validateArgs(args)).toThrow('You must provide a valid Android app path');
73+
});
74+
75+
it('should fail if iOS app path does not end with .ipa', () => {
76+
const args = { ...validIOSArgs, appPath: '/path/to/app.apk' };
77+
expect(() => validateArgs(args)).toThrow('You must provide a valid iOS app path');
78+
});
79+
});
80+
81+
describe('findMatchingDevice', () => {
82+
const devices = [
83+
{ device: 'Samsung Galaxy S20', display_name: 'Samsung Galaxy S20', os_version: '12.0', real_mobile: true },
84+
{ device: 'iPhone 12 Pro', display_name: 'iPhone 12 Pro', os_version: '16.0', real_mobile: true },
85+
{ device: 'Samsung Galaxy S21', display_name: 'Samsung Galaxy S21', os_version: '12.0', real_mobile: true },
86+
];
87+
88+
it('should find exact matching device', () => {
89+
const result = findMatchingDevice(devices, 'Samsung Galaxy S20');
90+
expect(result).toHaveLength(1);
91+
expect(result[0].display_name).toBe('Samsung Galaxy S20');
92+
});
93+
94+
it('should throw error if no device found', () => {
95+
expect(() => findMatchingDevice(devices, 'Invalid Device')).toThrow('No devices found');
96+
});
97+
98+
it('should throw error with suggestions for similar devices', () => {
99+
expect(() => findMatchingDevice(devices, 'Galaxy')).toThrow('Alternative devices found');
100+
});
101+
});
102+
103+
describe('getDeviceVersions', () => {
104+
const devices = [
105+
{ device: 'Device1', display_name: 'Device1', os_version: '11.0', real_mobile: true },
106+
{ device: 'Device2', display_name: 'Device2', os_version: '12.0', real_mobile: true },
107+
{ device: 'Device3', display_name: 'Device3', os_version: '11.0', real_mobile: true },
108+
{ device: 'Device4', display_name: 'Device4', os_version: '13.0', real_mobile: true },
109+
];
110+
111+
it('should return unique sorted versions', () => {
112+
const versions = getDeviceVersions(devices);
113+
expect(versions).toEqual(['11.0', '12.0', '13.0']);
114+
});
115+
});
116+
117+
describe('resolveVersion', () => {
118+
const versions = ['11.0', '12.0', '13.0'];
119+
120+
it('should resolve latest version', () => {
121+
expect(resolveVersion(versions, 'latest')).toBe('13.0');
122+
});
123+
124+
it('should resolve oldest version', () => {
125+
expect(resolveVersion(versions, 'oldest')).toBe('11.0');
126+
});
127+
128+
it('should resolve specific version', () => {
129+
expect(resolveVersion(versions, '12.0')).toBe('12.0');
130+
});
131+
132+
it('should throw error for invalid version', () => {
133+
expect(() => resolveVersion(versions, '10.0')).toThrow('Version "10.0" not found');
134+
});
135+
});
136+
});

0 commit comments

Comments
 (0)