diff --git a/test/extension/commands/configEASBuild.test.ts b/test/extension/commands/configEASBuild.test.ts new file mode 100644 index 000000000..d07c79400 --- /dev/null +++ b/test/extension/commands/configEASBuild.test.ts @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +import assert = require("assert"); +import Sinon = require("sinon"); +import proxyquire = require("proxyquire"); + +suite("configEASBuildCommand", function () { + function createMockProject(projectPath: string, isExpoStub: Sinon.SinonStub): any { + return { + getPackager: () => ({ + getProjectPath: () => projectPath, + }), + getExponentHelper: () => ({ + isExpoManagedApp: isExpoStub, + }), + }; + } + + function createCommandModule( + existsStub: Sinon.SinonStub, + executeStub: Sinon.SinonStub, + logger = { + info: Sinon.stub(), + error: Sinon.stub(), + warning: Sinon.stub(), + debug: Sinon.stub(), + }, + ) { + class FakeFileSystem { + public exists = existsStub; + } + + class FakeCommandExecutor { + public execute = executeStub; + } + + const module = proxyquire.noCallThru()("../../../src/extension/commands/configEASBuild", { + "../../common/node/fileSystem": { + FileSystem: FakeFileSystem, + }, + "../../common/commandExecutor": { + CommandExecutor: FakeCommandExecutor, + }, + "../log/OutputChannelLogger": { + OutputChannelLogger: { + getMainChannel: () => logger, + }, + }, + }) as typeof import("../../../src/extension/commands/configEASBuild"); + + return { + ConfigEASBuild: module.ConfigEASBuild, + logger, + }; + } + + async function runCommand( + commandClass: typeof import("../../../src/extension/commands/configEASBuild").ConfigEASBuild, + projectPath: string, + isExpoStub: Sinon.SinonStub, + ): Promise { + const command = commandClass.formInstance(); + (command as any).project = createMockProject(projectPath, isExpoStub); + (command as any).nodeModulesRoot = `${projectPath}/node_modules`; + await command.baseFn(); + } + + test("should reject non-Expo projects", async function () { + const existsStub = Sinon.stub().returns(Promise.resolve(false)); + const executeStub = Sinon.stub().returns(Promise.resolve()); + const isExpoStub = Sinon.stub().returns(Promise.resolve(false)); + const { ConfigEASBuild } = createCommandModule(existsStub, executeStub); + + await assert.rejects( + runCommand(ConfigEASBuild, "/workspace/app", isExpoStub), + /not an Expo application/, + ); + + assert.strictEqual(isExpoStub.calledWithExactly(true), true); + assert.strictEqual(existsStub.called, false); + assert.strictEqual(executeStub.called, false); + }); + + test("should return early when eas.json already exists", async function () { + const existsStub = Sinon.stub().returns(Promise.resolve(true)); + const executeStub = Sinon.stub().returns(Promise.resolve()); + const isExpoStub = Sinon.stub().returns(Promise.resolve(true)); + const { ConfigEASBuild, logger } = createCommandModule(existsStub, executeStub); + + await runCommand(ConfigEASBuild, "/workspace/app", isExpoStub); + + assert.strictEqual(existsStub.calledWithExactly("/workspace/app/eas.json"), true); + assert.strictEqual(executeStub.called, false); + assert.strictEqual( + logger.info.args.some((args: string[]) => + args[0].includes("eas.json file already existing"), + ), + true, + ); + }); + + test("should run eas build configure for Expo projects without eas.json", async function () { + const existsStub = Sinon.stub().returns(Promise.resolve(false)); + const executeStub = Sinon.stub().returns(Promise.resolve()); + const isExpoStub = Sinon.stub().returns(Promise.resolve(true)); + const { ConfigEASBuild, logger } = createCommandModule(existsStub, executeStub); + + await runCommand(ConfigEASBuild, "/workspace/app", isExpoStub); + + assert.strictEqual( + executeStub.calledWithExactly("eas build:configure --platform all"), + true, + ); + assert.strictEqual( + logger.info.args.some((args: string[]) => + args[0].includes("Create EAS build config file successfully"), + ), + true, + ); + }); +}); diff --git a/test/extension/commands/enableHermes.test.ts b/test/extension/commands/enableHermes.test.ts new file mode 100644 index 000000000..5a8dbf68d --- /dev/null +++ b/test/extension/commands/enableHermes.test.ts @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +import assert = require("assert"); +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import Sinon = require("sinon"); +import proxyquire = require("proxyquire"); + +suite("enableHermesCommand", function () { + let tempDir: string; + + function removeDirRecursive(dirPath: string): void { + if (fs.existsSync(dirPath)) { + fs.readdirSync(dirPath).forEach(file => { + const currentPath = path.join(dirPath, file); + if (fs.lstatSync(currentPath).isDirectory()) { + removeDirRecursive(currentPath); + } else { + fs.unlinkSync(currentPath); + } + }); + fs.rmdirSync(dirPath); + } + } + + function createMockProject(projectPath: string): any { + return { + getPackager: () => ({ + getProjectPath: () => projectPath, + }), + }; + } + + function createCommandModule( + showQuickPickStub: Sinon.SinonStub, + writeFileStub: Sinon.SinonStub, + spawnStub: Sinon.SinonStub, + logger = { + info: Sinon.stub(), + error: Sinon.stub(), + warning: Sinon.stub(), + debug: Sinon.stub(), + }, + ) { + class FakeFileSystem { + public writeFile = writeFileStub; + } + + class FakeCommandExecutor { + public spawn = spawnStub; + } + + const module = proxyquire.noCallThru()("../../../src/extension/commands/enableHermes", { + vscode: { + window: { + showQuickPick: showQuickPickStub, + }, + }, + "../../common/node/fileSystem": { + FileSystem: FakeFileSystem, + }, + "../../common/commandExecutor": { + CommandExecutor: FakeCommandExecutor, + }, + "../appLauncher": { + AppLauncher: { + getNodeModulesRootByProjectPath: () => path.join(tempDir, "node_modules"), + }, + }, + "../log/OutputChannelLogger": { + OutputChannelLogger: { + getMainChannel: () => logger, + }, + }, + }) as typeof import("../../../src/extension/commands/enableHermes"); + + return { + EnableHermes: module.EnableHermes, + logger, + }; + } + + async function runCommand( + commandClass: typeof import("../../../src/extension/commands/enableHermes").EnableHermes, + ): Promise { + const command = commandClass.formInstance(); + (command as any).project = createMockProject(tempDir); + await command.baseFn(); + } + + setup(function () { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "enableHermes-test-")); + }); + + teardown(function () { + removeDirRecursive(tempDir); + }); + + test("should warn when iOS Podfile is missing", async function () { + const showQuickPickStub = Sinon.stub(); + showQuickPickStub.onFirstCall().returns(Promise.resolve("iOS")); + showQuickPickStub.onSecondCall().returns(Promise.resolve("true")); + const writeFileStub = Sinon.stub().returns(Promise.resolve()); + const spawnStub = Sinon.stub().returns(Promise.resolve()); + const { EnableHermes, logger } = createCommandModule( + showQuickPickStub, + writeFileStub, + spawnStub, + ); + + await runCommand(EnableHermes); + + assert.strictEqual(logger.warning.calledWithExactly("Podfile not found"), true); + assert.strictEqual(writeFileStub.called, false); + assert.strictEqual(spawnStub.called, false); + }); + + test("should update Android hermesEnabled property", async function () { + const androidPath = path.join(tempDir, "android"); + const gradleFilePath = path.join(androidPath, "gradle.properties"); + fs.mkdirSync(androidPath, { recursive: true }); + fs.writeFileSync(gradleFilePath, "android.useAndroidX=true\nhermesEnabled=false"); + const showQuickPickStub = Sinon.stub(); + showQuickPickStub.onFirstCall().returns(Promise.resolve("Android")); + showQuickPickStub.onSecondCall().returns(Promise.resolve("true")); + const writeFileStub = Sinon.stub().returns(Promise.resolve()); + const spawnStub = Sinon.stub().returns(Promise.resolve()); + const { EnableHermes } = createCommandModule(showQuickPickStub, writeFileStub, spawnStub); + + await runCommand(EnableHermes); + + assert.strictEqual(writeFileStub.calledOnce, true); + assert.strictEqual(writeFileStub.firstCall.args[0], gradleFilePath); + assert.strictEqual( + writeFileStub.firstCall.args[1], + "android.useAndroidX=true\nhermesEnabled=true", + ); + assert.strictEqual(spawnStub.called, false); + }); + + test("should update iOS hermes flag and install pods", async function () { + const iosPath = path.join(tempDir, "ios"); + const podfilePath = path.join(iosPath, "Podfile"); + fs.mkdirSync(iosPath, { recursive: true }); + fs.writeFileSync( + podfilePath, + "use_react_native!(\n :path => config[:reactNativePath],\n :hermes_enabled => false\n)", + ); + const showQuickPickStub = Sinon.stub(); + showQuickPickStub.onFirstCall().returns(Promise.resolve("iOS")); + showQuickPickStub.onSecondCall().returns(Promise.resolve("true")); + const writeFileStub = Sinon.stub().returns(Promise.resolve()); + const spawnStub = Sinon.stub().returns(Promise.resolve()); + const { EnableHermes } = createCommandModule(showQuickPickStub, writeFileStub, spawnStub); + + await runCommand(EnableHermes); + + assert.strictEqual(writeFileStub.calledOnce, true); + assert.strictEqual(writeFileStub.firstCall.args[0], podfilePath); + assert.strictEqual( + writeFileStub.firstCall.args[1].includes(":hermes_enabled => true"), + true, + ); + assert.strictEqual(spawnStub.calledWithExactly("pod", ["install"]), true); + }); +}); diff --git a/test/extension/commands/killPort.test.ts b/test/extension/commands/killPort.test.ts new file mode 100644 index 000000000..76d32306e --- /dev/null +++ b/test/extension/commands/killPort.test.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +import assert = require("assert"); +import * as vscode from "vscode"; +import Sinon = require("sinon"); +import proxyquire = require("proxyquire"); + +suite("killPortCommand", function () { + let showInputBoxStub: Sinon.SinonStub; + + function createCommandModule( + execStub: Sinon.SinonStub, + logger = { + info: Sinon.stub(), + error: Sinon.stub(), + warning: Sinon.stub(), + debug: Sinon.stub(), + }, + ) { + class FakeChildProcess { + public exec = execStub; + } + + const module = proxyquire.noCallThru()("../../../src/extension/commands/killPort", { + "../../common/node/childProcess": { + ChildProcess: FakeChildProcess, + }, + "../../common/utils": { + wait: () => Promise.resolve(), + }, + "../log/OutputChannelLogger": { + OutputChannelLogger: { + getMainChannel: () => logger, + }, + }, + }) as typeof import("../../../src/extension/commands/killPort"); + + return { + KillPort: module.killPort, + logger, + }; + } + + function createExecResult(outcome: string): any { + return Promise.resolve({ + process: {}, + outcome: Promise.resolve(outcome), + }); + } + + teardown(function () { + if (showInputBoxStub) { + showInputBoxStub.restore(); + } + }); + + test("should do nothing when port input is empty", async function () { + showInputBoxStub = Sinon.stub(vscode.window, "showInputBox").returns(Promise.resolve("")); + const execStub = Sinon.stub().returns(createExecResult("")); + const { KillPort } = createCommandModule(execStub); + const command = KillPort.formInstance(); + (command as any).project = {}; + + await command.baseFn(); + + assert.strictEqual(execStub.called, false); + }); + + test("should kill the selected port", async function () { + showInputBoxStub = Sinon.stub(vscode.window, "showInputBox").returns( + Promise.resolve("8081"), + ); + const execStub = Sinon.stub().returns(createExecResult("killed")); + const { KillPort, logger } = createCommandModule(execStub); + const command = KillPort.formInstance(); + (command as any).project = {}; + + await command.baseFn(); + + assert.strictEqual(execStub.calledWithExactly("npx kill-port 8081"), true); + assert.strictEqual( + logger.info.args.some((args: string[]) => args[0].includes("killing port 8081")), + true, + ); + assert.strictEqual(logger.info.calledWithExactly("killed"), true); + }); +}); diff --git a/test/extension/commands/prebuild.test.ts b/test/extension/commands/prebuild.test.ts new file mode 100644 index 000000000..3657a8e50 --- /dev/null +++ b/test/extension/commands/prebuild.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +import assert = require("assert"); +import Sinon = require("sinon"); +import proxyquire = require("proxyquire"); + +suite("prebuildCommand", function () { + function createMockProject(projectPath: string): any { + return { + getPackager: () => ({ + getProjectPath: () => projectPath, + }), + }; + } + + test("should run expo prebuild from the project root", async function () { + const executeStub = Sinon.stub().returns(Promise.resolve()); + const getChannelStub = Sinon.stub().returns({ + info: Sinon.stub(), + error: Sinon.stub(), + warning: Sinon.stub(), + debug: Sinon.stub(), + }); + const getNodeModulesRootStub = Sinon.stub().returns("/workspace/app/node_modules"); + const constructorArgs: any[][] = []; + + class FakeCommandExecutor { + constructor(...args: any[]) { + constructorArgs.push(args); + } + + public execute = executeStub; + } + + const module = proxyquire.noCallThru()("../../../src/extension/commands/prebuild", { + "../../common/commandExecutor": { + CommandExecutor: FakeCommandExecutor, + }, + "../appLauncher": { + AppLauncher: { + getNodeModulesRootByProjectPath: getNodeModulesRootStub, + }, + }, + "../log/OutputChannelLogger": { + OutputChannelLogger: { + getChannel: getChannelStub, + }, + }, + }) as typeof import("../../../src/extension/commands/prebuild"); + + const command = module.Prebuild.formInstance(); + (command as any).project = createMockProject("/workspace/app"); + + await command.baseFn(); + + assert.strictEqual(getNodeModulesRootStub.calledWithExactly("/workspace/app"), true); + assert.deepStrictEqual(constructorArgs[0].slice(0, 2), [ + "/workspace/app/node_modules", + "/workspace/app", + ]); + assert.strictEqual(executeStub.calledWithExactly("npx expo prebuild"), true); + }); +});