Skip to content

Added check if project name starts with number when creating new proj… #1642

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ $injector.require("androidProjectService", "./services/android-project-service")
$injector.require("iOSProjectService", "./services/ios-project-service");

$injector.require("projectTemplatesService", "./services/project-templates-service");
$injector.require("projectNameService", "./services/project-name-service");
$injector.require("tnsModulesService", "./services/tns-modules-service");

$injector.require("platformsData", "./platforms-data");
Expand Down
19 changes: 16 additions & 3 deletions lib/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,23 +159,23 @@ interface IAndroidToolsInfo {
* @param {any} options Defines if the warning messages should treated as error and if the targetSdk value should be validated as well.
* @return {boolean} True if there are detected issues, false otherwise.
*/
validateInfo(options?: {showWarningsAsErrors: boolean, validateTargetSdk: boolean}): IFuture<boolean>;
validateInfo(options?: { showWarningsAsErrors: boolean, validateTargetSdk: boolean }): IFuture<boolean>;

/**
* Validates the information about required JAVA version.
* @param {string} installedJavaVersion The JAVA version that will be checked.
* @param {any} options Defines if the warning messages should treated as error.
* @return {boolean} True if there are detected issues, false otherwise.
*/
validateJavacVersion(installedJavaVersion: string, options?: {showWarningsAsErrors: boolean}): IFuture<boolean>;
validateJavacVersion(installedJavaVersion: string, options?: { showWarningsAsErrors: boolean }): IFuture<boolean>;

/**
* Returns the path to `android` executable. It should be `$ANDROID_HOME/tools/android`.
* In case ANDROID_HOME is not defined, check if `android` is part of $PATH.
* @param {any} options Defines if the warning messages should treated as error.
* @return {string} Path to the `android` executable.
*/
getPathToAndroidExecutable(options?: {showWarningsAsErrors: boolean}): IFuture<string>;
getPathToAndroidExecutable(options?: { showWarningsAsErrors: boolean }): IFuture<string>;

/**
* Gets the path to `adb` executable from ANDROID_HOME. It should be `$ANDROID_HOME/platform-tools/adb` in case it exists.
Expand Down Expand Up @@ -256,3 +256,16 @@ interface IXmlValidator {
*/
getXmlFileErrors(sourceFile: string): IFuture<string>;
}

/**
* Describes methods for project name.
*/
interface IProjectNameService {
/**
* Ensures the passed project name is valid. If the project name is not valid prompts for actions.
* @param {string} project name to be checked.
* @param {IOptions} current command options.
* @return {IFuture<strng>} returns the selected name of the project.
*/
ensureValidName(projectName: string, validateOptions?: {force: boolean}): IFuture<string>;
}
59 changes: 59 additions & 0 deletions lib/services/project-name-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use strict";

import { isInteractive } from "../common/helpers";

export class ProjectNameService implements IProjectNameService {
constructor(private $projectNameValidator: IProjectNameValidator,
private $errors: IErrors,
private $logger: ILogger,
private $prompter: IPrompter) { }

public ensureValidName(projectName: string, validateOptions?: { force: boolean }): IFuture<string> {
return (() => {
if (validateOptions && validateOptions.force) {
return projectName;
}

if (!this.$projectNameValidator.validate(projectName)) {
return this.promptForNewName(projectName, validateOptions).wait();
}

if (!this.checkIfNameStartsWithLetter(projectName)) {
if (!isInteractive()) {
this.$errors.fail("The project name does not start with letter and will fail to build for Android. If You want to create project with this name add --force to the create command.");
return;
}

return this.promptForNewName(projectName, validateOptions).wait();
}

return projectName;
}).future<string>()();
}

private checkIfNameStartsWithLetter(projectName: string): boolean {
let startsWithLetterExpression = /^[a-zA-Z]/;
return startsWithLetterExpression.test(projectName);
}

private promptForNewName(projectName: string, validateOptions?: { force: boolean }): IFuture<string> {
return (() => {
if (this.promptForForceNameConfirm().wait()) {
return projectName;
}

let newProjectName = this.$prompter.getString("Enter the new project name:").wait();
return this.ensureValidName(newProjectName, validateOptions).wait();
}).future<string>()();
}

private promptForForceNameConfirm(): IFuture<boolean> {
return (() => {
this.$logger.warn("The project name does not start with letter and will fail to build for Android.");

return this.$prompter.confirm("Do you want to create the project with this name?").wait();
}).future<boolean>()();
}
}

