// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as path from "path"; import assert = require("assert"); import { PromiseUtil } from "../../src/common/node/promise"; import { IAndroidRunOptions } from "../../src/extension/launchArgs"; import { ISpawnResult } from "../../src/common/node/childProcess"; import { FileSystem } from "../../src/common/node/fileSystem"; import { Package } from "../../src/common/node/package"; import { Recording, Simulator } from "./processExecution/simulator"; import { AdbHelper } from "../../src/extension/android/adb"; import { APKSerializer } from "./simulators/apkSerializer"; const sampleRNProjectPath = path.join(__dirname, "sampleReactNativeProject"); const processExecutionsRecordingsPath = path.join(__dirname, "processExecutionsRecordings"); // This class simulates calling the React-Native CLI v0.65. export class ReactNative065 { public static DEFAULT_PROJECT_FILE = path.join(sampleRNProjectPath, "package.json"); private static ANDROID_APK_RELATIVE_PATH = "android/app/build/outputs/apk/app-debug.apk"; private projectFileContent!: string; private simulator: Simulator = new Simulator({ beforeStart: () => this.readAndroidPackageName(), // 1. We read the package.json to verify this is a RN project outputBased: [ { eventPattern: /:app:assembleDebug/, action: () => this.createAPK(), // 2. We compile the application. }, { eventPattern: /Installed on [0-9]+ devices*\./, action: () => this.installAppInAllDevices(), // 3. We install it on all available devices. }, ], beforeSuccess: ( stdout: string, stderr: string, // 4. If we didn't had any errors after starting to launch the app, ) => this.launchApp(stdout, stderr), // it means we were succesful }); private recording!: Recording; private androidPackageName!: string; private projectRoot!: string; private androidAPKPath!: string; constructor(private fileSystem: FileSystem, private adbHelper: AdbHelper) { assert(this.fileSystem, "fileSystem shouldn't be null"); } public fromProjectFileContent(content: string): this { this.projectFileContent = content; return this; } public loadRecordingFromName(recordingName: string): Promise { return this.loadRecordingFromFile( path.join(processExecutionsRecordingsPath, `${recordingName}.json`), ); } public loadRecordingFromString(recordingContent: string): Promise { return Promise.resolve(this.loadRecording(JSON.parse(recordingContent))); } public loadRecordingFromFile(recordingPath: string): Promise { return new FileSystem().readFile(recordingPath).then(fileContents => { this.loadRecording(JSON.parse(fileContents.toString())); }); } public loadRecording(recording: Recording): void { assert(recording, "recording shouldn't be null"); this.recording = recording; } public createProject(projectRoot: string, projectName: string): Promise { return Promise.resolve() .then(() => { this.fileSystem.makeDirectoryRecursiveSync(projectRoot); return this.projectFileContent !== undefined ? this.projectFileContent : this.readDefaultProjectFile(); }) .then(defaultContents => { const reactNativeConfiguration = JSON.parse(defaultContents.toString()); reactNativeConfiguration.name = projectName; const reactNativeConfigurationFormatted = JSON.stringify(reactNativeConfiguration); return this.fileSystem.writeFile( this.getPackageJsonPath(projectRoot), reactNativeConfigurationFormatted, ); }) .then(() => { return this.fileSystem.mkDir(this.getAndroidProjectPath(projectRoot)); }); } public runAndroid(runOptions: IAndroidRunOptions): ISpawnResult { this.projectRoot = runOptions.projectRoot; this.simulator.simulate(this.recording).then(() => {}); return this.simulator.spawn(); } private getAndroidProjectPath(projectRoot = this.projectRoot): string { return path.join(projectRoot, "android"); } private getPackageJsonPath(projectRoot: string): string { return new Package(projectRoot, { fileSystem: this.fileSystem }).informationJsonFilePath(); } private readAndroidPackageName(): Promise { return new Package(this.projectRoot, { fileSystem: this.fileSystem }).name().then(name => { this.androidPackageName = `com.${name.toLowerCase()}`; }); } private createAPK(): Promise { return this.isAndroidProjectPresent() .then(isPresent => { return isPresent ? void 0 : Promise.reject( new Error( "The recording expects the Android project to be present, but it's not", ), ); }) .then(() => { this.androidAPKPath = path.join( this.projectRoot, ReactNative065.ANDROID_APK_RELATIVE_PATH, ); return new APKSerializer(this.fileSystem).writeApk(this.androidAPKPath, { packageName: this.androidPackageName, }); }); } private isAndroidProjectPresent(): Promise { // TODO: Make more checks as neccesary for the tests return this.fileSystem.directoryExists(this.getAndroidProjectPath()); } private installAppInAllDevices(): Promise { let devices = this.adbHelper.getConnectedTargets(); return PromiseUtil.reduce(devices, device => this.installAppInDevice(device.id)); } private installAppInDevice(deviceId: string): Promise { throw Error("Mock not implemented"); } private launchApp(stdout: string, stderr: string): Promise { /* Sample output we want to accept: BUILD SUCCESSFUL Total time: 9.052 secs Starting the app (C:\Program Files (x86)\Android\android-sdk/platform-tools/adb shell am start -n com.sampleapplication/.MainActivity)... Starting: Intent { cmp=com.sampleapplication/.MainActivity } Sample output we don't to accept: BUILD SUCCESSFUL Total time: 9.052 secs Starting the app (C:\Program Files (x86)\Android\android-sdk/platform-tools/adb shell am start -n com.sampleapplication/.MainActivity)... Starting: Intent { cmp=com.sampleapplication/.MainActivity } Error: some error happened **/ const succesfulOutputEnd = `Starting the app \\(.*adb shell am start -n ([^ /]+)\/\\.MainActivity\\)\\.\\.\\.\\s+` + `Starting: Intent { cmp=([^ /]+)\/\\.MainActivity }\\s+$`; const matches = stdout.match(new RegExp(succesfulOutputEnd)); if (matches) { if ( matches.length === 3 && matches[1] === this.androidPackageName && matches[2] === this.androidPackageName ) { return this.adbHelper.launchApp(this.projectRoot, this.androidPackageName); } else { return Promise.reject( new Error( "There was an error while trying to match the Starting the app messages." + "Expected to match the pattern and recognize the expected android package name, but it failed." + `Expected android package name: ${ this.androidPackageName }. Actual matches: ${JSON.stringify(matches)}`, ), ); } } else { // The record doesn't indicate that the app was launched, so we don't do anything return Promise.resolve(); } } private readDefaultProjectFile(): Promise { const realFileSystem = new FileSystem(); // We always use the real file system (not the mock one) to read the sample project return realFileSystem.readFile(ReactNative065.DEFAULT_PROJECT_FILE); } }