Commit 8737e446 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'add-input-wrapper-for-pipeline-wizard' into 'master'

Add the input wrapper widget to be used by the Pipeline Wizard

See merge request gitlab-org/gitlab!80405
parents 92368afa d40a5a41
<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