Skip to content
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

feat: make init command more suitable for automation #634

Merged
merged 11 commits into from
Sep 25, 2020
7 changes: 6 additions & 1 deletion bin/stencil-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ if (!versionCheck()) {

const cliOptions = program.opts();

new StencilInit().run(DOT_STENCIL_FILE_PATH, cliOptions);
new StencilInit().run(DOT_STENCIL_FILE_PATH,
{
normalStoreUrl: cliOptions.url,
accessToken: cliOptions.token,
port: cliOptions.port,
});
64 changes: 43 additions & 21 deletions lib/stencil-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,18 @@ class StencilInit {
/**
* @param {string} dotStencilFilePath
* @param {object} cliOptions
* @param {string} cliOptions.url
* @param {string} cliOptions.token
* @param {string} cliOptions.normalStoreUrl
* @param {string} cliOptions.accessToken
* @param {number} cliOptions.port
* @returns {Promise<void>}
*/
async run (dotStencilFilePath, cliOptions = {}) {

const oldStencilConfig = this.readStencilConfig(dotStencilFilePath);
const defaultAnswers = this.getDefaultAnswers(oldStencilConfig, cliOptions);
const answers = await this.askQuestions(defaultAnswers);
const updatedStencilConfig = this.applyAnswers(oldStencilConfig, answers);
const defaultAnswers = this.getDefaultAnswers(oldStencilConfig);
const questions = this.getQuestions(defaultAnswers, cliOptions);
const answers = await this.askQuestions(questions);
const updatedStencilConfig = this.applyAnswers(oldStencilConfig, answers, cliOptions);
this.saveStencilConfig(updatedStencilConfig, dotStencilFilePath);

this.logger.log('You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan);
Expand Down Expand Up @@ -73,38 +75,46 @@ class StencilInit {

/**
* @param {{port: (number), normalStoreUrl: (string), accessToken: (string)}} stencilConfig
* @param {{port: (number), url: (string), token: (string)}} cliOptions
* @returns {{port: (number), normalStoreUrl: (string), accessToken: (string)}}
*/
getDefaultAnswers (stencilConfig, cliOptions) {
getDefaultAnswers (stencilConfig) {
return {
normalStoreUrl: cliOptions.url || stencilConfig.normalStoreUrl,
accessToken: cliOptions.token || stencilConfig.accessToken,
port: cliOptions.port || stencilConfig.port || this.serverConfig.get('/server/port'),
normalStoreUrl: stencilConfig.normalStoreUrl,
accessToken: stencilConfig.accessToken,
port: stencilConfig.port || this.serverConfig.get('/server/port'),
};
}

/**
* @param {{port: (number), normalStoreUrl: (string), accessToken: (string)}} defaultAnswers
* @returns {Promise<object>}
* @param {{port: (number), normalStoreUrl: (string), accessToken: (string)}} cliOptions
* @returns {{object[]}}
*/
async askQuestions (defaultAnswers) {
return await this.inquirer.prompt([
{
getQuestions (defaultAnswers, cliOptions) {
const prompts = [];

if(!cliOptions.normalStoreUrl){
prompts.push({
type: 'input',
name: 'normalStoreUrl',
message: 'What is the URL of your store\'s home page?',
validate: val => /^https?:\/\//.test(val) || 'You must enter a URL',
default: defaultAnswers.normalStoreUrl,
},
{
});
}

if(!cliOptions.accessToken){
prompts.push({
type: 'input',
name: 'accessToken',
message: 'What is your Stencil OAuth Access Token?',
default: defaultAnswers.accessToken,
filter: val => val.trim(),
},
{
});
}

if(!cliOptions.port){
prompts.push({
type: 'input',
name: 'port',
message: 'What port would you like to run the server on?',
Expand All @@ -118,19 +128,31 @@ class StencilInit {
return true;
}
},
},
]);
});
}

return prompts;
}

/**
* @param {{object[]}} cliOptions
* @returns {Promise<object>}
MaxGenash marked this conversation as resolved.
Show resolved Hide resolved
*/
async askQuestions (questions) {
return questions.length ? this.inquirer.prompt(questions) : {};
}

/**
* @param {object} stencilConfig
* @param {object} answers
* @param {{port: (number), url: (string), token: (string)}} cliOptions
* @returns {object}
*/
applyAnswers (stencilConfig, answers) {
applyAnswers (stencilConfig, answers, cliOptions) {
MaxGenash marked this conversation as resolved.
Show resolved Hide resolved
return {
customLayouts: DEFAULT_CUSTOM_LAYOUTS_CONFIG,
...stencilConfig,
...cliOptions,
...answers,
};
}
Expand Down
155 changes: 89 additions & 66 deletions lib/stencil-init.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,47 @@ const getAnswers = () => ({
accessToken: "accessToken_from_answers",
});
const getCliOptions = () => ({
url: "https://url-from-cli-options.mybigcommerce.com",
normalStoreUrl: "https://url-from-cli-options.mybigcommerce.com",
port: 3002,
token: "accessToken_from_CLI_options",
accessToken: "accessToken_from_CLI_options",
});
const getQuestions = () => ([
{
type: 'input',
name: 'normalStoreUrl',
message: 'What is the URL of your store\'s home page?',
validate: val => /^https?:\/\//.test(val) || 'You must enter a URL',
default: 'https://url-from-answers.mybigcommerce.com',
},
{
type: 'input',
name: 'accessToken',
message: 'What is your Stencil OAuth Access Token?',
default: 'accessToken_from_answers',
filter: val => val.trim(),
},
{
type: 'input',
name: 'port',
message: 'What port would you like to run the server on?',
default: 3003,
validate: val => {
if (isNaN(val)) {
return 'You must enter an integer';
} else if (val < 1024 || val > 65535) {
return 'The port number must be between 1025 and 65535';
} else {
return true;
}
},
},
]);

afterEach(() => jest.restoreAllMocks());

describe('StencilInit integration tests', () => {
describe('run', () => {
it('should perform all the actions, save the result and inform the user about the successful finish', async () => {
it('using cli prompts, should perform all the actions, save the result and inform the user about the successful finish', async () => {
const dotStencilFilePath = './test/_mocks/bin/dotStencilFile.json';
const answers = getAnswers();
const expectedResult = JSON.stringify({ customLayouts: DEFAULT_CUSTOM_LAYOUTS_CONFIG, ...answers }, null, 2);
Expand All @@ -58,13 +89,40 @@ describe('StencilInit integration tests', () => {
serverConfig,
logger: console,
});
await instance.run(dotStencilFilePath, getCliOptions());
await instance.run(dotStencilFilePath);

expect(fsWriteFileSyncStub).toHaveBeenCalledTimes(1);
expect(inquirerPromptStub).toHaveBeenCalledTimes(1);
expect(consoleErrorStub).toHaveBeenCalledTimes(0);
expect(consoleLogStub).toHaveBeenCalledTimes(1);

expect(fsWriteFileSyncStub).toHaveBeenCalledWith(dotStencilFilePath, expectedResult);
expect(consoleLogStub).toHaveBeenCalledWith('You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan);
}),
it('using cli options, should perform all the actions, save the result and inform the user about the successful finish', async () => {
const dotStencilFilePath = './test/_mocks/bin/dotStencilFile.json';
const cliOptions = getCliOptions();
const expectedResult = JSON.stringify({ customLayouts: DEFAULT_CUSTOM_LAYOUTS_CONFIG, ...cliOptions }, null, 2);
const fsWriteFileSyncStub = jest.spyOn(fs, "writeFileSync").mockImplementation(jest.fn());
const inquirerPromptStub = jest.spyOn(inquirer, 'prompt').mockReturnValue({});
const consoleErrorStub = jest.spyOn(console, 'error').mockImplementation(jest.fn());
const consoleLogStub = jest.spyOn(console, 'log').mockImplementation(jest.fn());

// Test with real entities, just some methods stubbed
const instance = new StencilInit({
inquirer,
jsonLint,
fs,
serverConfig,
logger: console,
});
await instance.run(dotStencilFilePath, cliOptions);

expect(fsWriteFileSyncStub).toHaveBeenCalledTimes(1);
expect(inquirerPromptStub).toHaveBeenCalledTimes(0);
expect(consoleErrorStub).toHaveBeenCalledTimes(0);
expect(consoleLogStub).toHaveBeenCalledTimes(1);

expect(fsWriteFileSyncStub).toHaveBeenCalledWith(dotStencilFilePath, expectedResult);
expect(consoleLogStub).toHaveBeenCalledWith('You are now ready to go! To start developing, run $ ' + 'stencil start'.cyan);
});
Expand Down Expand Up @@ -186,97 +244,62 @@ describe('StencilInit unit tests', () => {
// eslint-disable-next-line jest/expect-expect
it('should not mutate the passed objects', async () => {
const stencilConfig = getStencilConfig();
const cliOptions = getCliOptions();
const instance = getStencilInitInstance();

await assertNoMutations(
[stencilConfig, cliOptions],
() => instance.getDefaultAnswers(stencilConfig, cliOptions),
[stencilConfig],
() => instance.getDefaultAnswers(stencilConfig),
);
});

it('should pick values from cliOptions first if present', async () => {
const stencilConfig = getStencilConfig();
const cliOptions = getCliOptions();
const instance = getStencilInitInstance();

const res = instance.getDefaultAnswers(stencilConfig, cliOptions);

expect(res.normalStoreUrl).toEqual(cliOptions.url);
expect(res.accessToken).toEqual(cliOptions.token);
expect(res.port).toEqual(cliOptions.port);
});

it('should pick values from stencilConfig if cliOptions are empty', async () => {
it('should pick values from stencilConfig if not empty', async () => {
const stencilConfig = getStencilConfig();
const cliOptions = {};
const instance = getStencilInitInstance();

const res = instance.getDefaultAnswers(stencilConfig, cliOptions);
const res = instance.getDefaultAnswers(stencilConfig);

expect(res.normalStoreUrl).toEqual(stencilConfig.normalStoreUrl);
expect(res.accessToken).toEqual(stencilConfig.accessToken);
expect(res.port).toEqual(stencilConfig.port);
});

it('should pick values from serverConfig if stencilConfig and cliOptions are empty', async () => {
const cliOptions = _.pick(getCliOptions(), ['url']);
const stencilConfig = _.pick(getStencilConfig(), ['accessToken']);
it('should pick values from serverConfig if stencilConfig are empty', async () => {
const stencilConfig = _.pick(getStencilConfig(), ['accessToken','url']);
const instance = getStencilInitInstance();

const res = instance.getDefaultAnswers(stencilConfig, cliOptions);
const res = instance.getDefaultAnswers(stencilConfig);

expect(res.port).toEqual(serverConfigPort);

expect(res.normalStoreUrl).toEqual(cliOptions.url);
expect(res.normalStoreUrl).toEqual(stencilConfig.url);
expect(res.accessToken).toEqual(stencilConfig.accessToken);
});
});

describe('askQuestions', () => {
it('should call inquirer.prompt with correct arguments', async () => {
describe('getQuestions', () => {
it('should get all questions if no cli options were passed', async () => {
const defaultAnswers = getAnswers();
const cliConfig = {};
const instance = getStencilInitInstance();

await instance.askQuestions(defaultAnswers);
const res = instance.getQuestions(defaultAnswers, cliConfig);

expect(inquirerStub.prompt).toHaveBeenCalledTimes(1);
// We compare the serialized results because the objects contain functions which hinders direct comparison
expect(JSON.stringify(inquirerStub.prompt.mock.calls)).toEqual(JSON.stringify([[[
{
type: 'input',
name: 'normalStoreUrl',
message: 'What is the URL of your store\'s home page?',
validate(val) {
return /^https?:\/\//.test(val)
? true
: 'You must enter a URL';
},
default: defaultAnswers.normalStoreUrl,
},
{
type: 'input',
name: 'accessToken',
message: 'What is your Stencil OAuth Access Token?',
default: defaultAnswers.accessToken,
filter: val => val.trim(),
},
{
type: 'input',
name: 'port',
message: 'What port would you like to run the server on?',
default: defaultAnswers.port,
validate: val => {
if (isNaN(val)) {
return 'You must enter an integer';
} else if (val < 1024 || val > 65535) {
return 'The port number must be between 1025 and 65535';
} else {
return true;
}
},
},
]]]));
expect(JSON.stringify(res)).toEqual(JSON.stringify(getQuestions()));
});
});

describe('askQuestions', () => {
it('should call inquirer.prompt with correct arguments', async () => {
const instance = getStencilInitInstance();
const questions = getQuestions();

await instance.askQuestions(questions);

expect(inquirerStub.prompt).toHaveBeenCalledTimes(1);

// We compare the serialized results because the objects contain functions which hinders direct comparison
expect(JSON.stringify(inquirerStub.prompt.mock.calls)).toEqual(JSON.stringify([[questions]]));
});
});

Expand Down