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

Better error handling #157

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/inquirer-autocomplete-prompt/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ function searchFood(answers, input = '') {

inquirer
.prompt([
{
type: 'input',
name: 'first_name',
},
{
type: 'autocomplete',
name: 'fruit',
Expand Down Expand Up @@ -161,4 +165,7 @@ inquirer
])
.then((answers) => {
console.log(JSON.stringify(answers, null, 2));
})
.catch((err) => {
console.log(err);
});
84 changes: 48 additions & 36 deletions packages/inquirer-autocomplete-prompt/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ import { takeWhile } from 'rxjs/operators';
const isSelectable = (choice) =>
choice.type !== 'separator' && !choice.disabled;

const tryCallPromiseFail = (fn, ...args) => {
try {
const result = fn(...args);
return Promise.resolve(result);
} catch (error) {
return Promise.reject(error);
}
};

class AutocompletePrompt extends Base {
constructor(
questions /*: Array<any> */,
Expand Down Expand Up @@ -52,8 +61,9 @@ class AutocompletePrompt extends Base {
* @param {Function} cb Callback when prompt is done
* @return {this}
*/
_run(cb /*: Function */) /*: this*/ {
_run(cb /*: Function */, errCb /*: Function */) /*: this*/ {
this.done = cb;
this.errCb = errCb;

if (Array.isArray(this.rl.history)) {
this.rl.history = [];
Expand Down Expand Up @@ -155,17 +165,21 @@ class AutocompletePrompt extends Base {

let validationResult;
if (this.opt.suggestOnly) {
validationResult = this.opt.validate(lineOrRl, this.answers);
validationResult = tryCallPromiseFail(
this.opt.validate,
lineOrRl,
this.answers
);
} else {
const choice = this.currentChoices.getChoice(this.selected);
validationResult = this.opt.validate(choice, this.answers);
validationResult = tryCallPromiseFail(
this.opt.validate,
choice,
this.answers
);
}

if (isPromise(validationResult)) {
validationResult.then(checkValidationResult);
} else {
checkValidationResult(validationResult);
}
validationResult.then(checkValidationResult, this.errCb);
} else {
this.onSubmitAfterValidation(lineOrRl);
}
Expand Down Expand Up @@ -226,38 +240,38 @@ class AutocompletePrompt extends Base {

this.lastSearchTerm = searchTerm;

let thisPromise;
try {
const result = this.opt.source(this.answers, searchTerm);
thisPromise = Promise.resolve(result);
} catch (error) {
thisPromise = Promise.reject(error);
}
const thisPromise = tryCallPromiseFail(
this.opt.source,
this.answers,
searchTerm
);

// Store this promise for check in the callback
this.lastPromise = thisPromise;

return thisPromise.then((choices) => {
// If another search is triggered before the current search finishes, don't set results
if (thisPromise !== this.lastPromise) return;
return thisPromise
.then((choices) => {
// If another search is triggered before the current search finishes, don't set results
if (thisPromise !== this.lastPromise) return;

this.currentChoices = new Choices(choices);
this.currentChoices = new Choices(choices);

const realChoices = choices.filter((choice) => isSelectable(choice));
this.nbChoices = realChoices.length;
const realChoices = choices.filter((choice) => isSelectable(choice));
this.nbChoices = realChoices.length;

const selectedIndex = realChoices.findIndex(
(choice) =>
choice === this.initialValue || choice.value === this.initialValue
);
const selectedIndex = realChoices.findIndex(
(choice) =>
choice === this.initialValue || choice.value === this.initialValue
);

if (selectedIndex >= 0) {
this.selected = selectedIndex;
}
if (selectedIndex >= 0) {
this.selected = selectedIndex;
}

this.searching = false;
this.render();
});
this.searching = false;
this.render();
})
.catch(this.errCb);
}

ensureSelectedInRange() {
Expand All @@ -269,7 +283,9 @@ class AutocompletePrompt extends Base {
* When user type
*/

onKeypress(e /* : {key: { name: string, ctrl: boolean }, value: string } */) {
async onKeypress(
e /* : {key: { name: string, ctrl: boolean }, value: string } */
) {
let len;
const keyName = (e.key && e.key.name) || undefined;

Expand Down Expand Up @@ -344,8 +360,4 @@ function listRender(choices, pointer /*: string */) /*: string */ {
return output.replace(/\n$/, '');
}

function isPromise(value) {
return typeof value === 'object' && typeof value.then === 'function';
}

export default AutocompletePrompt;
2 changes: 1 addition & 1 deletion packages/inquirer-autocomplete-prompt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"flow": "flow",
"pretest": "npm run lint && npm run flow",
"test": "vitest run test --coverage",
"develop": "vitest watch test --coverage --reporter=basic"
"develop": "vitest watch test -c ../../vitest.develop.config.mjs"
},
"type": "module"
}
108 changes: 104 additions & 4 deletions packages/inquirer-autocomplete-prompt/test/spec/indexSpec.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,99 @@ describe('inquirer-autocomplete-prompt', () => {
);
});

describe('default behaviour', () => {
describe('error handling', () => {
it('handles async error in source function', async () => {
const error = new Error('Something went wrong!');
source.returns(Promise.reject(error));

promiseForAnswer = getPromiseForAnswer();

const successSpy = sinon.spy();
const errorSpy = sinon.spy();

await promiseForAnswer.then(successSpy).catch(errorSpy);

sinon.assert.notCalled(successSpy);
sinon.assert.calledOnce(errorSpy);
sinon.assert.calledWithExactly(errorSpy, error);
});

it('handles sync error in source function', async () => {
const error = new Error('Something went wrong!');
source.throws(error);

promiseForAnswer = getPromiseForAnswer();

const successSpy = sinon.spy();
const errorSpy = sinon.spy();

await promiseForAnswer.then(successSpy).catch(errorSpy);

sinon.assert.notCalled(successSpy);
sinon.assert.calledOnce(errorSpy);
sinon.assert.calledWithExactly(errorSpy, error);
});

it('renders sync error in validate function as validation error', async () => {
const error = new Error('Something went wrong2!');
source.returns(['foo', 'bar']);

const validate = sinon.stub();

prompt = new Prompt(
{
message: 'test',
name: 'name',
validate,
source,
},
rl
);

promiseForAnswer = getPromiseForAnswer();
validate.throws(error);
enter();

const successSpy = sinon.spy();
const errorSpy = sinon.spy();

await promiseForAnswer.then(successSpy).catch(errorSpy);

sinon.assert.notCalled(successSpy);
sinon.assert.calledOnce(errorSpy);
});

it('renders async error in validate function', async () => {
const error = new Error('Something went wrong in validation!');
source.returns(['foo', 'bar']);

const validate = sinon.stub();

prompt = new Prompt(
{
message: 'test',
name: 'name',
validate,
source,
},
rl
);

promiseForAnswer = getPromiseForAnswer();
validate.rejects(error);
enter();

const successSpy = sinon.spy();
const errorSpy = sinon.spy();

await promiseForAnswer.then(successSpy).catch(errorSpy);

sinon.assert.notCalled(successSpy);
sinon.assert.calledOnce(errorSpy);
});
});

describe('default behavior', () => {
it('sets the first to selected when no default', () => {
prompt = new Prompt(
{
Expand Down Expand Up @@ -502,7 +594,7 @@ describe('inquirer-autocomplete-prompt', () => {
});
});

it('applies filter async with done calback', () => {
it('applies filter async with done callback', () => {
prompt = new Prompt(
{
message: 'test',
Expand Down Expand Up @@ -801,7 +893,7 @@ describe('inquirer-autocomplete-prompt', () => {
sinon.assert.callCount(source, 4);
});

it('does not search again if same searchterm (not input added)', () => {
it('does not search again if same search term (not input added)', () => {
type('ice');
sinon.assert.calledThrice(source);
source.reset();
Expand Down Expand Up @@ -975,8 +1067,10 @@ describe('inquirer-autocomplete-prompt', () => {
});

describe('submit', () => {
let validate;
describe('without choices in result', () => {
beforeEach(() => {
validate = sinon.stub().returns(true);
rl = new ReadlineStub();
prompt = new Prompt(
{
Expand All @@ -992,9 +1086,15 @@ describe('inquirer-autocomplete-prompt', () => {
return promise;
});

it('searches again, since not possible to select something that does not exist', () => {
it('searches again, since not possible to select something that does not exist', async () => {
// called once at start
sinon.assert.calledOnce(source);

// try to select and await validation result (even sync validation is async)
enter();
await validate;

// Now search again
sinon.assert.calledTwice(source);
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/inquirer-autocomplete-standalone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@
"clean": "rm -rf dist",
"build": "yarn run clean && yarn run tsc",
"tsc": "tsc -p ./tsconfig.json",
"develop": "vitest watch test --coverage --reporter=basic"
"develop": "vitest watch test -c ../../vitest.develop.config.mjs"
}
}
10 changes: 10 additions & 0 deletions vitest.develop.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
reporter: ['basic'],
coverage: {
reporter: ['lcov'],
},
},
});