@react-native-harness/platform-android 1.0.0-canary.1761729829908

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +99 -0
  3. package/dist/adb-id.d.ts +4 -0
  4. package/dist/adb-id.d.ts.map +1 -0
  5. package/dist/adb-id.js +24 -0
  6. package/dist/adb.d.ts +15 -0
  7. package/dist/adb.d.ts.map +1 -0
  8. package/dist/adb.js +64 -0
  9. package/dist/assertions.d.ts +5 -0
  10. package/dist/assertions.d.ts.map +1 -0
  11. package/dist/assertions.js +6 -0
  12. package/dist/config.d.ts +106 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +39 -0
  15. package/dist/emulator.d.ts +6 -0
  16. package/dist/emulator.d.ts.map +1 -0
  17. package/dist/emulator.js +27 -0
  18. package/dist/errors.d.ts +15 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +28 -0
  21. package/dist/factory.d.ts +6 -0
  22. package/dist/factory.d.ts.map +1 -0
  23. package/dist/factory.js +51 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/reader.d.ts +6 -0
  28. package/dist/reader.d.ts.map +1 -0
  29. package/dist/reader.js +57 -0
  30. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  31. package/dist/types.d.ts +381 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +107 -0
  34. package/dist/utils.d.ts +3 -0
  35. package/dist/utils.d.ts.map +1 -0
  36. package/dist/utils.js +7 -0
  37. package/eslint.config.mjs +19 -0
  38. package/package.json +25 -0
  39. package/src/adb-id.ts +36 -0
  40. package/src/adb.ts +99 -0
  41. package/src/config.ts +60 -0
  42. package/src/emulator.ts +41 -0
  43. package/src/factory.ts +85 -0
  44. package/src/index.ts +6 -0
  45. package/src/utils.ts +9 -0
  46. package/tsconfig.json +16 -0
  47. package/tsconfig.lib.json +21 -0
