Skip to content
This repository was archived by the owner on Dec 16, 2022. It is now read-only.

Commit c15426f

Browse files
test(cli): Test CLI config from arguments and prompt (#51)
* refactor(cli): Refactor `mainAttribute` * test(cli): Test `mainAttribute` * refactor(cli): Remove unused imports * refactor(cli): Refactor `getConfiguration()` * test(cli): Test `getConfiguration()` * fix(cli): Skip question if config file is passed
1 parent 8b14d31 commit c15426f

File tree

3 files changed

+208
-59
lines changed

3 files changed

+208
-59
lines changed

packages/cli/cli.js

Lines changed: 16 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ const program = require('commander');
44
const inquirer = require('inquirer');
55
const chalk = require('chalk');
66
const latestSemver = require('latest-semver');
7-
const loadJsonFile = require('load-json-file');
8-
const algoliasearch = require('algoliasearch');
97

108
const createInstantSearchApp = require('../create-instantsearch-app');
119
const {
@@ -16,7 +14,12 @@ const {
1614
getAllTemplates,
1715
getTemplatePath,
1816
} = require('../shared/utils');
19-
const { getOptionsFromArguments, isQuestionAsked } = require('./utils');
17+
const {
18+
getOptionsFromArguments,
19+
getAttributesFromAnswers,
20+
isQuestionAsked,
21+
getConfiguration,
22+
} = require('./utils');
2023
const { version } = require('../../package.json');
2124

2225
let appPath;
@@ -74,7 +77,8 @@ if (!appPath) {
7477
process.exit(1);
7578
}
7679

77-
const appName = path.basename(appPath);
80+
const optionsFromArguments = getOptionsFromArguments(options.rawArgs);
81+
const appName = optionsFromArguments.name || path.basename(appPath);
7882

7983
try {
8084
checkAppPath(appPath);
@@ -86,8 +90,6 @@ try {
8690
process.exit(1);
8791
}
8892

89-
const optionsFromArguments = getOptionsFromArguments(options.rawArgs);
90-
9193
const questions = [
9294
{
9395
type: 'list',
@@ -164,66 +166,22 @@ const questions = [
164166
type: 'list',
165167
name: 'mainAttribute',
166168
message: 'Attribute to display',
167-
choices: async answers => {
168-
const client = algoliasearch(answers.appId, answers.apiKey);
169-
const index = client.initIndex(answers.indexName);
170-
const defaultAttributes = ['title', 'name', 'description'];
171-
let attributes = [];
172-
173-
try {
174-
const { hits } = await index.search({ hitsPerPage: 1 });
175-
const [firstHit] = hits;
176-
attributes = Object.keys(firstHit._highlightResult).sort(
177-
value => !defaultAttributes.includes(value)
178-
);
179-
} catch (err) {
180-
attributes = defaultAttributes;
181-
}
182-
183-
return attributes;
184-
},
169+
choices: async answers => await getAttributesFromAnswers(answers),
185170
when: ({ appId, apiKey, indexName }) => appId && apiKey && indexName,
186171
},
187172
].filter(question => isQuestionAsked({ question, args: optionsFromArguments }));
188173

189-
async function getConfig() {
190-
let config;
191-
192-
if (optionsFromArguments.config) {
193-
// Get config from configuration file given as an argument
194-
config = await loadJsonFile(optionsFromArguments.config);
195-
} else {
196-
// Get config from the arguments and the prompt
197-
config = {
198-
...optionsFromArguments,
199-
...(await inquirer.prompt(questions)),
200-
};
201-
}
202-
203-
const templatePath = getTemplatePath(config.template);
204-
let libraryVersion = config.libraryVersion;
205-
206-
if (!libraryVersion) {
207-
const templateConfig = getAppTemplateConfig(templatePath);
208-
209-
libraryVersion = await fetchLibraryVersions(
210-
templateConfig.libraryName
211-
).then(latestSemver);
212-
}
213-
214-
return {
215-
...config,
216-
libraryVersion,
217-
name: config.name || appName,
218-
template: templatePath,
219-
};
220-
}
221-
222174
async function run() {
223175
console.log(`Creating a new InstantSearch app in ${chalk.green(appPath)}.`);
224176

225177
const config = {
226-
...(await getConfig()),
178+
...(await getConfiguration({
179+
options: {
180+
...optionsFromArguments,
181+
name: appName,
182+
},
183+
answers: await inquirer.prompt(questions),
184+
})),
227185
installation: program.installation,
228186
};
229187

packages/cli/utils.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
const algoliasearch = require('algoliasearch');
2+
const latestSemver = require('latest-semver');
3+
const loadJsonFile = require('load-json-file');
4+
5+
const {
6+
getAppTemplateConfig,
7+
fetchLibraryVersions,
8+
getTemplatePath,
9+
} = require('../shared/utils');
10+
111
function camelCase(string) {
212
return string.replace(/-([a-z])/g, str => str[1].toUpperCase());
313
}
@@ -22,7 +32,40 @@ function getOptionsFromArguments(rawArgs) {
2232
}, {});
2333
}
2434

35+
async function getAttributesFromAnswers({
36+
appId,
37+
apiKey,
38+
indexName,
39+
algoliasearchFn = algoliasearch,
40+
} = {}) {
41+
const client = algoliasearchFn(appId, apiKey);
42+
const defaultAttributes = ['title', 'name', 'description'];
43+
let attributes = [];
44+
45+
try {
46+
const { hits } = await client.search({ indexName, hitsPerPage: 1 });
47+
const [firstHit] = hits;
48+
const highlightedAttributes = Object.keys(firstHit._highlightResult);
49+
attributes = [
50+
...new Set([
51+
...defaultAttributes.map(
52+
attribute => highlightedAttributes.includes(attribute) && attribute
53+
),
54+
...highlightedAttributes,
55+
]),
56+
];
57+
} catch (err) {
58+
attributes = defaultAttributes;
59+
}
60+
61+
return attributes;
62+
}
63+
2564
function isQuestionAsked({ question, args }) {
65+
if (args.config) {
66+
return false;
67+
}
68+
2669
for (const optionName in args) {
2770
if (question.name === optionName) {
2871
// Skip if the arg in the command is valid
@@ -38,8 +81,41 @@ function isQuestionAsked({ question, args }) {
3881
return true;
3982
}
4083

84+
async function getConfiguration({
85+
options = {},
86+
answers = {},
87+
loadJsonFileFn = loadJsonFile,
88+
} = {}) {
89+
const config = options.config
90+
? await loadJsonFileFn(options.config) // From configuration file given as an argument
91+
: { ...options, ...answers }; // From the arguments and the prompt
92+
93+
if (!config.template) {
94+
throw new Error('The template is required in the config.');
95+
}
96+
97+
const templatePath = getTemplatePath(config.template);
98+
let { libraryVersion } = config;
99+
100+
if (!libraryVersion) {
101+
const templateConfig = getAppTemplateConfig(templatePath);
102+
103+
libraryVersion = await fetchLibraryVersions(
104+
templateConfig.libraryName
105+
).then(latestSemver);
106+
}
107+
108+
return {
109+
...config,
110+
libraryVersion,
111+
template: templatePath,
112+
};
113+
}
114+
41115
module.exports = {
42116
camelCase,
43117
getOptionsFromArguments,
118+
getAttributesFromAnswers,
44119
isQuestionAsked,
120+
getConfiguration,
45121
};

packages/cli/utils.test.js

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const path = require('path');
12
const utils = require('./utils');
23

34
describe('getOptionsFromArguments', () => {
@@ -79,7 +80,52 @@ describe('getOptionsFromArguments', () => {
7980
});
8081
});
8182

82-
describe('isQuestionAsked', () => {
83+
describe('getAttributesFromAnswers', () => {
84+
const algoliasearchSuccessFn = () => ({
85+
search: jest.fn(() => ({
86+
hits: [
87+
{
88+
_highlightResult: {
89+
brand: 'brand',
90+
description: 'description',
91+
name: 'name',
92+
title: 'title',
93+
},
94+
},
95+
],
96+
})),
97+
});
98+
99+
const algoliasearchFailureFn = () => ({
100+
search: jest.fn(() => {
101+
throw new Error();
102+
}),
103+
});
104+
105+
test('with search success should fetch attributes', async () => {
106+
const attributes = await utils.getAttributesFromAnswers({
107+
appId: 'appId',
108+
apiKey: 'apiKey',
109+
indexName: 'indexName',
110+
algoliasearchFn: algoliasearchSuccessFn,
111+
});
112+
113+
expect(attributes).toEqual(['title', 'name', 'description', 'brand']);
114+
});
115+
116+
test('with search failure should return default attributes', async () => {
117+
const attributes = await utils.getAttributesFromAnswers({
118+
appId: 'appId',
119+
apiKey: 'apiKey',
120+
indexName: 'indexName',
121+
algoliasearchFn: algoliasearchFailureFn,
122+
});
123+
124+
expect(attributes).toEqual(['title', 'name', 'description']);
125+
});
126+
});
127+
128+
test('isQuestionAsked', () => {
83129
expect(
84130
utils.isQuestionAsked({
85131
question: { name: 'appId', validate: input => Boolean(input) },
@@ -137,3 +183,72 @@ describe('camelCase', () => {
137183
expect(utils.camelCase('instant-search-js')).toBe('instantSearchJs');
138184
});
139185
});
186+
187+
describe('getConfiguration', () => {
188+
test('without template throws', async () => {
189+
expect.assertions(1);
190+
191+
try {
192+
await utils.getConfiguration({});
193+
} catch (err) {
194+
expect(err.message).toBe('The template is required in the config.');
195+
}
196+
});
197+
198+
test('with template transforms to its relative path', async () => {
199+
const configuration = await utils.getConfiguration({
200+
answers: { template: 'InstantSearch.js' },
201+
});
202+
203+
expect(configuration).toEqual(
204+
expect.objectContaining({
205+
template: path.resolve('templates/InstantSearch.js'),
206+
})
207+
);
208+
});
209+
210+
test('with options from arguments and prompt merge', async () => {
211+
const configuration = await utils.getConfiguration({
212+
options: {
213+
name: 'my-app',
214+
},
215+
answers: {
216+
template: 'InstantSearch.js',
217+
libraryVersion: '1.0.0',
218+
},
219+
});
220+
221+
expect(configuration).toEqual(
222+
expect.objectContaining({
223+
name: 'my-app',
224+
libraryVersion: '1.0.0',
225+
})
226+
);
227+
});
228+
229+
test('with config file overrides all options', async () => {
230+
const loadJsonFileFn = jest.fn(x => Promise.resolve(x));
231+
const ignoredOptions = {
232+
libraryVersion: '2.0.0',
233+
};
234+
const options = {
235+
config: {
236+
template: 'InstantSearch.js',
237+
libraryVersion: '1.0.0',
238+
},
239+
...ignoredOptions,
240+
};
241+
const answers = {
242+
ignoredKey: 'ignoredValue',
243+
};
244+
245+
const configuration = await utils.getConfiguration({
246+
options,
247+
answers,
248+
loadJsonFileFn,
249+
});
250+
251+
expect(configuration).toEqual(expect.not.objectContaining(ignoredOptions));
252+
expect(configuration).toEqual(expect.not.objectContaining(answers));
253+
});
254+
});

0 commit comments

Comments
 (0)