Skip to content

Commit adcd755

Browse files
Fix asking for user email on postinstall
On postinstall CLI asks the users if they want to subscribe for NativeScript's newsletter. This check has been broken in 3.0.0 because of incorrect await. In order to add tests for this functionality, introduce a new service that handles the check and sending of information about the user's mail. Add the following tests: - ensure postinstall.js calls correct CLI command - ensure the post-install-cli command calls the new service - ensure the new service works correctly
1 parent ef2570b commit adcd755

File tree

9 files changed

+471
-62
lines changed

9 files changed

+471
-62
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,4 @@ $injector.requireCommand("extension|uninstall", "./commands/extensibility/uninst
139139
$injector.requirePublic("extensibilityService", "./services/extensibility-service");
140140

141141
$injector.require("nodeModulesDependenciesBuilder", "./tools/node-modules/node-modules-dependencies-builder");
142+
$injector.require("subscriptionService", "./services/subscription-service");

lib/commands/post-install.ts

+2-60
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
import { PostInstallCommand } from "../common/commands/post-install";
2-
import * as emailValidator from "email-validator";
3-
import * as queryString from "querystring";
4-
import * as helpers from "../common/helpers";
52

63
export class PostInstallCliCommand extends PostInstallCommand {
7-
private logger: ILogger;
8-
94
constructor($fs: IFileSystem,
10-
private $httpClient: Server.IHttpClient,
11-
private $prompter: IPrompter,
12-
private $userSettingsService: IUserSettingsService,
5+
private $subscriptionService: ISubscriptionService,
136
$staticConfig: Config.IStaticConfig,
147
$commandsService: ICommandsService,
158
$htmlHelpService: IHtmlHelpService,
@@ -18,63 +11,12 @@ export class PostInstallCliCommand extends PostInstallCommand {
1811
$analyticsService: IAnalyticsService,
1912
$logger: ILogger) {
2013
super($fs, $staticConfig, $commandsService, $htmlHelpService, $options, $doctorService, $analyticsService, $logger);
21-
this.logger = $logger;
2214
}
2315

2416
public async execute(args: string[]): Promise<void> {
2517
await super.execute(args);
2618

27-
if (await this.shouldAskForEmail()) {
28-
this.logger.out("Leave your e-mail address here to subscribe for NativeScript newsletter and product updates, tips and tricks:");
29-
let email = await this.getEmail("(press Enter for blank)");
30-
await this.$userSettingsService.saveSetting("EMAIL_REGISTERED", true);
31-
await this.sendEmail(email);
32-
}
33-
}
34-
35-
private async shouldAskForEmail(): Promise<boolean> {
36-
return helpers.isInteractive() && await process.env.CLI_NOPROMPT !== "1" && !this.$userSettingsService.getSettingValue("EMAIL_REGISTERED");
37-
}
38-
39-
private async getEmail(prompt: string, options?: IPrompterOptions): Promise<string> {
40-
let schema: IPromptSchema = {
41-
message: prompt,
42-
type: "input",
43-
name: "inputEmail",
44-
validate: (value: any) => {
45-
if (value === "" || emailValidator.validate(value)) {
46-
return true;
47-
}
48-
return "Please provide a valid e-mail or simply leave it blank.";
49-
},
50-
default: options && options.defaultAction
51-
};
52-
53-
let result = await this.$prompter.get([schema]);
54-
return result.inputEmail;
55-
}
56-
57-
private async sendEmail(email: string): Promise<void> {
58-
if (email) {
59-
let postData = queryString.stringify({
60-
'elqFormName': "dev_uins_cli",
61-
'elqSiteID': '1325',
62-
'emailAddress': email,
63-
'elqCookieWrite': '0'
64-
});
65-
66-
let options = {
67-
url: 'https://s1325.t.eloqua.com/e/f2',
68-
method: 'POST',
69-
headers: {
70-
'Content-Type': 'application/x-www-form-urlencoded',
71-
'Content-Length': postData.length
72-
},
73-
body: postData
74-
};
75-
76-
await this.$httpClient.httpRequest(options);
77-
}
19+
await this.$subscriptionService.subscribeForNewsletter();
7820
}
7921
}
8022

lib/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ export const BUILD_OUTPUT_EVENT_NAME = "buildOutput";
8686
export const CONNECTION_ERROR_EVENT_NAME = "connectionError";
8787
export const VERSION_STRING = "version";
8888
export const INSPECTOR_CACHE_DIRNAME = "ios-inspector";
89+
export const POST_INSTALL_COMMAND_NAME = "post-install-cli";
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Describes methods for subscribing to different NativeScript campaigns.
3+
*/
4+
interface ISubscriptionService {
5+
/**
6+
* Subscribes users for NativeScript's newsletter by asking them for their email.
7+
* In case we've already asked the user for his email, this method will do nothing.
8+
* @returns {Promise<void>}
9+
*/
10+
subscribeForNewsletter(): Promise<void>;
11+
}

lib/services/subscription-service.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as emailValidator from "email-validator";
2+
import * as queryString from "querystring";
3+
import * as helpers from "../common/helpers";
4+
5+
export class SubscriptionService implements ISubscriptionService {
6+
constructor(private $httpClient: Server.IHttpClient,
7+
private $prompter: IPrompter,
8+
private $userSettingsService: IUserSettingsService,
9+
private $logger: ILogger) {
10+
}
11+
12+
public async subscribeForNewsletter(): Promise<void> {
13+
if (await this.shouldAskForEmail()) {
14+
this.$logger.out("Leave your e-mail address here to subscribe for NativeScript newsletter and product updates, tips and tricks:");
15+
let email = await this.getEmail("(press Enter for blank)");
16+
await this.$userSettingsService.saveSetting("EMAIL_REGISTERED", true);
17+
await this.sendEmail(email);
18+
}
19+
}
20+
21+
/**
22+
* Checks whether we should ask the current user if they want to subscribe to NativeScript newsletter.
23+
* NOTE: This method is protected, not private, only because of our unit tests.
24+
* @returns {Promise<boolean>}
25+
*/
26+
protected async shouldAskForEmail(): Promise<boolean> {
27+
return helpers.isInteractive() && process.env.CLI_NOPROMPT !== "1" && !(await this.$userSettingsService.getSettingValue("EMAIL_REGISTERED"));
28+
}
29+
30+
private async getEmail(prompt: string, options?: IPrompterOptions): Promise<string> {
31+
let schema: IPromptSchema = {
32+
message: prompt,
33+
type: "input",
34+
name: "inputEmail",
35+
validate: (value: any) => {
36+
if (value === "" || emailValidator.validate(value)) {
37+
return true;
38+
}
39+
40+
return "Please provide a valid e-mail or simply leave it blank.";
41+
}
42+
};
43+
44+
let result = await this.$prompter.get([schema]);
45+
return result.inputEmail;
46+
}
47+
48+
private async sendEmail(email: string): Promise<void> {
49+
if (email) {
50+
let postData = queryString.stringify({
51+
'elqFormName': "dev_uins_cli",
52+
'elqSiteID': '1325',
53+
'emailAddress': email,
54+
'elqCookieWrite': '0'
55+
});
56+
57+
let options = {
58+
url: 'https://s1325.t.eloqua.com/e/f2',
59+
method: 'POST',
60+
headers: {
61+
'Content-Type': 'application/x-www-form-urlencoded',
62+
'Content-Length': postData.length
63+
},
64+
body: postData
65+
};
66+
67+
await this.$httpClient.httpRequest(options);
68+
}
69+
}
70+
71+
}
72+
73+
$injector.register("subscriptionService", SubscriptionService);

postinstall.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"use strict";
22

33
var child_process = require("child_process");
4-
var commandArgs = ["bin/tns", "post-install-cli"];
54
var path = require("path");
5+
var constants = require(path.join(__dirname, "lib", "constants"));
6+
var commandArgs = [path.join(__dirname, "bin", "tns"), constants.POST_INSTALL_COMMAND_NAME];
67
var nodeArgs = require(path.join(__dirname, "lib", "common", "scripts", "node-args")).getNodeArgs();
78

8-
child_process.spawn(process.argv[0], nodeArgs.concat(commandArgs), {stdio: "inherit"});
9+
child_process.spawn(process.argv[0], nodeArgs.concat(commandArgs), { stdio: "inherit" });

test/commands/post-install.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Yok } from "../../lib/common/yok";
2+
import { assert } from "chai";
3+
import { PostInstallCliCommand } from "../../lib/commands/post-install";
4+
5+
const createTestInjector = (): IInjector => {
6+
const testInjector = new Yok();
7+
testInjector.register("fs", {
8+
setCurrentUserAsOwner: async (path: string, owner: string): Promise<void> => undefined
9+
});
10+
11+
testInjector.register("subscriptionService", {
12+
subscribeForNewsletter: async (): Promise<void> => undefined
13+
});
14+
15+
testInjector.register("staticConfig", {});
16+
17+
testInjector.register("commandsService", {
18+
tryExecuteCommand: async (commandName: string, commandArguments: string[]): Promise<void> => undefined
19+
});
20+
21+
testInjector.register("htmlHelpService", {
22+
generateHtmlPages: async (): Promise<void> => undefined
23+
});
24+
25+
testInjector.register("options", {});
26+
27+
testInjector.register("doctorService", {
28+
printWarnings: async (configOptions?: { trackResult: boolean }): Promise<boolean> => undefined
29+
});
30+
31+
testInjector.register("analyticsService", {
32+
checkConsent: async (): Promise<void> => undefined,
33+
track: async (featureName: string, featureValue: string): Promise<void> => undefined
34+
});
35+
36+
testInjector.register("logger", {
37+
out: (formatStr?: any, ...args: any[]): void => undefined,
38+
printMarkdown: (...args: any[]): void => undefined
39+
});
40+
41+
testInjector.registerCommand("post-install-cli", PostInstallCliCommand);
42+
43+
return testInjector;
44+
};
45+
46+
describe("post-install command", () => {
47+
it("calls subscriptionService.subscribeForNewsletter method", async () => {
48+
const testInjector = createTestInjector();
49+
const subscriptionService = testInjector.resolve<ISubscriptionService>("subscriptionService");
50+
let isSubscribeForNewsletterCalled = false;
51+
subscriptionService.subscribeForNewsletter = async (): Promise<void> => {
52+
isSubscribeForNewsletterCalled = true;
53+
};
54+
const postInstallCommand = testInjector.resolveCommand("post-install-cli");
55+
56+
await postInstallCommand.execute([]);
57+
assert.isTrue(isSubscribeForNewsletterCalled, "post-install-cli command must call subscriptionService.subscribeForNewsletter");
58+
});
59+
});

test/post-install.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { assert } from "chai";
2+
3+
// Use require instead of import in order to replace the `spawn` method of child_process
4+
let childProcess = require("child_process");
5+
6+
import { SpawnOptions, ChildProcess } from "child_process";
7+
import * as path from "path";
8+
import { POST_INSTALL_COMMAND_NAME } from "../lib/constants";
9+
10+
describe("postinstall.js", () => {
11+
it("calls post-install-cli command of CLI", () => {
12+
const originalSpawn = childProcess.spawn;
13+
let isSpawnCalled = false;
14+
let argsPassedToSpawn: string[] = [];
15+
childProcess.spawn = (command: string, args?: string[], options?: SpawnOptions): ChildProcess => {
16+
isSpawnCalled = true;
17+
argsPassedToSpawn = args;
18+
19+
return null;
20+
};
21+
22+
require(path.join(__dirname, "..", "postinstall"));
23+
24+
childProcess.spawn = originalSpawn;
25+
26+
assert.isTrue(isSpawnCalled, "child_process.spawn must be called from postinstall.js");
27+
28+
const expectedPathToCliExecutable = path.join(__dirname, "..", "bin", "tns");
29+
30+
assert.isTrue(argsPassedToSpawn.indexOf(expectedPathToCliExecutable) !== -1, `The spawned args must contain path to TNS.
31+
Expected path is: ${expectedPathToCliExecutable}, current args are: ${argsPassedToSpawn}.`);
32+
assert.isTrue(argsPassedToSpawn.indexOf(POST_INSTALL_COMMAND_NAME) !== -1, `The spawned args must contain the name of the post-install command.
33+
Expected path is: ${expectedPathToCliExecutable}, current args are: ${argsPassedToSpawn}.`);
34+
});
35+
});

0 commit comments

Comments
 (0)