@react-native-harness/platform-android 1.0.0-alpha.18

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.
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
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,81 @@
1
+ import {
2
+ DeviceNotFoundError,
3
+ AppNotInstalledError,
4
+ HarnessPlatform,
5
+ } from '@react-native-harness/platforms';
6
+ import {
7
+ AndroidPlatformConfigSchema,
8
+ type AndroidEmulator,
9
+ type AndroidPlatformConfig,
10
+ type PhysicalAndroidDevice,
11
+ } from './config.js';
12
+ import { getAdbId } from './adb-id.js';
13
+ import * as adb from './adb.js';
14
+ import { getDeviceName } from './utils.js';
15
+
16
+ export const androidEmulator = (name: string): AndroidEmulator => ({
17
+ type: 'emulator',
18
+ name,
19
+ });
20
+
21
+ export const physicalAndroidDevice = (
22
+ manufacturer: string,
23
+ model: string
24
+ ): PhysicalAndroidDevice => ({
25
+ type: 'physical',
26
+ manufacturer: manufacturer.toLowerCase(),
27
+ model: model.toLowerCase(),
28
+ });
29
+
30
+ export const androidPlatform = (
31
+ config: AndroidPlatformConfig
32
+ ): HarnessPlatform => ({
33
+ name: config.name,
34
+ getInstance: async () => {
35
+ const parsedConfig = AndroidPlatformConfigSchema.parse(config);
36
+ const adbId = await getAdbId(parsedConfig.device);
37
+
38
+ if (!adbId) {
39
+ throw new DeviceNotFoundError(getDeviceName(parsedConfig.device));
40
+ }
41
+
42
+ const isInstalled = await adb.isAppInstalled(adbId, parsedConfig.bundleId);
43
+
44
+ if (!isInstalled) {
45
+ throw new AppNotInstalledError(
46
+ parsedConfig.bundleId,
47
+ getDeviceName(parsedConfig.device)
48
+ );
49
+ }
50
+
51
+ await Promise.all([
52
+ adb.reversePort(adbId, 8081),
53
+ adb.reversePort(adbId, 8080),
54
+ adb.reversePort(adbId, 3001),
55
+ ]);
56
+
57
+ return {
58
+ startApp: async () => {
59
+ await adb.startApp(
60
+ adbId,
61
+ parsedConfig.bundleId,
62
+ parsedConfig.activityName
63
+ );
64
+ },
65
+ restartApp: async () => {
66
+ await adb.stopApp(adbId, parsedConfig.bundleId);
67
+ await adb.startApp(
68
+ adbId,
69
+ parsedConfig.bundleId,
70
+ parsedConfig.activityName
71
+ );
72
+ },
73
+ stopApp: async () => {
74
+ await adb.stopApp(adbId, parsedConfig.bundleId);
75
+ },
76
+ dispose: async () => {
77
+ await adb.stopApp(adbId, parsedConfig.bundleId);
78
+ },
79
+ };
80
+ },
81
+ });
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
+ }