$injector.register("projectNameService", ProjectNameService);
5 changes: 3 additions & 2 deletions lib/services/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class ProjectService implements IProjectService {
private $logger: ILogger,
private $projectDataService: IProjectDataService,
private $projectHelper: IProjectHelper,
private $projectNameValidator: IProjectNameValidator,
private $projectNameService: IProjectNameService,
private $projectTemplatesService: IProjectTemplatesService,
private $options: IOptions) { }

Expand All @@ -23,7 +23,8 @@ export class ProjectService implements IProjectService {
if (!projectName) {
this.$errors.fail("You must specify <App name> when creating a new project.");
}
this.$projectNameValidator.validate(projectName);

projectName = this.$projectNameService.ensureValidName(projectName, {force: this.$options.force}).wait();

let projectId = this.$options.appid || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX);

Expand Down
78 changes: 78 additions & 0 deletions test/project-name-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/// <reference path=".d.ts" />
"use strict";

import {Yok} from "../lib/common/yok";
import {ProjectNameService} from "../lib/services/project-name-service";
import {assert} from "chai";
import {ErrorsStub, LoggerStub} from "./stubs";
import Future = require("fibers/future");

let mockProjectNameValidator = {
validate: () => true
};

let dummyString: string = "dummyString";

function createTestInjector(): IInjector {
let testInjector: IInjector;

testInjector = new Yok();
testInjector.register("projectNameService", ProjectNameService);
testInjector.register("projectNameValidator", mockProjectNameValidator);
testInjector.register("errors", ErrorsStub);
testInjector.register("logger", LoggerStub);
testInjector.register("prompter", {
confirm: (message: string): IFuture<boolean> => Future.fromResult(true),
getString: (message: string): IFuture<string> => Future.fromResult(dummyString)
});

return testInjector;
}

describe("Project Name Service Tests", () => {
let testInjector: IInjector;
let projectNameService: IProjectNameService;
let validProjectName = "valid";
let invalidProjectName = "1invalid";

beforeEach(() => {
testInjector = createTestInjector();
projectNameService = testInjector.resolve("projectNameService");
});

it("returns correct name when valid name is entered", () => {
let actualProjectName = projectNameService.ensureValidName(validProjectName).wait();

assert.deepEqual(actualProjectName, validProjectName);
});

it("returns correct name when invalid name is entered several times and then valid name is entered", () => {
let prompter = testInjector.resolve("prompter");
prompter.confirm = (message: string): IFuture<boolean> => Future.fromResult(false);

let incorrectInputsLimit = 5;
let incorrectInputsCount = 0;

prompter.getString = (message: string): IFuture<string> => {
return (() => {
if (incorrectInputsCount < incorrectInputsLimit) {
incorrectInputsCount++;

return invalidProjectName;
} else {
return validProjectName;
}
}).future<string>()();
};

let actualProjectName = projectNameService.ensureValidName(invalidProjectName).wait();

assert.deepEqual(actualProjectName, validProjectName);
});

it("returns the invalid name when invalid name is entered and --force flag is present", () => {
let actualProjectName = projectNameService.ensureValidName(validProjectName, { force: true }).wait();

assert.deepEqual(actualProjectName, validProjectName);
});
});
127 changes: 126 additions & 1 deletion test/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as stubs from "./stubs";
import * as constants from "./../lib/constants";
import {ChildProcess} from "../lib/common/child-process";
import * as ProjectServiceLib from "../lib/services/project-service";
import {ProjectNameService} from "../lib/services/project-name-service";
import * as ProjectDataServiceLib from "../lib/services/project-data-service";
import * as ProjectDataLib from "../lib/project-data";
import * as ProjectHelperLib from "../lib/common/project-helper";
Expand All @@ -21,11 +22,16 @@ import {assert} from "chai";
import {Options} from "../lib/options";
import {HostInfo} from "../lib/common/host-info";
import {ProjectTemplatesService} from "../lib/services/project-templates-service";
import Future = require("fibers/future");

let mockProjectNameValidator = {
validate: () => { return true; }
validate: () => true
};

let dummyString: string = "dummyString";
let hasPromptedForString = false;
let originalIsInteractive = helpers.isInteractive;

