Skip to content

Commit 0fc40b4

Browse files
committed
test: cover Android localhost reverse edge cases
1 parent ef0e062 commit 0fc40b4

2 files changed

Lines changed: 92 additions & 3 deletions

File tree

src/platforms/android/__tests__/index.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,96 @@ test('openAndroidApp ensures Android reverse before localhost deep link launch',
582582
]);
583583
});
584584

585+
test('openAndroidApp ensures Android reverse before IPv6 localhost deep link launch', async () => {
586+
const device: DeviceInfo = {
587+
platform: 'android',
588+
id: 'emulator-5554',
589+
name: 'Pixel',
590+
kind: 'emulator',
591+
booted: true,
592+
};
593+
const calls: Array<
594+
{ kind: 'exec'; args: string[] } | { kind: 'reverse'; local: string; remote: string }
595+
> = [];
596+
597+
await withAndroidAdbProvider(
598+
{
599+
exec: async (args) => {
600+
calls.push({ kind: 'exec', args });
601+
return { stdout: '', stderr: '', exitCode: 0 };
602+
},
603+
reverse: {
604+
ensure: async (mapping) => {
605+
calls.push({ kind: 'reverse', local: mapping.local, remote: mapping.remote });
606+
},
607+
remove: async () => {},
608+
removeAllOwned: async () => {},
609+
},
610+
},
611+
{ serial: 'emulator-5554' },
612+
async () => await openAndroidApp(device, 'http://[::1]:8081/status'),
613+
);
614+
615+
assert.deepEqual(calls, [
616+
{ kind: 'reverse', local: 'tcp:8081', remote: 'tcp:8081' },
617+
{
618+
kind: 'exec',
619+
args: [
620+
'shell',
621+
'am',
622+
'start',
623+
'-W',
624+
'-a',
625+
'android.intent.action.VIEW',
626+
'-d',
627+
'http://[::1]:8081/status',
628+
],
629+
},
630+
]);
631+
});
632+
633+
test('openAndroidApp leaves localhost deep links without a port unchanged', async () => {
634+
const device: DeviceInfo = {
635+
platform: 'android',
636+
id: 'emulator-5554',
637+
name: 'Pixel',
638+
kind: 'emulator',
639+
booted: true,
640+
};
641+
const calls: string[][] = [];
642+
643+
await withAndroidAdbProvider(
644+
{
645+
exec: async (args) => {
646+
calls.push(args);
647+
return { stdout: '', stderr: '', exitCode: 0 };
648+
},
649+
reverse: {
650+
ensure: async () => {
651+
throw new Error('reverse should not run without a URL port');
652+
},
653+
remove: async () => {},
654+
removeAllOwned: async () => {},
655+
},
656+
},
657+
{ serial: 'emulator-5554' },
658+
async () => await openAndroidApp(device, 'http://localhost/path'),
659+
);
660+
661+
assert.deepEqual(calls, [
662+
[
663+
'shell',
664+
'am',
665+
'start',
666+
'-W',
667+
'-a',
668+
'android.intent.action.VIEW',
669+
'-d',
670+
'http://localhost/path',
671+
],
672+
]);
673+
});
674+
585675
test('openAndroidApp leaves non-localhost deep links unchanged', async () => {
586676
const device: DeviceInfo = {
587677
platform: 'android',

src/platforms/android/app-lifecycle.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const ANDROID_APPS_DISCOVERY_HINT =
4141
'Run agent-device apps --platform android to discover the installed package name, then retry open with that exact package.';
4242
const ANDROID_AMBIGUOUS_APP_HINT =
4343
'Run agent-device apps --platform android to see the exact installed package names before retrying open.';
44+
const ANDROID_LOCALHOST_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
4445

4546
type AndroidAppResolution = { type: 'intent' | 'package'; value: string };
4647

@@ -231,9 +232,7 @@ function androidLocalhostReverseEndpoint(target: string): AndroidPortReverseEndp
231232
}
232233

233234
const hostname = url.hostname.toLowerCase();
234-
if (hostname !== 'localhost' && hostname !== '127.0.0.1' && hostname !== '[::1]') {
235-
return null;
236-
}
235+
if (!ANDROID_LOCALHOST_HOSTNAMES.has(hostname)) return null;
237236
if (!url.port) return null;
238237
const port = Number(url.port);
239238
if (!Number.isInteger(port)) return null;

0 commit comments

Comments
 (0)