Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions src/tests/tools/app-management/resolve-app-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ const rehydratedDriver = {rehydrated: true};

const mockResolveDriver = jest.fn<(sessionId?: string) => Promise<any>>();
const mockListAppsFromDevice = jest.fn<(...args: any[]) => Promise<{packageName: string; appName: string}[]>>();
const mockGetPlatformName = jest.fn<() => string>();
const mockIsXCUITestDriverSession = jest.fn<() => boolean>();

jest.unstable_mockModule('../../../session-store', () => ({
// The in-memory cache is empty after an MCP process recycle.
getDriver: jest.fn(() => null),
getSessionId: jest.fn(() => undefined),
getPlatformName: jest.fn(() => 'Android'),
isXCUITestDriverSession: jest.fn(() => false),
getPlatformName: mockGetPlatformName,
isXCUITestDriverSession: mockIsXCUITestDriverSession,
PLATFORM: {ios: 'iOS', android: 'Android'},
}));

Expand All @@ -26,12 +28,14 @@ jest.unstable_mockModule('../../../tools/app-management/list-apps.js', () => ({

const {resolveAppId} = await import('../../../tools/app-management/resolve-app-id.js');

describe('resolveAppId on a persisted session', () => {
beforeEach(() => {
jest.clearAllMocks();
mockListAppsFromDevice.mockResolvedValue([{packageName: 'com.example.calc', appName: 'Calculator'}]);
});
beforeEach(() => {
jest.clearAllMocks();
mockGetPlatformName.mockReturnValue('Android');
mockIsXCUITestDriverSession.mockReturnValue(false);
mockListAppsFromDevice.mockResolvedValue([{packageName: 'com.example.calc', appName: 'Calculator'}]);
});

describe('resolveAppId on a persisted session', () => {
test('rehydrates the session instead of failing on an empty driver cache', async () => {
mockResolveDriver.mockResolvedValue({ok: true, driver: rehydratedDriver});

Expand All @@ -48,3 +52,30 @@ describe('resolveAppId on a persisted session', () => {
expect(mockListAppsFromDevice).not.toHaveBeenCalled();
});
});

describe('resolveAppId on a real iOS device', () => {
const deviceDriver = {isSimulator: () => false};

beforeEach(() => {
mockGetPlatformName.mockReturnValue('iOS');
mockIsXCUITestDriverSession.mockReturnValue(true);
mockResolveDriver.mockResolvedValue({ok: true, driver: deviceDriver});
});

test('keeps the apps that were listed when only one app type fails', async () => {
mockListAppsFromDevice
.mockResolvedValueOnce([{packageName: 'com.example.calc', appName: 'Calculator'}])
.mockRejectedValueOnce(new Error('System app list unavailable'));

await expect(resolveAppId('Calculator', 'device-1')).resolves.toBe('com.example.calc');
});

test('reports the device failure instead of caching an empty app list', async () => {
mockListAppsFromDevice.mockRejectedValue(new Error('Could not list applications on the device'));

await expect(resolveAppId('Calculator', 'device-2')).rejects.toThrow(/Could not list applications/);

mockListAppsFromDevice.mockResolvedValue([{packageName: 'com.example.calc', appName: 'Calculator'}]);
await expect(resolveAppId('Calculator', 'device-2')).resolves.toBe('com.example.calc');
});
});
4 changes: 4 additions & 0 deletions src/tools/app-management/resolve-app-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ async function getInstalledApps(sessionId?: string): Promise<{packageName: strin
listAppsFromDevice(driver, 'User'),
listAppsFromDevice(driver, 'System'),
]);
const failures = results.filter((result) => result.status === 'rejected');
if (failures.length === results.length) {
throw failures[0].reason;
}
const seen = new Set<string>();
apps = [];
for (const result of results) {
Expand Down