import { parseDocument, Document } from 'yaml'; import { omit } from 'lodash'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import PipelineWizardStep from '~/pipeline_wizard/components/step.vue'; import InputWrapper from '~/pipeline_wizard/components/input_wrapper.vue'; import StepNav from '~/pipeline_wizard/components/step_nav.vue'; import { stepInputs, stepTemplate, compiledYamlBeforeSetup, compiledYamlAfterInitialLoad, compiledYaml, } from '../mock/yaml'; describe('Pipeline Wizard - Step Page', () => { const inputs = parseDocument(stepInputs).toJS(); let wrapper; let input1; let input2; const getInputWrappers = () => wrapper.findAllComponents(InputWrapper); const forEachInputWrapper = (cb) => { getInputWrappers().wrappers.forEach(cb); }; const getStepNav = () => { return wrapper.findComponent(StepNav); }; const mockNextClick = () => { getStepNav().vm.$emit('next'); }; const mockPrevClick = () => { getStepNav().vm.$emit('back'); }; const expectFalsyAttributeValue = (testedWrapper, attributeName) => { expect([false, null, undefined]).toContain(testedWrapper.attributes(attributeName)); }; const findInputWrappers = () => { const inputWrappers = wrapper.findAllComponents(InputWrapper); input1 = inputWrappers.at(0); input2 = inputWrappers.at(1); }; const createComponent = (props = {}) => { const template = parseDocument(stepTemplate).get('template'); const defaultProps = { inputs, template, }; wrapper = shallowMountExtended(PipelineWizardStep, { propsData: { ...defaultProps, compiled: parseDocument(compiledYamlBeforeSetup), ...props, }, }); }; afterEach(async () => { await wrapper.destroy(); }); describe('input children', () => { beforeEach(() => { createComponent(); }); it('mounts an inputWrapper for each input type', () => { forEachInputWrapper((inputWrapper, i) => expect(inputWrapper.attributes('widget')).toBe(inputs[i].widget), ); }); it('passes all unused props to the inputWrapper', () => { const pickChildProperties = (from) => { return omit(from, ['target', 'widget']); }; forEachInputWrapper((inputWrapper, i) => { const expectedProps = pickChildProperties(inputs[i]); Object.entries(expectedProps).forEach(([key, value]) => { expect(inputWrapper.attributes(key.toLowerCase())).toEqual(value.toString()); }); }); }); }); const yamlDocument = new Document({ foo: { bar: 'baz' } }); const yamlNode = yamlDocument.get('foo'); describe('prop validation', () => { describe.each` componentProp | required | valid | invalid ${'inputs'} | ${true} | ${[inputs, []]} | ${[['invalid'], [null], [{}, {}]]} ${'template'} | ${true} | ${[yamlNode]} | ${['invalid', null, { foo: 1 }, yamlDocument]} ${'compiled'} | ${true} | ${[yamlDocument]} | ${['invalid', null, { foo: 1 }, yamlNode]} `('testing `$componentProp` prop', ({ componentProp, required, valid, invalid }) => { it('expects prop to be required', () => { expect(PipelineWizardStep.props[componentProp].required).toEqual(required); }); it('prop validators return false for invalid types', () => { const validatorFunc = PipelineWizardStep.props[componentProp].validator; invalid.forEach((invalidType) => { expect(validatorFunc(invalidType)).toBe(false); }); }); it('prop validators return true for valid types', () => { const validatorFunc = PipelineWizardStep.props[componentProp].validator; valid.forEach((validType) => { expect(validatorFunc(validType)).toBe(true); }); }); }); }); describe('navigation', () => { it('shows the next button', () => { createComponent(); expect(getStepNav().attributes('nextbuttonenabled')).toEqual('true'); }); it('does not show a back button if hasPreviousStep is false', () => { createComponent({ hasPreviousStep: false }); expectFalsyAttributeValue(getStepNav(), 'showbackbutton'); }); it('shows a back button if hasPreviousStep is true', () => { createComponent({ hasPreviousStep: true }); expect(getStepNav().attributes('showbackbutton')).toBe('true'); }); it('lets "back" event bubble upwards', async () => { createComponent(); await mockPrevClick(); await nextTick(); expect(wrapper.emitted().back).toBeTruthy(); }); it('lets "next" event bubble upwards', async () => { createComponent(); await mockNextClick(); await nextTick(); expect(wrapper.emitted().next).toBeTruthy(); }); }); describe('validation', () => { beforeEach(() => { createComponent({ hasNextPage: true }); findInputWrappers(); }); it('sets invalid once one input field has an invalid value', async () => { input1.vm.$emit('update:valid', true); input2.vm.$emit('update:valid', false); await mockNextClick(); expectFalsyAttributeValue(getStepNav(), 'nextbuttonenabled'); }); it('returns to valid state once the invalid input is valid again', async () => { input1.vm.$emit('update:valid', true); input2.vm.$emit('update:valid', false); await mockNextClick(); expectFalsyAttributeValue(getStepNav(), 'nextbuttonenabled'); input2.vm.$emit('update:valid', true); await nextTick(); expect(getStepNav().attributes('nextbuttonenabled')).toBe('true'); }); it('passes validate state to all input wrapper children when next is clicked', async () => { forEachInputWrapper((inputWrapper) => { expectFalsyAttributeValue(inputWrapper, 'validate'); }); await mockNextClick(); expect(input1.attributes('validate')).toBe('true'); }); it('not emitting a valid state is considered valid', async () => { // input1 does not emit a update:valid event input2.vm.$emit('update:valid', true); await mockNextClick(); expect(getStepNav().attributes('nextbuttonenabled')).toBe('true'); }); }); describe('template compilation', () => { beforeEach(() => { createComponent(); findInputWrappers(); }); it('injects the template when an input wrapper emits a beforeUpdate:compiled event', async () => { input1.vm.$emit('beforeUpdate:compiled'); expect(wrapper.vm.compiled.toString()).toBe(compiledYamlAfterInitialLoad); }); it('lets the "update:compiled" event bubble upwards', async () => { const compiled = parseDocument(compiledYaml); await input1.vm.$emit('update:compiled', compiled); const updateEvents = wrapper.emitted()['update:compiled']; const latestUpdateEvent = updateEvents[updateEvents.length - 1]; expect(latestUpdateEvent[0].toString()).toBe(compiled.toString()); }); }); });