Skip to main content
Glama
deep-link.test.ts13.9 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock the shell module vi.mock('../../../../src/utils/shell.js', () => ({ executeShell: vi.fn(), })); import { executeShell } from '../../../../src/utils/shell.js'; import { openAndroidDeepLink, isValidUri, isAppInstalled, getCurrentActivity, sendBroadcast, } from '../../../../src/platforms/android/deep-link.js'; const mockedExecuteShell = vi.mocked(executeShell); describe('Android Deep Link Handler', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('isValidUri', () => { it('should return true for valid custom scheme URIs', () => { expect(isValidUri('myapp://home')).toBe(true); expect(isValidUri('myapp://products/123')).toBe(true); expect(isValidUri('app.custom://path/to/resource')).toBe(true); }); it('should return true for valid https URIs', () => { expect(isValidUri('https://example.com')).toBe(true); expect(isValidUri('https://example.com/path?query=1')).toBe(true); expect(isValidUri('http://localhost:3000')).toBe(true); }); it('should return false for invalid URIs', () => { expect(isValidUri('')).toBe(false); expect(isValidUri('not-a-uri')).toBe(false); expect(isValidUri('://missing-scheme')).toBe(false); expect(isValidUri('scheme-only://')).toBe(false); }); it('should return false for URIs without scheme separator', () => { expect(isValidUri('myapp:home')).toBe(false); expect(isValidUri('file:/single-slash')).toBe(false); }); }); describe('openAndroidDeepLink', () => { it('should open deep link successfully', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent { act=android.intent.action.VIEW dat=myapp://home cmp=com.example.app/.MainActivity }', stderr: '', exitCode: 0, }); const result = await openAndroidDeepLink('myapp://home'); expect(result.success).toBe(true); expect(result.uri).toBe('myapp://home'); expect(result.durationMs).toBeGreaterThanOrEqual(0); }); it('should include device ID in command when provided', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent {}', stderr: '', exitCode: 0, }); await openAndroidDeepLink('myapp://home', { deviceId: 'emulator-5554' }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['-s', 'emulator-5554']), expect.any(Object) ); }); it('should return error for invalid URI', async () => { const result = await openAndroidDeepLink('not-a-valid-uri'); expect(result.success).toBe(false); expect(result.error).toContain('Invalid URI'); }); it('should handle no devices connected', async () => { mockedExecuteShell.mockResolvedValue({ stdout: '', stderr: 'error: no devices/emulators found', exitCode: 1, }); const result = await openAndroidDeepLink('myapp://home'); expect(result.success).toBe(false); expect(result.error).toContain('No Android device connected'); }); it('should handle activity not found', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Error: Activity not found', stderr: '', exitCode: 1, }); const result = await openAndroidDeepLink('myapp://unknown'); expect(result.success).toBe(false); expect(result.error).toContain('No app can handle this deep link'); }); it('should handle security exception', async () => { mockedExecuteShell.mockResolvedValue({ stdout: '', stderr: 'Security exception: permission denied', exitCode: 1, }); const result = await openAndroidDeepLink('myapp://secure'); expect(result.success).toBe(false); expect(result.error).toContain('Security exception'); }); it('should include custom action in command', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent {}', stderr: '', exitCode: 0, }); await openAndroidDeepLink('myapp://home', { action: 'com.example.CUSTOM_ACTION', }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['-a', 'com.example.CUSTOM_ACTION']), expect.any(Object) ); }); it('should include package name in command', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent {}', stderr: '', exitCode: 0, }); await openAndroidDeepLink('myapp://home', { packageName: 'com.example.app', }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['-p', 'com.example.app']), expect.any(Object) ); }); it('should include component when both package and activity are provided', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent {}', stderr: '', exitCode: 0, }); await openAndroidDeepLink('myapp://home', { packageName: 'com.example.app', activityName: '.DeepLinkActivity', }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['-n', 'com.example.app/.DeepLinkActivity']), expect.any(Object) ); }); it('should include category in command', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent {}', stderr: '', exitCode: 0, }); await openAndroidDeepLink('myapp://home', { category: 'android.intent.category.DEFAULT', }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['-c', 'android.intent.category.DEFAULT']), expect.any(Object) ); }); it('should include intent extras in command', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent {}', stderr: '', exitCode: 0, }); await openAndroidDeepLink('myapp://home', { extras: [ { type: 'string', key: 'userId', value: '123' }, { type: 'int', key: 'count', value: 5 }, { type: 'boolean', key: 'isNew', value: true }, ], }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['--es', 'userId', '123']), expect.any(Object) ); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['--ei', 'count', '5']), expect.any(Object) ); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['--ez', 'isNew', 'true']), expect.any(Object) ); }); it('should wait for launch when waitForLaunch is true', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent {}', stderr: '', exitCode: 0, }); await openAndroidDeepLink('myapp://home', { waitForLaunch: true }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['-W']), expect.any(Object) ); }); it('should parse launched activity from success output', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Starting: Intent { act=android.intent.action.VIEW dat=myapp://home cmp=com.example.app/.MainActivity }', stderr: '', exitCode: 0, }); const result = await openAndroidDeepLink('myapp://home'); expect(result.success).toBe(true); expect(result.details).toContain('com.example.app/.MainActivity'); }); }); describe('isAppInstalled', () => { it('should return true when app is installed', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'package:com.example.app\n', stderr: '', exitCode: 0, }); const result = await isAppInstalled('com.example.app'); expect(result).toBe(true); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', ['shell', 'pm', 'list', 'packages', 'com.example.app'], expect.any(Object) ); }); it('should return false when app is not installed', async () => { mockedExecuteShell.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0, }); const result = await isAppInstalled('com.nonexistent.app'); expect(result).toBe(false); }); it('should use device ID when provided', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'package:com.example.app', stderr: '', exitCode: 0, }); await isAppInstalled('com.example.app', 'emulator-5554'); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', ['-s', 'emulator-5554', 'shell', 'pm', 'list', 'packages', 'com.example.app'], expect.any(Object) ); }); it('should return false on command failure', async () => { mockedExecuteShell.mockRejectedValue(new Error('Command failed')); const result = await isAppInstalled('com.example.app'); expect(result).toBe(false); }); }); describe('getCurrentActivity', () => { it('should return current resumed activity', async () => { mockedExecuteShell.mockResolvedValue({ stdout: ` mResumedActivity: ActivityRecord{abc com.example.app/.MainActivity} `, stderr: '', exitCode: 0, }); const activity = await getCurrentActivity(); expect(activity).toBe('com.example.app/.MainActivity'); }); it('should fallback to focused activity', async () => { mockedExecuteShell.mockResolvedValue({ stdout: ` mFocusedActivity: ActivityRecord{abc com.other.app/.OtherActivity} `, stderr: '', exitCode: 0, }); const activity = await getCurrentActivity(); expect(activity).toBe('com.other.app/.OtherActivity'); }); it('should return undefined when no activity found', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Some output without activity info', stderr: '', exitCode: 0, }); const activity = await getCurrentActivity(); expect(activity).toBeUndefined(); }); it('should use device ID when provided', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'mResumedActivity: com.example.app/.MainActivity', stderr: '', exitCode: 0, }); await getCurrentActivity('emulator-5554'); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', ['-s', 'emulator-5554', 'shell', 'dumpsys', 'activity', 'activities'], expect.any(Object) ); }); it('should return undefined on command failure', async () => { mockedExecuteShell.mockRejectedValue(new Error('Command failed')); const activity = await getCurrentActivity(); expect(activity).toBeUndefined(); }); }); describe('sendBroadcast', () => { it('should send broadcast successfully', async () => { mockedExecuteShell.mockResolvedValue({ stdout: 'Broadcast completed: result=0', stderr: '', exitCode: 0, }); const result = await sendBroadcast('com.example.ACTION_TEST'); expect(result.success).toBe(true); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', ['shell', 'am', 'broadcast', '-a', 'com.example.ACTION_TEST'], expect.any(Object) ); }); it('should include device ID when provided', async () => { mockedExecuteShell.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0, }); await sendBroadcast('com.example.ACTION_TEST', { deviceId: 'emulator-5554' }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['-s', 'emulator-5554']), expect.any(Object) ); }); it('should include package name when provided', async () => { mockedExecuteShell.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0, }); await sendBroadcast('com.example.ACTION_TEST', { packageName: 'com.example.app' }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['-p', 'com.example.app']), expect.any(Object) ); }); it('should include extras when provided', async () => { mockedExecuteShell.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0, }); await sendBroadcast('com.example.ACTION_TEST', { extras: [ { type: 'string', key: 'message', value: 'hello' }, ], }); expect(mockedExecuteShell).toHaveBeenCalledWith( 'adb', expect.arrayContaining(['--es', 'message', 'hello']), expect.any(Object) ); }); it('should return error on broadcast failure', async () => { mockedExecuteShell.mockResolvedValue({ stdout: '', stderr: 'Error: permission denied', exitCode: 1, }); const result = await sendBroadcast('com.example.ACTION_TEST'); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); it('should handle exception during execution', async () => { mockedExecuteShell.mockRejectedValue(new Error('Network error')); const result = await sendBroadcast('com.example.ACTION_TEST'); expect(result.success).toBe(false); expect(result.error).toContain('Network error'); }); }); });

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