From 1b0b7ed989cf594a622f5efdf27742ff1e8da525 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Thu, 8 Apr 2021 13:18:43 +0800 Subject: [PATCH] test: migrate logic.spec from javascript to typescript (#1587) --- tests/unit/backend/utils/logic.spec.js | 816 ------------------- tests/unit/backend/utils/logic.spec.ts | 1004 ++++++++++++++++++++++++ 2 files changed, 1004 insertions(+), 816 deletions(-) delete mode 100644 tests/unit/backend/utils/logic.spec.js create mode 100644 tests/unit/backend/utils/logic.spec.ts diff --git a/tests/unit/backend/utils/logic.spec.js b/tests/unit/backend/utils/logic.spec.js deleted file mode 100644 index 45fbde26ef..0000000000 --- a/tests/unit/backend/utils/logic.spec.js +++ /dev/null @@ -1,816 +0,0 @@ -const { - getVisibleFieldIds, - getLogicUnitPreventingSubmit, -} = require('../../../../dist/backend/shared/util/logic') -describe('Logic validation', () => { - /** - * Mock a field - * @param {String} fieldId - */ - const makeField = (fieldId) => { - return { _id: fieldId } - } - /** - * Mock a response - * @param {String} fieldId field id of the field that this response is meant for - * @param {String} answer - * @param {Array} [answerArray] array of answers passed in for checkbox and table - * @param {Boolean} [isVisible] - */ - const makeResponse = ( - fieldId, - answer, - answerArray = null, - isVisible = true, - ) => { - let response = { _id: fieldId, answer, isVisible } - if (answerArray) response.answerArray = answerArray - return response - } - describe('visibility for different states', () => { - const form = { _id: 'f1' } - const conditionField = makeField('001') - const logicField = makeField('002') - const logicResponse = makeResponse(logicField._id, 'lorem') - form.form_fields = [conditionField, logicField] - it('should compute the correct visibility for "is equals to"', () => { - form.form_logics = [ - { - show: [logicField._id], - conditions: [ - { - ifValueType: 'number', - _id: '58169', - field: conditionField._id, - state: 'is equals to', - value: 0, - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'showFields', - }, - ] - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 0), logicResponse], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 1), logicResponse], - form, - ).has(logicField._id), - ).toBe(false) - }) - it('should compute the correct visibility for "is less than or equal to"', () => { - form.form_logics = [ - { - show: [logicField._id], - conditions: [ - { - ifValueType: 'number', - _id: '58169', - field: conditionField._id, - state: 'is less than or equal to', - value: 99, - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'showFields', - }, - ] - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 98), logicResponse], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 99), logicResponse], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 100), logicResponse], - form, - ).has(logicField._id), - ).toBe(false) - }) - it('should compute the correct visibility for "is more than or equal to"', () => { - form.form_logics = [ - { - show: [logicField._id], - conditions: [ - { - ifValueType: 'number', - _id: '58169', - field: conditionField._id, - state: 'is more than or equal to', - value: 22, - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'showFields', - }, - ] - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 23), logicResponse], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 22), logicResponse], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 21), logicResponse], - form, - ).has(logicField._id), - ).toBe(false) - }) - it('should compute the correct visibility for "is either"', () => { - form.form_logics = [ - { - show: [logicField._id], - conditions: [ - { - ifValueType: 'multi-select', - _id: '58169', - field: conditionField._id, - state: 'is either', - value: ['Option 1', 'Option 2'], - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'showFields', - }, - ] - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 'Option 1'), logicResponse], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 'Option 2'), logicResponse], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [makeResponse(conditionField._id, 'Option 3'), logicResponse], - form, - ).has(logicField._id), - ).toBe(false) - }) - }) - describe('preventing submission for different states', () => { - const form = { _id: 'f1' } - const conditionField = makeField('001') - const logicField = makeField('002') - const logicResponse = makeResponse(logicField._id, 'lorem') - form.form_fields = [conditionField, logicField] - it('should compute that submission should be prevented for "is equals to"', () => { - form.form_logics = [ - { - show: [], - conditions: [ - { - ifValueType: 'number', - _id: '58169', - field: conditionField._id, - state: 'is equals to', - value: 0, - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'preventSubmit', - }, - ] - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 0), logicResponse], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 1), logicResponse], - form, - ), - ).toBeUndefined() - }) - it('should compute that submission should be prevented for "is less than or equal to"', () => { - form.form_logics = [ - { - show: [], - conditions: [ - { - ifValueType: 'number', - _id: '58169', - field: conditionField._id, - state: 'is less than or equal to', - value: 99, - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'preventSubmit', - }, - ] - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 98), logicResponse], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 99), logicResponse], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 100), logicResponse], - form, - ), - ).toBeUndefined() - }) - it('should compute that submission should be prevented for "is more than or equal to"', () => { - form.form_logics = [ - { - show: [], - conditions: [ - { - ifValueType: 'number', - _id: '58169', - field: conditionField._id, - state: 'is more than or equal to', - value: 22, - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'preventSubmit', - }, - ] - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 23), logicResponse], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 22), logicResponse], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 21), logicResponse], - form, - ), - ).toBeUndefined() - }) - it('should compute that submission should be prevented for "is either"', () => { - form.form_logics = [ - { - show: [], - conditions: [ - { - ifValueType: 'multi-select', - _id: '58169', - field: conditionField._id, - state: 'is either', - value: ['Option 1', 'Option 2'], - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'preventSubmit', - }, - ] - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 'Option 1'), logicResponse], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 'Option 2'), logicResponse], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [makeResponse(conditionField._id, 'Option 3'), logicResponse], - form, - ), - ).toBeUndefined() - }) - }) - describe('show fields with multiple conditions', () => { - const form = { _id: 'f1' } - const conditionField1 = makeField('001') - const conditionField2 = makeField('002') - const logicField = makeField('003') - const logicResponse = makeResponse(logicField._id, 'lorem') - form.form_fields = [conditionField1, conditionField2, logicField] - it('should compute the correct visibility for AND conditions', () => { - form.form_logics = [ - { - show: [logicField._id], - _id: '5df11ee1e6b6e7108a939c8a', - conditions: [ - { - ifValueType: 'single-select', - _id: '9577', - field: conditionField1._id, - state: 'is equals to', - value: 'Yes', - }, - { - ifValueType: 'single-select', - _id: '45633', - field: conditionField2._id, - state: 'is equals to', - value: 20, - }, - ], - logicType: 'showFields', - }, - ] - expect( - getVisibleFieldIds( - [ - makeResponse(conditionField1._id, 'Yes'), - makeResponse(conditionField2._id, 20), - logicResponse, - ], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [ - makeResponse(conditionField1._id, 'Yes'), - makeResponse(conditionField2._id, 100), - logicResponse, - ], - form, - ).has(logicField._id), - ).toBe(false) - expect( - getVisibleFieldIds( - [ - makeResponse(conditionField1._id, 'No'), - makeResponse(conditionField2._id, 20), - logicResponse, - ], - form, - ).has(logicField._id), - ).toBe(false) - }) - it('should compute the correct visibility for OR conditions', () => { - form.form_logics = [ - { - show: [logicField._id], - conditions: [ - { - ifValueType: 'single-select', - _id: '9577', - field: conditionField1._id, - state: 'is equals to', - value: 'Yes', - }, - ], - _id: '5df11ee1e6b6e7108a939c8a', - logicType: 'showFields', - }, - { - show: [logicField._id], - conditions: [ - { - ifValueType: 'single-select', - _id: '89906', - field: conditionField2._id, - state: 'is equals to', - value: 20, - }, - ], - _id: '5df127a2e6b6e7108a939c90', - logicType: 'showFields', - }, - ] - expect( - getVisibleFieldIds( - [ - makeResponse(conditionField1._id, 'Yes'), - makeResponse(conditionField2._id, 20), - logicResponse, - ], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [ - makeResponse(conditionField1._id, 'Yes'), - makeResponse(conditionField2._id, 100), - logicResponse, - ], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [ - makeResponse(conditionField1._id, 'No'), - makeResponse(conditionField2._id, 20), - logicResponse, - ], - form, - ).has(logicField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [ - makeResponse(conditionField1._id, 'No'), - makeResponse(conditionField2._id, 100), - logicResponse, - ], - form, - ).has(logicField._id), - ).toBe(false) - }) - }) - describe('prevent submit with multiple conditions', () => { - const form = { _id: 'f1' } - const conditionField1 = makeField('001') - const conditionField2 = makeField('002') - const logicField = makeField('003') - const logicResponse = makeResponse(logicField._id, 'lorem') - form.form_fields = [conditionField1, conditionField2, logicField] - it('should correctly prevent submission for AND conditions', () => { - form.form_logics = [ - { - show: [], - _id: '5df11ee1e6b6e7108a939c8a', - conditions: [ - { - ifValueType: 'single-select', - _id: '9577', - field: conditionField1._id, - state: 'is equals to', - value: 'Yes', - }, - { - ifValueType: 'single-select', - _id: '45633', - field: conditionField2._id, - state: 'is equals to', - value: 20, - }, - ], - logicType: 'preventSubmit', - }, - ] - expect( - getLogicUnitPreventingSubmit( - [ - makeResponse(conditionField1._id, 'Yes'), - makeResponse(conditionField2._id, 20), - logicResponse, - ], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [ - makeResponse(conditionField1._id, 'Yes'), - makeResponse(conditionField2._id, 100), - logicResponse, - ], - form, - ), - ).toBeUndefined() - expect( - getLogicUnitPreventingSubmit( - [ - makeResponse(conditionField1._id, 'No'), - makeResponse(conditionField2._id, 20), - logicResponse, - ], - form, - ), - ).toBeUndefined() - }) - it('should correctly prevent submission for OR conditions', () => { - form.form_logics = [ - { - show: [], - conditions: [ - { - ifValueType: 'single-select', - _id: '9577', - field: conditionField1._id, - state: 'is equals to', - value: 'Yes', - }, - ], - _id: '5df11ee1e6b6e7108a939c8a', - logicType: 'preventSubmit', - }, - { - show: [], - conditions: [ - { - ifValueType: 'single-select', - _id: '89906', - field: conditionField2._id, - state: 'is equals to', - value: 20, - }, - ], - _id: '5df127a2e6b6e7108a939c90', - logicType: 'preventSubmit', - }, - ] - expect( - getLogicUnitPreventingSubmit( - [ - makeResponse(conditionField1._id, 'Yes'), - makeResponse(conditionField2._id, 20), - logicResponse, - ], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [ - makeResponse(conditionField1._id, 'Yes'), - makeResponse(conditionField2._id, 100), - logicResponse, - ], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [ - makeResponse(conditionField1._id, 'No'), - makeResponse(conditionField2._id, 20), - logicResponse, - ], - form, - ), - ).toEqual(form.form_logics[1]) - expect( - getLogicUnitPreventingSubmit( - [ - makeResponse(conditionField1._id, 'No'), - makeResponse(conditionField2._id, 100), - logicResponse, - ], - form, - ), - ).toBeUndefined() - }) - }) - describe('visibility for others value', () => { - const form = { _id: 'f1' } - const radioButton = { - _id: '001', - fieldType: 'radiobutton', - fieldOptions: ['Option 1', 'Option 2'], - othersRadioButton: true, - } - const textField = { _id: '002', fieldType: 'textfield' } - it('should compute the correct visibility for radiobutton Others on clientside', () => { - const textFieldResponse = Object.assign({}, textField, { - fieldValue: 'lorem', - }) - form.form_fields = [radioButton, textField] - form.form_logics = [ - { - show: [textField._id], - conditions: [ - { - ifValueType: 'single-select', - _id: '58169', - field: radioButton._id, - state: 'is equals to', - value: 'Others', - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'showFields', - }, - ] - const fillInRadioButton = (fieldValue) => - Object.assign({}, radioButton, { fieldValue, isVisible: true }) - expect( - getVisibleFieldIds( - [fillInRadioButton('radioButtonOthers'), textFieldResponse], - form, - ).has(textField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [fillInRadioButton('Option 1'), textFieldResponse], - form, - ).has(textField._id), - ).toBe(false) - }) - it('should compute the correct visibility for radiobutton Others on serverside', () => { - const textFieldResponse = makeResponse('002', 'lorem') - form.form_fields = [radioButton, textField] - form.form_logics = [ - { - show: [textField._id], - conditions: [ - { - ifValueType: 'single-select', - _id: '58169', - field: radioButton._id, - state: 'is equals to', - value: 'Others', - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'showFields', - }, - ] - expect( - getVisibleFieldIds( - [makeResponse(radioButton._id, 'Others: School'), textFieldResponse], - form, - ).has(textField._id), - ).toBe(true) - expect( - getVisibleFieldIds( - [makeResponse(radioButton._id, 'Option 1'), textFieldResponse], - form, - ).has(textField._id), - ).toBe(false) - }) - }) - describe('visibility for circular logic', () => { - const form = { _id: 'f1' } - const field1 = makeField('001') - const field2 = makeField('002') - const visibleField = makeField('003') - form.form_logics = [ - { - show: [field2._id], - conditions: [ - { - ifValueType: 'single-select', - _id: '9577', - field: field1._id, - state: 'is equals to', - value: 'Yes', - }, - ], - _id: '5df11ee1e6b6e7108a939c8a', - logicType: 'showFields', - }, - { - show: [field1._id], - conditions: [ - { - ifValueType: 'single-select', - _id: '89906', - field: field2._id, - state: 'is equals to', - value: 'Yes', - }, - ], - _id: '5df127a2e6b6e7108a939c90', - logicType: 'showFields', - }, - ] - it('should compute the correct visibility for circular logic where all fields are hidden', () => { - form.form_fields = [field1, field2] - for (let field1Response of ['Yes', 'No']) { - for (let field2Response of ['Yes', 'No']) { - const visibleFieldIds = getVisibleFieldIds( - [ - makeResponse(field1._id, field1Response), - makeResponse(field2._id, field2Response), - ], - form, - ) - expect(visibleFieldIds.has(field1._id)).toBe(false) - expect(visibleFieldIds.has(field2._id)).toBe(false) - } - } - }) - it('should compute the correct visibility for circular logic with a mix of shown and hidden fields', () => { - form.form_fields = [field1, field2, visibleField] - for (let field1Response of ['Yes', 'No']) { - for (let field2Response of ['Yes', 'No']) { - const visibleFieldIds = getVisibleFieldIds( - [ - makeResponse(field1._id, field1Response), - makeResponse(field2._id, field2Response), - makeResponse(visibleField._id, 'Yes'), - ], - form, - ) - expect(visibleFieldIds.has(field1._id)).toBe(false) - expect(visibleFieldIds.has(field2._id)).toBe(false) - expect(visibleFieldIds.has(visibleField._id)).toBe(true) - } - } - }) - }) - describe('prevent submit for others value', () => { - const form = { _id: 'f1' } - const radioButton = { - _id: '001', - fieldType: 'radiobutton', - fieldOptions: ['Option 1', 'Option 2'], - othersRadioButton: true, - } - const textField = { _id: '002', fieldType: 'textfield' } - it('should correctly prevent submission for radiobutton Others on clientside', () => { - const textFieldResponse = Object.assign({}, textField, { - fieldValue: 'lorem', - }) - form.form_fields = [radioButton, textField] - form.form_logics = [ - { - show: [], - conditions: [ - { - ifValueType: 'single-select', - _id: '58169', - field: radioButton._id, - state: 'is equals to', - value: 'Others', - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'preventSubmit', - }, - ] - const fillInRadioButton = (fieldValue) => - Object.assign({}, radioButton, { fieldValue, isVisible: true }) - expect( - getLogicUnitPreventingSubmit( - [fillInRadioButton('radioButtonOthers'), textFieldResponse], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [fillInRadioButton('Option 1'), textFieldResponse], - form, - ), - ).toBeUndefined() - }) - it('should correctly prevent submission for radiobutton Others on serverside', () => { - const textFieldResponse = makeResponse('002', 'lorem') - form.form_fields = [radioButton, textField] - form.form_logics = [ - { - show: [], - conditions: [ - { - ifValueType: 'single-select', - _id: '58169', - field: radioButton._id, - state: 'is equals to', - value: 'Others', - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'preventSubmit', - }, - ] - expect( - getLogicUnitPreventingSubmit( - [makeResponse(radioButton._id, 'Others: School'), textFieldResponse], - form, - ), - ).toEqual(form.form_logics[0]) - expect( - getLogicUnitPreventingSubmit( - [makeResponse(radioButton._id, 'Option 1'), textFieldResponse], - form, - ), - ).toBeUndefined() - }) - }) -}) diff --git a/tests/unit/backend/utils/logic.spec.ts b/tests/unit/backend/utils/logic.spec.ts new file mode 100644 index 0000000000..675ff223f6 --- /dev/null +++ b/tests/unit/backend/utils/logic.spec.ts @@ -0,0 +1,1004 @@ +import { ObjectId } from 'bson-ext' + +import { + getLogicUnitPreventingSubmit, + getVisibleFieldIds, +} from 'src/shared/util/logic' +import { + BasicField, + FieldResponse, + IFieldSchema, + IFormDocument, + IPreventSubmitLogicSchema, + IRadioFieldSchema, + IShortTextFieldSchema, + IShowFieldsLogicSchema, + LogicConditionState, + LogicIfValue, + LogicType, +} from 'src/types' + +describe('Logic validation', () => { + /** Mock a field's bare essentials */ + const makeField = (fieldId: string) => ({ _id: fieldId } as IFieldSchema) + + /** + * Mock a response + * @param fieldId field id of the field that this response is meant for + * @param answer + * @param answerArray array of answers passed in for checkbox and table + * @param isVisible whether field is visible + */ + const makeResponse = ( + fieldId: string, + answer: string | number, + answerArray: string[] | null = null, + isVisible = true, + ): FieldResponse => { + const response: Record = { _id: fieldId, answer, isVisible } + if (answerArray) { + response.answerArray = answerArray + } + return response as FieldResponse + } + + describe('visibility for different states', () => { + const CONDITION_FIELD = makeField(new ObjectId().toHexString()) + const LOGIC_FIELD = makeField(new ObjectId().toHexString()) + const LOGIC_RESPONSE = makeResponse(LOGIC_FIELD._id, 'lorem') + const MOCK_LOGIC_ID = new ObjectId().toHexString() + + let form: IFormDocument + + beforeEach(() => { + form = { + _id: new ObjectId(), + form_fields: [CONDITION_FIELD, LOGIC_FIELD], + } as IFormDocument + }) + + it('should compute the correct visibility for "is equals to"', () => { + // Arrange + const equalsCondition = { + show: [LOGIC_FIELD._id], + conditions: [ + { + ifValueType: LogicIfValue.Number, + _id: '58169', + field: CONDITION_FIELD._id, + state: LogicConditionState.Equal, + value: 0, + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema + + form.form_logics = [equalsCondition] + + // Act + Assert + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, 0), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, 1), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(false) + }) + + it('should compute the correct visibility for "is less than or equal to"', () => { + // Arrange + const lteCondition = { + show: [LOGIC_FIELD._id], + conditions: [ + { + ifValueType: LogicIfValue.Number, + _id: '58169', + field: CONDITION_FIELD._id, + state: LogicConditionState.Lte, + value: 99, + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema + + form.form_logics = [lteCondition] + + // Act + Assert + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, 98), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, 99), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, 100), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(false) + }) + + it('should compute the correct visibility for "is more than or equal to"', () => { + // Arrange + const gteCondition = { + show: [LOGIC_FIELD._id], + conditions: [ + { + ifValueType: LogicIfValue.Number, + _id: '58169', + field: CONDITION_FIELD._id, + state: LogicConditionState.Gte, + value: 22, + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema + form.form_logics = [gteCondition] + + // Act + Assert + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, 23), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, 22), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, 21), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(false) + }) + + it('should compute the correct visibility for "is either"', () => { + // Arrange + const validOptions = ['Option 1', 'Option 2'] + const eitherCondition = { + show: [LOGIC_FIELD._id], + conditions: [ + { + ifValueType: LogicIfValue.MultiSelect, + _id: '58169', + field: CONDITION_FIELD._id, + state: LogicConditionState.Either, + value: validOptions, + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema + + form.form_logics = [eitherCondition] + + // Act + Assert + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, validOptions[0]), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, validOptions[1]), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [makeResponse(CONDITION_FIELD._id, 'invalid option'), LOGIC_RESPONSE], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(false) + }) + }) + + describe('preventing submission for different states', () => { + const CONDITION_FIELD = makeField(new ObjectId().toHexString()) + const LOGIC_FIELD = makeField(new ObjectId().toHexString()) + const LOGIC_RESPONSE = makeResponse(LOGIC_FIELD._id, 'lorem') + const MOCK_LOGIC_ID = new ObjectId().toHexString() + + let form: IFormDocument + + beforeEach(() => { + form = { + _id: new ObjectId(), + form_fields: [CONDITION_FIELD, LOGIC_FIELD], + } as IFormDocument + }) + + it('should compute that submission should be prevented for "is equals to"', () => { + // Arrange + const equalCondition = { + conditions: [ + { + ifValueType: LogicIfValue.Number, + _id: '58169', + field: CONDITION_FIELD._id, + state: LogicConditionState.Equal, + value: 0, + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.PreventSubmit, + preventSubmitMessage: "oh no you don't", + } as IPreventSubmitLogicSchema + + form.form_logics = [equalCondition] + + // Act + Assert + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, 0), LOGIC_RESPONSE], + form, + ), + ).toEqual(form.form_logics[0]) + + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, 1), LOGIC_RESPONSE], + form, + ), + ).toBeUndefined() + }) + + it('should compute that submission should be prevented for "is less than or equal to"', () => { + // Arrange + const lteCondition = { + conditions: [ + { + ifValueType: LogicIfValue.Number, + _id: '58169', + field: CONDITION_FIELD._id, + state: LogicConditionState.Lte, + value: 99, + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.PreventSubmit, + preventSubmitMessage: "oh no you don't", + } as IPreventSubmitLogicSchema + + form.form_logics = [lteCondition] + + // Act + Assert + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, 98), LOGIC_RESPONSE], + form, + ), + ).toEqual(form.form_logics[0]) + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, 99), LOGIC_RESPONSE], + form, + ), + ).toEqual(form.form_logics[0]) + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, 100), LOGIC_RESPONSE], + form, + ), + ).toBeUndefined() + }) + + it('should compute that submission should be prevented for "is more than or equal to"', () => { + // Arrange + const gteCondition = { + conditions: [ + { + ifValueType: LogicIfValue.Number, + _id: '58169', + field: CONDITION_FIELD._id, + state: LogicConditionState.Gte, + value: 22, + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.PreventSubmit, + preventSubmitMessage: "oh no you don't", + } as IPreventSubmitLogicSchema + + form.form_logics = [gteCondition] + + // Act + Assert + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, 23), LOGIC_RESPONSE], + form, + ), + ).toEqual(form.form_logics[0]) + + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, 22), LOGIC_RESPONSE], + form, + ), + ).toEqual(form.form_logics[0]) + + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, 21), LOGIC_RESPONSE], + form, + ), + ).toBeUndefined() + }) + + it('should compute that submission should be prevented for "is either"', () => { + // Arrange + const validOptions = ['Option 1', 'Option 2'] + const eitherCondition = { + conditions: [ + { + ifValueType: LogicIfValue.MultiSelect, + _id: '58169', + field: CONDITION_FIELD._id, + state: LogicConditionState.Either, + value: validOptions, + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.PreventSubmit, + preventSubmitMessage: "oh no you don't", + } as IPreventSubmitLogicSchema + + form.form_logics = [eitherCondition] + + // Act + Assert + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, validOptions[0]), LOGIC_RESPONSE], + form, + ), + ).toEqual(form.form_logics[0]) + + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, validOptions[1]), LOGIC_RESPONSE], + form, + ), + ).toEqual(form.form_logics[0]) + + expect( + getLogicUnitPreventingSubmit( + [makeResponse(CONDITION_FIELD._id, 'Option 3'), LOGIC_RESPONSE], + form, + ), + ).toBeUndefined() + }) + }) + + describe('show fields with multiple conditions', () => { + const CONDITION_FIELD_1 = makeField(new ObjectId().toHexString()) + const CONDITION_FIELD_2 = makeField(new ObjectId().toHexString()) + const LOGIC_FIELD = makeField(new ObjectId().toHexString()) + const LOGIC_RESPONSE = makeResponse(LOGIC_FIELD._id, 'lorem') + const MOCK_LOGIC_ID_1 = new ObjectId().toHexString() + const MOCK_LOGIC_ID_2 = new ObjectId().toHexString() + + let form: IFormDocument + + beforeEach(() => { + form = { + _id: new ObjectId(), + form_fields: [CONDITION_FIELD_1, CONDITION_FIELD_2, LOGIC_FIELD], + } as IFormDocument + }) + + it('should compute the correct visibility for AND conditions', () => { + // Arrange + const multipleEqualConditions = { + show: [LOGIC_FIELD._id], + _id: MOCK_LOGIC_ID_1, + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '9577', + field: CONDITION_FIELD_1._id, + state: LogicConditionState.Equal, + value: 'Yes', + }, + { + ifValueType: LogicIfValue.SingleSelect, + _id: '45633', + field: CONDITION_FIELD_2._id, + state: LogicConditionState.Equal, + value: 20, + }, + ], + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema + + form.form_logics = [multipleEqualConditions] + + // Act + expect( + getVisibleFieldIds( + [ + makeResponse(CONDITION_FIELD_1._id, 'Yes'), + makeResponse(CONDITION_FIELD_2._id, 20), + LOGIC_RESPONSE, + ], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [ + makeResponse(CONDITION_FIELD_1._id, 'Yes'), + makeResponse(CONDITION_FIELD_2._id, 100), + LOGIC_RESPONSE, + ], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(false) + + expect( + getVisibleFieldIds( + [ + makeResponse(CONDITION_FIELD_1._id, 'No'), + makeResponse(CONDITION_FIELD_2._id, 20), + LOGIC_RESPONSE, + ], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(false) + }) + + it('should compute the correct visibility for OR conditions', () => { + // Arrange + const equalCondition = { + show: [LOGIC_FIELD._id], + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '9577', + field: CONDITION_FIELD_1._id, + state: LogicConditionState.Equal, + value: 'Yes', + }, + ], + _id: MOCK_LOGIC_ID_1, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema + + const equalCondition2 = { + show: [LOGIC_FIELD._id], + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '89906', + field: CONDITION_FIELD_2._id, + state: LogicConditionState.Equal, + value: 20, + }, + ], + _id: MOCK_LOGIC_ID_2, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema + + form.form_logics = [equalCondition, equalCondition2] + + // Act + Assert + expect( + getVisibleFieldIds( + [ + makeResponse(CONDITION_FIELD_1._id, 'Yes'), + makeResponse(CONDITION_FIELD_2._id, 20), + LOGIC_RESPONSE, + ], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [ + makeResponse(CONDITION_FIELD_1._id, 'Yes'), + makeResponse(CONDITION_FIELD_2._id, 100), + LOGIC_RESPONSE, + ], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [ + makeResponse(CONDITION_FIELD_1._id, 'No'), + makeResponse(CONDITION_FIELD_2._id, 20), + LOGIC_RESPONSE, + ], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [ + makeResponse(CONDITION_FIELD_1._id, 'No'), + makeResponse(CONDITION_FIELD_2._id, 100), + LOGIC_RESPONSE, + ], + form, + ).has(LOGIC_FIELD._id), + ).toEqual(false) + }) + }) + + describe('prevent submit with multiple conditions', () => { + const CONDITION_FIELD_1 = makeField(new ObjectId().toHexString()) + const CONDITION_FIELD_2 = makeField(new ObjectId().toHexString()) + const LOGIC_FIELD = makeField(new ObjectId().toHexString()) + const LOGIC_RESPONSE = makeResponse(LOGIC_FIELD._id, 'lorem') + const MOCK_LOGIC_ID_1 = new ObjectId().toHexString() + const MOCK_LOGIC_ID_2 = new ObjectId().toHexString() + + let form: IFormDocument + + beforeEach(() => { + form = { + _id: new ObjectId(), + form_fields: [CONDITION_FIELD_1, CONDITION_FIELD_2, LOGIC_FIELD], + } as IFormDocument + }) + + it('should correctly prevent submission for AND conditions', () => { + // Arrange + const multipleEqualConditions = { + _id: MOCK_LOGIC_ID_1, + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '9577', + field: CONDITION_FIELD_1._id, + state: LogicConditionState.Equal, + value: 'Yes', + }, + { + ifValueType: LogicIfValue.SingleSelect, + _id: '45633', + field: CONDITION_FIELD_2._id, + state: LogicConditionState.Equal, + value: 20, + }, + ], + logicType: LogicType.PreventSubmit, + preventSubmitMessage: 'orh hor i tell teacher', + } as IPreventSubmitLogicSchema + form.form_logics = [multipleEqualConditions] + + // Act + Assert + expect( + getLogicUnitPreventingSubmit( + [ + makeResponse(CONDITION_FIELD_1._id, 'Yes'), + makeResponse(CONDITION_FIELD_2._id, 20), + LOGIC_RESPONSE, + ], + form, + ), + ).toEqual(form.form_logics[0]) + + expect( + getLogicUnitPreventingSubmit( + [ + makeResponse(CONDITION_FIELD_1._id, 'Yes'), + makeResponse(CONDITION_FIELD_2._id, 100), + LOGIC_RESPONSE, + ], + form, + ), + ).toBeUndefined() + + expect( + getLogicUnitPreventingSubmit( + [ + makeResponse(CONDITION_FIELD_1._id, 'No'), + makeResponse(CONDITION_FIELD_2._id, 20), + LOGIC_RESPONSE, + ], + form, + ), + ).toBeUndefined() + }) + + it('should correctly prevent submission for OR conditions', () => { + // Arrange + const equalCondition = { + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '9577', + field: CONDITION_FIELD_1._id, + state: LogicConditionState.Equal, + value: 'Yes', + }, + ], + _id: MOCK_LOGIC_ID_1, + logicType: LogicType.PreventSubmit, + preventSubmitMessage: 'this one cannot', + } as IPreventSubmitLogicSchema + + const equalCondition2 = { + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '89906', + field: CONDITION_FIELD_2._id, + state: LogicConditionState.Equal, + value: 20, + }, + ], + _id: MOCK_LOGIC_ID_2, + logicType: LogicType.PreventSubmit, + preventSubmitMessage: 'this one also cannot', + } as IPreventSubmitLogicSchema + + form.form_logics = [equalCondition, equalCondition2] + + // Act + Assert + expect( + getLogicUnitPreventingSubmit( + [ + makeResponse(CONDITION_FIELD_1._id, 'Yes'), + makeResponse(CONDITION_FIELD_2._id, 20), + LOGIC_RESPONSE, + ], + form, + ), + ).toEqual(form.form_logics[0]) + + expect( + getLogicUnitPreventingSubmit( + [ + makeResponse(CONDITION_FIELD_1._id, 'Yes'), + makeResponse(CONDITION_FIELD_2._id, 100), + LOGIC_RESPONSE, + ], + form, + ), + ).toEqual(form.form_logics[0]) + + expect( + getLogicUnitPreventingSubmit( + [ + makeResponse(CONDITION_FIELD_1._id, 'No'), + makeResponse(CONDITION_FIELD_2._id, 20), + LOGIC_RESPONSE, + ], + form, + ), + ).toEqual(form.form_logics[1]) + + expect( + getLogicUnitPreventingSubmit( + [ + makeResponse(CONDITION_FIELD_1._id, 'No'), + makeResponse(CONDITION_FIELD_2._id, 100), + LOGIC_RESPONSE, + ], + form, + ), + ).toBeUndefined() + }) + }) + + describe('visibility for others value', () => { + const MOCK_LOGIC_ID = new ObjectId().toHexString() + + const MOCK_RADIO_FIELD = { + _id: new ObjectId().toHexString(), + fieldType: BasicField.Radio, + fieldOptions: ['Option 1', 'Option 2'], + othersRadioButton: true, + } as IRadioFieldSchema + + const MOCK_TEXT_FIELD = { + _id: new ObjectId().toHexString(), + fieldType: BasicField.ShortText, + } as IShortTextFieldSchema + + const fillInRadioButton = (fieldValue: string) => + Object.assign({}, MOCK_RADIO_FIELD, { fieldValue, isVisible: true }) + + let form: IFormDocument + + beforeEach(() => { + form = { + _id: new ObjectId(), + } as IFormDocument + }) + + it('should compute the correct visibility for radiobutton Others on clientside', () => { + // Arrange + const equalCondition = { + show: [MOCK_TEXT_FIELD._id], + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '58169', + field: MOCK_RADIO_FIELD._id, + state: LogicConditionState.Equal, + value: 'Others', + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema + + const textFieldResponse = Object.assign({}, MOCK_TEXT_FIELD, { + fieldValue: 'lorem', + }) + form.form_fields = [MOCK_RADIO_FIELD, MOCK_TEXT_FIELD] + form.form_logics = [equalCondition] + // Act + Assert + expect( + getVisibleFieldIds( + [fillInRadioButton('radioButtonOthers'), textFieldResponse], + form, + ).has(MOCK_TEXT_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [fillInRadioButton('Option 1'), textFieldResponse], + form, + ).has(MOCK_TEXT_FIELD._id), + ).toEqual(false) + }) + + it('should compute the correct visibility for radiobutton Others on serverside', () => { + // Arrange + const equalCondition = { + show: [MOCK_TEXT_FIELD._id], + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '58169', + field: MOCK_RADIO_FIELD._id, + state: LogicConditionState.Equal, + value: 'Others', + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema + + const textFieldResponse = makeResponse( + new ObjectId().toHexString(), + 'lorem', + ) + form.form_fields = [MOCK_RADIO_FIELD, MOCK_TEXT_FIELD] + form.form_logics = [equalCondition] + + // Act + Assert + expect( + getVisibleFieldIds( + [ + makeResponse(MOCK_RADIO_FIELD._id, 'Others: School'), + textFieldResponse, + ], + form, + ).has(MOCK_TEXT_FIELD._id), + ).toEqual(true) + + expect( + getVisibleFieldIds( + [makeResponse(MOCK_RADIO_FIELD._id, 'Option 1'), textFieldResponse], + form, + ).has(MOCK_TEXT_FIELD._id), + ).toEqual(false) + }) + }) + + describe('visibility for circular logic', () => { + const FIELD_1 = makeField(new ObjectId().toHexString()) + const FIELD_2 = makeField(new ObjectId().toHexString()) + const VISIBLE_FIELD = makeField(new ObjectId().toHexString()) + const MOCK_LOGIC_ID_1 = new ObjectId() + const MOCK_LOGIC_ID_2 = new ObjectId() + + let form: IFormDocument + + beforeEach(() => { + form = ({ + _id: new ObjectId(), + form_logics: [ + { + show: [FIELD_2._id], + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '9577', + field: FIELD_1._id, + state: LogicConditionState.Equal, + value: 'Yes', + }, + ], + _id: MOCK_LOGIC_ID_1, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema, + { + show: [FIELD_1._id], + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '89906', + field: FIELD_2._id, + state: LogicConditionState.Equal, + value: 'Yes', + }, + ], + _id: MOCK_LOGIC_ID_2, + logicType: LogicType.ShowFields, + } as IShowFieldsLogicSchema, + ], + } as unknown) as IFormDocument + }) + + it('should compute the correct visibility for circular logic where all fields are hidden', () => { + form.form_fields = [FIELD_1, FIELD_2] + for (const field1Response of ['Yes', 'No']) { + for (const field2Response of ['Yes', 'No']) { + const visibleFieldIds = getVisibleFieldIds( + [ + makeResponse(FIELD_1._id, field1Response), + makeResponse(FIELD_2._id, field2Response), + ], + form, + ) + expect(visibleFieldIds.has(FIELD_1._id)).toEqual(false) + expect(visibleFieldIds.has(FIELD_2._id)).toEqual(false) + } + } + }) + + it('should compute the correct visibility for circular logic with a mix of shown and hidden fields', () => { + form.form_fields = [FIELD_1, FIELD_2, VISIBLE_FIELD] + for (const field1Response of ['Yes', 'No']) { + for (const field2Response of ['Yes', 'No']) { + const visibleFieldIds = getVisibleFieldIds( + [ + makeResponse(FIELD_1._id, field1Response), + makeResponse(FIELD_2._id, field2Response), + makeResponse(VISIBLE_FIELD._id, 'Yes'), + ], + form, + ) + expect(visibleFieldIds.has(FIELD_1._id)).toEqual(false) + expect(visibleFieldIds.has(FIELD_2._id)).toEqual(false) + expect(visibleFieldIds.has(VISIBLE_FIELD._id)).toEqual(true) + } + } + }) + }) + + describe('prevent submit for others value', () => { + const MOCK_LOGIC_ID = new ObjectId() + const MOCK_RADIO_FIELD = { + _id: new ObjectId(), + fieldType: BasicField.Radio, + fieldOptions: ['Option 1', 'Option 2'], + othersRadioButton: true, + } as IRadioFieldSchema + const MOCK_TEXT_FIELD = { + _id: new ObjectId(), + fieldType: BasicField.ShortText, + } as IShortTextFieldSchema + + const fillInRadioButton = (fieldValue: string) => + Object.assign({}, MOCK_RADIO_FIELD, { fieldValue, isVisible: true }) + + let form: IFormDocument + + beforeEach(() => { + form = { _id: new ObjectId() } as IFormDocument + }) + + it('should correctly prevent submission for radiobutton Others on clientside', () => { + // Arrange + const textFieldResponse = Object.assign({}, MOCK_TEXT_FIELD, { + fieldValue: 'lorem', + }) + form.form_fields = [MOCK_RADIO_FIELD, MOCK_TEXT_FIELD] + form.form_logics = [ + { + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '58169', + field: MOCK_RADIO_FIELD._id, + state: LogicConditionState.Equal, + value: 'Others', + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.PreventSubmit, + } as IPreventSubmitLogicSchema, + ] + + // Act + Assert + expect( + getLogicUnitPreventingSubmit( + [fillInRadioButton('radioButtonOthers'), textFieldResponse], + form, + ), + ).toEqual(form.form_logics[0]) + + expect( + getLogicUnitPreventingSubmit( + [fillInRadioButton('Option 1'), textFieldResponse], + form, + ), + ).toBeUndefined() + }) + + it('should correctly prevent submission for radiobutton Others on serverside', () => { + // Arrange + const textFieldResponse = makeResponse( + new ObjectId().toHexString(), + 'lorem', + ) + form.form_fields = [MOCK_RADIO_FIELD, MOCK_TEXT_FIELD] + form.form_logics = [ + { + conditions: [ + { + ifValueType: LogicIfValue.SingleSelect, + _id: '58169', + field: MOCK_RADIO_FIELD._id, + state: LogicConditionState.Equal, + value: 'Others', + }, + ], + _id: MOCK_LOGIC_ID, + logicType: LogicType.PreventSubmit, + } as IPreventSubmitLogicSchema, + ] + + // Act + Assert + expect( + getLogicUnitPreventingSubmit( + [ + makeResponse(MOCK_RADIO_FIELD._id, 'Others: School'), + textFieldResponse, + ], + form, + ), + ).toEqual(form.form_logics[0]) + expect( + getLogicUnitPreventingSubmit( + [makeResponse(MOCK_RADIO_FIELD._id, 'Option 1'), textFieldResponse], + form, + ), + ).toBeUndefined() + }) + }) +})