From ae30db7b18f1963c2afebb19968039deaabbf82d Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Sat, 16 Apr 2022 09:08:34 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../components/source_editor_toolbar.vue | 70 +++++++++ .../source_editor_toolbar_button.vue | 89 +++++++++++ app/assets/javascripts/editor/constants.js | 3 + .../editor/graphql/get_item.query.graphql | 9 ++ .../editor/graphql/get_items.query.graphql | 5 + .../graphql/update_item.mutation.graphql | 3 + spec/frontend/editor/components/helpers.js | 12 ++ .../source_editor_toolbar_button_spec.js | 146 ++++++++++++++++++ .../components/source_editor_toolbar_spec.js | 116 ++++++++++++++ 9 files changed, 453 insertions(+) create mode 100644 app/assets/javascripts/editor/components/source_editor_toolbar.vue create mode 100644 app/assets/javascripts/editor/components/source_editor_toolbar_button.vue create mode 100644 app/assets/javascripts/editor/graphql/get_item.query.graphql create mode 100644 app/assets/javascripts/editor/graphql/get_items.query.graphql create mode 100644 app/assets/javascripts/editor/graphql/update_item.mutation.graphql create mode 100644 spec/frontend/editor/components/helpers.js create mode 100644 spec/frontend/editor/components/source_editor_toolbar_button_spec.js create mode 100644 spec/frontend/editor/components/source_editor_toolbar_spec.js diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar.vue b/app/assets/javascripts/editor/components/source_editor_toolbar.vue new file mode 100644 index 00000000000..1427f2df461 --- /dev/null +++ b/app/assets/javascripts/editor/components/source_editor_toolbar.vue @@ -0,0 +1,70 @@ + + diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue new file mode 100644 index 00000000000..2595d67af34 --- /dev/null +++ b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue @@ -0,0 +1,89 @@ + + diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js index 2ae9c377683..361122d8890 100644 --- a/app/assets/javascripts/editor/constants.js +++ b/app/assets/javascripts/editor/constants.js @@ -12,6 +12,9 @@ export const EDITOR_TYPE_DIFF = 'vs.editor.IDiffEditor'; export const EDITOR_CODE_INSTANCE_FN = 'createInstance'; export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance'; +export const EDITOR_TOOLBAR_LEFT_GROUP = 'left'; +export const EDITOR_TOOLBAR_RIGHT_GROUP = 'right'; + export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = s__( 'SourceEditor|"el" parameter is required for createInstance()', ); diff --git a/app/assets/javascripts/editor/graphql/get_item.query.graphql b/app/assets/javascripts/editor/graphql/get_item.query.graphql new file mode 100644 index 00000000000..7c8bc09f7b0 --- /dev/null +++ b/app/assets/javascripts/editor/graphql/get_item.query.graphql @@ -0,0 +1,9 @@ +query ToolbarItem($id: String!) { + item(id: $id) @client { + id + label + icon + selected + group + } +} diff --git a/app/assets/javascripts/editor/graphql/get_items.query.graphql b/app/assets/javascripts/editor/graphql/get_items.query.graphql new file mode 100644 index 00000000000..bfac816d276 --- /dev/null +++ b/app/assets/javascripts/editor/graphql/get_items.query.graphql @@ -0,0 +1,5 @@ +query ToolbarItems { + items @client { + nodes + } +} diff --git a/app/assets/javascripts/editor/graphql/update_item.mutation.graphql b/app/assets/javascripts/editor/graphql/update_item.mutation.graphql new file mode 100644 index 00000000000..f8424c65181 --- /dev/null +++ b/app/assets/javascripts/editor/graphql/update_item.mutation.graphql @@ -0,0 +1,3 @@ +mutation updateItem($id: String!, $propsToUpdate: Item!) { + updateToolbarItem(id: $id, propsToUpdate: $propsToUpdate) @client +} diff --git a/spec/frontend/editor/components/helpers.js b/spec/frontend/editor/components/helpers.js new file mode 100644 index 00000000000..3e6cd2a236d --- /dev/null +++ b/spec/frontend/editor/components/helpers.js @@ -0,0 +1,12 @@ +import { EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants'; + +export const buildButton = (id = 'foo-bar-btn', options = {}) => { + return { + __typename: 'Item', + id, + label: options.label || 'Foo Bar Button', + icon: options.icon || 'foo-bar', + selected: options.selected || false, + group: options.group || EDITOR_TOOLBAR_RIGHT_GROUP, + }; +}; diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js new file mode 100644 index 00000000000..5135091af4a --- /dev/null +++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js @@ -0,0 +1,146 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue'; +import getToolbarItemQuery from '~/editor/graphql/get_item.query.graphql'; +import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql'; +import { buildButton } from './helpers'; + +Vue.use(VueApollo); + +describe('Source Editor Toolbar button', () => { + let wrapper; + let mockApollo; + const defaultBtn = buildButton(); + + const findButton = () => wrapper.findComponent(GlButton); + + const createComponentWithApollo = ({ propsData } = {}) => { + mockApollo = createMockApollo(); + mockApollo.clients.defaultClient.cache.writeQuery({ + query: getToolbarItemQuery, + variables: { id: defaultBtn.id }, + data: { + item: { + ...defaultBtn, + }, + }, + }); + + wrapper = shallowMount(SourceEditorToolbarButton, { + propsData, + apolloProvider: mockApollo, + }); + }; + + afterEach(() => { + wrapper.destroy(); + mockApollo = null; + }); + + describe('default', () => { + const defaultProps = { + category: 'primary', + variant: 'default', + }; + const customProps = { + category: 'secondary', + variant: 'info', + }; + it('renders a default button without props', async () => { + createComponentWithApollo(); + const btn = findButton(); + expect(btn.exists()).toBe(true); + expect(btn.props()).toMatchObject(defaultProps); + }); + + it('renders a button based on the props passed', async () => { + createComponentWithApollo({ + propsData: { + button: customProps, + }, + }); + const btn = findButton(); + expect(btn.props()).toMatchObject(customProps); + }); + }); + + describe('button updates', () => { + it('it properly updates button on Apollo cache update', async () => { + const { id } = defaultBtn; + + createComponentWithApollo({ + propsData: { + button: { + id, + }, + }, + }); + + expect(findButton().props('selected')).toBe(false); + + mockApollo.clients.defaultClient.cache.writeQuery({ + query: getToolbarItemQuery, + variables: { id }, + data: { + item: { + ...defaultBtn, + selected: true, + }, + }, + }); + + jest.runOnlyPendingTimers(); + await nextTick(); + + expect(findButton().props('selected')).toBe(true); + }); + }); + + describe('click handler', () => { + it('fires the click handler on the button when available', () => { + const spy = jest.fn(); + createComponentWithApollo({ + propsData: { + button: { + onClick: spy, + }, + }, + }); + expect(spy).not.toHaveBeenCalled(); + findButton().vm.$emit('click'); + expect(spy).toHaveBeenCalled(); + }); + it('emits the "click" event', () => { + createComponentWithApollo(); + jest.spyOn(wrapper.vm, '$emit'); + expect(wrapper.vm.$emit).not.toHaveBeenCalled(); + findButton().vm.$emit('click'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('click'); + }); + it('triggers the mutation exposing the changed "selected" prop', () => { + const { id } = defaultBtn; + createComponentWithApollo({ + propsData: { + button: { + id, + }, + }, + }); + jest.spyOn(wrapper.vm.$apollo, 'mutate'); + expect(wrapper.vm.$apollo.mutate).not.toHaveBeenCalled(); + findButton().vm.$emit('click'); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateToolbarItemMutation, + variables: { + id, + propsToUpdate: { + selected: true, + }, + }, + }); + }); + }); +}); diff --git a/spec/frontend/editor/components/source_editor_toolbar_spec.js b/spec/frontend/editor/components/source_editor_toolbar_spec.js new file mode 100644 index 00000000000..6e99eadbd97 --- /dev/null +++ b/spec/frontend/editor/components/source_editor_toolbar_spec.js @@ -0,0 +1,116 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlButtonGroup } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue'; +import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue'; +import { EDITOR_TOOLBAR_LEFT_GROUP, EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants'; +import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql'; +import { buildButton } from './helpers'; + +Vue.use(VueApollo); + +describe('Source Editor Toolbar', () => { + let wrapper; + let mockApollo; + + const findButtons = () => wrapper.findAllComponents(SourceEditorToolbarButton); + + const createApolloMockWithCache = (items = []) => { + mockApollo = createMockApollo(); + mockApollo.clients.defaultClient.cache.writeQuery({ + query: getToolbarItemsQuery, + data: { + items: { + nodes: items, + }, + }, + }); + }; + + const createComponentWithApollo = (items = []) => { + createApolloMockWithCache(items); + wrapper = shallowMount(SourceEditorToolbar, { + apolloProvider: mockApollo, + stubs: { + GlButtonGroup, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + mockApollo = null; + }); + + describe('groups', () => { + it.each` + group | expectedGroup + ${EDITOR_TOOLBAR_LEFT_GROUP} | ${EDITOR_TOOLBAR_LEFT_GROUP} + ${EDITOR_TOOLBAR_RIGHT_GROUP} | ${EDITOR_TOOLBAR_RIGHT_GROUP} + ${undefined} | ${EDITOR_TOOLBAR_RIGHT_GROUP} + ${'non-existing'} | ${EDITOR_TOOLBAR_RIGHT_GROUP} + `('puts item with group="$group" into $expectedGroup group', ({ group, expectedGroup }) => { + const item = buildButton('first', { + group, + }); + createComponentWithApollo([item]); + expect(findButtons()).toHaveLength(1); + [EDITOR_TOOLBAR_RIGHT_GROUP, EDITOR_TOOLBAR_LEFT_GROUP].forEach((g) => { + if (g === expectedGroup) { + expect(wrapper.vm.getGroupItems(g)).toEqual([expect.objectContaining({ id: 'first' })]); + } else { + expect(wrapper.vm.getGroupItems(g)).toHaveLength(0); + } + }); + }); + }); + + describe('buttons update', () => { + it('it properly updates buttons on Apollo cache update', async () => { + const item = buildButton('first', { + group: EDITOR_TOOLBAR_RIGHT_GROUP, + }); + createComponentWithApollo(); + + expect(findButtons()).toHaveLength(0); + + mockApollo.clients.defaultClient.cache.writeQuery({ + query: getToolbarItemsQuery, + data: { + items: { + nodes: [item], + }, + }, + }); + + jest.runOnlyPendingTimers(); + await nextTick(); + + expect(findButtons()).toHaveLength(1); + }); + }); + + describe('click handler', () => { + it('emits the "click" event when a button is clicked', () => { + const item1 = buildButton('first', { + group: EDITOR_TOOLBAR_LEFT_GROUP, + }); + const item2 = buildButton('second', { + group: EDITOR_TOOLBAR_RIGHT_GROUP, + }); + createComponentWithApollo([item1, item2]); + jest.spyOn(wrapper.vm, '$emit'); + expect(wrapper.vm.$emit).not.toHaveBeenCalled(); + + findButtons().at(0).vm.$emit('click'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item1); + + findButtons().at(1).vm.$emit('click'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item2); + + expect(wrapper.vm.$emit.mock.calls).toHaveLength(2); + }); + }); +});