diff --git a/package-lock.json b/package-lock.json index 4569f488..de029cb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "eslint-plugin-vue": "^9.15.1", "jest": "^27.0.5", "jest-junit": "^13.0.0", + "jest-serializer-vue": "^3.1.0", "jsdoc": "^4.0.2", "jsdom": "^22.1.0", "sass": "^1.32.7", @@ -4413,6 +4414,15 @@ } } }, + "node_modules/@vue/cli-plugin-unit-jest/node_modules/jest-serializer-vue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jest-serializer-vue/-/jest-serializer-vue-2.0.2.tgz", + "integrity": "sha512-nK/YIFo6qe3i9Ge+hr3h4PpRehuPPGZFt8LDBdTHYldMb7ZWlkanZS8Ls7D8h6qmQP2lBQVDLP0DKn5bJ9QApQ==", + "dev": true, + "dependencies": { + "pretty": "2.0.0" + } + }, "node_modules/@vue/cli-plugin-vuex": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-5.0.8.tgz", @@ -12245,9 +12255,9 @@ } }, "node_modules/jest-serializer-vue": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jest-serializer-vue/-/jest-serializer-vue-2.0.2.tgz", - "integrity": "sha512-nK/YIFo6qe3i9Ge+hr3h4PpRehuPPGZFt8LDBdTHYldMb7ZWlkanZS8Ls7D8h6qmQP2lBQVDLP0DKn5bJ9QApQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jest-serializer-vue/-/jest-serializer-vue-3.1.0.tgz", + "integrity": "sha512-vXz9/3IgBbLhsaVANYLG4ROCQd+Wg3qbB6ICofzFL+fbhSFPlqb0/MMGXcueVsjaovdWlYiRaLQLpdi1PTcoRQ==", "dev": true, "dependencies": { "pretty": "2.0.0" diff --git a/package.json b/package.json index dbbcf42e..36f4c832 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", - "test:unit": "vue-cli-service test:unit --coverage --ci", + "test:unit": "vue-cli-service test:unit --coverage --ci --colors", "test:unit:lite": "vue-cli-service test:unit --ci" }, "dependencies": { @@ -50,6 +50,7 @@ "eslint-plugin-vue": "^9.15.1", "jest": "^27.0.5", "jest-junit": "^13.0.0", + "jest-serializer-vue": "^3.1.0", "jsdoc": "^4.0.2", "jsdom": "^22.1.0", "sass": "^1.32.7", diff --git a/src/constants/cms-widget-fields.js b/src/constants/cms-widget-fields.js new file mode 100644 index 00000000..f8fd23fe --- /dev/null +++ b/src/constants/cms-widget-fields.js @@ -0,0 +1,45 @@ +const widgetFields = Object.freeze({ + HEADER_WIDGET: { + NAME: 'Name', + ANSWERS: 'Answers' + }, + SUB_HEADER_WIDGET: { + NAME: 'Name', + SUB_HEADER_TEXT: 'SubHeaderText', + SECONDARY_TEXT: 'SecondaryText', + BACK_BUTTON_ACCESSIBLE_TEXT: 'BackButtonAccessibleText' + }, + INPUT_QUESTION_WIDGET: { + NAME: 'Name', + QUESTION_TEXT: 'QuestionText', + BUTTON_TEXT: 'ButtonText', + ANSWERS: 'Answers' + }, + TEXT_BLOCK_WIDGET: { + NAME: 'Name', + TEXT: 'Text' + }, + CONTENT_GROUP_WIDGET: { + NAME: 'Name', + HEADER_TEXT: 'HeaderText', + SUBHEADER_TEXT: 'SubheaderText', + BODY_TEXT: 'BodyText', + BODY_TEXT_2: 'BodyText2', + FOOTER_TEXT: 'FooterText', + IMAGE: 'Image' + }, + ALERT_WIDGET: { + NAME: 'Name', + HEADLINE_TEXT: 'HeadlineText', + BODY_TEXT: 'BodyText' + }, + FOOTER_WIDGET: { + NAME: 'Name', + BACK_BUTTON_TEXT: 'BackButtonText', + FORWARD_BUTTON_TEXT: 'ForwardButtonText', + FOOTER_IMAGE: 'FooterImage', + ALT_TEXT: 'AltText' + } +}); + +export default widgetFields; diff --git a/src/constants/global-rules.js b/src/constants/global-rules.js index d8c6f406..da36b822 100644 --- a/src/constants/global-rules.js +++ b/src/constants/global-rules.js @@ -13,6 +13,8 @@ const globalRules = Object.freeze({ EMAIL_ADDRESS_FORMAT: 'email-address-format', PHONE_NUMBER_REQUIRED: 'phone-number-required', PHONE_NUMBER_FORMAT: 'phone-number-format', + ZIP_CODE_REQUIRED: 'zip-code-required', + ZIP_CODE_SEARCH_FORMAT: 'zip-code-search-format', OPTION_REQUIRED: 'option-required' }); diff --git a/src/digital-components/textbox-question/textbox-question.spec.js b/src/digital-components/textbox-question/textbox-question.spec.js index c98e7837..f7a31e03 100644 --- a/src/digital-components/textbox-question/textbox-question.spec.js +++ b/src/digital-components/textbox-question/textbox-question.spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { useField, validate } from 'vee-validate'; import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue'; // Mock CMS content @@ -8,150 +9,946 @@ const mockMixin = { getCmsContent: jest.fn().mockImplementation(() => questionText) } }; -const maska = jest.fn(); -describe('textboxQuestion.vue', () => { - it('Should render a text input', async () => { - // Arrange - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska - } - }, - mixins: [mockMixin] +jest.mock('vee-validate', () => ({ + useField: jest.fn(), + validate: jest.fn() +})); + +useField.mockImplementation(() => Promise.resolve({})); +validate.mockImplementation(() => Promise.resolve({})); + +const searchButtonSelector = 'button[type="submit"][aria-label="Search button"]'; +const selectButtonSelector = 'button[type="submit"][aria-label="Select button"]'; +const errorMessageSelector = { ref: 'errorMessageDiv' }; + +describe('TextboxQuestion', () => { + describe('should render', () => { + describe('an input element', () => { + test('that exists with expected classes', () => { + // Arrange + const wrapper = shallowMount(textboxQuestion, { + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.exists()).toBeTruthy(); + expect(input.classes()).toContain('form-control'); + }); + describe('with correct attributes and classes given prop', () => { + test('type', () => { + // Arrange + const type = 'crocodile'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { type }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes().type).toBe(type); + }); + test('placeholderText', () => { + // Arrange + const placeholderText = 'text in place'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { placeholderText }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes().placeholder).toBe(placeholderText); + }); + test('inputId', () => { + // Arrange + const inputId = '123009'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { inputId }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes('id')).toBe(inputId); + expect(input.attributes('name')).toBe(inputId); + }); + describe('isDisabled', () => { + test('with value "true"', () => { + // Arrange + const isDisabled = true; + const wrapper = shallowMount(textboxQuestion, { + propsData: { isDisabled }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes('aria-disabled')).toBe(String(isDisabled)); + expect(input.attributes().disabled).toBeDefined(); + }); + test('with value "false"', () => { + // Arrange + const isDisabled = false; + const wrapper = shallowMount(textboxQuestion, { + propsData: { isDisabled }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes('aria-disabled')).toBe(String(isDisabled)); + expect(input.attributes().disabled).not.toBeDefined(); + }); + }); + describe('isRequired', () => { + test('with value "true"', () => { + // Arrange + const isRequired = true; + const wrapper = shallowMount(textboxQuestion, { + propsData: { isRequired }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes().required).toBeDefined(); + }); + test('with value "false"', () => { + // Arrange + const isRequired = false; + const wrapper = shallowMount(textboxQuestion, { + propsData: { isRequired }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes().required).not.toBeDefined(); + }); + }); + describe('hasIcon', () => { + test('with value "true"', () => { + // Arrange + const hasIcon = true; + const wrapper = shallowMount(textboxQuestion, { + propsData: { hasIcon }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.classes()).toContain('has-icon'); + }); + test('with value "false"', () => { + // Arrange + const hasIcon = false; + const wrapper = shallowMount(textboxQuestion, { + propsData: { hasIcon }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.classes()).not.toContain('has-icon'); + }); + }); + describe('iconRight', () => { + test('with value "true"', () => { + // Arrange + const iconRight = true; + const wrapper = shallowMount(textboxQuestion, { + propsData: { iconRight }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.classes()).toContain('icon-right'); + }); + test('with value "false"', () => { + // Arrange + const iconRight = false; + const wrapper = shallowMount(textboxQuestion, { + propsData: { iconRight }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.classes()).not.toContain('icon-right'); + }); + }); + test('validationRules', () => { + // Arrange + const validationRules = 'random validation rules'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { validationRules }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes().validationrules).toBe(validationRules); + }); + test('cmsWidgetName', () => { + // Arrange + const cmsWidgetName = 'name of widget'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { cmsWidgetName }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + const expected = '#name of widget'; + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes('data-bs-target')).toBe(expected); + }); + describe('maxLength', () => { + test('with no value', () => { + // Arrange + const wrapper = shallowMount(textboxQuestion, { + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + const expected = '999'; + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes('maxlength')).toBe(expected); + }); + test('with some value', () => { + // Arrange + const maxLength = '123'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { maxLength }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes('maxlength')).toBe(maxLength); + }); + }); + describe('cornerStyle', () => { + test('with value "rounded"', () => { + // Arrange + const cornerStyle = 'rounded'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { cornerStyle }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.classes()).toContain('rounded-pill'); + }); + test('with value other than "rounded"', () => { + // Arrange + const cornerStyle = 'decidedly not rounded'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { cornerStyle }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.classes()).not.toContain('rounded-pill'); + }); + }); + describe('includeSelectIcon', () => { + test('with value "true"', () => { + // Arrange + const includeSelectIcon = true; + const wrapper = shallowMount(textboxQuestion, { + propsData: { includeSelectIcon }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes('data-bs-toggle')).toBe('modal'); + }); + test('with value "false"', () => { + // Arrange + const includeSelectIcon = false; + const wrapper = shallowMount(textboxQuestion, { + propsData: { includeSelectIcon }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes('data-bs-toggle')).toBe(''); + }); + }); + test('min', () => { + // Arrange + const min = 'some value'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { min }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes().min).toBe(min); + }); + test('max', () => { + // Arrange + const max = 'some value'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { max }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const input = wrapper.find('input'); + + // Assert + expect(input.attributes().max).toBe(max); + }); + }); }); + describe('a label element given that displayQuestionText is true with correct attributes and classes set with prop', () => { + const displayQuestionText = true; - wrapper.getCmsContent = jest.fn(); + test('inputId', () => { + // Arrange + const inputId = '123758'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { displayQuestionText, inputId }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); - // Act - const input = wrapper.find('input'); + // Act + const label = wrapper.find('label'); - // Assert - expect(input.exists()).toBe(true); + // Assert + expect(label.exists()).toBeTruthy(); + expect(label.attributes().for).toBe(inputId); + expect(label.attributes('aria-label')).toBe(questionText); + expect(label.classes()).toContain('form-label'); + }); + describe('questionAlignment', () => { + test('having value "center"', () => { + // Arrange + const questionAlignment = 'center'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { displayQuestionText, questionAlignment }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const label = wrapper.find('label'); + + // Assert + expect(label.exists()).toBeTruthy(); + expect(label.attributes('aria-label')).toBe(questionText); + expect(label.classes()).toContain('form-label'); + expect(label.classes()).toContain('text-center'); + expect(label.classes()).toContain('w-100'); + expect(label.classes()).toContain('mb-5'); + }); + test('having value other than "center"', () => { + // Arrange + const questionAlignment = 'not center'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { displayQuestionText, questionAlignment }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const label = wrapper.find('label'); + + // Assert + expect(label.exists()).toBeTruthy(); + expect(label.attributes('aria-label')).toBe(questionText); + expect(label.classes()).toContain('form-label'); + expect(label.classes()).not.toContain('text-center'); + expect(label.classes()).not.toContain('w-100'); + expect(label.classes()).not.toContain('mb-5'); + }); + }); + }); + test('a search button with the correct attributes and classes if includeSearchIcon is true', () => { + // Arrange + const includeSearchIcon = true; + const wrapper = shallowMount(textboxQuestion, { + propsData: { includeSearchIcon }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const searchButton = wrapper.find(searchButtonSelector); + + // Assert + expect(searchButton.exists()).toBeTruthy(); + expect(searchButton.attributes('type')).toBe('submit'); + expect(searchButton.attributes('aria-label')).toBe('Search button'); + }); + describe('a select button with the correct attributes and classes if includeSelectIcon is true', () => { + const includeSelectIcon = true; + test('where cmsWidgetName defined', () => { + // Arrange + const cmsWidgetName = 'name of cms widget'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { includeSelectIcon, cmsWidgetName }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + const expectedDataBsTarget = '#name of cms widget'; + + // Act + const selectButton = wrapper.find(selectButtonSelector); + + // Assert + expect(selectButton.exists()).toBeTruthy(); + expect(selectButton.attributes().type).toBe('submit'); + expect(selectButton.attributes('data-bs-toggle')).toBe('modal'); + expect(selectButton.attributes('data-bs-target')).toBe(expectedDataBsTarget); + expect(selectButton.attributes('aria-label')).toBe('Select button'); + }); + test('where cmsWidgetName not defined', () => { + // Arrange + const wrapper = shallowMount(textboxQuestion, { + propsData: { includeSelectIcon }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + const expectedDataBsTarget = '#'; + + // Act + const selectButton = wrapper.find(selectButtonSelector); + + // Assert + expect(selectButton.exists()).toBeTruthy(); + expect(selectButton.attributes().type).toBe('submit'); + expect(selectButton.attributes('data-bs-toggle')).toBe('modal'); + expect(selectButton.attributes('data-bs-target')).toBe(expectedDataBsTarget); + expect(selectButton.attributes('aria-label')).toBe('Select button'); + }); + }); + test('an error with the correct attributes and classes if errorMessage is not empty', () => { + // Arrange + const errorMessage = 'some not empty message'; + useField.mockReturnValueOnce({ errorMessage }); + const wrapper = shallowMount(textboxQuestion, { + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const error = wrapper.find(errorMessageSelector); + + // Assert + expect(error.exists()).toBeTruthy(); + expect(error.attributes().style).not.toBeDefined(); + expect(error.classes()).toContain('form-test-error'); + expect(error.classes()).toContain('row'); + expect(error.classes()).toContain('my-1'); + }); + }); + describe('does not render', () => { + test('a label element if displayQuestionText is false', () => { + // Arrange + const displayQuestionText = false; + const wrapper = shallowMount(textboxQuestion, { + propsData: { displayQuestionText }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const label = wrapper.find('label'); + + // Assert + expect(label.exists()).not.toBeTruthy(); + }); + test('a search button element if includeSearchIcon is false', () => { + // Arrange + const includeSearchIcon = false; + const wrapper = shallowMount(textboxQuestion, { + propsData: { includeSearchIcon }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const searchButton = wrapper.find(searchButtonSelector); + + // Assert + expect(searchButton.exists()).not.toBeTruthy(); + }); + test('a select button if includeSelectIcon is false', () => { + // Arrange + const includeSelectIcon = false; + const wrapper = shallowMount(textboxQuestion, { + propsData: { includeSelectIcon }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const selectButton = wrapper.find(selectButtonSelector); + + // Assert + expect(selectButton.exists()).not.toBeTruthy(); + }); + test('an error span if errorMessage is empty', () => { + // Arrange + const errorMessage = ''; + const wrapper = shallowMount(textboxQuestion, { + propsData: { errorMessage }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + const errorSpan = wrapper.find(errorMessageSelector); + + // Assert + expect(errorSpan.attributes().style).toContain('display: none;'); + }); }); - it("Should render the 'questionText' data value as the label text when disableAutoFill is false.", async () => { - // Arrange - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska - } - }, - mixins: [mockMixin] - }); - - // Act - const label = wrapper.find('label'); - - // Assert - expect(label.text()).toContain(questionText); - }); - - it('Should return input id as the id of the input field', async () => { - // Arrange - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska - } - }, - propsData: { - inputId: 'input ID' - }, - mixins: [mockMixin] - }); - - // Act - const input = wrapper.find('input'); - - // Assert - expect(input.attributes().id).toEqual('input ID'); - }); - - it("Should render the 'questionText' data value as the aria-label attribute.", async () => { + test('should return the errorMessage, handleBlur, handleChange, meta, validate and errors values from the useField hook', () => { // Arrange + const errorMessage = 'some not empty message'; + const handleBlur = () => { console.log('blur'); }; + const handleChange = () => { console.log('change'); }; + const meta = 'meta content'; + const setValidate = () => { console.log('validate'); }; + const errors = 'content of errors'; + useField.mockReturnValueOnce({ + errorMessage, handleBlur, handleChange, meta, errors, validate: setValidate + }); const wrapper = shallowMount(textboxQuestion, { + mixins: [mockMixin], global: { - directives: { - maska - } - }, - mixins: [mockMixin] + directives: { maska: jest.fn() } + } }); - // Act - const label = wrapper.find('label'); - // Assert - expect(label.attributes('aria-label')).toContain(questionText); + expect(wrapper.vm.errorMessage).toBe(errorMessage); + expect(wrapper.vm.handleBlur).toBe(handleBlur); + expect(wrapper.vm.handleChange).toBe(handleChange); + expect(wrapper.vm.meta).toBe(meta); + expect(wrapper.vm.validate).toBe(setValidate); + expect(wrapper.vm.errors).toBe(errors); }); - it('Should return aria-disabled state as disabled', async () => { - // Arrange - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska + describe('computed', () => { + test('"questionText" should return value based on getCmsContent result', () => { + // Arrange + const cmsContent = 'content from cms'; + mockMixin.methods.getCmsContent.mockReturnValueOnce(cmsContent); + const wrapper = shallowMount(textboxQuestion, { + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } } - }, - propsData: { - isDisabled: true - }, - mixins: [mockMixin] + }); + + // Assert + expect(wrapper.vm.questionText).toBe(cmsContent); }); + describe('"value" should', () => { + test('get the modelValue prop', () => { + // Arrange + const modelValue = 'value of modelValue'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { modelValue }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); - // Assert - const input = wrapper.find('input'); + // Assert + expect(wrapper.vm.value).toBe(modelValue); + }); + test('emit the "update:modelValue" event when updated', () => { + // Arrange + const wrapper = shallowMount(textboxQuestion, { + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + const newValue = 'updated value'; - // Expect - expect(input.attributes('aria-disabled')).toEqual('true'); + // Act + wrapper.vm.value = newValue; + + // Assert + expect(wrapper.emitted('update:modelValue')).toBeTruthy(); + expect(wrapper.emitted('update:modelValue').length).toBe(1); // event emitted once + expect(wrapper.emitted('update:modelValue')[0]).toEqual([newValue]); // emitted with expected payload + }); + }); + describe('labelText', () => { + test('should return the question text with no break characters if disableAutoFill is true', () => { + // Arrange + const disableAutoFill = true; + const wrapper = shallowMount(textboxQuestion, { + propsData: { disableAutoFill }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + // TODO this seems like strange behavior to me + const expectedLabelText = 'Q⁠uestion T⁠ext'; + + // Assert + expect(wrapper.vm.labelText).toBe(expectedLabelText); + }); + test('should return the question text as it is if disableAutoFill is false', () => { + // Arrange + const disableAutoFill = false; + const wrapper = shallowMount(textboxQuestion, { + propsData: { disableAutoFill }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Assert + expect(wrapper.vm.labelText).toBe(questionText); + }); + }); }); - it('Should emit new value when modelValue is changed', async () => { - // Act - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska + describe('watch', () => { + test('should validate the new value and trigger full validation if valid', async () => { + // Arrange + const handleChange = jest.fn(); + const modelValue = 'initial value'; + const validationRules = 'required'; + validate.mockReturnValueOnce({ valid: true }); + useField.mockReturnValueOnce({ handleChange }); + const wrapper = shallowMount(textboxQuestion, { + propsData: { modelValue, validationRules }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } } - }, - propsData: { - modelValue: 'val' - }, - mixins: [mockMixin] + }); + const newValue = 'new value'; + + // Act + await wrapper.vm.$options.watch.value.call(wrapper.vm, newValue); + + // Assert + expect(validate).toHaveBeenCalledWith(newValue, validationRules); + expect(handleChange).toHaveBeenCalledWith(newValue); }); + test('should not trigger full validation if invalid', async () => { + // Arrange + const handleChange = jest.fn(); + const modelValue = 'initial value'; + const validationRules = 'required'; + validate.mockReturnValueOnce({ valid: false }); + useField.mockReturnValueOnce({ handleChange }); + const wrapper = shallowMount(textboxQuestion, { + propsData: { modelValue, validationRules }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + const newValue = 'failing value'; - await wrapper.find('input').setValue('val2'); + // Act + await wrapper.vm.$options.watch.value.call(wrapper.vm, newValue); - // Assert - expect(wrapper.emitted()).toHaveProperty('change'); + // Assert + expect(validate).toHaveBeenCalledWith(newValue, validationRules); + expect(handleChange).not.toHaveBeenCalled(); + }); + test('should emit new value when modelValue is changed', async () => { + // Act + const wrapper = shallowMount(textboxQuestion, { + global: { + directives: { maska: jest.fn() } + }, + propsData: { + modelValue: 'val' + }, + mixins: [mockMixin] + }); + await wrapper.find('input').setValue('val2'); + + // Assert + expect(wrapper.emitted()).toHaveProperty('change'); + }); }); - // TODO Correct test so it actually calls toHaveBeenCalled -> () <- - it.skip('Should call this.handleChange with new value when the value is changed and the new value is valid', async () => { - // Arrange - const wrapper = shallowMount(textboxQuestion, { - global: { - directives: { - maska + describe('mounted hook', () => { + test('should emit an inputId assigned event with the inputId prop as an argument', () => { + // Arrange + const inputId = '126743'; + const wrapper = shallowMount(textboxQuestion, { + propsData: { inputId }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } } - }, - propsData: { - options: {}, - modelValue: 'foo' - }, - mixins: [mockMixin] + }); + + // Assert + expect(wrapper.emitted()['textboxQuestionEvent.inputIdAssigned']).toBeTruthy(); + expect(wrapper.emitted()['textboxQuestionEvent.inputIdAssigned'][0]).toEqual([inputId]); }); + }); - wrapper.vm.handleChange = jest.fn().mockImplementation(() => {}); - wrapper.vm.validate = jest.fn().mockImplementation(() => true); + describe('method', () => { + describe('trimOnPaste', () => { + test('should trim the pasted text and update the model value', () => { + // Arrange + document.getElementById = jest.fn(() => ({ + value: ' hello ', + selectionStart: 7, + selectionEnd: 7 + })); + const stopPropagation = jest.fn(); + const preventDefault = jest.fn(); + const data = { getData: jest.fn(() => ' world ') }; + const inputId = '123758'; + const pasteEvent = { + type: 'paste', + srcElement: { id: inputId }, + clipboardData: data, + stopPropagation, + preventDefault + }; + const wrapper = shallowMount(textboxQuestion, { + propsData: { inputId }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); - // Act - wrapper.vm.$options.watch.value.call(wrapper.vm, 'bar'); + // Act + wrapper.vm.trimOnPaste(pasteEvent); - // Assert - expect(wrapper.vm.handleChange).toHaveBeenCalled; + // Assert + expect(stopPropagation).toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalled(); + expect(data.getData).toHaveBeenCalledWith('Text'); + expect(wrapper.emitted()['update:modelValue']).toBeTruthy(); + expect(wrapper.emitted()['update:modelValue'][0]).toEqual([' hello world ']); + }); + test('should trim the dropped text and update the model value', () => { + // Arrange + document.getElementById = jest.fn(() => ({ + value: ' hello ', + selectionStart: 7, + selectionEnd: 7 + })); + const stopPropagation = jest.fn(); + const preventDefault = jest.fn(); + const data = { getData: jest.fn(() => ' world ') }; + const inputId = '123758'; + const dropEvent = { + type: 'drop', + srcElement: { id: inputId }, + dataTransfer: data, + stopPropagation, + preventDefault + }; + const wrapper = shallowMount(textboxQuestion, { + propsData: { inputId }, + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + wrapper.vm.trimOnPaste(dropEvent); + + // Assert + expect(stopPropagation).toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalled(); + expect(data.getData).toHaveBeenCalledWith('Text'); + expect(wrapper.emitted()['update:modelValue']).toBeTruthy(); + expect(wrapper.emitted()['update:modelValue'][0]).toEqual([' hello world ']); + }); + }); + describe('clickedSearch', () => { + test('should emit a click event if the meta valid flag is true', () => { + // Arrange + const meta = { valid: true }; + useField.mockReturnValueOnce({ meta }); + const wrapper = shallowMount(textboxQuestion, { + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + wrapper.vm.clickedSearch(); + + // Assert + expect(wrapper.emitted()['click-event']).toBeTruthy(); + }); + test('should not emit a click event if the meta valid flag is falsy', () => { + // Arrange + const meta = { }; + useField.mockReturnValueOnce({ meta }); + const wrapper = shallowMount(textboxQuestion, { + mixins: [mockMixin], + global: { + directives: { maska: jest.fn() } + } + }); + + // Act + wrapper.vm.clickedSearch(); + + // Assert + expect(wrapper.emitted()['click-event']).not.toBeTruthy(); + }); + }); }); }); diff --git a/src/digital-components/textbox-question/textbox-question.vue b/src/digital-components/textbox-question/textbox-question.vue index b619460e..86bcfeea 100644 --- a/src/digital-components/textbox-question/textbox-question.vue +++ b/src/digital-components/textbox-question/textbox-question.vue @@ -30,7 +30,7 @@ :aria-label="questionText" :min="min" :max="max" - required + :required="isRequired" :class="[ hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '', @@ -48,7 +48,8 @@