Skip to content
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export const navigationItems: QuaireNavigationItem[] = [
];
```

A navigation item without an own question is shown only if it has children with questions.
For a parent item, `hasValue` and `isValid` are derived from its children.
Navigation items without any question are not shown.

## Use quaire behaviour

To use the default behavior you need to initialize `Quaire` with the data you
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('branched-and-merged-flow', () => {
Q = new Quaire({ items, navigationItems });
});

test('should go through branch a', () => {
test('should go through branch A (range slider) and merge into question 4', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 1');

Expand Down Expand Up @@ -67,7 +67,7 @@ describe('branched-and-merged-flow', () => {
]);
});

test('should go through branch b', () => {
test('should go through branch B (input) and merge into question 4', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 1');

Expand Down
24 changes: 18 additions & 6 deletions examples/custom-component-types/custom-component-types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ describe('custom-component-types', () => {
Q = new MyQuaire({ items, navigationItems });
});

test('should validate new component types based on dependent answers', () => {
const answerAllQuestionsWithOption1 = () => {
Q.saveAnswer('option 1');
Q.saveAnswer(['option 2', 'option 1']);
Q.saveAnswer(true);
};

test('should go through MULTI_SELECT and BOOLEAN questions', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 1');

Expand Down Expand Up @@ -77,10 +83,13 @@ describe('custom-component-types', () => {
value: true,
},
]);
});

test('should reset the MULTI_SELECT answer when the first answer changes', () => {
answerAllQuestionsWithOption1();

// reset flow by choosing different answer
Q.saveAnswer('option 2');
activeQuestion = Q.getActiveQuestion();
const activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 2');
expect(activeQuestion.componentType).toBe(MyComponentType.MULTI_SELECT);
expect(activeQuestion.selectOptions).toEqual([
Expand Down Expand Up @@ -132,13 +141,16 @@ describe('custom-component-types', () => {
value: true,
},
]);
});

test('should only accept MULTI_SELECT options that match the first answer', () => {
answerAllQuestionsWithOption1();
Q.saveAnswer('option 2');

// provide wrong answer
Q.saveAnswer('option 1');
activeQuestion = Q.getActiveQuestion();
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 2');

// provide correct answer
Q.saveAnswer(['option 3', 'option 4']);
activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 3');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('dependencies-between-questions', () => {
Q = new Quaire({ items, navigationItems });
});

test('should have select options based on interdependencies', () => {
test('should show select options based on the previous answers', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.selectOptions).toEqual([
{
Expand Down Expand Up @@ -94,49 +94,11 @@ describe('dependencies-between-questions', () => {
]);
});

test('should react and validate later questions based on first question', () => {
test('should reset and invalidate dependent questions when the first answer changes', () => {
Q.saveAnswer('option 1');
Q.saveAnswer('option 1.1');
Q.saveAnswer('option 1.1.1');

expect(Q.getResult()).toEqual({
foo: 'option 1',
bar: 'option 1.1',
baz: 'option 1.1.1',
});
expect(Q.getNavigation()).toEqual([
{
active: false,
componentType: 'SINGLE_SELECT',
hasValue: true,
id: 1,
isValid: true,
name: 'Category 1',
subNavigation: [
{
active: false,
componentType: 'SINGLE_SELECT',
hasValue: true,
id: 2,
isValid: true,
name: 'Subcategory 1',
value: 'Option 1.1',
},
],
value: 'Option 1',
},
{
active: true,
componentType: 'SINGLE_SELECT',
hasValue: true,
id: 3,
isValid: true,
name: 'Category 2',
subNavigation: [],
value: 'Option 1.1.1',
},
]);

Q.setActiveQuestionByQuestionId(1);
Q.saveAnswer('option 2');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('extending-data-definition', () => {
Q = new MyQuaire({ items, navigationItems });
});

test('should validate new component types based on dependent answers', () => {
test('should add custom properties to questions and navigation items', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 1');
expect(activeQuestion.progress).toBe(50);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { NO_VALUE, Quaire } from '../../src';
import { items, navigationItems } from './data';

describe('linear-flow', () => {
describe('linear-flow-skip-question', () => {
let Q;

beforeEach(() => {
Q = new Quaire({ items, navigationItems });
});

test('should go through the linear flow and skip 2. question', () => {
test('should skip question 2 when option 2 is chosen', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 1');

Expand Down Expand Up @@ -61,7 +61,7 @@ describe('linear-flow', () => {
]);
});

test('should answer the 2. question with the skip option', () => {
test('should save NO_VALUE when question 2 is skipped', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 1');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('linear-flow-with-loop', () => {
Q = new Quaire({ items, navigationItems });
});

test('should go through the linear flow and back to the first question', () => {
test('should go back to question 1 after the last question', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 1');

Expand Down
14 changes: 9 additions & 5 deletions examples/linear-flow/linear-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ describe('linear-flow', () => {
Q = new Quaire({ items, navigationItems });
});

test('should go through the linear flow', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 1');

test('should stay on the question when the answer is not a valid option', () => {
Q.saveAnswer('option 4');
activeQuestion = Q.getActiveQuestion();

expect(Q.getActiveQuestion().question).toBe('Question 1');
expect(Q.getResult()).toEqual({ foo: null });
expect(Q.getValidationErrors()).toEqual({ '1': 'REQUIRED', '2': 'REQUIRED', '3': 'REQUIRED' });
});

test('should go through all questions in order', () => {
let activeQuestion = Q.getActiveQuestion();
expect(activeQuestion.question).toBe('Question 1');

Q.saveAnswer('option 1');
Expand Down
8 changes: 4 additions & 4 deletions examples/restore-state.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Quaire } from '../src';
import { items, navigationItems } from './dependencies-between-questions/data';

describe('restore state', () => {
test('should restore state from existing result', () => {
describe('restore-state', () => {
test('should restore the state from a complete result', () => {
const Q = new Quaire({
items,
navigationItems,
Expand Down Expand Up @@ -58,7 +58,7 @@ describe('restore state', () => {
]);
});

test('should restore state from partially existing result', () => {
test('should restore the state from a partial result', () => {
const Q = new Quaire({ items, navigationItems, result: { foo: 'option 1', bar: 'option 1.2' } });

expect(Q.getActiveQuestion().selectOptions).toEqual([
Expand Down Expand Up @@ -109,7 +109,7 @@ describe('restore state', () => {
]);
});

test('should restore state from empty existing result', () => {
test('should start at question 1 with an empty result', () => {
const Q = new Quaire({ items, navigationItems, result: {} });

expect(Q.getActiveQuestion().selectOptions).toEqual([
Expand Down
130 changes: 123 additions & 7 deletions examples/misc.spec.ts → src/quaire.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Quaire, QuaireComponentType, QuaireItem, QuaireNavigationItem } from '../src';
import { Quaire, QuaireComponentType, QuaireItem, QuaireNavigationItem } from './index';

describe('misc', () => {
describe('Quaire', () => {
const items: Array<QuaireItem> = [
{
id: 1,
Expand Down Expand Up @@ -102,12 +102,12 @@ describe('misc', () => {
{
id: 4,
parentId: 1,
name: 'Subcategory 2',
name: 'Subcategory 3',
},
];

describe('components', () => {
test('different default values for input and range components based on dependencies', () => {
describe('dependent options', () => {
test('should use range, input and default values based on the dependent answer', () => {
const Q = new Quaire({ items });

let activeQuestion = Q.getActiveQuestion();
Expand Down Expand Up @@ -169,14 +169,14 @@ describe('misc', () => {
});

describe('navigation', () => {
test('empty items and navigation', () => {
test('should return an empty navigation without items', () => {
const Q = new Quaire({ items: [] });

expect(Q.getActiveQuestion()).toBeNull();
expect(Q.getNavigation()).toEqual([]);
});

test('navigate via navigation item ID', () => {
test('should set the active question by navigation item ID', () => {
const Q = new Quaire({ items, navigationItems });

Q.setActiveQuestionByNavigationItemId(3);
Expand All @@ -188,5 +188,121 @@ describe('misc', () => {
Q.setActiveQuestionByNavigationItemId(1);
expect(Q.getActiveQuestion().id).toBe(1);
});
test('should derive a parent navigation item from its children when it has no question', () => {
const Q = new Quaire({ items, navigationItems });

expect(Q.getNavigation()).toEqual([
{
id: 1,
name: 'Category 1',
value: null,
icon: undefined,
active: true,
isValid: false,
hasValue: false,
componentType: null,
subNavigation: [
{
id: 2,
name: 'Subcategory 1',
value: null,
icon: undefined,
active: true,
isValid: false,
hasValue: false,
componentType: 'SINGLE_SELECT',
},
{
id: 3,
name: 'Subcategory 2',
value: null,
icon: undefined,
active: false,
isValid: false,
hasValue: false,
componentType: 'RANGE_SLIDER',
},
{
id: 4,
name: 'Subcategory 3',
value: null,
icon: undefined,
active: false,
isValid: false,
hasValue: false,
componentType: 'INPUT',
},
],
},
]);

Q.saveAnswer('option 1');
Q.saveAnswer([20, 50]);
Q.saveAnswer('text');

const [parent] = Q.getNavigation();
expect(parent.hasValue).toBe(true);
expect(parent.isValid).toBe(true);
});

test('should skip navigation items without questions', () => {
const navigationItemsWithoutQuestions: Array<QuaireNavigationItem> = [
{ id: 1, parentId: null, name: 'Category 1' },
{ id: 2, parentId: 1, name: 'Subcategory 1' },
{ id: 3, parentId: 1, name: 'Subcategory without question' },
{ id: 5, parentId: null, name: 'Category without question' },
];
const Q = new Quaire({ items: [items[0]], navigationItems: navigationItemsWithoutQuestions });

const navigation = Q.getNavigation();

expect(navigation.map((item) => item.name)).toEqual(['Category 1']);
expect(navigation[0].subNavigation.map((item) => item.name)).toEqual(['Subcategory 1']);
});
});

describe('active question', () => {
test('should have no active question for an unknown question ID', () => {
const Q = new Quaire({ items });

Q.setActiveQuestionByQuestionId(99);

expect(Q.getActiveQuestion()).toBeNull();
});

test('should ignore answers when no question is active', () => {
const Q = new Quaire({ items, navigationItems });

Q.setActiveQuestionByNavigationItemId(99);
Q.saveAnswer('option 1');

expect(Q.getActiveQuestion()).toBeNull();
expect(Q.getResult()).toEqual({});
});
});

describe('validation', () => {
test('should be invalid until all required questions are answered', () => {
const Q = new Quaire({ items });

expect(Q.isValid()).toBe(false);
expect(Q.getValidationErrors()).toEqual({ '1': 'REQUIRED', '2': 'REQUIRED', '3': 'REQUIRED' });

Q.saveAnswer('option 1');
Q.saveAnswer([20, 50]);
Q.saveAnswer('text');

expect(Q.isValid()).toBe(true);
expect(Q.getValidationErrors()).toEqual({});
});
});

describe('restore state', () => {
test('should keep the result when there are no items', () => {
const Q = new Quaire({ items: [], result: { foo: 'option 1' } });

expect(Q.getActiveQuestion()).toBeNull();
expect(Q.getResult()).toEqual({ foo: 'option 1' });
});
});
});
Loading
Loading