@react-native-harness/cli 1.0.0-alpha.7
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/LICENSE +20 -0
- package/README.md +7 -0
- package/dist/bundlers/metro.d.ts +5 -0
- package/dist/bundlers/metro.d.ts.map +1 -0
- package/dist/bundlers/metro.js +61 -0
- package/dist/bundlers/webpack.d.ts +2 -0
- package/dist/bundlers/webpack.d.ts.map +1 -0
- package/dist/bundlers/webpack.js +49 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +136 -0
- package/dist/errors/appNotInstalledError.d.ts +7 -0
- package/dist/errors/appNotInstalledError.d.ts.map +1 -0
- package/dist/errors/appNotInstalledError.js +12 -0
- package/dist/errors/bridgeTimeoutError.d.ts +7 -0
- package/dist/errors/bridgeTimeoutError.d.ts.map +1 -0
- package/dist/errors/bridgeTimeoutError.js +12 -0
- package/dist/errors/errorHandler.d.ts +2 -0
- package/dist/errors/errorHandler.d.ts.map +1 -0
- package/dist/errors/errorHandler.js +150 -0
- package/dist/errors/errors.d.ts +46 -0
- package/dist/errors/errors.d.ts.map +1 -0
- package/dist/errors/errors.js +93 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/platforms/android/build.d.ts +5 -0
- package/dist/platforms/android/build.d.ts.map +1 -0
- package/dist/platforms/android/build.js +29 -0
- package/dist/platforms/android/device.d.ts +5 -0
- package/dist/platforms/android/device.d.ts.map +1 -0
- package/dist/platforms/android/device.js +36 -0
- package/dist/platforms/android/emulator.d.ts +11 -0
- package/dist/platforms/android/emulator.d.ts.map +1 -0
- package/dist/platforms/android/emulator.js +110 -0
- package/dist/platforms/android/index.d.ts +4 -0
- package/dist/platforms/android/index.d.ts.map +1 -0
- package/dist/platforms/android/index.js +56 -0
- package/dist/platforms/ios/build.d.ts +7 -0
- package/dist/platforms/ios/build.d.ts.map +1 -0
- package/dist/platforms/ios/build.js +44 -0
- package/dist/platforms/ios/device.d.ts +7 -0
- package/dist/platforms/ios/device.d.ts.map +1 -0
- package/dist/platforms/ios/device.js +51 -0
- package/dist/platforms/ios/index.d.ts +4 -0
- package/dist/platforms/ios/index.d.ts.map +1 -0
- package/dist/platforms/ios/index.js +43 -0
- package/dist/platforms/ios/simulator.d.ts +11 -0
- package/dist/platforms/ios/simulator.d.ts.map +1 -0
- package/dist/platforms/ios/simulator.js +124 -0
- package/dist/platforms/platform-adapter.d.ts +10 -0
- package/dist/platforms/platform-adapter.d.ts.map +1 -0
- package/dist/platforms/platform-adapter.js +1 -0
- package/dist/platforms/platform-registry.d.ts +3 -0
- package/dist/platforms/platform-registry.d.ts.map +1 -0
- package/dist/platforms/platform-registry.js +19 -0
- package/dist/platforms/web/index.d.ts +4 -0
- package/dist/platforms/web/index.d.ts.map +1 -0
- package/dist/platforms/web/index.js +9 -0
- package/dist/process.d.ts +3 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +28 -0
- package/dist/reporters/default-reporter.d.ts +3 -0
- package/dist/reporters/default-reporter.d.ts.map +1 -0
- package/dist/reporters/default-reporter.js +116 -0
- package/dist/reporters/junit-reporter.d.ts +3 -0
- package/dist/reporters/junit-reporter.d.ts.map +1 -0
- package/dist/reporters/junit-reporter.js +119 -0
- package/dist/reporters/live-reporter.d.ts +20 -0
- package/dist/reporters/live-reporter.d.ts.map +1 -0
- package/dist/reporters/live-reporter.js +176 -0
- package/dist/src/reporters/default-reporter.js +135 -0
- package/dist/test-reporter-demo.js +95 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +11 -0
- package/eslint.config.mjs +19 -0
- package/package.json +20 -0
- package/src/bundlers/metro.ts +89 -0
- package/src/commands/test.ts +218 -0
- package/src/errors/errorHandler.ts +196 -0
- package/src/errors/errors.ts +119 -0
- package/src/index.ts +50 -0
- package/src/platforms/android/build.ts +49 -0
- package/src/platforms/android/device.ts +48 -0
- package/src/platforms/android/emulator.ts +139 -0
- package/src/platforms/android/index.ts +87 -0
- package/src/platforms/ios/build.ts +68 -0
- package/src/platforms/ios/device.ts +76 -0
- package/src/platforms/ios/index.ts +66 -0
- package/src/platforms/ios/simulator.ts +166 -0
- package/src/platforms/platform-adapter.ts +11 -0
- package/src/platforms/platform-registry.ts +24 -0
- package/src/platforms/web/index.ts +16 -0
- package/src/process.ts +33 -0
- package/src/utils.ts +12 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +33 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { testCommand } from './commands/test.js';
|
|
6
|
+
import { handleError } from './errors/errorHandler.js';
|
|
7
|
+
import { logger } from '@react-native-harness/tools';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const packageJsonPath = join(__dirname, '../package.json');
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
12
|
+
const program = new Command();
|
|
13
|
+
logger.setVerbose(true);
|
|
14
|
+
program
|
|
15
|
+
.name('react-native-harness')
|
|
16
|
+
.description('React Native Test Harness - A comprehensive testing framework for React Native applications')
|
|
17
|
+
.version(packageJson.version);
|
|
18
|
+
program
|
|
19
|
+
.command('test')
|
|
20
|
+
.description('Run tests using the specified runner')
|
|
21
|
+
.argument('[runner]', 'test runner name (uses defaultRunner from config if not specified)')
|
|
22
|
+
.argument('[pattern]', 'glob pattern to match test files (uses config.include if not specified)')
|
|
23
|
+
.action(async (runner, pattern) => {
|
|
24
|
+
try {
|
|
25
|
+
await testCommand(runner, pattern);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
handleError(error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
process.on('uncaughtException', (error) => {
|
|
33
|
+
handleError(error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
program.parse();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const buildAndroidApp: () => Promise<void>;
|
|
2
|
+
export declare const installApp: (deviceId: string) => Promise<void>;
|
|
3
|
+
export declare const killApp: (deviceId: string, bundleId: string) => Promise<void>;
|
|
4
|
+
export declare const runApp: (deviceId: string, bundleId: string, activityName: string) => Promise<void>;
|
|
5
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/platforms/android/build.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,QAAa,OAAO,CAAC,IAAI,CAEpD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,IAAI,CAiB/D,CAAC;AAEF,eAAO,MAAM,OAAO,GAClB,UAAU,MAAM,EAChB,UAAU,MAAM,KACf,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF,eAAO,MAAM,MAAM,GACjB,UAAU,MAAM,EAChB,UAAU,MAAM,EAChB,cAAc,MAAM,KACnB,OAAO,CAAC,IAAI,CAWd,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawn } from '@react-native-harness/tools';
|
|
3
|
+
export const buildAndroidApp = async () => {
|
|
4
|
+
await spawn('react-native', ['build-android', '--tasks', 'assembleDebug']);
|
|
5
|
+
};
|
|
6
|
+
export const installApp = async (deviceId) => {
|
|
7
|
+
await spawn('adb', [
|
|
8
|
+
'-s',
|
|
9
|
+
deviceId,
|
|
10
|
+
'install',
|
|
11
|
+
'-r',
|
|
12
|
+
path.join(process.cwd(), 'android', 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk'),
|
|
13
|
+
]);
|
|
14
|
+
};
|
|
15
|
+
export const killApp = async (deviceId, bundleId) => {
|
|
16
|
+
await spawn('adb', ['-s', deviceId, 'shell', 'am', 'force-stop', bundleId]);
|
|
17
|
+
};
|
|
18
|
+
export const runApp = async (deviceId, bundleId, activityName) => {
|
|
19
|
+
await killApp(deviceId, bundleId);
|
|
20
|
+
await spawn('adb', [
|
|
21
|
+
'-s',
|
|
22
|
+
deviceId,
|
|
23
|
+
'shell',
|
|
24
|
+
'am',
|
|
25
|
+
'start',
|
|
26
|
+
'-n',
|
|
27
|
+
`${bundleId}/${activityName}`,
|
|
28
|
+
]);
|
|
29
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const killApp: (deviceId: string, bundleId: string) => Promise<void>;
|
|
2
|
+
export declare const runApp: (deviceId: string, bundleId: string) => Promise<void>;
|
|
3
|
+
export declare const isAppInstalled: (deviceId: string, bundleId: string) => Promise<boolean>;
|
|
4
|
+
export declare const reversePort: (port: number) => Promise<void>;
|
|
5
|
+
//# sourceMappingURL=device.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../../src/platforms/android/device.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,OAAO,GAClB,UAAU,MAAM,EAChB,UAAU,MAAM,KACf,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF,eAAO,MAAM,MAAM,GACjB,UAAU,MAAM,EAChB,UAAU,MAAM,KACf,OAAO,CAAC,IAAI,CAWd,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,UAAU,MAAM,EAChB,UAAU,MAAM,KACf,OAAO,CAAC,OAAO,CAejB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,IAAI,CAE5D,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from '@react-native-harness/tools';
|
|
2
|
+
export const killApp = async (deviceId, bundleId) => {
|
|
3
|
+
await spawn('adb', ['-s', deviceId, 'shell', 'am', 'force-stop', bundleId]);
|
|
4
|
+
};
|
|
5
|
+
export const runApp = async (deviceId, bundleId) => {
|
|
6
|
+
await killApp(deviceId, bundleId);
|
|
7
|
+
await spawn('adb', [
|
|
8
|
+
'-s',
|
|
9
|
+
deviceId,
|
|
10
|
+
'shell',
|
|
11
|
+
'am',
|
|
12
|
+
'start',
|
|
13
|
+
'-n',
|
|
14
|
+
`${bundleId}/.MainActivity`,
|
|
15
|
+
]);
|
|
16
|
+
};
|
|
17
|
+
export const isAppInstalled = async (deviceId, bundleId) => {
|
|
18
|
+
try {
|
|
19
|
+
const { stdout } = await spawn('adb', [
|
|
20
|
+
'-s',
|
|
21
|
+
deviceId,
|
|
22
|
+
'shell',
|
|
23
|
+
'pm',
|
|
24
|
+
'list',
|
|
25
|
+
'packages',
|
|
26
|
+
bundleId,
|
|
27
|
+
]);
|
|
28
|
+
return stdout.trim() !== '';
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export const reversePort = async (port) => {
|
|
35
|
+
await spawn('adb', ['reverse', `tcp:${port}`, `tcp:${port}`]);
|
|
36
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ChildProcess } from 'node:child_process';
|
|
2
|
+
export type AndroidEmulatorStatus = 'running' | 'loading' | 'stopped';
|
|
3
|
+
export declare const getEmulatorNameFromId: (emulatorId: string) => Promise<string | null>;
|
|
4
|
+
export declare const getEmulatorDeviceId: (avdName: string) => Promise<string | null>;
|
|
5
|
+
export declare const getEmulatorStatus: (avdName: string) => Promise<AndroidEmulatorStatus>;
|
|
6
|
+
export declare const runEmulator: (name: string) => Promise<ChildProcess>;
|
|
7
|
+
export declare const stopEmulator: (avdName: string) => Promise<void>;
|
|
8
|
+
export declare const isAppInstalled: (emulatorId: string, bundleId: string) => Promise<boolean>;
|
|
9
|
+
export declare const reversePort: (port: number) => Promise<void>;
|
|
10
|
+
export declare const getEmulatorScreenshot: (emulatorId: string, name?: string) => Promise<string>;
|
|
11
|
+
//# sourceMappingURL=emulator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emulator.d.ts","sourceRoot":"","sources":["../../../src/platforms/android/emulator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEtE,eAAO,MAAM,qBAAqB,GAChC,YAAY,MAAM,KACjB,OAAO,CAAC,MAAM,GAAG,IAAI,CAQvB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,MAAM,KACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAmBvB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,SAAS,MAAM,KACd,OAAO,CAAC,qBAAqB,CAc/B,CAAC;AAEF,eAAO,MAAM,WAAW,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,YAAY,CA2BpE,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CAQhE,CAAC;AAOF,eAAO,MAAM,cAAc,GACzB,YAAY,MAAM,EAClB,UAAU,MAAM,KACf,OAAO,CAAC,OAAO,CAOjB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,IAAI,CAE5D,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,YAAY,MAAM,EAClB,OAAM,MAGsB,KAC3B,OAAO,CAAC,MAAM,CAchB,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { spawn } from '@react-native-harness/tools';
|
|
2
|
+
export const getEmulatorNameFromId = async (emulatorId) => {
|
|
3
|
+
try {
|
|
4
|
+
const { stdout } = await spawn('adb', ['-s', emulatorId, 'emu', 'avd', 'name']);
|
|
5
|
+
const avdName = stdout.split('\n')[0].trim();
|
|
6
|
+
return avdName || null;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export const getEmulatorDeviceId = async (avdName) => {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout } = await spawn('adb', ['devices']);
|
|
15
|
+
const lines = stdout.split('\n');
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
const parts = line.trim().split('\t');
|
|
18
|
+
if (parts.length === 2 && parts[0].startsWith('emulator-')) {
|
|
19
|
+
const emulatorId = parts[0].trim();
|
|
20
|
+
const name = await getEmulatorNameFromId(emulatorId);
|
|
21
|
+
if (name === avdName) {
|
|
22
|
+
return emulatorId;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export const getEmulatorStatus = async (avdName) => {
|
|
33
|
+
const emulatorId = await getEmulatorDeviceId(avdName);
|
|
34
|
+
if (!emulatorId) {
|
|
35
|
+
return 'stopped';
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
// Check if device is fully booted by checking boot completion
|
|
39
|
+
const { stdout } = await spawn('adb', ['-s', emulatorId, 'shell', 'getprop', 'sys.boot_completed']);
|
|
40
|
+
const bootCompleted = stdout.trim() === '1';
|
|
41
|
+
return bootCompleted ? 'running' : 'loading';
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return 'loading';
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export const runEmulator = async (name) => {
|
|
48
|
+
// Start the emulator
|
|
49
|
+
const process = spawn('emulator', ['-avd', name]);
|
|
50
|
+
const nodeChildProcess = await process.nodeChildProcess;
|
|
51
|
+
// Poll for emulator status until it's fully running
|
|
52
|
+
const checkStatus = async () => {
|
|
53
|
+
const status = await getEmulatorStatus(name);
|
|
54
|
+
if (status === 'running') {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
else if (status === 'loading') {
|
|
58
|
+
// Check again in 2 seconds
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
60
|
+
await checkStatus();
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Still stopped, check again in 1 second
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
65
|
+
await checkStatus();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// Start checking status after a brief delay to allow emulator to start
|
|
69
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
70
|
+
await checkStatus();
|
|
71
|
+
return nodeChildProcess;
|
|
72
|
+
};
|
|
73
|
+
export const stopEmulator = async (avdName) => {
|
|
74
|
+
// First, get the emulator device ID
|
|
75
|
+
const emulatorId = await getEmulatorDeviceId(avdName);
|
|
76
|
+
if (!emulatorId) {
|
|
77
|
+
return; // No emulator running, nothing to stop
|
|
78
|
+
}
|
|
79
|
+
await stopEmulatorById(emulatorId);
|
|
80
|
+
};
|
|
81
|
+
const stopEmulatorById = async (emulatorId) => {
|
|
82
|
+
// Stop the emulator using the found ID
|
|
83
|
+
await spawn('adb', ['-s', emulatorId, 'emu', 'kill']);
|
|
84
|
+
};
|
|
85
|
+
export const isAppInstalled = async (emulatorId, bundleId) => {
|
|
86
|
+
try {
|
|
87
|
+
const { stdout } = await spawn('adb', ['-s', emulatorId, 'shell', 'pm', 'list', 'packages', bundleId]);
|
|
88
|
+
return stdout.trim() !== '';
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
export const reversePort = async (port) => {
|
|
95
|
+
await spawn('adb', ['reverse', `tcp:${port}`, `tcp:${port}`]);
|
|
96
|
+
};
|
|
97
|
+
export const getEmulatorScreenshot = async (emulatorId, name = `${emulatorId}-${new Date()
|
|
98
|
+
.toISOString()
|
|
99
|
+
.replace(/:/g, '-')
|
|
100
|
+
.replace(/\//g, '-')}.png`) => {
|
|
101
|
+
// Use screencap to save directly to device, then pull the file
|
|
102
|
+
const devicePath = '/sdcard/screenshot.png';
|
|
103
|
+
// Take screenshot and save to device
|
|
104
|
+
await spawn('adb', ['-s', emulatorId, 'shell', 'screencap', '-p', devicePath]);
|
|
105
|
+
// Pull the file from device to local
|
|
106
|
+
await spawn('adb', ['-s', emulatorId, 'pull', devicePath, name]);
|
|
107
|
+
// Clean up the file on device
|
|
108
|
+
await spawn('adb', ['-s', emulatorId, 'shell', 'rm', devicePath]);
|
|
109
|
+
return name;
|
|
110
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/platforms/android/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAa9D,QAAA,MAAM,sBAAsB,EAAE,eAgE7B,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { assertAndroidRunnerConfig, } from '@react-native-harness/config';
|
|
2
|
+
import { logger } from '@react-native-harness/tools';
|
|
3
|
+
import { runEmulator, getEmulatorDeviceId, reversePort, isAppInstalled, getEmulatorStatus, } from './emulator.js';
|
|
4
|
+
import { runApp, killApp } from './build.js';
|
|
5
|
+
import { killWithAwait } from '../../process.js';
|
|
6
|
+
import { runMetro } from '../../bundlers/metro.js';
|
|
7
|
+
import { AppNotInstalledError } from '../../errors/errors.js';
|
|
8
|
+
const androidPlatformAdapter = {
|
|
9
|
+
name: 'android',
|
|
10
|
+
getEnvironment: async (runner) => {
|
|
11
|
+
assertAndroidRunnerConfig(runner);
|
|
12
|
+
let emulator = null;
|
|
13
|
+
const emulatorStatus = await getEmulatorStatus(runner.deviceId);
|
|
14
|
+
logger.debug(`Emulator status: ${emulatorStatus}`);
|
|
15
|
+
const metroPromise = runMetro();
|
|
16
|
+
if (emulatorStatus === 'stopped') {
|
|
17
|
+
logger.debug(`Emulator ${runner.deviceId} is stopped, starting it`);
|
|
18
|
+
emulator = await runEmulator(runner.deviceId);
|
|
19
|
+
}
|
|
20
|
+
const deviceId = await getEmulatorDeviceId(runner.deviceId);
|
|
21
|
+
logger.debug(`Device ID: ${deviceId}`);
|
|
22
|
+
if (!deviceId) {
|
|
23
|
+
throw new Error('Emulator not found');
|
|
24
|
+
}
|
|
25
|
+
await Promise.all([
|
|
26
|
+
reversePort(8081),
|
|
27
|
+
reversePort(8080),
|
|
28
|
+
reversePort(3001),
|
|
29
|
+
]);
|
|
30
|
+
logger.debug('Ports reversed');
|
|
31
|
+
const isInstalled = await isAppInstalled(deviceId, runner.bundleId);
|
|
32
|
+
logger.debug(`App is installed: ${isInstalled}`);
|
|
33
|
+
if (!isInstalled) {
|
|
34
|
+
throw new AppNotInstalledError(runner.deviceId, runner.bundleId, 'android');
|
|
35
|
+
}
|
|
36
|
+
logger.debug('Waiting for Metro to start');
|
|
37
|
+
const metro = await metroPromise;
|
|
38
|
+
logger.debug('Metro started');
|
|
39
|
+
logger.debug('Running app');
|
|
40
|
+
await runApp(deviceId, runner.bundleId, runner.activityName);
|
|
41
|
+
logger.debug('App running');
|
|
42
|
+
return {
|
|
43
|
+
restart: async () => {
|
|
44
|
+
await runApp(runner.deviceId, runner.bundleId, runner.activityName);
|
|
45
|
+
},
|
|
46
|
+
dispose: async () => {
|
|
47
|
+
await killApp(deviceId, runner.bundleId);
|
|
48
|
+
if (emulator) {
|
|
49
|
+
await killWithAwait(emulator);
|
|
50
|
+
}
|
|
51
|
+
await killWithAwait(metro);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
export default androidPlatformAdapter;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const listDevices: () => Promise<any>;
|
|
2
|
+
export declare const getDeviceByName: (simulatorName: string, systemVersion: string) => Promise<any | null>;
|
|
3
|
+
export declare const listApps: (udid: string) => Promise<string[]>;
|
|
4
|
+
export declare const isAppInstalled: (udid: string, bundleId: string) => Promise<boolean>;
|
|
5
|
+
export declare const runApp: (udid: string, appName: string) => Promise<void>;
|
|
6
|
+
export declare const killApp: (udid: string, appName: string) => Promise<void>;
|
|
7
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/platforms/ios/build.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,GAAG,CAQ/C,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,eAAe,MAAM,EACrB,eAAe,MAAM,KACpB,OAAO,CAAC,GAAG,GAAG,IAAI,CAqBpB,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,MAAM,EAAE,CAY7D,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EACZ,UAAU,MAAM,KACf,OAAO,CAAC,OAAO,CAGjB,CAAC;AAEF,eAAO,MAAM,MAAM,GAAU,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CAGxE,CAAC;AAEF,eAAO,MAAM,OAAO,GAAU,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CAEzE,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn, spawnAndForget } from '@react-native-harness/tools';
|
|
2
|
+
export const listDevices = async () => {
|
|
3
|
+
const { stdout } = await spawn('xcrun', [
|
|
4
|
+
'simctl',
|
|
5
|
+
'list',
|
|
6
|
+
'devices',
|
|
7
|
+
'--json',
|
|
8
|
+
]);
|
|
9
|
+
return JSON.parse(stdout);
|
|
10
|
+
};
|
|
11
|
+
export const getDeviceByName = async (simulatorName, systemVersion) => {
|
|
12
|
+
const devices = await listDevices();
|
|
13
|
+
const expectedRuntimeId = `com.apple.CoreSimulator.SimRuntime.iOS-${systemVersion.replace(/\./, '-')}`;
|
|
14
|
+
const runtime = devices.devices[expectedRuntimeId];
|
|
15
|
+
if (!runtime) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const runtimeDevices = devices.devices[runtime];
|
|
19
|
+
const device = runtimeDevices.find((d) => d.name === simulatorName);
|
|
20
|
+
if (device) {
|
|
21
|
+
return device;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
export const listApps = async (udid) => {
|
|
26
|
+
const { stdout: plistOutput } = await spawn('xcrun', [
|
|
27
|
+
'simctl',
|
|
28
|
+
'listapps',
|
|
29
|
+
udid,
|
|
30
|
+
]);
|
|
31
|
+
const { stdout: jsonOutput } = await spawn('plutil', ['-convert', 'json', '-o', '-', '-'], { stdin: { string: plistOutput } });
|
|
32
|
+
return Object.keys(JSON.parse(jsonOutput));
|
|
33
|
+
};
|
|
34
|
+
export const isAppInstalled = async (udid, bundleId) => {
|
|
35
|
+
const appList = await listApps(udid);
|
|
36
|
+
return appList.includes(bundleId);
|
|
37
|
+
};
|
|
38
|
+
export const runApp = async (udid, appName) => {
|
|
39
|
+
await killApp(udid, appName);
|
|
40
|
+
await spawn('xcrun', ['simctl', 'launch', udid, appName]);
|
|
41
|
+
};
|
|
42
|
+
export const killApp = async (udid, appName) => {
|
|
43
|
+
await spawnAndForget('xcrun', ['simctl', 'terminate', udid, appName]);
|
|
44
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const listDevices: () => Promise<any>;
|
|
2
|
+
export declare const getDeviceByName: (simulatorName: string) => Promise<any | null>;
|
|
3
|
+
export declare const listApps: (udid: string) => Promise<string[]>;
|
|
4
|
+
export declare const isAppInstalled: (simulatorName: string, bundleId: string) => Promise<boolean>;
|
|
5
|
+
export declare const runApp: (simulatorName: string, appName: string) => Promise<void>;
|
|
6
|
+
export declare const killApp: (simulatorName: string, appName: string) => Promise<void>;
|
|
7
|
+
//# sourceMappingURL=device.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../../src/platforms/ios/device.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,GAAG,CAQ/C,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,eAAe,MAAM,KACpB,OAAO,CAAC,GAAG,GAAG,IAAI,CAapB,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,MAAM,EAAE,CAY7D,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,eAAe,MAAM,EACrB,UAAU,MAAM,KACf,OAAO,CAAC,OAAO,CASjB,CAAC;AAEF,eAAO,MAAM,MAAM,GACjB,eAAe,MAAM,EACrB,SAAS,MAAM,KACd,OAAO,CAAC,IAAI,CAGd,CAAC;AAEF,eAAO,MAAM,OAAO,GAClB,eAAe,MAAM,EACrB,SAAS,MAAM,KACd,OAAO,CAAC,IAAI,CAOd,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { spawn, spawnAndForget } from '@react-native-harness/tools';
|
|
2
|
+
export const listDevices = async () => {
|
|
3
|
+
const { stdout } = await spawn('xcrun', [
|
|
4
|
+
'simctl',
|
|
5
|
+
'list',
|
|
6
|
+
'devices',
|
|
7
|
+
'--json',
|
|
8
|
+
]);
|
|
9
|
+
return JSON.parse(stdout);
|
|
10
|
+
};
|
|
11
|
+
export const getDeviceByName = async (simulatorName) => {
|
|
12
|
+
const devices = await listDevices();
|
|
13
|
+
for (const runtime in devices.devices) {
|
|
14
|
+
const runtimeDevices = devices.devices[runtime];
|
|
15
|
+
for (const device of runtimeDevices) {
|
|
16
|
+
if (device.name === simulatorName && device.isAvailable) {
|
|
17
|
+
return device;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
export const listApps = async (udid) => {
|
|
24
|
+
const { stdout: plistOutput } = await spawn('xcrun', [
|
|
25
|
+
'simctl',
|
|
26
|
+
'listapps',
|
|
27
|
+
udid,
|
|
28
|
+
]);
|
|
29
|
+
const { stdout: jsonOutput } = await spawn('plutil', ['-convert', 'json', '-o', '-', '-'], { stdin: { string: plistOutput } });
|
|
30
|
+
return Object.keys(JSON.parse(jsonOutput));
|
|
31
|
+
};
|
|
32
|
+
export const isAppInstalled = async (simulatorName, bundleId) => {
|
|
33
|
+
const device = await getDeviceByName(simulatorName);
|
|
34
|
+
if (!device) {
|
|
35
|
+
throw new Error(`Simulator ${simulatorName} not found`);
|
|
36
|
+
}
|
|
37
|
+
const appList = await listApps(device.udid);
|
|
38
|
+
return appList.includes(bundleId);
|
|
39
|
+
};
|
|
40
|
+
export const runApp = async (simulatorName, appName) => {
|
|
41
|
+
await killApp(simulatorName, appName);
|
|
42
|
+
await spawn('xcrun', ['simctl', 'launch', simulatorName, appName]);
|
|
43
|
+
};
|
|
44
|
+
export const killApp = async (simulatorName, appName) => {
|
|
45
|
+
await spawnAndForget('xcrun', [
|
|
46
|
+
'simctl',
|
|
47
|
+
'terminate',
|
|
48
|
+
simulatorName,
|
|
49
|
+
appName,
|
|
50
|
+
]);
|
|
51
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/platforms/ios/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAa9D,QAAA,MAAM,kBAAkB,EAAE,eA8CzB,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { assertIOSRunnerConfig, } from '@react-native-harness/config';
|
|
2
|
+
import { getSimulatorDeviceId, getSimulatorStatus, runSimulator, stopSimulator, } from './simulator.js';
|
|
3
|
+
import { isAppInstalled, runApp, killApp } from './build.js';
|
|
4
|
+
import { killWithAwait } from '../../process.js';
|
|
5
|
+
import { runMetro } from '../../bundlers/metro.js';
|
|
6
|
+
import { AppNotInstalledError } from '../../errors/errors.js';
|
|
7
|
+
import { assert } from '../../utils.js';
|
|
8
|
+
const iosPlatformAdapter = {
|
|
9
|
+
name: 'ios',
|
|
10
|
+
getEnvironment: async (runner) => {
|
|
11
|
+
assertIOSRunnerConfig(runner);
|
|
12
|
+
// TODO: system version is also important as there may be two emulators with the same name
|
|
13
|
+
// but different system versions
|
|
14
|
+
let shouldStopSimulator = false;
|
|
15
|
+
const udid = await getSimulatorDeviceId(runner.deviceId, runner.systemVersion);
|
|
16
|
+
assert(!!udid, 'Simulator not found');
|
|
17
|
+
const simulatorStatus = await getSimulatorStatus(udid);
|
|
18
|
+
const metroPromise = runMetro();
|
|
19
|
+
if (simulatorStatus === 'stopped') {
|
|
20
|
+
await runSimulator(udid);
|
|
21
|
+
shouldStopSimulator = true;
|
|
22
|
+
}
|
|
23
|
+
const isInstalled = await isAppInstalled(udid, runner.bundleId);
|
|
24
|
+
if (!isInstalled) {
|
|
25
|
+
throw new AppNotInstalledError(runner.deviceId, runner.bundleId, 'ios');
|
|
26
|
+
}
|
|
27
|
+
const metro = await metroPromise;
|
|
28
|
+
await runApp(udid, runner.bundleId);
|
|
29
|
+
return {
|
|
30
|
+
restart: async () => {
|
|
31
|
+
await runApp(udid, runner.bundleId);
|
|
32
|
+
},
|
|
33
|
+
dispose: async () => {
|
|
34
|
+
await killApp(udid, runner.bundleId);
|
|
35
|
+
if (shouldStopSimulator) {
|
|
36
|
+
await stopSimulator(udid);
|
|
37
|
+
}
|
|
38
|
+
await killWithAwait(metro);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
export default iosPlatformAdapter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type IOSSimulatorStatus = 'stopped' | 'loading' | 'running';
|
|
2
|
+
export declare const getSimulatorDeviceId: (simulatorName: string, systemVersion: string) => Promise<string | null>;
|
|
3
|
+
export declare const getAvailableSimulators: () => Promise<Array<{
|
|
4
|
+
name: string;
|
|
5
|
+
udid: string;
|
|
6
|
+
runtime: string;
|
|
7
|
+
}>>;
|
|
8
|
+
export declare const getSimulatorStatus: (udid: string) => Promise<IOSSimulatorStatus>;
|
|
9
|
+
export declare const runSimulator: (udid: string) => Promise<void>;
|
|
10
|
+
export declare const stopSimulator: (udid: string) => Promise<void>;
|
|
11
|
+
//# sourceMappingURL=simulator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simulator.d.ts","sourceRoot":"","sources":["../../../src/platforms/ios/simulator.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEnE,eAAO,MAAM,oBAAoB,GAC/B,eAAe,MAAM,EACrB,eAAe,MAAM,KACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CA8BvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,QAAa,OAAO,CACrD,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAmCvD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,MAAM,MAAM,KACX,OAAO,CAAC,kBAAkB,CAgC5B,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,IAAI,CAiC7D,CAAC;AAEF,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,IAAI,CAE9D,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { spawn } from '@react-native-harness/tools';
|
|
2
|
+
export const getSimulatorDeviceId = async (simulatorName, systemVersion) => {
|
|
3
|
+
try {
|
|
4
|
+
const { stdout } = await spawn('xcrun', [
|
|
5
|
+
'simctl',
|
|
6
|
+
'list',
|
|
7
|
+
'devices',
|
|
8
|
+
'--json',
|
|
9
|
+
]);
|
|
10
|
+
const devices = JSON.parse(stdout);
|
|
11
|
+
const expectedRuntimeId = `com.apple.CoreSimulator.SimRuntime.iOS-${systemVersion.replace(/\./, '-')}`;
|
|
12
|
+
const runtime = devices.devices[expectedRuntimeId];
|
|
13
|
+
if (!runtime) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const device = runtime.find((d) => d.name === simulatorName);
|
|
17
|
+
if (device) {
|
|
18
|
+
return device.udid;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export const getAvailableSimulators = async () => {
|
|
27
|
+
try {
|
|
28
|
+
const { stdout } = await spawn('xcrun', [
|
|
29
|
+
'simctl',
|
|
30
|
+
'list',
|
|
31
|
+
'devices',
|
|
32
|
+
'--json',
|
|
33
|
+
]);
|
|
34
|
+
const devices = JSON.parse(stdout);
|
|
35
|
+
const simulators = [];
|
|
36
|
+
for (const runtime in devices.devices) {
|
|
37
|
+
if (runtime.includes('iOS')) {
|
|
38
|
+
const runtimeDevices = devices.devices[runtime];
|
|
39
|
+
runtimeDevices.forEach((device) => {
|
|
40
|
+
if (device.isAvailable) {
|
|
41
|
+
simulators.push({
|
|
42
|
+
name: device.name,
|
|
43
|
+
udid: device.udid,
|
|
44
|
+
runtime: runtime,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return simulators;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
export const getSimulatorStatus = async (udid) => {
|
|
57
|
+
try {
|
|
58
|
+
const { stdout } = await spawn('xcrun', [
|
|
59
|
+
'simctl',
|
|
60
|
+
'list',
|
|
61
|
+
'devices',
|
|
62
|
+
'--json',
|
|
63
|
+
]);
|
|
64
|
+
const devices = JSON.parse(stdout);
|
|
65
|
+
for (const runtime in devices.devices) {
|
|
66
|
+
if (runtime.includes('iOS')) {
|
|
67
|
+
const runtimeDevices = devices.devices[runtime];
|
|
68
|
+
const device = runtimeDevices.find((d) => d.udid === udid);
|
|
69
|
+
if (device) {
|
|
70
|
+
switch (device.state) {
|
|
71
|
+
case 'Booted':
|
|
72
|
+
return 'running';
|
|
73
|
+
case 'Booting':
|
|
74
|
+
return 'loading';
|
|
75
|
+
default:
|
|
76
|
+
return 'stopped';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return 'stopped';
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return 'stopped';
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
export const runSimulator = async (udid) => {
|
|
88
|
+
try {
|
|
89
|
+
await spawn('xcrun', ['simctl', 'boot', udid]);
|
|
90
|
+
}
|
|
91
|
+
catch (bootError) {
|
|
92
|
+
// Ignore if simulator is already booted
|
|
93
|
+
if (!bootError.stderr?.includes('Unable to boot device in current state: Booted')) {
|
|
94
|
+
throw bootError;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await spawn('open', ['-a', 'Simulator']);
|
|
98
|
+
let attempts = 0;
|
|
99
|
+
while (true) {
|
|
100
|
+
attempts++;
|
|
101
|
+
const status = await getSimulatorStatus(udid);
|
|
102
|
+
if (status === 'running') {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
if (attempts > 10) {
|
|
106
|
+
throw new Error('Simulator not running');
|
|
107
|
+
}
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
export const stopSimulator = async (udid) => {
|
|
112
|
+
await stopSimulatorById(udid);
|
|
113
|
+
};
|
|
114
|
+
const stopSimulatorById = async (udid) => {
|
|
115
|
+
try {
|
|
116
|
+
await spawn('xcrun', ['simctl', 'shutdown', udid]);
|
|
117
|
+
}
|
|
118
|
+
catch (shutdownError) {
|
|
119
|
+
// Ignore if simulator is already shut down
|
|
120
|
+
if (!shutdownError.stderr?.includes('Unable to shutdown device in current state: Shutdown')) {
|
|
121
|
+
throw shutdownError;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TestRunnerConfig } from '@react-native-harness/config';
|
|
2
|
+
export type Environment = {
|
|
3
|
+
restart: () => Promise<void>;
|
|
4
|
+
dispose: () => Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export type PlatformAdapter = {
|
|
7
|
+
name: string;
|
|
8
|
+
getEnvironment: (runner: TestRunnerConfig) => Promise<Environment>;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=platform-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-adapter.d.ts","sourceRoot":"","sources":["../../src/platforms/platform-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEhE,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CACpE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|