Skip to main content
Glama
navigation.test.ts15.2 kB
/** * Unit tests for Deep Link Navigation tools * Tests deep link handling for Android and iOS */ import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock shell execution vi.mock('../../../src/utils/shell.js', () => ({ executeShell: vi.fn(), executeShellOrThrow: vi.fn(), })); import { executeShell } from '../../../src/utils/shell.js'; describe('Deep Link Navigation', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('DeepLinkResult structure', () => { it('should have required fields for success', () => { const result = { success: true, platform: 'android', uri: 'myapp://home/profile', durationMs: 150, }; expect(result.success).toBe(true); expect(result.platform).toBe('android'); expect(result.uri).toBe('myapp://home/profile'); expect(result.durationMs).toBeGreaterThan(0); }); it('should have error field for failure', () => { const result = { success: false, platform: 'ios', uri: 'myapp://invalid', error: 'Activity not found for intent', durationMs: 50, }; expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); describe('Android Deep Links', () => { describe('adb shell am start', () => { it('should construct correct command for simple URI', () => { const uri = 'myapp://home'; const deviceId = 'emulator-5554'; const expectedArgs = [ '-s', deviceId, 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', uri, ]; expect(expectedArgs).toContain('-a'); expect(expectedArgs).toContain('android.intent.action.VIEW'); expect(expectedArgs).toContain('-d'); expect(expectedArgs).toContain(uri); }); it('should handle URI with query parameters', () => { const uri = 'myapp://product?id=123&source=home'; // Query parameters should be properly escaped expect(uri).toContain('?'); expect(uri).toContain('id=123'); }); it('should handle https:// URLs for App Links', () => { const uri = 'https://example.com/products/123'; expect(uri.startsWith('https://')).toBe(true); }); it('should support package targeting', () => { const packageName = 'com.example.myapp'; const uri = 'myapp://home'; const expectedArgs = [ 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', uri, '-n', `${packageName}/.MainActivity`, ]; expect(expectedArgs).toContain('-n'); }); it('should handle intent extras', () => { const extras = { stringExtra: { key: 'user_id', value: '123' }, booleanExtra: { key: 'is_premium', value: true }, }; const args: string[] = []; args.push('--es', extras.stringExtra.key, extras.stringExtra.value); args.push('--ez', extras.booleanExtra.key, String(extras.booleanExtra.value)); expect(args).toContain('--es'); expect(args).toContain('--ez'); }); }); describe('Android deep link execution', () => { it('should execute adb command successfully', async () => { const mockExecuteShell = vi.mocked(executeShell); mockExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent { act=android.intent.action.VIEW dat=myapp://home }', stderr: '', exitCode: 0, }); const result = await mockExecuteShell('adb', [ '-s', 'emulator-5554', 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', 'myapp://home', ]); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Starting:'); }); it('should detect activity not found error', async () => { const mockExecuteShell = vi.mocked(executeShell); mockExecuteShell.mockResolvedValue({ stdout: '', stderr: 'Error: Activity not found to handle Intent', exitCode: 1, }); const result = await mockExecuteShell('adb', ['shell', 'am', 'start', '-d', 'unknown://test']); expect(result.exitCode).toBe(1); expect(result.stderr).toContain('Activity not found'); }); it('should detect no device error', async () => { const mockExecuteShell = vi.mocked(executeShell); mockExecuteShell.mockResolvedValue({ stdout: '', stderr: 'error: no devices/emulators found', exitCode: 1, }); const result = await mockExecuteShell('adb', ['shell', 'am', 'start', '-d', 'myapp://home']); expect(result.stderr).toContain('no devices'); }); }); }); describe('iOS Deep Links', () => { describe('simctl openurl', () => { it('should construct correct command for custom scheme', () => { const uri = 'myapp://home/profile'; const deviceId = 'booted'; const expectedArgs = ['simctl', 'openurl', deviceId, uri]; expect(expectedArgs[0]).toBe('simctl'); expect(expectedArgs[1]).toBe('openurl'); expect(expectedArgs[3]).toBe(uri); }); it('should handle Universal Links (https://)', () => { const uri = 'https://example.com/app/products/123'; expect(uri.startsWith('https://')).toBe(true); }); it('should support specific device UDID', () => { const deviceId = 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890'; const uri = 'myapp://home'; const args = ['simctl', 'openurl', deviceId, uri]; expect(args[2]).toBe(deviceId); }); }); describe('iOS deep link execution', () => { it('should execute xcrun simctl successfully', async () => { const mockExecuteShell = vi.mocked(executeShell); mockExecuteShell.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0, }); const result = await mockExecuteShell('xcrun', [ 'simctl', 'openurl', 'booted', 'myapp://home', ]); expect(result.exitCode).toBe(0); }); it('should detect no booted device error', async () => { const mockExecuteShell = vi.mocked(executeShell); mockExecuteShell.mockResolvedValue({ stdout: '', stderr: 'No devices are booted.', exitCode: 1, }); const result = await mockExecuteShell('xcrun', ['simctl', 'openurl', 'booted', 'myapp://home']); expect(result.stderr).toContain('No devices are booted'); }); it('should detect invalid URL scheme', async () => { const mockExecuteShell = vi.mocked(executeShell); mockExecuteShell.mockResolvedValue({ stdout: '', stderr: 'An error was encountered processing the command', exitCode: 1, }); const result = await mockExecuteShell('xcrun', ['simctl', 'openurl', 'booted', 'invalid://test']); expect(result.exitCode).toBe(1); }); }); }); describe('URI Validation', () => { it('should validate custom scheme URIs', () => { const validURIs = [ 'myapp://home', 'myapp://products/123', 'app-name://settings/profile', 'com.example.app://deep/link', ]; for (const uri of validURIs) { expect(uri).toMatch(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//); } }); it('should validate http/https URLs', () => { const validURLs = [ 'https://example.com/app/home', 'http://localhost:3000/test', 'https://app.example.com/products?id=123', ]; for (const url of validURLs) { expect(url).toMatch(/^https?:\/\//); } }); it('should reject invalid URIs', () => { const invalidURIs = [ 'not-a-uri', '://missing-scheme', 'scheme-only://', '', ]; for (const uri of invalidURIs) { const isValid = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/.+/.test(uri); expect(isValid).toBe(false); } }); }); describe('deep_link_navigate tool', () => { describe('tool registration', () => { it('should define required input schema', () => { const expectedSchema = { uri: { type: 'string', description: expect.any(String) }, platform: { type: 'string', enum: ['android', 'ios'] }, deviceId: { type: 'string', description: expect.any(String) }, packageName: { type: 'string', description: expect.any(String) }, waitForReady: { type: 'boolean', description: expect.any(String) }, }; expect(expectedSchema.uri.type).toBe('string'); expect(expectedSchema.platform.enum).toContain('android'); expect(expectedSchema.platform.enum).toContain('ios'); }); it('should require uri parameter', () => { const required = ['uri', 'platform']; expect(required).toContain('uri'); expect(required).toContain('platform'); }); }); describe('tool execution', () => { it('should return success result on valid deep link', () => { const result = { success: true, platform: 'android', uri: 'myapp://home', details: 'Deep link opened successfully', durationMs: 250, }; expect(result.success).toBe(true); expect(result.details).toBeDefined(); }); it('should return error for invalid URI', () => { const result = { success: false, platform: 'android', uri: 'invalid', error: 'Invalid URI format. Must be scheme://path', durationMs: 5, }; expect(result.success).toBe(false); expect(result.error).toContain('Invalid URI'); }); it('should return error when no device available', () => { const result = { success: false, platform: 'ios', uri: 'myapp://home', error: 'No iOS simulator is currently booted', durationMs: 100, }; expect(result.success).toBe(false); expect(result.error).toContain('simulator'); }); it('should return error when app not installed', () => { const result = { success: false, platform: 'android', uri: 'unknownapp://home', error: 'No activity found to handle deep link', durationMs: 150, }; expect(result.success).toBe(false); expect(result.error).toContain('activity'); }); }); describe('waitForReady option', () => { it('should wait for screen transition when enabled', () => { const args = { uri: 'myapp://products/123', platform: 'android', waitForReady: true, waitTimeMs: 2000, }; expect(args.waitForReady).toBe(true); expect(args.waitTimeMs).toBe(2000); }); it('should return immediately when disabled', () => { const args = { uri: 'myapp://home', platform: 'ios', waitForReady: false, }; expect(args.waitForReady).toBe(false); }); }); }); describe('Intent Extras (Android)', () => { it('should support string extras', () => { const extras = [ { type: 'string', key: 'user_id', value: '12345' }, ]; const args = extras.flatMap((e) => ['--es', e.key, e.value]); expect(args).toEqual(['--es', 'user_id', '12345']); }); it('should support integer extras', () => { const extras = [ { type: 'int', key: 'count', value: 42 }, ]; const args = extras.flatMap((e) => ['--ei', e.key, String(e.value)]); expect(args).toEqual(['--ei', 'count', '42']); }); it('should support boolean extras', () => { const extras = [ { type: 'boolean', key: 'is_premium', value: true }, ]; const args = extras.flatMap((e) => ['--ez', e.key, String(e.value)]); expect(args).toEqual(['--ez', 'is_premium', 'true']); }); it('should support multiple extras', () => { const extras = [ { type: 'string', key: 'source', value: 'push' }, { type: 'int', key: 'notification_id', value: 999 }, { type: 'boolean', key: 'show_modal', value: false }, ]; const args: string[] = []; for (const e of extras) { switch (e.type) { case 'string': args.push('--es', e.key, String(e.value)); break; case 'int': args.push('--ei', e.key, String(e.value)); break; case 'boolean': args.push('--ez', e.key, String(e.value)); break; } } expect(args).toHaveLength(9); // 3 extras * 3 args each }); }); describe('Platform Detection', () => { it('should detect Android from device ID format', () => { const androidDeviceIds = [ 'emulator-5554', 'emulator-5556', 'RF8M33XXXXX', // Physical device serial ]; for (const id of androidDeviceIds) { // Emulator IDs start with 'emulator-' or are alphanumeric serials const isAndroid = id.startsWith('emulator-') || /^[A-Z0-9]+$/.test(id); expect(isAndroid).toBe(true); } }); it('should detect iOS from device ID format', () => { const iosDeviceIds = [ 'booted', 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890', // UDID format ]; for (const id of iosDeviceIds) { // iOS UDIDs are UUID format or 'booted' const isIOS = id === 'booted' || /^[A-F0-9-]{36}$/i.test(id); expect(isIOS).toBe(true); } }); }); describe('Error Handling', () => { it('should handle timeout gracefully', async () => { const mockExecuteShell = vi.mocked(executeShell); mockExecuteShell.mockRejectedValue(new Error('Command timed out')); await expect( mockExecuteShell('adb', ['shell', 'am', 'start', '-d', 'myapp://home'], { timeoutMs: 1000 }) ).rejects.toThrow('timed out'); }); it('should handle shell execution errors', async () => { const mockExecuteShell = vi.mocked(executeShell); mockExecuteShell.mockRejectedValue(new Error('adb: command not found')); await expect( mockExecuteShell('adb', ['shell', 'am', 'start']) ).rejects.toThrow('command not found'); }); it('should provide helpful error for common issues', () => { const errorMessages = { noDevice: 'No device connected. Use manage_env to boot a device first.', noActivity: 'No app can handle this deep link. Ensure the app is installed and configured.', invalidScheme: 'Invalid URI scheme. Use format: scheme://path or https://domain/path', timeout: 'Deep link navigation timed out. The app may be slow to respond.', }; expect(errorMessages.noDevice).toContain('manage_env'); expect(errorMessages.noActivity).toContain('installed'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/abd3lraouf/specter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server