Commit d40a5a41 authored by Janis Altherr's avatar Janis Altherr Committed by Jose Ivan Vargas

Add the input wrapper component for Pipeline Wizard

This introduces an Input Wrapper component that acts as
a single interface for the Pipeline Wizard's step component.
It is responsible for mounting the appropriate widget that
will then do the input handling
Signed-off-by: default avatarJanis Altherr <jaltherr@gitlab.com>
parent c1c597bc
<script>
import { isNode, isDocument, isSeq, visit } from 'yaml';
import { capitalize } from 'lodash';
import TextWidget from '~/pipeline_wizard/components/widgets/text.vue';
import ListWidget from '~/pipeline_wizard/components/widgets/list.vue';
const widgets = {
TextWidget,
ListWidget,
};
function isNullOrUndefined(v) {
return [undefined, null].includes(v);
}
export default {
components: {
...widgets,
},
props: {
template: {
type: Object,
required: true,
validator: (v) => isNode(v),
},
compiled: {
type: Object,
required: true,
validator: (v) => isDocument(v) || isNode(v),
},
target: {
type: String,
required: true,
validator: (v) => /^\$.*/g.test(v),
},
widget: {
type: String,
required: true,
validator: (v) => {
return Object.keys(widgets).includes(`${capitalize(v)}Widget`);
},
},
validate: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
path() {
let res;
visit(this.template, (seqKey, node, path) => {
if (node && node.value === this.target) {
// `path` is an array of objects (all the node's parents)
// So this reducer will reduce it to an array of the path's keys,
// e.g. `[ 'foo', 'bar', '0' ]`
res = path.reduce((p, { key }) => (key ? [...p, `${key}`] : p), []);
const parent = path[path.length - 1];
if (isSeq(parent)) {
res.push(seqKey);
}
}
});
return res;
},
},
methods: {
compile(v) {
if (!this.path) return;
if (isNullOrUndefined(v)) {
this.compiled.deleteIn(this.path);
}
this.compiled.setIn(this.path, v);
},
onModelChange(v) {
this.$emit('beforeUpdate:compiled');
this.compile(v);
this.$emit('update:compiled', this.compiled);
this.$emit('highlight', this.path);
},
onValidationStateChange(v) {
this.$emit('update:valid', v);
},
},
};
</script>
<template>
<div>
<component
:is="`${widget}-widget`"
ref="widget"
:validate="validate"
v-bind="$attrs"
@input="onModelChange"
@update:valid="onValidationStateChange"
/>
</div>
</template>
import { mount, shallowMount } from '@vue/test-utils';
import { Document } from 'yaml';
import InputWrapper from '~/pipeline_wizard/components/input.vue';
import TextWidget from '~/pipeline_wizard/components/widgets/text.vue';
describe('Pipeline Wizard -- Input Wrapper', () => {
let wrapper;
const createComponent = (props = {}, mountFunc = mount) => {
wrapper = mountFunc(InputWrapper, {
propsData: {
template: new Document({
template: {
bar: 'baz',
foo: { some: '$TARGET' },
},
}).get('template'),
compiled: new Document({ bar: 'baz', foo: { some: '$TARGET' } }),
target: '$TARGET',
widget: 'text',
label: 'some label (required by the text widget)',
...props,
},
});
};
describe('API', () => {
const inputValue = 'dslkfjsdlkfjlskdjfn';
let inputChild;
beforeEach(() => {
createComponent({});
inputChild = wrapper.find(TextWidget);
});
afterEach(() => {
wrapper.destroy();
});
it('will replace its value in compiled', async () => {
await inputChild.vm.$emit('input', inputValue);
const expected = new Document({
bar: 'baz',
foo: { some: inputValue },
});
expect(wrapper.emitted()['update:compiled']).toEqual([[expected]]);
});
it('will emit a highlight event with the correct path if child emits an input event', async () => {
await inputChild.vm.$emit('input', inputValue);
const expected = ['foo', 'some'];
expect(wrapper.emitted().highlight).toEqual([[expected]]);
});
});
describe('Target Path Discovery', () => {
afterEach(() => {
wrapper.destroy();
});
it.each`
scenario | template | target | expected
${'simple nested object'} | ${{ foo: { bar: { baz: '$BOO' } } }} | ${'$BOO'} | ${['foo', 'bar', 'baz']}
${'list, first pos.'} | ${{ foo: ['$BOO'] }} | ${'$BOO'} | ${['foo', 0]}
${'list, second pos.'} | ${{ foo: ['bar', '$BOO'] }} | ${'$BOO'} | ${['foo', 1]}
${'lowercase target'} | ${{ foo: { bar: '$jupp' } }} | ${'$jupp'} | ${['foo', 'bar']}
${'root list'} | ${['$BOO']} | ${'$BOO'} | ${[0]}
`('$scenario', ({ template, target, expected }) => {
createComponent(
{
template: new Document({ template }).get('template'),
target,
},
shallowMount,
);
expect(wrapper.vm.path).toEqual(expected);
});
});
});
import fs from 'fs';
import { mount } from '@vue/test-utils';
import { Document } from 'yaml';
import InputWrapper from '~/pipeline_wizard/components/input.vue';
describe('Test all widgets in ./widgets/* whether they provide a minimal api', () => {
const createComponent = (props = {}, mountFunc = mount) => {
mountFunc(InputWrapper, {
propsData: {
template: new Document({
template: {
bar: 'baz',
foo: { some: '$TARGET' },
},
}).get('template'),
compiled: new Document({ bar: 'baz', foo: { some: '$TARGET' } }),
target: '$TARGET',
widget: 'text',
label: 'some label (required by the text widget)',
...props,
},
});
};
const widgets = fs
.readdirSync('./app/assets/javascripts/pipeline_wizard/components/widgets')
.map((filename) => [filename.match(/^(.*).vue$/)[1]]);
let consoleErrorSpy;
beforeAll(() => {
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterAll(() => {
consoleErrorSpy.mockRestore();
});
describe.each(widgets)('`%s` Widget', (name) => {
it('passes the input validator', () => {
const validatorFunc = InputWrapper.props.widget.validator;
expect(validatorFunc(name)).toBe(true);
});
it('mounts without error', () => {
createComponent({ widget: name });
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment