Skip to content

Commit e49ca89

Browse files
Added check if project name starts with number when creating new project.
Created project name service with method to ensure the project name does not start with number or if it starts with number the client is informed about the consequences when building for Android. When invalid project name is entered the client gets warning about the problem and a prompt with choice to create the project with invalid name or to enter valid name. Added integration tests for the prompts logic. Added unit tests for the project name service.
1 parent 219a1ad commit e49ca89

File tree

5 files changed

+245
-5
lines changed

5 files changed

+245
-5
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ $injector.require("androidProjectService", "./services/android-project-service")
1212
$injector.require("iOSProjectService", "./services/ios-project-service");
1313

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

1718
$injector.require("platformsData", "./platforms-data");

lib/declarations.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -158,23 +158,23 @@ interface IAndroidToolsInfo {
158158
* @param {any} options Defines if the warning messages should treated as error and if the targetSdk value should be validated as well.
159159
* @return {boolean} True if there are detected issues, false otherwise.
160160
*/
161-
validateInfo(options?: {showWarningsAsErrors: boolean, validateTargetSdk: boolean}): IFuture<boolean>;
161+
validateInfo(options?: { showWarningsAsErrors: boolean, validateTargetSdk: boolean }): IFuture<boolean>;
162162

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

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

179179
/**
180180
* Gets the path to `adb` executable from ANDROID_HOME. It should be `$ANDROID_HOME/platform-tools/adb` in case it exists.
@@ -255,3 +255,16 @@ interface IXmlValidator {
255255
*/
256256
getXmlFileErrors(sourceFile: string): IFuture<string>;
257257
}
258+
259+
/**
260+
* Describes methods for project name.
261+
*/
262+
interface IProjectNameService {
263+
/**
264+
* Ensures the passed project name is valid. If the project name is not valida prompts for actions.
265+
* @param {string} project name to be checked.
266+
* @param {IOptions} current command options.
267+
* @return {IFuture<strng>} returns the selected name of the project.
268+
*/
269+
ensureValidName(projectName: string, validateOptions?: {force: boolean}): IFuture<string>;
270+
}

lib/services/project-name-service.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"use strict";
2+
3+
import { isInteractive } from "../common/helpers";
4+
5+
export class ProjectNameService implements IProjectNameService {
6+
constructor(private $projectNameValidator: IProjectNameValidator,
7+
private $errors: IErrors,
8+
private $logger: ILogger,
9+
private $prompter: IPrompter) { }
10+
11+
public ensureValidName(projectName: string, validateOptions?: { force: boolean }): IFuture<string> {
12+
return (() => {
13+
if (validateOptions && validateOptions.force) {
14+
return projectName;
15+
}
16+
17+
if (!this.$projectNameValidator.validate(projectName)) {
18+
return this.promptForNewName(projectName, validateOptions).wait();
19+
}
20+
21+
if (!this.checkIfNameStartsWithLetter(projectName)) {
22+
if (!isInteractive()) {
23+
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.");
24+
return;
25+
}
26+
27+
return this.promptForNewName(projectName, validateOptions).wait();
28+
}
29+
30+
return projectName;
31+
}).future<string>()();
32+
}
33+
34+
private checkIfNameStartsWithLetter(projectName: string): boolean {
35+
let startsWithLetterExpression = /^[a-zA-Z]/;
36+
return startsWithLetterExpression.test(projectName);
37+
}
38+
39+
private promptForNewName(projectName: string, validateOptions?: { force: boolean }): IFuture<string> {
40+
return (() => {
41+
if (this.promptForForceNameConfirm().wait()) {
42+
return projectName;
43+
}
44+
45+
let newProjectName = this.$prompter.getString("Enter the new project name:").wait();
46+
return this.ensureValidName(newProjectName, validateOptions).wait();
47+
}).future<string>()();
48+
}
49+
50+
private promptForForceNameConfirm(): IFuture<boolean> {
51+
return (() => {
52+
this.$logger.warn("The project name does not start with letter and will fail to build for Android.");
53+
54+
return this.$prompter.confirm("Do You want to create the project with this name?").wait();
55+
}).future<boolean>()();
56+
}
57+
}
58+
59+
$injector.register("projectNameService", ProjectNameService);

lib/services/project-service.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class ProjectService implements IProjectService {
1414
private $logger: ILogger,
1515
private $projectDataService: IProjectDataService,
1616
private $projectHelper: IProjectHelper,
17-
private $projectNameValidator: IProjectNameValidator,
17+
private $projectNameService: IProjectNameService,
1818
private $projectTemplatesService: IProjectTemplatesService,
1919
private $options: IOptions) { }
2020

@@ -23,7 +23,8 @@ export class ProjectService implements IProjectService {
2323
if (!projectName) {
2424
this.$errors.fail("You must specify <App name> when creating a new project.");
2525
}
26-
this.$projectNameValidator.validate(projectName);
26+
27+
projectName = this.$projectNameService.ensureValidName(projectName, {force: this.$options.force}).wait();
2728

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

test/project-service.ts

+166
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as stubs from "./stubs";
66
import * as constants from "./../lib/constants";
77
import {ChildProcess} from "../lib/common/child-process";
88
import * as ProjectServiceLib from "../lib/services/project-service";
9+
import { ProjectNameService } from "../lib/services/project-name-service";
910
import * as ProjectDataServiceLib from "../lib/services/project-data-service";
1011
import * as ProjectDataLib from "../lib/project-data";
1112
import * as ProjectHelperLib from "../lib/common/project-helper";
@@ -26,6 +27,10 @@ let mockProjectNameValidator = {
2627
validate: () => { return true; }
2728
};
2829

30+
let dummyString: string = "dummyString";
31+
let hasPromptedForString = false;
32+
let originalIsInteractive = helpers.isInteractive;
33+
2934
temp.track();
3035

3136
class ProjectIntegrationTest {
@@ -121,6 +126,7 @@ class ProjectIntegrationTest {
121126
this.testInjector.register("errors", stubs.ErrorsStub);
122127
this.testInjector.register('logger', stubs.LoggerStub);
123128
this.testInjector.register("projectService", ProjectServiceLib.ProjectService);
129+
this.testInjector.register("projectNameService", ProjectNameService);
124130
this.testInjector.register("projectHelper", ProjectHelperLib.ProjectHelper);
125131
this.testInjector.register("projectTemplatesService", ProjectTemplatesService);
126132
this.testInjector.register("projectNameValidator", mockProjectNameValidator);
@@ -136,6 +142,19 @@ class ProjectIntegrationTest {
136142

137143
this.testInjector.register("options", Options);
138144
this.testInjector.register("hostInfo", HostInfo);
145+
this.testInjector.register("prompter", {
146+
confirm: (message: string): IFuture<boolean> => {
147+
return (() => {
148+
return true;
149+
}).future<boolean>()();
150+
},
151+
getString: (message: string): IFuture<string> => {
152+
return (() => {
153+
hasPromptedForString = true;
154+
return dummyString;
155+
}).future<string>()();
156+
}
157+
});
139158
}
140159
}
141160

@@ -273,6 +292,153 @@ describe("Project Service Tests", () => {
273292
projectIntegrationTest.createProject(projectName).wait();
274293
projectIntegrationTest.assertProject(tempFolder, projectName, options.appid).wait();
275294
});
295+
296+
describe("project name validation tests", () => {
297+
let validProjectName = "valid";
298+
let invalidProjectName = "1invalid";
299+
300+
beforeEach(() => {
301+
hasPromptedForString = false;
302+
})
303+
304+
afterEach(() => {
305+
helpers.isInteractive = originalIsInteractive;
306+
});
307+
308+
it("creates project when is interactive and incorrect name is specified and the --force option is set", () => {
309+
let projectIntegrationTest = new ProjectIntegrationTest();
310+
let tempFolder = temp.mkdirSync("project");
311+
let projectName = invalidProjectName;
312+
helpers.isInteractive = () => { return true; };
313+
314+
let options: IOptions = projectIntegrationTest.testInjector.resolve("options");
315+
options.force = true;
316+
317+
options.path = tempFolder;
318+
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
319+
320+
projectIntegrationTest.createProject(projectName).wait();
321+
projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`).wait();
322+
});
323+
324+
it("creates project when is interactive and incorrect name is specified and the user confirms to use the incorrect name", () => {
325+
let projectIntegrationTest = new ProjectIntegrationTest();
326+
let tempFolder = temp.mkdirSync("project");
327+
let projectName = invalidProjectName;
328+
helpers.isInteractive = () => { return true; };
329+
330+
let prompter = projectIntegrationTest.testInjector.resolve("prompter");
331+
prompter.confirm = (message: string): IFuture<boolean> => {
332+
return (() => {
333+
return true;
334+
}).future<boolean>()();
335+
};
336+
337+
let options: IOptions = projectIntegrationTest.testInjector.resolve("options");
338+
339+
options.path = tempFolder;
340+
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
341+
342+
projectIntegrationTest.createProject(projectName).wait();
343+
projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`).wait();
344+
});
345+
346+
it("prompts for new name when is interactive and incorrect name is specified and the user does not confirm to use the incorrect name", () => {
347+
let projectIntegrationTest = new ProjectIntegrationTest();
348+
let tempFolder = temp.mkdirSync("project");
349+
let projectName = invalidProjectName;
350+
helpers.isInteractive = () => { return true; };
351+
352+
let prompter = projectIntegrationTest.testInjector.resolve("prompter");
353+
prompter.confirm = (message: string): IFuture<boolean> => {
354+
return (() => {
355+
return false;
356+
}).future<boolean>()();
357+
};
358+
359+
let options: IOptions = projectIntegrationTest.testInjector.resolve("options");
360+
361+
options.path = tempFolder;
362+
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
363+
364+
projectIntegrationTest.createProject(projectName).wait();
365+
assert.isTrue(hasPromptedForString);
366+
});
367+
368+
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", () => {
369+
let projectIntegrationTest = new ProjectIntegrationTest();
370+
let tempFolder = temp.mkdirSync("project");
371+
let projectName = invalidProjectName;
372+
helpers.isInteractive = () => { return true; };
373+
374+
let prompter = projectIntegrationTest.testInjector.resolve("prompter");
375+
prompter.confirm = (message: string): IFuture<boolean> => {
376+
return (() => {
377+
return false;
378+
}).future<boolean>()();
379+
};
380+
381+
let incorrectInputsLimit = 20;
382+
let incorrectInputsCount = 0;
383+
384+
prompter.getString = (message: string): IFuture<string> => {
385+
return (() => {
386+
if (incorrectInputsCount < incorrectInputsLimit) {
387+
incorrectInputsCount++;
388+
}
389+
else {
390+
hasPromptedForString = true;
391+
392+
return validProjectName;
393+
}
394+
395+
return projectName;
396+
}).future<string>()();
397+
};
398+
399+
let options: IOptions = projectIntegrationTest.testInjector.resolve("options");
400+
401+
options.path = tempFolder;
402+
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
403+
404+
projectIntegrationTest.createProject(projectName).wait();
405+
assert.isTrue(hasPromptedForString);
406+
});
407+
408+
it("does not create project when is not interactive and incorrect name is specified", () => {
409+
let projectIntegrationTest = new ProjectIntegrationTest();
410+
let tempFolder = temp.mkdirSync("project");
411+
let projectName = invalidProjectName;
412+
helpers.isInteractive = () => { return false; };
413+
414+
let options: IOptions = projectIntegrationTest.testInjector.resolve("options");
415+
options.force = false;
416+
417+
options.path = tempFolder;
418+
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
419+
420+
assert.throws(() => {
421+
projectIntegrationTest.createProject(projectName).wait();
422+
});
423+
});
424+
425+
it("creates project when is not interactive and incorrect name is specified and the --force option is set", () => {
426+
let projectIntegrationTest = new ProjectIntegrationTest();
427+
let tempFolder = temp.mkdirSync("project");
428+
let projectName = invalidProjectName;
429+
helpers.isInteractive = () => { return false; };
430+
431+
let options: IOptions = projectIntegrationTest.testInjector.resolve("options");
432+
options.force = true;
433+
434+
options.path = tempFolder;
435+
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
436+
437+
projectIntegrationTest.createProject(projectName).wait();
438+
projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`).wait();
439+
});
440+
});
441+
276442
});
277443
});
278444

0 commit comments

Comments
 (0)