From c2f74d3a58e43be6835128f8458717eef4457e2d Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Fri, 22 Feb 2019 16:23:02 +0200 Subject: [PATCH 1/2] fix: validate test init before executing the test command --- lib/commands/test-init.ts | 4 +- lib/commands/test.ts | 89 +++++++++++++++++++------- lib/common/declarations.d.ts | 1 + lib/common/errors.ts | 3 +- lib/definitions/project.d.ts | 1 + lib/services/test-execution-service.ts | 15 ++++- test/services/test-execution-serice.ts | 67 +++++++++++++++++++ test/stubs.ts | 14 +++- 8 files changed, 165 insertions(+), 29 deletions(-) create mode 100644 test/services/test-execution-serice.ts diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index 82e00616d2..07d093518a 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -37,9 +37,7 @@ class TestInitCommand implements ICommand { const dependencies = this.frameworkDependencies[frameworkToInstall] || []; const modulesToInstall: IDependencyInformation[] = [ { - name: 'karma', - // Hardcode the version unitl https://github.com/karma-runner/karma/issues/3052 is fixed - version: "2.0.2" + name: 'karma' }, { name: `karma-${frameworkToInstall}` diff --git a/lib/commands/test.ts b/lib/commands/test.ts index bbce9b5d43..8c2f948163 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -1,30 +1,73 @@ import * as helpers from "../common/helpers"; -function RunKarmaTestCommandFactory(platform: string) { - return function RunKarmaTestCommand($options: IOptions, $testExecutionService: ITestExecutionService, $projectData: IProjectData, $analyticsService: IAnalyticsService, $platformEnvironmentRequirements: IPlatformEnvironmentRequirements) { - $projectData.initializeProjectData(); - $analyticsService.setShouldDispose($options.justlaunch || !$options.watch); - const projectFilesConfig = helpers.getProjectFilesConfig({ isReleaseBuild: $options.release }); - this.execute = (args: string[]): Promise => $testExecutionService.startKarmaServer(platform, $projectData, projectFilesConfig); - this.canExecute = (args: string[]): Promise => canExecute({ $platformEnvironmentRequirements, $projectData, $options, platform }); - this.allowedParameters = []; - }; -} +abstract class TestCommandBase { + public allowedParameters: ICommandParameter[] = []; + private projectFilesConfig: IProjectFilesConfig; + protected abstract $projectData: IProjectData; + protected abstract $testExecutionService: ITestExecutionService; + protected abstract $analyticsService: IAnalyticsService; + protected abstract $options: IOptions; + protected abstract $platformEnvironmentRequirements: IPlatformEnvironmentRequirements; + protected abstract $errors: IErrors; + + constructor(private platform: string) { + } + + async execute(args: string[]): Promise { + await this.$testExecutionService.startKarmaServer(this.platform, this.$projectData, this.projectFilesConfig); + } -async function canExecute(input: { $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, $projectData: IProjectData, $options: IOptions, platform: string }): Promise { - const { $platformEnvironmentRequirements, $projectData, $options, platform } = input; - const output = await $platformEnvironmentRequirements.checkEnvironmentRequirements({ - platform, - projectDir: $projectData.projectDir, - options: $options, - notConfiguredEnvOptions: { - hideSyncToPreviewAppOption: true, - hideCloudBuildOption: true + async canExecute(args: string[]): Promise { + this.$projectData.initializeProjectData(); + this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch); + this.projectFilesConfig = helpers.getProjectFilesConfig({ isReleaseBuild: this.$options.release }); + + const output = await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({ + platform: this.platform, + projectDir: this.$projectData.projectDir, + options: this.$options, + notConfiguredEnvOptions: { + hideSyncToPreviewAppOption: true, + hideCloudBuildOption: true + } + }); + + const canStartKarmaServer = await this.$testExecutionService.canStartKarmaServer(this.$projectData); + if (!canStartKarmaServer) { + this.$errors.fail({ + formatStr: "Error: In order to run unit tests, your project must already be configured by running $ tns test init.", + suppressCommandHelp: true, + errorCode: ErrorCodes.TESTS_INIT_REQUIRED + }); } - }); - return output.canExecute; + return output.canExecute && canStartKarmaServer; + } +} + +class TestAndroidCommand extends TestCommandBase implements ICommand { + constructor(protected $projectData: IProjectData, + protected $testExecutionService: ITestExecutionService, + protected $analyticsService: IAnalyticsService, + protected $options: IOptions, + protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, + protected $errors: IErrors) { + super("android"); + } + +} + +class TestIosCommand extends TestCommandBase implements ICommand { + constructor(protected $projectData: IProjectData, + protected $testExecutionService: ITestExecutionService, + protected $analyticsService: IAnalyticsService, + protected $options: IOptions, + protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, + protected $errors: IErrors) { + super("iOS"); + } + } -$injector.registerCommand("test|android", RunKarmaTestCommandFactory('android')); -$injector.registerCommand("test|ios", RunKarmaTestCommandFactory('iOS')); +$injector.registerCommand("test|android", TestAndroidCommand); +$injector.registerCommand("test|ios", TestIosCommand); diff --git a/lib/common/declarations.d.ts b/lib/common/declarations.d.ts index cd9c9e5b94..e13a0fb299 100644 --- a/lib/common/declarations.d.ts +++ b/lib/common/declarations.d.ts @@ -596,6 +596,7 @@ declare const enum ErrorCodes { KARMA_FAIL = 130, UNHANDLED_REJECTION_FAILURE = 131, DELETED_KILL_FILE = 132, + TESTS_INIT_REQUIRED = 133 } interface IFutureDispatcher { diff --git a/lib/common/errors.ts b/lib/common/errors.ts index bb3bf6e981..6d4a6353fb 100644 --- a/lib/common/errors.ts +++ b/lib/common/errors.ts @@ -1,6 +1,7 @@ import * as util from "util"; import * as path from "path"; import { SourceMapConsumer } from "source-map"; +import { isInteractive } from "./helpers"; // we need this to overwrite .stack property (read-only in Error) function Exception() { @@ -159,7 +160,7 @@ export class Errors implements IErrors { } catch (ex) { const loggerLevel: string = $injector.resolve("logger").getLevel().toUpperCase(); const printCallStack = this.printCallStack || loggerLevel === "TRACE" || loggerLevel === "DEBUG"; - const message = printCallStack ? resolveCallStack(ex) : `\x1B[31;1m${ex.message}\x1B[0m`; + const message = printCallStack ? resolveCallStack(ex) : isInteractive() ? `\x1B[31;1m${ex.message}\x1B[0m` : ex.message; if (ex.printOnStdout) { this.$injector.resolve("logger").out(message); diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index aea94962e5..35fecc016e 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -465,6 +465,7 @@ interface IValidatePlatformOutput { interface ITestExecutionService { startKarmaServer(platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise; + canStartKarmaServer(projectData: IProjectData): Promise; } /** diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index b722d68e16..d2f5d9a72c 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -7,7 +7,7 @@ interface IKarmaConfigOptions { debugTransport: boolean; } -class TestExecutionService implements ITestExecutionService { +export class TestExecutionService implements ITestExecutionService { private static CONFIG_FILE_NAME = `node_modules/${constants.TEST_RUNNER_NAME}/config.js`; private static SOCKETIO_JS_FILE_NAME = `node_modules/${constants.TEST_RUNNER_NAME}/socket.io.js`; @@ -163,6 +163,19 @@ class TestExecutionService implements ITestExecutionService { }); } + public async canStartKarmaServer(projectData: IProjectData): Promise { + let canStartKarmaServer = true; + const requiredDependencies = ["karma", "nativescript-unit-test-runner"]; + _.each(requiredDependencies, (dep) => { + if (!projectData.dependencies[dep] && !projectData.devDependencies[dep]) { + canStartKarmaServer = false; + return; + } + }); + + return canStartKarmaServer; + } + allowedParameters: ICommandParameter[] = []; private generateConfig(port: string, options: any): string { diff --git a/test/services/test-execution-serice.ts b/test/services/test-execution-serice.ts new file mode 100644 index 0000000000..9d9db7d547 --- /dev/null +++ b/test/services/test-execution-serice.ts @@ -0,0 +1,67 @@ +import { InjectorStub } from "../stubs"; +import { TestExecutionService } from "../../lib/services/test-execution-service"; +import { assert } from "chai"; + +const karmaPluginName = "karma"; +const unitTestsPluginName = "nativescript-unit-test-runner"; + +function getTestExecutionService(): ITestExecutionService { + const injector = new InjectorStub(); + injector.register("testExecutionService", TestExecutionService); + + return injector.resolve("testExecutionService"); +} + +function getDependenciesObj(deps: string[]): IDictionary { + const depsObj: IDictionary = {}; + deps.forEach(dep => { + depsObj[dep] = "1.0.0"; + }); + + return depsObj; +} + +describe("testExecutionService", () => { + const testCases = [ + { + name: "should return false when the project has no dependencies and dev dependencies", + expectedCanStartKarmaServer: false, + projectData: { dependencies: {}, devDependencies: {} } + }, + { + name: "should return false when the project has no karma", + expectedCanStartKarmaServer: false, + projectData: { dependencies: getDependenciesObj([unitTestsPluginName]), devDependencies: {} } + }, + { + name: "should return false when the project has no unit test runner", + expectedCanStartKarmaServer: false, + projectData: { dependencies: getDependenciesObj([karmaPluginName]), devDependencies: {} } + }, + { + name: "should return true when the project has the required plugins as dependencies", + expectedCanStartKarmaServer: true, + projectData: { dependencies: getDependenciesObj([karmaPluginName, unitTestsPluginName]), devDependencies: {} } + }, + { + name: "should return true when the project has the required plugins as dev dependencies", + expectedCanStartKarmaServer: true, + projectData: { dependencies: {}, devDependencies: getDependenciesObj([karmaPluginName, unitTestsPluginName]) } + }, + { + name: "should return true when the project has the required plugins as dev and normal dependencies", + expectedCanStartKarmaServer: true, + projectData: { dependencies: getDependenciesObj([karmaPluginName]), devDependencies: getDependenciesObj([unitTestsPluginName]) } + } + ]; + + describe("canStartKarmaServer", () => { + _.each(testCases, (testCase: any) => { + it(`${testCase.name}`, async () => { + const testExecutionService = getTestExecutionService(); + const canStartKarmaServer = await testExecutionService.canStartKarmaServer(testCase.projectData); + assert.equal(canStartKarmaServer, testCase.expectedCanStartKarmaServer); + }); + }); + }); +}); diff --git a/test/stubs.ts b/test/stubs.ts index 45c04dd3c9..3ab4d35eb7 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -913,7 +913,7 @@ export class AndroidBundleValidatorHelper implements IAndroidBundleValidatorHelp export class PerformanceService implements IPerformanceService { now(): number { return 10; } - processExecutionData() {} + processExecutionData() { } } export class InjectorStub extends Yok implements IInjector { @@ -942,5 +942,17 @@ export class InjectorStub extends Yok implements IInjector { this.register('projectData', ProjectDataStub); this.register('packageInstallationManager', PackageInstallationManagerStub); this.register('packageInstallationManager', PackageInstallationManagerStub); + this.register("httpClient", { + httpRequest: async (options: any, proxySettings?: IProxySettings): Promise => undefined + }); + this.register("pluginsService", { + add: async (): Promise => undefined, + remove: async (): Promise => undefined, + ensureAllDependenciesAreInstalled: () => { return Promise.resolve(); }, + }); + this.register("devicesService", { + getDevice: (): Mobile.IDevice => undefined, + getDeviceByIdentifier: (): Mobile.IDevice => undefined + }); } } From c5a8767231d88f2084915e59ab5bd55a456326dd Mon Sep 17 00:00:00 2001 From: DimitarTachev Date: Mon, 25 Feb 2019 14:52:14 +0200 Subject: [PATCH 2/2] chore: fix pr comments --- lib/commands/test-init.ts | 4 +++- lib/commands/test.ts | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index 07d093518a..82e00616d2 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -37,7 +37,9 @@ class TestInitCommand implements ICommand { const dependencies = this.frameworkDependencies[frameworkToInstall] || []; const modulesToInstall: IDependencyInformation[] = [ { - name: 'karma' + name: 'karma', + // Hardcode the version unitl https://github.com/karma-runner/karma/issues/3052 is fixed + version: "2.0.2" }, { name: `karma-${frameworkToInstall}` diff --git a/lib/commands/test.ts b/lib/commands/test.ts index 8c2f948163..62cc07418d 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -3,6 +3,7 @@ import * as helpers from "../common/helpers"; abstract class TestCommandBase { public allowedParameters: ICommandParameter[] = []; private projectFilesConfig: IProjectFilesConfig; + protected abstract platform: string; protected abstract $projectData: IProjectData; protected abstract $testExecutionService: ITestExecutionService; protected abstract $analyticsService: IAnalyticsService; @@ -10,9 +11,6 @@ abstract class TestCommandBase { protected abstract $platformEnvironmentRequirements: IPlatformEnvironmentRequirements; protected abstract $errors: IErrors; - constructor(private platform: string) { - } - async execute(args: string[]): Promise { await this.$testExecutionService.startKarmaServer(this.platform, this.$projectData, this.projectFilesConfig); } @@ -46,25 +44,29 @@ abstract class TestCommandBase { } class TestAndroidCommand extends TestCommandBase implements ICommand { + protected platform = "android"; + constructor(protected $projectData: IProjectData, protected $testExecutionService: ITestExecutionService, protected $analyticsService: IAnalyticsService, protected $options: IOptions, protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, protected $errors: IErrors) { - super("android"); + super(); } } class TestIosCommand extends TestCommandBase implements ICommand { + protected platform = "iOS"; + constructor(protected $projectData: IProjectData, protected $testExecutionService: ITestExecutionService, protected $analyticsService: IAnalyticsService, protected $options: IOptions, protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, protected $errors: IErrors) { - super("iOS"); + super(); } }