package/src/adb-id.ts ADDED
@@ -0,0 +1,36 @@
1
+ import * as adb from './adb.js';
2
+ import {
3
+ isAndroidDeviceEmulator,
4
+ isAndroidDevicePhysical,
5
+ AndroidDevice,
6
+ } from './config.js';
7
+
8
+ export const isAdbIdEmulator = (adbId: string): boolean => {
9
+ return adbId.startsWith('emulator-');
10
+ };
11
+
12
+ export const getAdbId = async (
13
+ device: AndroidDevice
14
+ ): Promise<string | null> => {
15
+ const adbIds = await adb.getDeviceIds();
16
+
17
+ for (const adbId of adbIds) {
18
+ if (isAndroidDeviceEmulator(device)) {
19
+ const emulatorName = await adb.getEmulatorName(adbId);
20
+
21
+ if (emulatorName === device.name) {
22
+ return adbId;
23
+ }
24
+ } else if (isAndroidDevicePhysical(device)) {
25
+ const deviceInfo = await adb.getDeviceInfo(adbId);
26
+ if (
27
+ deviceInfo?.manufacturer === device.manufacturer &&
28
+ deviceInfo?.model === device.model
29
+ ) {
30
+ return adbId;
31
+ }
32
+ }
33
+ }
34
+
35
+ return null;
36
+ };
package/src/adb.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { spawn } from '@react-native-harness/tools';
2
+
3
+ export const isAppInstalled = async (
4
+ adbId: string,
5
+ bundleId: string
6
+ ): Promise<boolean> => {
7
+ const { stdout } = await spawn('adb', [
8
+ '-s',
9
+ adbId,
10
+ 'shell',
11
+ 'pm',
12
+ 'list',
13
+ 'packages',
14
+ bundleId,
15
+ ]);
16
+ return stdout.trim() !== '';
17
+ };
18
+
19
+ export const reversePort = async (
20
+ adbId: string,
21
+ port: number
22
+ ): Promise<void> => {
23
+ await spawn('adb', ['-s', adbId, 'reverse', `tcp:${port}`, `tcp:${port}`]);
24
+ };
25
+
26
+ export const stopApp = async (
27
+ adbId: string,
28
+ bundleId: string
29
+ ): Promise<void> => {
30
+ await spawn('adb', ['-s', adbId, 'shell', 'am', 'force-stop', bundleId]);
31
+ };
32
+
33
+ export const startApp = async (
34
+ adbId: string,
35
+ bundleId: string,
36
+ activityName: string
37
+ ): Promise<void> => {
38
+ await spawn('adb', [
39
+ '-s',
40
+ adbId,
41
+ 'shell',
42
+ 'am',
43
+ 'start',
44
+ '-n',
45
+ `${bundleId}/${activityName}`,
46
+ ]);
47
+ };
48
+
49
+ export const getDeviceIds = async (): Promise<string[]> => {
50
+ const { stdout } = await spawn('adb', ['devices']);
51
+ return stdout
52
+ .split('\n')
53
+ .slice(1) // Skip header
54
+ .filter((line) => line.trim() !== '')
55
+ .map((line) => line.split('\t')[0]);
56
+ };
57
+
58
+ export const getEmulatorName = async (
59
+ adbId: string
60
+ ): Promise<string | null> => {
61
+ const { stdout } = await spawn('adb', ['-s', adbId, 'emu', 'avd', 'name']);
62
+ return stdout.split('\n')[0].trim() || null;
63
+ };
64
+
65
+ export const getShellProperty = async (
66
+ adbId: string,
67
+ property: string
68
+ ): Promise<string | null> => {
69
+ const { stdout } = await spawn('adb', [
70
+ '-s',
71
+ adbId,
72
+ 'shell',
73
+ 'getprop',
74
+ property,
75
+ ]);
76
+ return stdout.trim() || null;
77
+ };
78
+
79
+ export type DeviceInfo = {
80
+ manufacturer: string | null;
81
+ model: string | null;
82
+ };
83
+
84
+ export const getDeviceInfo = async (
85
+ adbId: string
86
+ ): Promise<DeviceInfo | null> => {
87
+ const manufacturer = await getShellProperty(adbId, 'ro.product.manufacturer');
88
+ const model = await getShellProperty(adbId, 'ro.product.model');
89
+ return { manufacturer, model };
90
+ };
91
+
92
+ export const isBootCompleted = async (adbId: string): Promise<boolean> => {
93
+ const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
94
+ return bootCompleted === '1';
95
+ };
96
+
97
+ export const stopEmulator = async (adbId: string): Promise<void> => {
98
+ await spawn('adb', ['-s', adbId, 'emu', 'kill']);
99
+ };
package/src/config.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod';
2
+
3
+ export const AndroidEmulatorSchema = z.object({
4
+ type: z.literal('emulator'),
5
+ name: z.string().min(1, 'Name is required'),
6
+ });
7
+
8
+ export const PhysicalAndroidDeviceSchema = z.object({
9
+ type: z.literal('physical'),
10
+ manufacturer: z.string().min(1, 'Manufacturer is required'),
11
+ model: z.string().min(1, 'Model is required'),
12
+ });
13
+
14
+ export const AndroidDeviceSchema = z.discriminatedUnion('type', [
15
+ AndroidEmulatorSchema,
16
+ PhysicalAndroidDeviceSchema,
17
+ ]);
18
+
19
+ export const AndroidPlatformConfigSchema = z.object({
20
+ name: z.string().min(1, 'Name is required'),
21
+ device: AndroidDeviceSchema,
22
+ bundleId: z.string().min(1, 'Bundle ID is required'),
23
+ activityName: z
24
+ .string()
25
+ .min(1, 'Activity name is required')
26
+ .default('.MainActivity'),
27
+ });
28
+
29
+ export type AndroidEmulator = z.infer<typeof AndroidEmulatorSchema>;
30
+ export type PhysicalAndroidDevice = z.infer<typeof PhysicalAndroidDeviceSchema>;
31
+ export type AndroidDevice = z.infer<typeof AndroidDeviceSchema>;
32
+ export type AndroidPlatformConfig = z.infer<typeof AndroidPlatformConfigSchema>;
33
+
34
+ export const isAndroidDeviceEmulator = (
35
+ device: AndroidDevice
36
+ ): device is AndroidEmulator => {
37
+ return device.type === 'emulator';
38
+ };
39
+
40
+ export const isAndroidDevicePhysical = (
41
+ device: AndroidDevice
42
+ ): device is PhysicalAndroidDevice => {
43
+ return device.type === 'physical';
44
+ };
45
+
46
+ export function assertAndroidDeviceEmulator(
47
+ device: AndroidDevice
48
+ ): asserts device is AndroidEmulator {
49
+ if (!isAndroidDeviceEmulator(device)) {
50
+ throw new Error('Device is not an emulator');
51
+ }
52
+ }
53
+
54
+ export function assertAndroidDevicePhysical(
55
+ device: AndroidDevice
56
+ ): asserts device is PhysicalAndroidDevice {
57
+ if (!isAndroidDevicePhysical(device)) {
58
+ throw new Error('Device is not a physical device');
59
+ }
60
+ }
@@ -0,0 +1,41 @@
1
+ import { spawn } from '@react-native-harness/tools';
2
+ import * as adb from './adb.js';
3
+
4
+ export type AndroidEmulator = {
5
+ adbId: string;
6
+ stop: () => Promise<void>;
7
+ };
8
+
9
+ export const runEmulator = async (
10
+ avdName: string
11
+ ): Promise<AndroidEmulator> => {
12
+ const process = spawn('emulator', ['-avd', avdName]);
13
+ await process.nodeChildProcess;
14
+
15
+ const adbId = await adb.getEmulatorName(avdName);
16
+
17
+ if (!adbId) {
18
+ throw new Error('Emulator not found');
19
+ }
20
+
21
+ // Poll for emulator status until it's fully running
22
+ const checkStatus = async (): Promise<void> => {
23
+ const status = await adb.isBootCompleted(adbId);
24
+
25
+ if (!status) {
26
+ await new Promise((resolve) => setTimeout(resolve, 2000));
27
+ await checkStatus();
28
+ }
29
+ };
30
+
31
+ // Start checking status after a brief delay to allow emulator to start
32
+ await new Promise((resolve) => setTimeout(resolve, 3000));
33
+ await checkStatus();
34
+
35
+ return {
36
+ adbId,
37
+ stop: async () => {
38
+ await adb.stopEmulator(adbId);
39
+ },
40
+ };
41
+ };
package/src/factory.ts ADDED
@@ -0,0 +1,85 @@
1
+ import {
2
+ DeviceNotFoundError,
3
+ AppNotInstalledError,
4
+ HarnessPlatform,
5
+ } from '@react-native-harness/platforms';
6
+ import {
7
+ AndroidPlatformConfigSchema,
8
+ isAndroidDevicePhysical,
9
+ type AndroidEmulator,
10
+ type AndroidPlatformConfig,
11
+ type PhysicalAndroidDevice,
12
+ } from './config.js';
13
+ import { getAdbId } from './adb-id.js';
14
+ import * as adb from './adb.js';
15
+ import { getDeviceName } from './utils.js';
16
+
17
+ export const androidEmulator = (name: string): AndroidEmulator => ({
18
+ type: 'emulator',
19
+ name,
20
+ });
21
+
22
+ export const physicalAndroidDevice = (
23
+ manufacturer: string,
24
+ model: string
25
+ ): PhysicalAndroidDevice => ({
26
+ type: 'physical',
27
+ manufacturer: manufacturer.toLowerCase(),
28
+ model: model.toLowerCase(),
29
+ });
30
+
31
+ export const androidPlatform = (
32
+ config: AndroidPlatformConfig
33
+ ): HarnessPlatform => ({
34
+ name: config.name,
35
+ getInstance: async () => {
36
+ const parsedConfig = AndroidPlatformConfigSchema.parse(config);
37
+ const adbId = await getAdbId(parsedConfig.device);
38
+
39
+ if (!adbId) {
40
+ throw new DeviceNotFoundError(getDeviceName(parsedConfig.device));
41
+ }
42
+
43
+ const isInstalled = await adb.isAppInstalled(adbId, parsedConfig.bundleId);
44
+
45
+ if (!isInstalled) {
46
+ throw new AppNotInstalledError(
47
+ parsedConfig.bundleId,
48
+ getDeviceName(parsedConfig.device)
49
+ );
50
+ }
51
+
52
+ if (isAndroidDevicePhysical(parsedConfig.device)) {
53
+ // Reverse ports to allow the app to load from a local machine.
54
+ await Promise.all([
55
+ adb.reversePort(adbId, 8081),
56
+ adb.reversePort(adbId, 8080),
57
+ adb.reversePort(adbId, 3001),
58
+ ]);
59
+ }
60
+
61
+ return {
62
+ startApp: async () => {
63
+ await adb.startApp(
64
+ adbId,
65
+ parsedConfig.bundleId,
66
+ parsedConfig.activityName
67
+ );
68
+ },
69
+ restartApp: async () => {
70
+ await adb.stopApp(adbId, parsedConfig.bundleId);
71
+ await adb.startApp(
72
+ adbId,
73
+ parsedConfig.bundleId,
74
+ parsedConfig.activityName
75
+ );
76
+ },
77
+ stopApp: async () => {
78
+ await adb.stopApp(adbId, parsedConfig.bundleId);
79
+ },
80
+ dispose: async () => {
81
+ await adb.stopApp(adbId, parsedConfig.bundleId);
82
+ },
83
+ };
84
+ },
85
+ });
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export {
2
+ androidEmulator,
3
+ physicalAndroidDevice,
4
+ androidPlatform,
5
+ } from './factory.js';
6
+ export type { AndroidPlatformConfig } from './config.js';
package/src/utils.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { isAndroidDeviceEmulator, type AndroidDevice } from './config.js';
2
+
3
+ export const getDeviceName = (device: AndroidDevice): string => {
4
+ if (isAndroidDeviceEmulator(device)) {
5
+ return `${device.name} (emulator)`;
6
+ }
7
+
8
+ return `${device.manufacturer} ${device.model}`;
9
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "../tools"
8
+ },
9
+ {
10
+ "path": "../platforms"
11
+ },
12
+ {
13
+ "path": "./tsconfig.lib.json"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
8
+ "emitDeclarationOnly": false,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["src/**/*.ts"],
13
+ "references": [
14
+ {
15
+ "path": "../tools/tsconfig.lib.json"
16
+ },
17
+ {
18
+ "path": "../platforms/tsconfig.lib.json"
19
+ }
20
+ ]
21
+ }