diff --git a/lib/commands/update.ts b/lib/commands/update.ts index 2669740cca..d097f37125 100644 --- a/lib/commands/update.ts +++ b/lib/commands/update.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import * as shelljs from "shelljs"; +import * as constants from "../constants"; export class UpdateCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; @@ -15,49 +15,41 @@ export class UpdateCommand implements ICommand { this.$projectData.initializeProjectData(); } + static readonly folders: string[] = [ + constants.LIB_DIR_NAME, + constants.HOOKS_DIR_NAME, + constants.PLATFORMS_DIR_NAME, + constants.NODE_MODULES_FOLDER_NAME + ]; + static readonly tempFolder: string = ".tmp_backup"; + static readonly updateFailMessage: string = "Could not update the project!"; + static readonly backupFailMessage: string = "Could not backup project folders!"; + public async execute(args: string[]): Promise { - const folders = ["lib", "hooks", "platforms", "node_modules"]; - const tmpDir = path.join(this.$projectData.projectDir, ".tmp_backup"); + const tmpDir = path.join(this.$projectData.projectDir, UpdateCommand.tempFolder); try { - shelljs.rm("-fr", tmpDir); - shelljs.mkdir(tmpDir); - shelljs.cp(path.join(this.$projectData.projectDir, "package.json"), tmpDir); - for (const folder of folders) { - const folderToCopy = path.join(this.$projectData.projectDir, folder); - if (this.$fs.exists(folderToCopy)) { - shelljs.cp("-rf", folderToCopy, tmpDir); - } - } + this.backup(tmpDir); } catch (error) { - this.$logger.error("Could not backup project folders!"); + this.$logger.error(UpdateCommand.backupFailMessage); + this.$fs.deleteDirectory(tmpDir); return; } try { - await this.executeCore(args, folders); + await this.executeCore(args); } catch (error) { - shelljs.cp("-f", path.join(tmpDir, "package.json"), this.$projectData.projectDir); - for (const folder of folders) { - shelljs.rm("-rf", path.join(this.$projectData.projectDir, folder)); - - const folderToCopy = path.join(tmpDir, folder); - - if (this.$fs.exists(folderToCopy)) { - shelljs.cp("-fr", folderToCopy, this.$projectData.projectDir); - } - } - - this.$logger.error("Could not update the project!"); + this.restoreBackup(tmpDir); + this.$logger.error(UpdateCommand.updateFailMessage); } finally { - shelljs.rm("-fr", tmpDir); + this.$fs.deleteDirectory(tmpDir); } } public async canExecute(args: string[]): Promise { - for (const arg of args) { - const platform = arg.split("@")[0]; - this.$platformService.validatePlatformInstalled(platform, this.$projectData); + const platforms = this.getPlatforms(); + + for (const platform of platforms.packagePlatforms) { const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); const platformProjectService = platformData.platformProjectService; await platformProjectService.validate(this.$projectData); @@ -66,42 +58,79 @@ export class UpdateCommand implements ICommand { return args.length < 2 && this.$projectData.projectDir !== ""; } - private async executeCore(args: string[], folders: string[]): Promise { - let platforms = this.$platformService.getInstalledPlatforms(this.$projectData); - const availablePlatforms = this.$platformService.getAvailablePlatforms(this.$projectData); - const packagePlatforms: string[] = []; + private async executeCore(args: string[]): Promise { + const platforms = this.getPlatforms(); - for (const platform of availablePlatforms) { + for (const platform of _.xor(platforms.installed, platforms.packagePlatforms)) { const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); - const platformVersion = this.$projectDataService.getNSValue(this.$projectData.projectDir, platformData.frameworkPackageName); - if (platformVersion) { - packagePlatforms.push(platform); - this.$projectDataService.removeNSProperty(this.$projectData.projectDir, platformData.frameworkPackageName); - } + this.$projectDataService.removeNSProperty(this.$projectData.projectDir, platformData.frameworkPackageName); } - await this.$platformService.removePlatforms(platforms, this.$projectData); + await this.$platformService.removePlatforms(platforms.installed, this.$projectData); await this.$pluginsService.remove("tns-core-modules", this.$projectData); await this.$pluginsService.remove("tns-core-modules-widgets", this.$projectData); - for (const folder of folders) { - shelljs.rm("-fr", folder); + for (const folder of UpdateCommand.folders) { + this.$fs.deleteDirectory(path.join(this.$projectData.projectDir, folder)); } - platforms = platforms.concat(packagePlatforms); if (args.length === 1) { - for (const platform of platforms) { + for (const platform of platforms.packagePlatforms) { await this.$platformService.addPlatforms([platform + "@" + args[0]], this.$options.platformTemplate, this.$projectData, this.$options, this.$options.frameworkPath); } await this.$pluginsService.add("tns-core-modules@" + args[0], this.$projectData); } else { - await this.$platformService.addPlatforms(platforms, this.$options.platformTemplate, this.$projectData, this.$options, this.$options.frameworkPath); + await this.$platformService.addPlatforms(platforms.packagePlatforms, this.$options.platformTemplate, this.$projectData, this.$options, this.$options.frameworkPath); await this.$pluginsService.add("tns-core-modules", this.$projectData); } await this.$pluginsService.ensureAllDependenciesAreInstalled(this.$projectData); } + + private getPlatforms(): {installed: string[], packagePlatforms: string[]} { + const installedPlatforms = this.$platformService.getInstalledPlatforms(this.$projectData); + const availablePlatforms = this.$platformService.getAvailablePlatforms(this.$projectData); + const packagePlatforms: string[] = []; + + for (const platform of availablePlatforms) { + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformVersion = this.$projectDataService.getNSValue(this.$projectData.projectDir, platformData.frameworkPackageName); + if (platformVersion) { + packagePlatforms.push(platform); + } + } + + return { + installed: installedPlatforms, + packagePlatforms: installedPlatforms.concat(packagePlatforms) + }; + } + + private restoreBackup(tmpDir: string): void { + this.$fs.copyFile(path.join(tmpDir, constants.PACKAGE_JSON_FILE_NAME), this.$projectData.projectDir); + for (const folder of UpdateCommand.folders) { + this.$fs.deleteDirectory(path.join(this.$projectData.projectDir, folder)); + + const folderToCopy = path.join(tmpDir, folder); + + if (this.$fs.exists(folderToCopy)) { + this.$fs.copyFile(folderToCopy, this.$projectData.projectDir); + } + } + } + + private backup(tmpDir: string): void { + this.$fs.deleteDirectory(tmpDir); + this.$fs.createDirectory(tmpDir); + this.$fs.copyFile(path.join(this.$projectData.projectDir, constants.PACKAGE_JSON_FILE_NAME), tmpDir); + for (const folder of UpdateCommand.folders) { + const folderToCopy = path.join(this.$projectData.projectDir, folder); + if (this.$fs.exists(folderToCopy)) { + this.$fs.copyFile(folderToCopy, tmpDir); + } + } + } } $injector.registerCommand("update", UpdateCommand); diff --git a/lib/constants.ts b/lib/constants.ts index 66d6b3b65c..0525e4f425 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -16,6 +16,8 @@ export const TEST_RUNNER_NAME = "nativescript-unit-test-runner"; export const LIVESYNC_EXCLUDED_FILE_PATTERNS = ["**/*.js.map", "**/*.ts"]; export const XML_FILE_EXTENSION = ".xml"; export const PLATFORMS_DIR_NAME = "platforms"; +export const HOOKS_DIR_NAME = "hooks"; +export const LIB_DIR_NAME = "lib"; export const CODE_SIGN_ENTITLEMENTS = "CODE_SIGN_ENTITLEMENTS"; export const AWAIT_NOTIFICATION_TIMEOUT_SECONDS = 9; export const SRC_DIR = "src"; diff --git a/package.json b/package.json index 963004dc46..912d7dfa70 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "analyze": true, "devDependencies": { "@types/chai": "4.0.1", + "@types/sinon": "4.0.0", "@types/chai-as-promised": "0.0.31", "@types/chokidar": "1.6.0", "@types/lockfile": "1.0.0", @@ -104,6 +105,7 @@ "grunt-ts": "6.0.0-beta.16", "istanbul": "0.4.5", "mocha": "3.1.2", + "sinon": "4.1.2", "should": "7.0.2", "source-map-support": "^0.4.14", "tslint": "5.4.3", diff --git a/test/update.ts b/test/update.ts new file mode 100644 index 0000000000..3f9343d41c --- /dev/null +++ b/test/update.ts @@ -0,0 +1,247 @@ +import * as path from "path"; +import * as stubs from "./stubs"; +import * as yok from "../lib/common/yok"; +import { UpdateCommand } from "../lib/commands/update"; +import { assert } from "chai"; +import * as sinon from 'sinon'; +import { Options } from "../lib/options"; +import { AndroidProjectService } from "../lib/services/android-project-service"; +import {StaticConfig } from "../lib/config"; +import { SettingsService } from "../lib/common/test/unit-tests/stubs"; +const projectFolder = "test"; + +function createTestInjector( + installedPlatforms: string[] = [], + availablePlatforms: string[] = [], + projectDir: string = projectFolder, + validate: Function = (): Promise => Promise.resolve() +): IInjector { + const testInjector: IInjector = new yok.Yok(); + testInjector.register("logger", stubs.LoggerStub); + testInjector.register("options", Options); + testInjector.register('fs', stubs.FileSystemStub); + testInjector.register("analyticsService", { + trackException: async (): Promise => undefined, + checkConsent: async (): Promise => undefined, + trackFeature: async (): Promise => undefined + }); + testInjector.register('hostInfo', {}); + testInjector.register('errors', stubs.ErrorsStub); + testInjector.register("staticConfig", StaticConfig); + testInjector.register("androidProjectService", AndroidProjectService); + testInjector.register("androidToolsInfo", stubs.AndroidToolsInfoStub); + testInjector.register("projectData", { projectDir, initializeProjectData: () => { /* empty */ } }); + testInjector.register("projectDataService", { + getNSValue: () => { + return "1.0.0"; + } + }); + testInjector.register("pluginVariablesService", {}); + testInjector.register("platformService", { + getInstalledPlatforms: function(): string[]{ + return installedPlatforms; + }, + getAvailablePlatforms: function(): string[]{ + return availablePlatforms; + }, + removePlatforms: async (): Promise => undefined, + addPlatforms: async (): Promise => undefined, + }); + testInjector.register("platformsData", { + availablePlatforms: { + Android: "Android", + iOS: "iOS" + }, + getPlatformData: () => { + return { + platformProjectService: { + validate + } + }; + } + }); + testInjector.register("settingsService", SettingsService); + testInjector.register("pluginsService", { + add: async (): Promise => undefined, + remove: async (): Promise => undefined, + ensureAllDependenciesAreInstalled: () => { return Promise.resolve(); }, + }); + testInjector.register("update", UpdateCommand); + + return testInjector; +} + +describe("update command method tests", () => { + describe("canExecute", () => { + it("calls platform service validate", async () => { + let validated = false; + const testInjector = createTestInjector( + [], + ["android"], + projectFolder, + () => { + validated = true; + return Promise.resolve(); + }); + const updateCommand = testInjector.resolve(UpdateCommand); + const canExecute = updateCommand.canExecute(["3.3.0"]); + + return canExecute.then(() => { + assert.equal(validated, true); + }); + }); + + it("returns false if too many artuments", async () => { + const testInjector = createTestInjector([], ["android"]); + const updateCommand = testInjector.resolve(UpdateCommand); + const canExecute = updateCommand.canExecute(["333", "111", "444"]); + + return assert.eventually.equal(canExecute, false); + }); + + it("returns false if projectDir empty string", async () => { + const testInjector = createTestInjector([], ["android"], ""); + const updateCommand = testInjector.resolve(UpdateCommand); + const canExecute = updateCommand.canExecute([]); + + return assert.eventually.equal(canExecute, false); + }); + + it("returns true all ok", async () => { + const testInjector = createTestInjector([], ["android"]); + const updateCommand = testInjector.resolve(UpdateCommand); + const canExecute = updateCommand.canExecute(["3.3.0"]); + + return assert.eventually.equal(canExecute, true); + }); + }); + + describe("execute", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("if backup fails, pltforms not deleted and added, temp removed", async () => { + const installedPlatforms: string[] = ["android"]; + const testInjector = createTestInjector(installedPlatforms); + const fs = testInjector.resolve("fs"); + const deleteDirectory: sinon.SinonStub = sandbox.stub(fs, "deleteDirectory"); + const platformService = testInjector.resolve("platformService"); + sandbox.stub(fs, "copyFile").throws(); + sandbox.spy(platformService, "addPlatforms"); + sandbox.spy(platformService, "removePlatforms"); + const updateCommand = testInjector.resolve(UpdateCommand); + + return updateCommand.execute(["3.3.0"]).then(() => { + assert.isTrue(deleteDirectory.calledWith(path.join(projectFolder, UpdateCommand.tempFolder))); + assert.isFalse(platformService.removePlatforms.calledWith(installedPlatforms)); + assert.isFalse(platformService.addPlatforms.calledWith(installedPlatforms)); + }); + }); + + it("calls copy to temp for package.json and folders(backup)", async () => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + const copyFileStub = sandbox.stub(fs, "copyFile"); + const updateCommand = testInjector.resolve(UpdateCommand); + return updateCommand.execute(["3.3.0"]).then( () => { + assert.isTrue(copyFileStub.calledWith(path.join(projectFolder, "package.json"))); + for (const folder of UpdateCommand.folders) { + assert.isTrue(copyFileStub.calledWith(path.join(projectFolder, folder))); + } + }); + }); + + it("calls copy from temp for package.json and folders to project folder(restore)", async () => { + const testInjector = createTestInjector(); + testInjector.resolve("platformService").removePlatforms = () => { + throw new Error(); + }; + const fs = testInjector.resolve("fs"); + const deleteDirectoryStub: sinon.SinonStub = sandbox.stub(fs, "deleteDirectory"); + const copyFileStub = sandbox.stub(fs, "copyFile"); + const updateCommand = testInjector.resolve(UpdateCommand); + const tempDir = path.join(projectFolder, UpdateCommand.tempFolder); + + return updateCommand.execute(["3.3.0"]).then(() => { + assert.isTrue(copyFileStub.calledWith(path.join(tempDir, "package.json"), projectFolder)); + for (const folder of UpdateCommand.folders) { + assert.isTrue(deleteDirectoryStub.calledWith(path.join(projectFolder, folder))); + assert.isTrue(copyFileStub.calledWith(path.join(tempDir, folder), projectFolder)); + } + }); + }); + + it("calls remove for all folders", async () => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + const deleteDirectory: sinon.SinonStub = sandbox.stub(fs, "deleteDirectory"); + const updateCommand = testInjector.resolve(UpdateCommand); + return updateCommand.execute([]).then(() => { + for (const folder of UpdateCommand.folders) { + assert.isTrue(deleteDirectory.calledWith(path.join(projectFolder, folder))); + } + }); + }); + + it("calls remove platforms and add platforms", async () => { + const installedPlatforms: string[] = ["android"]; + const testInjector = createTestInjector(installedPlatforms); + const platformService = testInjector.resolve("platformService"); + sandbox.spy(platformService, "addPlatforms"); + sandbox.spy(platformService, "removePlatforms"); + const updateCommand = testInjector.resolve(UpdateCommand); + return updateCommand.execute([]).then(() => { + assert(platformService.removePlatforms.calledWith(installedPlatforms)); + assert(platformService.addPlatforms.calledWith(installedPlatforms)); + }); + }); + + it("call add platforms with specific verison", async () => { + const version = "3.3.0"; + const installedPlatforms: string[] = ["android"]; + const testInjector = createTestInjector(installedPlatforms); + const platformService = testInjector.resolve("platformService"); + sandbox.spy(platformService, "addPlatforms"); + sandbox.spy(platformService, "removePlatforms"); + const updateCommand = testInjector.resolve(UpdateCommand); + return updateCommand.execute([version]).then(() => { + assert(platformService.addPlatforms.calledWith([`${installedPlatforms}@${version}`])); + }); + }); + + it("calls remove and add of core modules and widgets", async () => { + const testInjector = createTestInjector(); + const pluginsService = testInjector.resolve("pluginsService"); + sandbox.spy(pluginsService, "remove"); + sandbox.spy(pluginsService, "add"); + sandbox.spy(pluginsService, "ensureAllDependenciesAreInstalled"); + const updateCommand = testInjector.resolve(UpdateCommand); + return updateCommand.execute([]).then(() => { + assert(pluginsService.add.calledWith("tns-core-modules")); + assert(pluginsService.remove.calledWith("tns-core-modules")); + assert(pluginsService.remove.calledWith("tns-core-modules-widgets")); + assert(pluginsService.ensureAllDependenciesAreInstalled.called); + }); + }); + + it("calls add of core modules with specific version", async () => { + const version = "3.3.0"; + const testInjector = createTestInjector(); + const pluginsService = testInjector.resolve("pluginsService"); + sandbox.spy(pluginsService, "remove"); + sandbox.spy(pluginsService, "add"); + sandbox.spy(pluginsService, "ensureAllDependenciesAreInstalled"); + const updateCommand = testInjector.resolve(UpdateCommand); + return updateCommand.execute([version]).then(() => { + assert(pluginsService.add.calledWith(`tns-core-modules@${version}`)); + }); + }); + }); +});