temp.track();

class ProjectIntegrationTest {
Expand Down Expand Up @@ -121,6 +127,7 @@ class ProjectIntegrationTest {
this.testInjector.register("errors", stubs.ErrorsStub);
this.testInjector.register('logger', stubs.LoggerStub);
this.testInjector.register("projectService", ProjectServiceLib.ProjectService);
this.testInjector.register("projectNameService", ProjectNameService);
this.testInjector.register("projectHelper", ProjectHelperLib.ProjectHelper);
this.testInjector.register("projectTemplatesService", ProjectTemplatesService);
this.testInjector.register("projectNameValidator", mockProjectNameValidator);
Expand All @@ -136,6 +143,15 @@ class ProjectIntegrationTest {

this.testInjector.register("options", Options);
this.testInjector.register("hostInfo", HostInfo);
this.testInjector.register("prompter", {
confirm: (message: string): IFuture<boolean> => Future.fromResult(true),
getString: (message: string): IFuture<string> => {
return (() => {
hasPromptedForString = true;
return dummyString;
}).future<string>()();
}
});
}
}

Expand Down Expand Up @@ -299,6 +315,115 @@ describe("Project Service Tests", () => {
projectIntegrationTest.createProject(projectName).wait();
projectIntegrationTest.assertProject(tempFolder, projectName, options.appid).wait();
});

describe("project name validation tests", () => {
let validProjectName = "valid";
let invalidProjectName = "1invalid";
let projectIntegrationTest: ProjectIntegrationTest;
let tempFolder: string;
let options: IOptions;
let prompter: IPrompter;

beforeEach(() => {
hasPromptedForString = false;
helpers.isInteractive = () => true;
projectIntegrationTest = new ProjectIntegrationTest();
tempFolder = temp.mkdirSync("project");
options = projectIntegrationTest.testInjector.resolve("options");
prompter = projectIntegrationTest.testInjector.resolve("prompter");
});

afterEach(() => {
helpers.isInteractive = originalIsInteractive;
});

it("creates project when is interactive and incorrect name is specified and the --force option is set", () => {
let projectName = invalidProjectName;

options.force = true;
options.path = tempFolder;
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this line?


projectIntegrationTest.createProject(projectName).wait();
projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`).wait();
});

it("creates project when is interactive and incorrect name is specified and the user confirms to use the incorrect name", () => {
let projectName = invalidProjectName;
prompter.confirm = (message: string): IFuture<boolean> => Future.fromResult(true);

options.path = tempFolder;
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, I think we do not need this line


projectIntegrationTest.createProject(projectName).wait();
projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`).wait();
});

it("prompts for new name when is interactive and incorrect name is specified and the user does not confirm to use the incorrect name", () => {
let projectName = invalidProjectName;

prompter.confirm = (message: string): IFuture<boolean> => Future.fromResult(false);

options.path = tempFolder;

projectIntegrationTest.createProject(projectName).wait();
assert.isTrue(hasPromptedForString);
});

it("creates project when is interactive and incorrect name is specified and the user does not confirm to use the incorrect name and enters incorrect name again several times and then enters correct name", () => {
let projectName = invalidProjectName;

prompter.confirm = (message: string): IFuture<boolean> => Future.fromResult(false);

let incorrectInputsLimit = 5;
let incorrectInputsCount = 0;

prompter.getString = (message: string): IFuture<string> => {
return (() => {
if (incorrectInputsCount < incorrectInputsLimit) {
incorrectInputsCount++;
}
else {
hasPromptedForString = true;

return validProjectName;
}

return projectName;
}).future<string>()();
};

options.path = tempFolder;

projectIntegrationTest.createProject(projectName).wait();
assert.isTrue(hasPromptedForString);
});

it("does not create project when is not interactive and incorrect name is specified", () => {
let projectName = invalidProjectName;
helpers.isInteractive = () => false;

options.force = false;
options.path = tempFolder;

assert.throws(() => {
projectIntegrationTest.createProject(projectName).wait();
});
});

it("creates project when is not interactive and incorrect name is specified and the --force option is set", () => {
let projectName = invalidProjectName;
helpers.isInteractive = () => false;

options.force = true;
options.path = tempFolder;

projectIntegrationTest.createProject(projectName).wait();
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`).wait();
});
});

});
});

Expand Down