diff --git a/bin/stencil-init.js b/bin/stencil-init.js index 90f26451..348c56cd 100755 --- a/bin/stencil-init.js +++ b/bin/stencil-init.js @@ -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, + }); diff --git a/lib/stencil-init.js b/lib/stencil-init.js index e1476af0..700b973a 100644 --- a/lib/stencil-init.js +++ b/lib/stencil-init.js @@ -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} */ 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); @@ -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} + * @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?', @@ -118,19 +128,31 @@ class StencilInit { return true; } }, - }, - ]); + }); + } + + return prompts; + } + + /** + * @param {{object[]}} cliOptions + * @returns {Promise} + */ + 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) { return { customLayouts: DEFAULT_CUSTOM_LAYOUTS_CONFIG, ...stencilConfig, + ...cliOptions, ...answers, }; } diff --git a/lib/stencil-init.spec.js b/lib/stencil-init.spec.js index 6c2d42c6..83803393 100644 --- a/lib/stencil-init.spec.js +++ b/lib/stencil-init.spec.js @@ -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); @@ -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); }); @@ -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]])); }); });