From 85dc423f7090da0a52c73eb66faf22ddb20efff9 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Sat, 19 Sep 2020 01:45:44 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-4-stable-ee --- .../__snapshots__/clone_dropdown_spec.js.snap | 12 +- .../__snapshots__/expand_button_spec.js.snap | 8 +- .../vue_shared/components/actions_button_spec.js | 203 ++++++ .../components/alert_detail_table_spec.js | 74 +++ .../components/blob_viewers/rich_viewer_spec.js | 2 +- .../components/changed_file_icon_spec.js | 4 +- .../vue_shared/components/clone_dropdown_spec.js | 10 +- spec/frontend/vue_shared/components/commit_spec.js | 6 +- .../vue_shared/components/confirm_modal_spec.js | 22 +- .../date_time_picker/date_time_picker_spec.js | 9 +- .../components/diff_viewer/viewers/renamed_spec.js | 16 +- .../dropdown/dropdown_search_input_spec.js | 6 - .../components/file_finder/index_spec.js | 41 +- .../vue_shared/components/file_row_spec.js | 14 +- .../filtered_search_bar_root_spec.js | 138 ++-- .../filtered_search_utils_spec.js | 203 +++++- .../components/filtered_search_bar/mock_data.js | 73 +- .../tokens/author_token_spec.js | 97 ++- .../tokens/branch_token_spec.js | 207 ++++++ .../filtered_search_bar/tokens/label_token_spec.js | 106 ++- .../tokens/milestone_token_spec.js | 102 ++- .../components/filtered_search_dropdown_spec.js | 190 ------ spec/frontend/vue_shared/components/icon_spec.js | 78 --- .../components/issue/issue_milestone_spec.js | 4 +- .../components/issue/related_issuable_item_spec.js | 38 +- .../vue_shared/components/markdown/header_spec.js | 58 +- .../markdown/suggestion_diff_header_spec.js | 20 +- .../markdown/suggestion_diff_row_spec.js | 20 +- .../components/markdown/toolbar_button_spec.js | 47 ++ .../components/notes/noteable_warning_spec.js | 8 +- .../components/notes/timeline_entry_item_spec.js | 4 +- .../vue_shared/components/ordered_layout_spec.js | 4 +- .../vue_shared/components/paginated_list_spec.js | 2 +- .../project_selector/project_list_item_spec.js | 6 +- .../__snapshots__/code_instruction_spec.js.snap | 60 ++ .../__snapshots__/history_item_spec.js.snap | 42 ++ .../components/registry/code_instruction_spec.js | 117 ++++ .../components/registry/details_row_spec.js | 71 ++ .../components/registry/history_item_spec.js | 67 ++ .../components/registry/list_item_spec.js | 135 ++++ .../components/registry/metadata_item_spec.js | 101 +++ .../components/registry/title_area_spec.js | 98 +++ .../components/remove_member_modal_spec.js | 2 +- .../__snapshots__/skeleton_loader_spec.js.snap | 739 ++++++++++++--------- .../resizable_chart/skeleton_loader_spec.js | 4 +- .../services/build_custom_renderer_spec.js | 9 +- .../build_html_to_markdown_renderer_spec.js | 87 ++- .../services/renderers/mock_data.js | 40 +- .../renderers/render_attribute_definition_spec.js | 25 + .../services/renderers/render_heading_spec.js | 12 + .../renderers/render_identifier_paragraph_spec.js | 48 +- .../renderers/render_kramdown_list_spec.js | 38 -- .../renderers/render_kramdown_text_spec.js | 24 - .../services/renderers/render_list_item_spec.js | 12 + .../services/renderers/render_utils_spec.js | 70 +- .../labels_select/dropdown_create_label_spec.js | 18 +- .../sidebar/labels_select/dropdown_header_spec.js | 2 +- .../labels_select/dropdown_value_collapsed_spec.js | 2 +- .../components/sidebar/labels_select/mock_data.js | 46 +- .../labels_select_vue/dropdown_button_spec.js | 2 +- .../labels_select_vue/labels_select_root_spec.js | 30 +- .../labels_select_vue/store/actions_spec.js | 15 + .../labels_select_vue/store/mutations_spec.js | 13 + .../vue_shared/components/table_pagination_spec.js | 6 +- .../vue_shared/components/todo_button_spec.js | 48 ++ .../user_avatar/user_avatar_link_spec.js | 4 - .../components/user_popover/user_popover_spec.js | 15 +- .../vue_shared/components/web_ide_link_spec.js | 106 +++ 68 files changed, 2913 insertions(+), 1027 deletions(-) create mode 100644 spec/frontend/vue_shared/components/actions_button_spec.js create mode 100644 spec/frontend/vue_shared/components/alert_detail_table_spec.js create mode 100644 spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js delete mode 100644 spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js delete mode 100644 spec/frontend/vue_shared/components/icon_spec.js create mode 100644 spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js create mode 100644 spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/registry/code_instruction_spec.js create mode 100644 spec/frontend/vue_shared/components/registry/details_row_spec.js create mode 100644 spec/frontend/vue_shared/components/registry/history_item_spec.js create mode 100644 spec/frontend/vue_shared/components/registry/list_item_spec.js create mode 100644 spec/frontend/vue_shared/components/registry/metadata_item_spec.js create mode 100644 spec/frontend/vue_shared/components/registry/title_area_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js delete mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js delete mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js create mode 100644 spec/frontend/vue_shared/components/todo_button_spec.js create mode 100644 spec/frontend/vue_shared/components/web_ide_link_spec.js (limited to 'spec/frontend/vue_shared') diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap index e84eb7789d3..dfd114a2d1c 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` - - + Clone with SSH - +
- + Clone with HTTP - +
-
+ `; diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap index cd4728baeaa..c2b97f1e7f9 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap @@ -4,7 +4,7 @@ exports[`Expand button on click when short text is provided renders button after + + + +`; diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap new file mode 100644 index 00000000000..2abae33bc19 --- /dev/null +++ b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`History Item renders the correct markup 1`] = ` +
  • +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
  • +`; diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js new file mode 100644 index 00000000000..84c738764a3 --- /dev/null +++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js @@ -0,0 +1,117 @@ +import { mount } from '@vue/test-utils'; +import Tracking from '~/tracking'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +describe('Package code instruction', () => { + let wrapper; + + const defaultProps = { + instruction: 'npm i @my-package', + copyText: 'Copy npm install command', + }; + + function createComponent(props = {}) { + wrapper = mount(CodeInstruction, { + propsData: { + ...defaultProps, + ...props, + }, + }); + } + + const findCopyButton = () => wrapper.find(ClipboardButton); + const findInputElement = () => wrapper.find('[data-testid="instruction-input"]'); + const findMultilineInstruction = () => wrapper.find('[data-testid="multiline-instruction"]'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('single line', () => { + beforeEach(() => + createComponent({ + label: 'foo_label', + }), + ); + + it('to match the default snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('multiline', () => { + beforeEach(() => + createComponent({ + instruction: 'this is some\nmultiline text', + copyText: 'Copy the command', + label: 'foo_label', + multiline: true, + }), + ); + + it('to match the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('tracking', () => { + let eventSpy; + const trackingAction = 'test_action'; + const trackingLabel = 'foo_label'; + + beforeEach(() => { + eventSpy = jest.spyOn(Tracking, 'event'); + }); + + it('should not track when no trackingAction is provided', () => { + createComponent(); + findCopyButton().trigger('click'); + + expect(eventSpy).toHaveBeenCalledTimes(0); + }); + + describe('when trackingAction is provided for single line', () => { + beforeEach(() => + createComponent({ + trackingAction, + trackingLabel, + }), + ); + + it('should track when copying from the input', () => { + findInputElement().trigger('copy'); + + expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, { + label: trackingLabel, + }); + }); + + it('should track when the copy button is pressed', () => { + findCopyButton().trigger('click'); + + expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, { + label: trackingLabel, + }); + }); + }); + + describe('when trackingAction is provided for multiline', () => { + beforeEach(() => + createComponent({ + trackingAction, + trackingLabel, + multiline: true, + }), + ); + + it('should track when copying from the multiline pre element', () => { + findMultilineInstruction().trigger('copy'); + + expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, { + label: trackingLabel, + }); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/registry/details_row_spec.js b/spec/frontend/vue_shared/components/registry/details_row_spec.js new file mode 100644 index 00000000000..16a55b84787 --- /dev/null +++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js @@ -0,0 +1,71 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import component from '~/vue_shared/components/registry/details_row.vue'; + +describe('DetailsRow', () => { + let wrapper; + + const findIcon = () => wrapper.find(GlIcon); + const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]'); + + const mountComponent = props => { + wrapper = shallowMount(component, { + propsData: { + icon: 'clock', + ...props, + }, + slots: { + default: '
    ', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('has a default slot', () => { + mountComponent(); + expect(findDefaultSlot().exists()).toBe(true); + }); + + describe('icon prop', () => { + it('contains an icon', () => { + mountComponent(); + expect(findIcon().exists()).toBe(true); + }); + + it('icon has the correct props', () => { + mountComponent(); + expect(findIcon().props()).toMatchObject({ + name: 'clock', + }); + }); + }); + + describe('padding prop', () => { + it('padding has a default', () => { + mountComponent(); + expect(wrapper.classes('gl-py-2')).toBe(true); + }); + + it('is reflected in the template', () => { + mountComponent({ padding: 'gl-py-4' }); + expect(wrapper.classes('gl-py-4')).toBe(true); + }); + }); + + describe('dashed prop', () => { + const borderClasses = ['gl-border-b-solid', 'gl-border-gray-100', 'gl-border-b-1']; + it('by default component has no border', () => { + mountComponent(); + expect(wrapper.classes).not.toEqual(expect.arrayContaining(borderClasses)); + }); + + it('has a border when dashed is true', () => { + mountComponent({ dashed: true }); + expect(wrapper.classes()).toEqual(expect.arrayContaining(borderClasses)); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/registry/history_item_spec.js b/spec/frontend/vue_shared/components/registry/history_item_spec.js new file mode 100644 index 00000000000..d51ddda2e3e --- /dev/null +++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js @@ -0,0 +1,67 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import component from '~/vue_shared/components/registry/history_item.vue'; + +describe('History Item', () => { + let wrapper; + const defaultProps = { + icon: 'pencil', + }; + + const mountComponent = () => { + wrapper = shallowMount(component, { + propsData: { ...defaultProps }, + stubs: { + TimelineEntryItem, + }, + slots: { + default: '
    ', + body: '
    ', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findTimelineEntry = () => wrapper.find(TimelineEntryItem); + const findGlIcon = () => wrapper.find(GlIcon); + const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]'); + const findBodySlot = () => wrapper.find('[data-testid="body-slot"]'); + + it('renders the correct markup', () => { + mountComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('has a default slot', () => { + mountComponent(); + + expect(findDefaultSlot().exists()).toBe(true); + }); + + it('has a body slot', () => { + mountComponent(); + + expect(findBodySlot().exists()).toBe(true); + }); + + it('has a timeline entry', () => { + mountComponent(); + + expect(findTimelineEntry().exists()).toBe(true); + }); + + it('has an icon', () => { + mountComponent(); + + const icon = findGlIcon(); + + expect(icon.exists()).toBe(true); + expect(icon.attributes('name')).toBe(defaultProps.icon); + }); +}); diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js new file mode 100644 index 00000000000..e2cfdedb4bf --- /dev/null +++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js @@ -0,0 +1,135 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import component from '~/vue_shared/components/registry/list_item.vue'; + +describe('list item', () => { + let wrapper; + + const findLeftActionSlot = () => wrapper.find('[data-testid="left-action"]'); + const findLeftPrimarySlot = () => wrapper.find('[data-testid="left-primary"]'); + const findLeftSecondarySlot = () => wrapper.find('[data-testid="left-secondary"]'); + const findRightPrimarySlot = () => wrapper.find('[data-testid="right-primary"]'); + const findRightSecondarySlot = () => wrapper.find('[data-testid="right-secondary"]'); + const findRightActionSlot = () => wrapper.find('[data-testid="right-action"]'); + const findDetailsSlot = name => wrapper.find(`[data-testid="${name}"]`); + const findToggleDetailsButton = () => wrapper.find(GlButton); + + const mountComponent = (propsData, slots) => { + wrapper = shallowMount(component, { + propsData, + slots: { + 'left-action': '
    ', + 'left-primary': '
    ', + 'left-secondary': '
    ', + 'right-primary': '
    ', + 'right-secondary': '
    ', + 'right-action': '
    ', + ...slots, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe.each` + slotName | finderFunction + ${'left-primary'} | ${findLeftPrimarySlot} + ${'left-secondary'} | ${findLeftSecondarySlot} + ${'right-primary'} | ${findRightPrimarySlot} + ${'right-secondary'} | ${findRightSecondarySlot} + ${'left-action'} | ${findLeftActionSlot} + ${'right-action'} | ${findRightActionSlot} + `('$slotName slot', ({ finderFunction, slotName }) => { + it('exist when the slot is filled', () => { + mountComponent(); + + expect(finderFunction().exists()).toBe(true); + }); + + it('does not exist when the slot is empty', () => { + mountComponent({}, { [slotName]: '' }); + + expect(finderFunction().exists()).toBe(false); + }); + }); + + describe.each` + slotNames + ${['details_foo']} + ${['details_foo', 'details_bar']} + ${['details_foo', 'details_bar', 'details_baz']} + `('$slotNames details slots', ({ slotNames }) => { + const slotMocks = slotNames.reduce((acc, current) => { + acc[current] = `
    `; + return acc; + }, {}); + + it('are visible when details is shown', async () => { + mountComponent({}, slotMocks); + + await wrapper.vm.$nextTick(); + findToggleDetailsButton().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + slotNames.forEach(name => { + expect(findDetailsSlot(name).exists()).toBe(true); + }); + }); + it('are not visible when details are not shown', () => { + mountComponent({}, slotMocks); + + slotNames.forEach(name => { + expect(findDetailsSlot(name).exists()).toBe(false); + }); + }); + }); + + describe('details toggle button', () => { + it('is visible when at least one details slot exists', async () => { + mountComponent({}, { details_foo: '' }); + await wrapper.vm.$nextTick(); + expect(findToggleDetailsButton().exists()).toBe(true); + }); + + it('is hidden without details slot', () => { + mountComponent(); + expect(findToggleDetailsButton().exists()).toBe(false); + }); + }); + + describe('disabled prop', () => { + it('when true applies disabled-content class', () => { + mountComponent({ disabled: true }); + + expect(wrapper.classes('disabled-content')).toBe(true); + }); + + it('when false does not apply disabled-content class', () => { + mountComponent({ disabled: false }); + + expect(wrapper.classes('disabled-content')).toBe(false); + }); + }); + + describe('borders and selection', () => { + it.each` + first | selected | shouldHave | shouldNotHave + ${true} | ${true} | ${['gl-bg-blue-50', 'gl-border-blue-200']} | ${['gl-border-t-transparent', 'gl-border-t-gray-100']} + ${false} | ${true} | ${['gl-bg-blue-50', 'gl-border-blue-200']} | ${['gl-border-t-transparent', 'gl-border-t-gray-100']} + ${true} | ${false} | ${['gl-border-b-gray-100']} | ${['gl-bg-blue-50', 'gl-border-blue-200']} + ${false} | ${false} | ${['gl-border-b-gray-100']} | ${['gl-bg-blue-50', 'gl-border-blue-200']} + `( + 'when first is $first and selected is $selected', + ({ first, selected, shouldHave, shouldNotHave }) => { + mountComponent({ first, selected }); + + expect(wrapper.classes()).toEqual(expect.arrayContaining(shouldHave)); + + expect(wrapper.classes()).toEqual(expect.not.arrayContaining(shouldNotHave)); + }, + ); + }); +}); diff --git a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js new file mode 100644 index 00000000000..ff968ff1831 --- /dev/null +++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js @@ -0,0 +1,101 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon, GlLink } from '@gitlab/ui'; +import component from '~/vue_shared/components/registry/metadata_item.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; + +describe('Metadata Item', () => { + let wrapper; + const defaultProps = { + text: 'foo', + }; + + const mountComponent = (propsData = defaultProps) => { + wrapper = shallowMount(component, { + propsData, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findIcon = () => wrapper.find(GlIcon); + const findLink = (w = wrapper) => w.find(GlLink); + const findText = () => wrapper.find('[data-testid="metadata-item-text"]'); + const findTooltipOnTruncate = (w = wrapper) => w.find(TooltipOnTruncate); + + describe.each(['xs', 's', 'm', 'l', 'xl'])('size class', size => { + const className = `mw-${size}`; + + it(`${size} is assigned correctly to text`, () => { + mountComponent({ ...defaultProps, size }); + + expect(findText().classes()).toContain(className); + }); + + it(`${size} is assigned correctly to link`, () => { + mountComponent({ ...defaultProps, link: 'foo', size }); + + expect(findTooltipOnTruncate().classes()).toContain(className); + }); + }); + + describe('text', () => { + it('display a proper text', () => { + mountComponent(); + + expect(findText().text()).toBe(defaultProps.text); + }); + + it('uses tooltip_on_truncate', () => { + mountComponent(); + + const tooltip = findTooltipOnTruncate(findText()); + expect(tooltip.exists()).toBe(true); + expect(tooltip.attributes('title')).toBe(defaultProps.text); + }); + }); + + describe('link', () => { + it('if a link prop is passed shows a link and hides the text', () => { + mountComponent({ ...defaultProps, link: 'bar' }); + + expect(findLink().exists()).toBe(true); + expect(findText().exists()).toBe(false); + + expect(findLink().attributes('href')).toBe('bar'); + }); + + it('uses tooltip_on_truncate', () => { + mountComponent({ ...defaultProps, link: 'bar' }); + + const tooltip = findTooltipOnTruncate(); + expect(tooltip.exists()).toBe(true); + expect(tooltip.attributes('title')).toBe(defaultProps.text); + expect(findLink(tooltip).exists()).toBe(true); + }); + + it('hides the link and shows the test if a link prop is not passed', () => { + mountComponent(); + + expect(findText().exists()).toBe(true); + expect(findLink().exists()).toBe(false); + }); + }); + + describe('icon', () => { + it('if a icon prop is passed shows a icon', () => { + mountComponent({ ...defaultProps, icon: 'pencil' }); + + expect(findIcon().exists()).toBe(true); + expect(findIcon().props('name')).toBe('pencil'); + }); + + it('if a icon prop is not passed hides the icon', () => { + mountComponent(); + + expect(findIcon().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js new file mode 100644 index 00000000000..6740d6097a4 --- /dev/null +++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js @@ -0,0 +1,98 @@ +import { GlAvatar } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import component from '~/vue_shared/components/registry/title_area.vue'; + +describe('title area', () => { + let wrapper; + + const findSubHeaderSlot = () => wrapper.find('[data-testid="sub-header"]'); + const findRightActionsSlot = () => wrapper.find('[data-testid="right-actions"]'); + const findMetadataSlot = name => wrapper.find(`[data-testid="${name}"]`); + const findTitle = () => wrapper.find('[data-testid="title"]'); + const findAvatar = () => wrapper.find(GlAvatar); + + const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => { + wrapper = shallowMount(component, { + propsData, + slots: { + 'sub-header': '
    ', + 'right-actions': '
    ', + ...slots, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('title', () => { + it('if slot is not present defaults to prop', () => { + mountComponent(); + + expect(findTitle().text()).toBe('foo'); + }); + it('if slot is present uses slot', () => { + mountComponent({ + slots: { + title: 'slot_title', + }, + }); + expect(findTitle().text()).toBe('slot_title'); + }); + }); + + describe('avatar', () => { + it('is shown if avatar props exist', () => { + mountComponent({ propsData: { title: 'foo', avatar: 'baz' } }); + + expect(findAvatar().props('src')).toBe('baz'); + }); + + it('is hidden if avatar props does not exist', () => { + mountComponent(); + + expect(findAvatar().exists()).toBe(false); + }); + }); + + describe.each` + slotName | finderFunction + ${'sub-header'} | ${findSubHeaderSlot} + ${'right-actions'} | ${findRightActionsSlot} + `('$slotName slot', ({ finderFunction, slotName }) => { + it('exist when the slot is filled', () => { + mountComponent(); + + expect(finderFunction().exists()).toBe(true); + }); + + it('does not exist when the slot is empty', () => { + mountComponent({ slots: { [slotName]: '' } }); + + expect(finderFunction().exists()).toBe(false); + }); + }); + + describe.each` + slotNames + ${['metadata_foo']} + ${['metadata_foo', 'metadata_bar']} + ${['metadata_foo', 'metadata_bar', 'metadata_baz']} + `('$slotNames metadata slots', ({ slotNames }) => { + const slotMocks = slotNames.reduce((acc, current) => { + acc[current] = `
    `; + return acc; + }, {}); + + it('exist when the slot is present', async () => { + mountComponent({ slots: slotMocks }); + + await wrapper.vm.$nextTick(); + slotNames.forEach(name => { + expect(findMetadataSlot(name).exists()).toBe(true); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/remove_member_modal_spec.js b/spec/frontend/vue_shared/components/remove_member_modal_spec.js index 2d380b25a0a..78fe6d53eee 100644 --- a/spec/frontend/vue_shared/components/remove_member_modal_spec.js +++ b/spec/frontend/vue_shared/components/remove_member_modal_spec.js @@ -48,7 +48,7 @@ describe('RemoveMemberModal', () => { }); it(`${checkboxTestDescription}`, () => { - expect(wrapper.contains(GlFormCheckbox)).toBe(checkboxExpected); + expect(wrapper.find(GlFormCheckbox).exists()).toBe(checkboxExpected); }); it('submits the form when the modal is submitted', () => { diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap index 103b53cb280..3990248d021 100644 --- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap +++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap @@ -1,324 +1,433 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Resizable Skeleton Loader default setup renders the bars, labels, and grid with correct position, size, and rx percentages 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    `; exports[`Resizable Skeleton Loader with custom settings renders the correct position, and size percentages for bars and labels with different settings 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    `; diff --git a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js index 7facd02e596..bfc3aeb0303 100644 --- a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js +++ b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js @@ -1,11 +1,11 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; describe('Resizable Skeleton Loader', () => { let wrapper; const createComponent = (propsData = {}) => { - wrapper = shallowMount(ChartSkeletonLoader, { + wrapper = mount(ChartSkeletonLoader, { propsData, }); }; diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js index cafe53e6bb2..a823d04024d 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js @@ -5,8 +5,13 @@ describe('Build Custom Renderer Service', () => { it('should return an object with the default renderer functions when lacking arguments', () => { expect(buildCustomHTMLRenderer()).toEqual( expect.objectContaining({ - list: expect.any(Function), + htmlBlock: expect.any(Function), + htmlInline: expect.any(Function), + heading: expect.any(Function), + item: expect.any(Function), + paragraph: expect.any(Function), text: expect.any(Function), + softbreak: expect.any(Function), }), ); }); @@ -20,8 +25,6 @@ describe('Build Custom Renderer Service', () => { expect(buildCustomHTMLRenderer(customRenderers)).toEqual( expect.objectContaining({ html: expect.any(Function), - list: expect.any(Function), - text: expect.any(Function), }), ); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js index a90d3528d60..fd745c21bb6 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js @@ -1,9 +1,10 @@ import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; +import { attributeDefinition } from './renderers/mock_data'; -describe('HTMLToMarkdownRenderer', () => { +describe('rich_content_editor/services/html_to_markdown_renderer', () => { let baseRenderer; let htmlToMarkdownRenderer; - const NODE = { nodeValue: 'mock_node' }; + let fakeNode; beforeEach(() => { baseRenderer = { @@ -12,14 +13,20 @@ describe('HTMLToMarkdownRenderer', () => { getSpaceControlled: jest.fn(input => `space controlled ${input}`), convert: jest.fn(), }; + + fakeNode = { nodeValue: 'mock_node', dataset: {} }; + }); + + afterEach(() => { + htmlToMarkdownRenderer = null; }); describe('TEXT_NODE visitor', () => { it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => { htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); - expect(htmlToMarkdownRenderer.TEXT_NODE(NODE)).toBe( - `space controlled trimmed space collapsed ${NODE.nodeValue}`, + expect(htmlToMarkdownRenderer.TEXT_NODE(fakeNode)).toBe( + `space controlled trimmed space collapsed ${fakeNode.nodeValue}`, ); }); }); @@ -43,8 +50,8 @@ describe('HTMLToMarkdownRenderer', () => { baseRenderer.convert.mockReturnValueOnce(list); - expect(htmlToMarkdownRenderer['LI OL, LI UL'](NODE, list)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list); + expect(htmlToMarkdownRenderer['LI OL, LI UL'](fakeNode, list)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, list); }); }); @@ -62,10 +69,21 @@ describe('HTMLToMarkdownRenderer', () => { }); baseRenderer.convert.mockReturnValueOnce(listItem); - expect(htmlToMarkdownRenderer['UL LI'](NODE, listItem)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, listItem); + expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, listItem); }, ); + + it('detects attribute definitions and attaches them to the list item', () => { + const listItem = '- list item'; + const result = `${listItem}\n${attributeDefinition}\n`; + + fakeNode.dataset.attributeDefinition = attributeDefinition; + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); + baseRenderer.convert.mockReturnValueOnce(`${listItem}\n`); + + expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result); + }); }); describe('OL LI visitor', () => { @@ -85,8 +103,8 @@ describe('HTMLToMarkdownRenderer', () => { }); baseRenderer.convert.mockReturnValueOnce(listItem); - expect(htmlToMarkdownRenderer['OL LI'](NODE, subContent)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, subContent); + expect(htmlToMarkdownRenderer['OL LI'](fakeNode, subContent)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, subContent); }, ); }); @@ -105,8 +123,8 @@ describe('HTMLToMarkdownRenderer', () => { baseRenderer.convert.mockReturnValueOnce(input); - expect(htmlToMarkdownRenderer['STRONG, B'](NODE, input)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input); + expect(htmlToMarkdownRenderer['STRONG, B'](fakeNode, input)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input); }, ); }); @@ -125,9 +143,50 @@ describe('HTMLToMarkdownRenderer', () => { baseRenderer.convert.mockReturnValueOnce(input); - expect(htmlToMarkdownRenderer['EM, I'](NODE, input)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input); + expect(htmlToMarkdownRenderer['EM, I'](fakeNode, input)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input); }, ); }); + + describe('H1, H2, H3, H4, H5, H6 visitor', () => { + it('detects attribute definitions and attaches them to the heading', () => { + const heading = 'heading text'; + const result = `${heading.trimRight()}\n${attributeDefinition}\n\n`; + + fakeNode.dataset.attributeDefinition = attributeDefinition; + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); + baseRenderer.convert.mockReturnValueOnce(`${heading}\n\n`); + + expect(htmlToMarkdownRenderer['H1, H2, H3, H4, H5, H6'](fakeNode, heading)).toBe(result); + }); + }); + + describe('PRE CODE', () => { + let node; + const subContent = 'sub content'; + const originalConverterResult = 'base result'; + + beforeEach(() => { + node = document.createElement('PRE'); + + node.innerText = 'reference definition content'; + node.dataset.sseReferenceDefinition = true; + + baseRenderer.convert.mockReturnValueOnce(originalConverterResult); + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); + }); + + it('returns raw text when pre node has sse-reference-definitions class', () => { + expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe( + `\n\n${node.innerText}\n\n`, + ); + }); + + it('returns base result when pre node does not have sse-reference-definitions class', () => { + delete node.dataset.sseReferenceDefinition; + + expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(originalConverterResult); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js index 660c21281fd..5cf3961819e 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js @@ -1,12 +1,6 @@ // Node spec helpers -export const buildMockTextNode = literal => { - return { - firstChild: null, - literal, - type: 'text', - }; -}; +export const buildMockTextNode = literal => ({ literal, type: 'text' }); export const normalTextNode = buildMockTextNode('This is just normal text.'); @@ -23,17 +17,20 @@ const buildMockUneditableOpenToken = type => { }; }; -const buildMockUneditableCloseToken = type => { - return { type: 'closeTag', tagName: type }; +const buildMockTextToken = content => { + return { + type: 'text', + tagName: null, + content, + }; }; -export const originToken = { - type: 'text', - tagName: null, - content: '{:.no_toc .hidden-md .hidden-lg}', -}; +const buildMockUneditableCloseToken = type => ({ type: 'closeTag', tagName: type }); + +export const originToken = buildMockTextToken('{:.no_toc .hidden-md .hidden-lg}'); +const uneditableOpenToken = buildMockUneditableOpenToken('div'); +export const uneditableOpenTokens = [uneditableOpenToken, originToken]; export const uneditableCloseToken = buildMockUneditableCloseToken('div'); -export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken]; export const uneditableCloseTokens = [originToken, uneditableCloseToken]; export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken]; @@ -41,6 +38,7 @@ export const originInlineToken = { type: 'text', content: 'Inline content', }; + export const uneditableInlineTokens = [ buildMockUneditableOpenToken('a'), originInlineToken, @@ -48,11 +46,9 @@ export const uneditableInlineTokens = [ ]; export const uneditableBlockTokens = [ - buildMockUneditableOpenToken('div'), - { - type: 'text', - tagName: null, - content: '

    Some header

    Some paragraph

    ', - }, - buildMockUneditableCloseToken('div'), + uneditableOpenToken, + buildMockTextToken('

    Some header

    Some paragraph

    '), + uneditableCloseToken, ]; + +export const attributeDefinition = '{:.no_toc .hidden-md .hidden-lg}'; diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js new file mode 100644 index 00000000000..69fd9a67a21 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js @@ -0,0 +1,25 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition'; +import { attributeDefinition } from './mock_data'; + +describe('rich_content_editor/renderers/render_attribute_definition', () => { + describe('canRender', () => { + it.each` + input | result + ${{ literal: attributeDefinition }} | ${true} + ${{ literal: `FOO${attributeDefinition}` }} | ${false} + ${{ literal: `${attributeDefinition}BAR` }} | ${false} + ${{ literal: 'foobar' }} | ${false} + `('returns $result when input is $input', ({ input, result }) => { + expect(renderer.canRender(input)).toBe(result); + }); + }); + + describe('render', () => { + it('returns an empty HTML comment', () => { + expect(renderer.render()).toEqual({ + type: 'html', + content: '', + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js new file mode 100644 index 00000000000..76abc1ec3d8 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js @@ -0,0 +1,12 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_heading'; +import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; + +describe('rich_content_editor/renderers/render_heading', () => { + it('canRender delegates to renderUtils.willAlwaysRender', () => { + expect(renderer.canRender).toBe(renderUtils.willAlwaysRender); + }); + + it('render delegates to renderUtils.renderWithAttributeDefinitions', () => { + expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js index f4a06b91a10..b3d9576f38b 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js @@ -1,5 +1,4 @@ import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph'; -import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { buildMockTextNode } from './mock_data'; @@ -17,7 +16,7 @@ const identifierParagraphNode = buildMockParagraphNode( `[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example2.com`, ); -describe('Render Identifier Paragraph renderer', () => { +describe('rich_content_editor/renderers_render_identifier_paragraph', () => { describe('canRender', () => { it.each` node | paragraph | target @@ -37,8 +36,49 @@ describe('Render Identifier Paragraph renderer', () => { }); describe('render', () => { - it('should delegate rendering to the renderUneditableBranch util', () => { - expect(renderer.render).toBe(renderUneditableBranch); + let context; + let result; + + beforeEach(() => { + const node = { + firstChild: { + type: 'text', + literal: '[Some text]: https://link.com', + next: { + type: 'linebreak', + next: { + type: 'text', + literal: '[identifier]: http://example1.com "title"', + }, + }, + }, + }; + context = { skipChildren: jest.fn() }; + result = renderer.render(node, context); + }); + + it('renders the reference definitions as a code block', () => { + expect(result).toEqual([ + { + type: 'openTag', + tagName: 'pre', + classNames: ['code-block', 'language-markdown'], + attributes: { + 'data-sse-reference-definition': true, + }, + }, + { type: 'openTag', tagName: 'code' }, + { + type: 'text', + content: '[Some text]: https://link.com\n[identifier]: http://example1.com "title"', + }, + { type: 'closeTag', tagName: 'code' }, + { type: 'closeTag', tagName: 'pre' }, + ]); + }); + + it('skips the reference definition node children from rendering', () => { + expect(context.skipChildren).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js deleted file mode 100644 index 7d427108ba6..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list'; -import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -import { buildMockTextNode } from './mock_data'; - -const buildMockListNode = literal => { - return { - firstChild: { - firstChild: { - firstChild: buildMockTextNode(literal), - type: 'paragraph', - }, - type: 'item', - }, - type: 'list', - }; -}; - -const normalListNode = buildMockListNode('Just another bullet point'); -const kramdownListNode = buildMockListNode('TOC'); - -describe('Render Kramdown List renderer', () => { - describe('canRender', () => { - it('should return true when the argument is a special kramdown TOC ordered/unordered list', () => { - expect(renderer.canRender(kramdownListNode)).toBe(true); - }); - - it('should return false when the argument is a normal ordered/unordered list', () => { - expect(renderer.canRender(normalListNode)).toBe(false); - }); - }); - - describe('render', () => { - it('should delegate rendering to the renderUneditableBranch util', () => { - expect(renderer.render).toBe(renderUneditableBranch); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js deleted file mode 100644 index 1d2d152ffc3..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text'; -import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -import { buildMockTextNode, normalTextNode } from './mock_data'; - -const kramdownTextNode = buildMockTextNode('{:toc}'); - -describe('Render Kramdown Text renderer', () => { - describe('canRender', () => { - it('should return true when the argument `literal` has kramdown syntax', () => { - expect(renderer.canRender(kramdownTextNode)).toBe(true); - }); - - it('should return false when the argument `literal` lacks kramdown syntax', () => { - expect(renderer.canRender(normalTextNode)).toBe(false); - }); - }); - - describe('render', () => { - it('should delegate rendering to the renderUneditableLeaf util', () => { - expect(renderer.render).toBe(renderUneditableLeaf); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js new file mode 100644 index 00000000000..c1ab700535b --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js @@ -0,0 +1,12 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_list_item'; +import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; + +describe('rich_content_editor/renderers/render_list_item', () => { + it('canRender delegates to renderUtils.willAlwaysRender', () => { + expect(renderer.canRender).toBe(renderUtils.willAlwaysRender); + }); + + it('render delegates to renderUtils.renderWithAttributeDefinitions', () => { + expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js index 92435b3e4e3..774f830f421 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js @@ -1,6 +1,8 @@ import { renderUneditableLeaf, renderUneditableBranch, + renderWithAttributeDefinitions, + willAlwaysRender, } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { @@ -8,9 +10,9 @@ import { buildUneditableOpenTokens, } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; -import { originToken, uneditableCloseToken } from './mock_data'; +import { originToken, uneditableCloseToken, attributeDefinition } from './mock_data'; -describe('Render utils', () => { +describe('rich_content_editor/renderers/render_utils', () => { describe('renderUneditableLeaf', () => { it('should return uneditable block tokens around an origin token', () => { const context = { origin: jest.fn().mockReturnValueOnce(originToken) }; @@ -41,4 +43,68 @@ describe('Render utils', () => { expect(result).toStrictEqual(uneditableCloseToken); }); }); + + describe('willAlwaysRender', () => { + it('always returns true', () => { + expect(willAlwaysRender()).toBe(true); + }); + }); + + describe('renderWithAttributeDefinitions', () => { + let openTagToken; + let closeTagToken; + let node; + const attributes = { + 'data-attribute-definition': attributeDefinition, + }; + + beforeEach(() => { + openTagToken = { type: 'openTag' }; + closeTagToken = { type: 'closeTag' }; + node = { + next: { + firstChild: { + literal: attributeDefinition, + }, + }, + }; + }); + + describe('when token type is openTag', () => { + it('attaches attributes when attributes exist in the node’s next sibling', () => { + const context = { origin: () => openTagToken }; + + expect(renderWithAttributeDefinitions(node, context)).toEqual({ + ...openTagToken, + attributes, + }); + }); + + it('attaches attributes when attributes exist in the node’s children', () => { + const context = { origin: () => openTagToken }; + node = { + firstChild: { + firstChild: { + next: { + next: { + literal: attributeDefinition, + }, + }, + }, + }, + }; + + expect(renderWithAttributeDefinitions(node, context)).toEqual({ + ...openTagToken, + attributes, + }); + }); + }); + + it('does not attach attributes when token type is "closeTag"', () => { + const context = { origin: () => closeTagToken }; + + expect(renderWithAttributeDefinitions({}, context)).toBe(closeTagToken); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js index edec3b138b3..c2091a681f2 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js @@ -14,6 +14,7 @@ const createComponent = headerTitle => { }; describe('DropdownCreateLabelComponent', () => { + const colorsCount = Object.keys(mockSuggestedColors).length; let vm; beforeEach(() => { @@ -27,7 +28,7 @@ describe('DropdownCreateLabelComponent', () => { describe('created', () => { it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => { - expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length); + expect(vm.suggestedColors.length).toBe(colorsCount); }); }); @@ -37,12 +38,10 @@ describe('DropdownCreateLabelComponent', () => { }); it('renders `Go back` button on component header', () => { - const backButtonEl = vm.$el.querySelector( - '.dropdown-title button.dropdown-title-button.dropdown-menu-back', - ); + const backButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-back'); expect(backButtonEl).not.toBe(null); - expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null); + expect(backButtonEl.querySelector('[data-testid="arrow-left-icon"]')).not.toBe(null); }); it('renders component header element as `Create new label` when `headerTitle` prop is not provided', () => { @@ -61,12 +60,9 @@ describe('DropdownCreateLabelComponent', () => { }); it('renders `Close` button on component header', () => { - const closeButtonEl = vm.$el.querySelector( - '.dropdown-title button.dropdown-title-button.dropdown-menu-close', - ); + const closeButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-close'); expect(closeButtonEl).not.toBe(null); - expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null); }); it('renders `Name new label` input element', () => { @@ -78,11 +74,11 @@ describe('DropdownCreateLabelComponent', () => { const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown'); expect(colorsListContainerEl).not.toBe(null); - expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length); + expect(colorsListContainerEl.querySelectorAll('a').length).toBe(colorsCount); const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0]; - expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]); + expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0].colorCode); expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);'); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js index 2e721d75b40..0b9a7262e41 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js @@ -33,7 +33,7 @@ describe('DropdownHeaderComponent', () => { ); expect(closeBtnEl).not.toBeNull(); - expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull(); + expect(closeBtnEl.querySelector('.dropdown-menu-close-icon')).not.toBeNull(); }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index e09f0006359..7847e0ee71d 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -87,7 +87,7 @@ describe('DropdownValueCollapsedComponent', () => { }); it('renders tags icon element', () => { - expect(vm.$el.querySelector('.fa-tags')).not.toBeNull(); + expect(vm.$el.querySelector('[data-testid="labels-icon"]')).not.toBeNull(); }); it('renders labels count', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js index 6564c012e67..648ba84fe8f 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js @@ -15,29 +15,29 @@ export const mockLabels = [ }, ]; -export const mockSuggestedColors = [ - '#0033CC', - '#428BCA', - '#44AD8E', - '#A8D695', - '#5CB85C', - '#69D100', - '#004E00', - '#34495E', - '#7F8C8D', - '#A295D6', - '#5843AD', - '#8E44AD', - '#FFECDB', - '#AD4363', - '#D10069', - '#CC0033', - '#FF0000', - '#D9534F', - '#D1D100', - '#F0AD4E', - '#AD8D43', -]; +export const mockSuggestedColors = { + '#0033CC': 'UA blue', + '#428BCA': 'Moderate blue', + '#44AD8E': 'Lime green', + '#A8D695': 'Feijoa', + '#5CB85C': 'Slightly desaturated green', + '#69D100': 'Bright green', + '#004E00': 'Very dark lime green', + '#34495E': 'Very dark desaturated blue', + '#7F8C8D': 'Dark grayish cyan', + '#A295D6': 'Slightly desaturated blue', + '#5843AD': 'Dark moderate blue', + '#8E44AD': 'Dark moderate violet', + '#FFECDB': 'Very pale orange', + '#AD4363': 'Dark moderate pink', + '#D10069': 'Strong pink', + '#CC0033': 'Strong red', + '#FF0000': 'Pure red', + '#D9534F': 'Soft red', + '#D1D100': 'Strong yellow', + '#F0AD4E': 'Soft orange', + '#AD8D43': 'Dark moderate orange', +}; export const mockConfig = { showCreate: true, diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js index cb758797c63..951f706421f 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js @@ -62,7 +62,7 @@ describe('DropdownButton', () => { describe('template', () => { it('renders component container element', () => { - expect(wrapper.is('gl-button-stub')).toBe(true); + expect(wrapper.find(GlButton).element).toBe(wrapper.element); }); it('renders default button text element', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js index a1e0db4d29e..8c17a974b39 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js @@ -65,6 +65,33 @@ describe('LabelsSelectRoot', () => { ]), ); }); + + it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => { + wrapper = createComponent({ + ...mockConfig, + variant: 'embedded', + }); + + jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation(); + + wrapper.vm.handleVuexActionDispatch( + { type: 'toggleDropdownContents' }, + { + showDropdownButton: false, + showDropdownContents: false, + labels: [{ id: 1 }, { id: 2, set: true }], + }, + ); + + expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith( + expect.arrayContaining([ + { + id: 2, + set: true, + }, + ]), + ); + }); }); describe('handleDropdownClose', () => { @@ -123,11 +150,10 @@ describe('LabelsSelectRoot', () => { expect(wrapper.find(DropdownTitle).exists()).toBe(true); }); - it('renders `dropdown-value` component with slot when `showDropdownButton` prop is `false`', () => { + it('renders `dropdown-value` component', () => { const wrapperDropdownValue = createComponent(mockConfig, { default: 'None', }); - wrapperDropdownValue.vm.$store.state.showDropdownButton = false; return wrapperDropdownValue.vm.$nextTick(() => { const valueComp = wrapperDropdownValue.find(DropdownValue); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js index c742220ba8a..bfb8e263d81 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js @@ -259,6 +259,21 @@ describe('LabelsSelect Actions', () => { }); }); + describe('replaceSelectedLabels', () => { + it('replaces `state.selectedLabels`', done => { + const selectedLabels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; + + testAction( + actions.replaceSelectedLabels, + selectedLabels, + state, + [{ type: types.REPLACE_SELECTED_LABELS, payload: selectedLabels }], + [], + done, + ); + }); + }); + describe('updateSelectedLabels', () => { it('updates `state.labels` based on provided `labels` param', done => { const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js index 8081806e314..3414eec8a63 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js @@ -152,6 +152,19 @@ describe('LabelsSelect Mutations', () => { }); }); + describe(`${types.REPLACE_SELECTED_LABELS}`, () => { + it('replaces `state.selectedLabels`', () => { + const state = { + selectedLabels: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }], + }; + const newSelectedLabels = [{ id: 2 }, { id: 5 }]; + + mutations[types.REPLACE_SELECTED_LABELS](state, newSelectedLabels); + + expect(state.selectedLabels).toEqual(newSelectedLabels); + }); + }); + describe(`${types.UPDATE_SELECTED_LABELS}`, () => { const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js index ef3ae088eec..058dfcdbde2 100644 --- a/spec/frontend/vue_shared/components/table_pagination_spec.js +++ b/spec/frontend/vue_shared/components/table_pagination_spec.js @@ -34,7 +34,7 @@ describe('Pagination component', () => { change: spy, }); - expect(wrapper.isEmpty()).toBe(true); + expect(wrapper.html()).toBe(''); }); it('renders if there is a next page', () => { @@ -50,7 +50,7 @@ describe('Pagination component', () => { change: spy, }); - expect(wrapper.isEmpty()).toBe(false); + expect(wrapper.find(GlPagination).exists()).toBe(true); }); it('renders if there is a prev page', () => { @@ -66,7 +66,7 @@ describe('Pagination component', () => { change: spy, }); - expect(wrapper.isEmpty()).toBe(false); + expect(wrapper.find(GlPagination).exists()).toBe(true); }); }); diff --git a/spec/frontend/vue_shared/components/todo_button_spec.js b/spec/frontend/vue_shared/components/todo_button_spec.js new file mode 100644 index 00000000000..482b5de11f6 --- /dev/null +++ b/spec/frontend/vue_shared/components/todo_button_spec.js @@ -0,0 +1,48 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import TodoButton from '~/vue_shared/components/todo_button.vue'; + +describe('Todo Button', () => { + let wrapper; + + const createComponent = (props = {}, mountFn = shallowMount) => { + wrapper = mountFn(TodoButton, { + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders GlButton', () => { + createComponent(); + + expect(wrapper.find(GlButton).exists()).toBe(true); + }); + + it('emits click event when clicked', () => { + createComponent({}, mount); + wrapper.find(GlButton).trigger('click'); + + expect(wrapper.emitted().click).toBeTruthy(); + }); + + it.each` + label | isTodo + ${'Mark as done'} | ${true} + ${'Add a To-Do'} | ${false} + `('sets correct label when isTodo is $isTodo', ({ label, isTodo }) => { + createComponent({ isTodo }); + + expect(wrapper.find(GlButton).text()).toBe(label); + }); + + it('binds additional props to GlButton', () => { + createComponent({ loading: true }); + + expect(wrapper.find(GlButton).props('loading')).toBe(true); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js index 902e83da7be..84e7a6a162e 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -38,10 +38,6 @@ describe('User Avatar Link Component', () => { wrapper = null; }); - it('should return a defined Vue component', () => { - expect(wrapper.isVueInstance()).toBe(true); - }); - it('should have user-avatar-image registered as child component', () => { expect(wrapper.vm.$options.components.userAvatarImage).toBeDefined(); }); diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index a4ff6ac0c16..b43bb6b10e0 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -1,7 +1,6 @@ -import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; -import Icon from '~/vue_shared/components/icon.vue'; const DEFAULT_PROPS = { user: { @@ -74,7 +73,7 @@ describe('User Popover Component', () => { }); it('shows icon for location', () => { - const iconEl = wrapper.find(Icon); + const iconEl = wrapper.find(GlIcon); expect(iconEl.props('name')).toEqual('location'); }); @@ -139,9 +138,9 @@ describe('User Popover Component', () => { createWrapper({ user }); - expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'profile').length).toEqual( - 1, - ); + expect( + wrapper.findAll(GlIcon).filter(icon => icon.props('name') === 'profile').length, + ).toEqual(1); }); it('shows icon for work information', () => { @@ -152,7 +151,9 @@ describe('User Popover Component', () => { createWrapper({ user }); - expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'work').length).toEqual(1); + expect(wrapper.findAll(GlIcon).filter(icon => icon.props('name') === 'work').length).toEqual( + 1, + ); }); }); diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js new file mode 100644 index 00000000000..57f511903d9 --- /dev/null +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -0,0 +1,106 @@ +import { shallowMount } from '@vue/test-utils'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; +import ActionsButton from '~/vue_shared/components/actions_button.vue'; + +const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/master/-/'; +const TEST_GITPOD_URL = 'https://gitpod.test/'; + +const ACTION_WEB_IDE = { + href: TEST_WEB_IDE_URL, + key: 'webide', + secondaryText: 'Quickly and easily edit multiple files in your project.', + tooltip: '', + text: 'Web IDE', + attrs: { + 'data-qa-selector': 'web_ide_button', + }, +}; +const ACTION_WEB_IDE_FORK = { + ...ACTION_WEB_IDE, + href: '#modal-confirm-fork', + handle: expect.any(Function), +}; +const ACTION_GITPOD = { + href: TEST_GITPOD_URL, + key: 'gitpod', + secondaryText: 'Launch a ready-to-code development environment for your project.', + tooltip: 'Launch a ready-to-code development environment for your project.', + text: 'Gitpod', + attrs: { + 'data-qa-selector': 'gitpod_button', + }, +}; +const ACTION_GITPOD_ENABLE = { + ...ACTION_GITPOD, + href: '#modal-enable-gitpod', + handle: expect.any(Function), +}; + +describe('Web IDE link component', () => { + let wrapper; + + function createComponent(props) { + wrapper = shallowMount(WebIdeLink, { + propsData: { + webIdeUrl: TEST_WEB_IDE_URL, + gitpodUrl: TEST_GITPOD_URL, + ...props, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findActionsButton = () => wrapper.find(ActionsButton); + const findLocalStorageSync = () => wrapper.find(LocalStorageSync); + + it.each` + props | expectedActions + ${{}} | ${[ACTION_WEB_IDE]} + ${{ needsToFork: true }} | ${[ACTION_WEB_IDE_FORK]} + ${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true }} | ${[ACTION_GITPOD]} + ${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_GITPOD_ENABLE]} + ${{ showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_WEB_IDE, ACTION_GITPOD_ENABLE]} + `('renders actions with props=$props', ({ props, expectedActions }) => { + createComponent(props); + + expect(findActionsButton().props('actions')).toEqual(expectedActions); + }); + + describe('with multiple actions', () => { + beforeEach(() => { + createComponent({ showWebIdeButton: true, showGitpodButton: true, gitpodEnabled: true }); + }); + + it('selected Web IDE by default', () => { + expect(findActionsButton().props()).toMatchObject({ + actions: [ACTION_WEB_IDE, ACTION_GITPOD], + selectedKey: ACTION_WEB_IDE.key, + }); + }); + + it('should set selection with local storage value', async () => { + expect(findActionsButton().props('selectedKey')).toBe(ACTION_WEB_IDE.key); + + findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key); + + await wrapper.vm.$nextTick(); + + expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key); + }); + + it('should update local storage when selection changes', async () => { + expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key); + + findActionsButton().vm.$emit('select', ACTION_GITPOD.key); + + await wrapper.vm.$nextTick(); + + expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key); + expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key); + }); + }); +}); -- cgit v1.2.1