diff --git a/lib/commands/test.ts b/lib/commands/test.ts index bbce9b5d43..62cc07418d 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -1,30 +1,75 @@ 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 platform: string; + protected abstract $projectData: IProjectData; + protected abstract $testExecutionService: ITestExecutionService; + protected abstract $analyticsService: IAnalyticsService; + protected abstract $options: IOptions; + protected abstract $platformEnvironmentRequirements: IPlatformEnvironmentRequirements; + protected abstract $errors: IErrors; + + async execute(args: string[]): Promise { + await this.$testExecutionService.startKarmaServer(this.platform, this.$projectData, this.projectFilesConfig); + } + + 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 }); -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 + 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 { + protected platform = "android"; + + constructor(protected $projectData: IProjectData, + protected $testExecutionService: ITestExecutionService, + protected $analyticsService: IAnalyticsService, + protected $options: IOptions, + protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, + protected $errors: IErrors) { + 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(); + } + } -$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 + }); } }