Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-14 15:09:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-14 15:09:51 +0300
commit9223573b85bcfdd21953f52e0d2c5cb587e366a1 (patch)
tree7dfd09536b948d560fc442014a95a221327b6567 /spec
parent1fc72cb8765dab466da8555b70eb744a53a74a80 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/finders/groups_finder_spec.rb16
-rw-r--r--spec/frontend/boards/components/board_card_move_to_position_spec.js39
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js15
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap8
-rw-r--r--spec/frontend/design_management/pages/index_spec.js3
-rw-r--r--spec/frontend/diffs/store/utils_spec.js8
-rw-r--r--spec/frontend/notes/components/diff_with_note_spec.js42
-rw-r--r--spec/frontend/projects/compare/components/repo_dropdown_spec.js27
-rw-r--r--spec/frontend/projects/project_new_spec.js33
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_menu_spec.js204
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_preparing_spec.js29
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js100
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap (renamed from spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap)0
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js121
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js84
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js93
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js192
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js173
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js7
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb6
23 files changed, 734 insertions, 519 deletions
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index 25f9331005d..23d73b48199 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe GroupsFinder do
+RSpec.describe GroupsFinder, feature_category: :groups_and_projects do
include AdminModeHelper
- shared_examples '#execute' do
+ describe '#execute' do
let(:user) { create(:user) }
describe 'root level groups' do
@@ -376,16 +376,4 @@ RSpec.describe GroupsFinder do
end
end
end
-
- describe '#execute' do
- include_examples '#execute'
-
- context 'when use_traversal_ids_groups_finder feature flags is disabled' do
- before do
- stub_feature_flags(use_traversal_ids_groups_finder: false)
- end
-
- include_examples '#execute'
- end
- end
end
diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js
index 8af772ba6d0..5f308be5580 100644
--- a/spec/frontend/boards/components/board_card_move_to_position_spec.js
+++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js
@@ -51,9 +51,12 @@ describe('Board Card Move to position', () => {
};
};
- const createComponent = (propsData) => {
+ const createComponent = (propsData, isApolloBoard = false) => {
wrapper = shallowMount(BoardCardMoveToPosition, {
store,
+ provide: {
+ isApolloBoard,
+ },
propsData: {
item: mockIssue2,
list: mockList,
@@ -134,5 +137,39 @@ describe('Board Card Move to position', () => {
},
);
});
+
+ describe('Apollo boards', () => {
+ beforeEach(() => {
+ createComponent({ index: itemIndex }, true);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it.each`
+ dropdownIndex | dropdownItem | trackLabel | positionInList
+ ${0} | ${dropdownOptions[0]} | ${'move_to_start'} | ${0}
+ ${1} | ${dropdownOptions[1]} | ${'move_to_end'} | ${-1}
+ `(
+ 'on click of dropdown index $dropdownIndex with label $dropdownLabel emits moveToPosition event with tracking label $trackLabel',
+ async ({ dropdownIndex, dropdownItem, trackLabel, positionInList }) => {
+ await findMoveToPositionDropdown().vm.$emit('shown');
+
+ expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownItem.text);
+
+ await findMoveToPositionDropdown().vm.$emit('action', dropdownItem);
+
+ expect(trackingSpy).toHaveBeenCalledWith('boards:list', 'click_toggle_button', {
+ category: 'boards:list',
+ label: trackLabel,
+ property: 'type_card',
+ });
+
+ expect(wrapper.emitted('moveToPosition')).toEqual([[positionInList]]);
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index 40b04ca3e00..e9484cfce57 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -495,6 +495,21 @@ describe('Ci variable modal', () => {
});
});
+ describe('when the value is empty', () => {
+ beforeEach(() => {
+ const [variable] = mockVariables;
+ const emptyValueVariable = { ...variable, value: '' };
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: emptyValueVariable },
+ });
+ });
+
+ it('allows user to submit', () => {
+ expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
+ });
+ });
+
describe('when the mask state is invalid', () => {
beforeEach(async () => {
const [variable] = mockVariables;
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index 9451f35ac5b..0bbb44bb517 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -11,13 +11,13 @@ exports[`Design management list item component when item appears in view after i
exports[`Design management list item component with notes renders item with multiple comments 1`] = `
<router-link-stub
ariacurrentvalue="page"
- class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new gl-mb-0"
+ class="card gl-cursor-pointer text-plain js-design-list-item design-list-item gl-mb-0"
event="click"
tag="a"
to="[object Object]"
>
<div
- class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
+ class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative gl-rounded-top-base"
>
<!---->
@@ -91,13 +91,13 @@ exports[`Design management list item component with notes renders item with mult
exports[`Design management list item component with notes renders item with single comment 1`] = `
<router-link-stub
ariacurrentvalue="page"
- class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new gl-mb-0"
+ class="card gl-cursor-pointer text-plain js-design-list-item design-list-item gl-mb-0"
event="click"
tag="a"
to="[object Object]"
>
<div
- class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
+ class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative gl-rounded-top-base"
>
<!---->
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 90caf956126..961ea27f0f4 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -227,7 +227,6 @@ describe('Design management index page', () => {
it('has correct classes applied to design dropzone', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
expect(dropzoneClasses()).toContain('design-list-item');
- expect(dropzoneClasses()).toContain('design-list-item-new');
});
it('has correct classes applied to dropzone wrapper', () => {
@@ -253,7 +252,6 @@ describe('Design management index page', () => {
it('has correct classes applied to design dropzone', () => {
expect(dropzoneClasses()).not.toContain('design-list-item');
- expect(dropzoneClasses()).not.toContain('design-list-item-new');
});
it('has correct classes applied to dropzone wrapper', () => {
@@ -355,7 +353,6 @@ describe('Design management index page', () => {
expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
expect(wrapper.vm.isSaving).toBe(true);
expect(dropzoneClasses()).toContain('design-list-item');
- expect(dropzoneClasses()).toContain('design-list-item-new');
});
it('sets isSaving', async () => {
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index bc7c81d2390..888df06d6b9 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -715,6 +715,14 @@ describe('DiffsStoreUtils', () => {
).toBe('mode_changed');
});
+ it('returns no_preview if key has no match', () => {
+ expect(
+ utils.getDiffMode({
+ viewer: { name: 'no_preview' },
+ }),
+ ).toBe('no_preview');
+ });
+
it('defaults to replaced', () => {
expect(utils.getDiffMode({})).toBe('replaced');
});
diff --git a/spec/frontend/notes/components/diff_with_note_spec.js b/spec/frontend/notes/components/diff_with_note_spec.js
index c352265654b..508f2ced4c4 100644
--- a/spec/frontend/notes/components/diff_with_note_spec.js
+++ b/spec/frontend/notes/components/diff_with_note_spec.js
@@ -3,6 +3,7 @@ import discussionFixture from 'test_fixtures/merge_requests/diff_discussion.json
import imageDiscussionFixture from 'test_fixtures/merge_requests/image_diff_discussion.json';
import { createStore } from '~/mr_notes/stores';
import DiffWithNote from '~/notes/components/diff_with_note.vue';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
describe('diff_with_note', () => {
let store;
@@ -20,6 +21,8 @@ describe('diff_with_note', () => {
},
};
+ const findDiffViewer = () => wrapper.findComponent(DiffViewer);
+
beforeEach(() => {
store = createStore();
store.replaceState({
@@ -85,4 +88,43 @@ describe('diff_with_note', () => {
expect(selectors.diffTable.exists()).toBe(false);
});
});
+
+ describe('legacy diff note', () => {
+ const mockCommitId = 'abc123';
+
+ beforeEach(() => {
+ const diffDiscussion = {
+ ...discussionFixture[0],
+ commit_id: mockCommitId,
+ diff_file: {
+ ...discussionFixture[0].diff_file,
+ diff_refs: null,
+ viewer: {
+ ...discussionFixture[0].diff_file.viewer,
+ name: 'no_preview',
+ },
+ },
+ };
+
+ wrapper = shallowMount(DiffWithNote, {
+ propsData: {
+ discussion: diffDiscussion,
+ },
+ store,
+ });
+ });
+
+ it('shows file diff', () => {
+ expect(selectors.diffTable.exists()).toBe(false);
+ });
+
+ it('uses "no_preview" diff mode', () => {
+ expect(findDiffViewer().props('diffMode')).toBe('no_preview');
+ });
+
+ it('falls back to discussion.commit_id for baseSha and headSha', () => {
+ expect(findDiffViewer().props('oldSha')).toBe(mockCommitId);
+ expect(findDiffViewer().props('newSha')).toBe(mockCommitId);
+ });
+ });
});
diff --git a/spec/frontend/projects/compare/components/repo_dropdown_spec.js b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
index 0b1085470b8..44aaac21733 100644
--- a/spec/frontend/projects/compare/components/repo_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue';
@@ -13,10 +13,14 @@ describe('RepoDropdown component', () => {
...defaultProps,
...props,
},
+ stubs: {
+ GlCollapsibleListbox,
+ GlListboxItem,
+ },
});
};
- const findGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findHiddenInput = () => wrapper.find('input[type="hidden"]');
describe('Source Revision', () => {
@@ -29,8 +33,10 @@ describe('RepoDropdown component', () => {
});
it('displays the project name in the disabled dropdown', () => {
- expect(findGlDropdown().props('text')).toBe(defaultProps.selectedProject.name);
- expect(findGlDropdown().props('disabled')).toBe(true);
+ expect(findGlCollapsibleListbox().props('toggleText')).toBe(
+ defaultProps.selectedProject.name,
+ );
+ expect(findGlCollapsibleListbox().props('disabled')).toBe(true);
});
it('does not emit `changeTargetProject` event', async () => {
@@ -57,18 +63,21 @@ describe('RepoDropdown component', () => {
});
it('displays matching project name of the source revision initially in the dropdown', () => {
- expect(findGlDropdown().props('text')).toBe(defaultProps.selectedProject.name);
+ expect(findGlCollapsibleListbox().props('toggleText')).toBe(
+ defaultProps.selectedProject.name,
+ );
});
- it('updates the hidden input value when onClick method is triggered', async () => {
+ it('updates the hidden input value when dropdown item is selected', () => {
const repoId = '1';
- wrapper.vm.onClick({ id: repoId });
- await nextTick();
+ findGlCollapsibleListbox().vm.$emit('select', repoId);
expect(findHiddenInput().attributes('value')).toBe(repoId);
});
it('emits `selectProject` event when another target project is selected', async () => {
- findGlDropdown().findAllComponents(GlDropdownItem).at(0).vm.$emit('click');
+ const repoId = '1';
+ findGlCollapsibleListbox().vm.$emit('select', repoId);
+
await nextTick();
expect(wrapper.emitted('selectProject')[0][0]).toEqual({
diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js
index 8a1e9904a3f..54d0cfaa8c6 100644
--- a/spec/frontend/projects/project_new_spec.js
+++ b/spec/frontend/projects/project_new_spec.js
@@ -13,6 +13,8 @@ describe('New Project', () => {
const mockKeyup = (el) => el.dispatchEvent(new KeyboardEvent('keyup'));
const mockChange = (el) => el.dispatchEvent(new Event('change'));
+ const mockSubmit = () =>
+ document.getElementById('new_project').dispatchEvent(new Event('submit'));
beforeEach(() => {
setHTMLFixture(`
@@ -311,4 +313,35 @@ describe('New Project', () => {
expect($projectName.value).toEqual(dummyProjectName);
});
});
+
+ describe('project path trimming', () => {
+ beforeEach(() => {
+ projectNew.bindEvents();
+ });
+
+ describe('when the project path field is filled in', () => {
+ const dirtyProjectPath = ' my-awesome-project ';
+ const cleanProjectPath = dirtyProjectPath.trim();
+
+ beforeEach(() => {
+ $projectPath.value = dirtyProjectPath;
+ mockSubmit();
+ });
+
+ it('trims the project path on submit', () => {
+ expect($projectPath.value).not.toBe(dirtyProjectPath);
+ expect($projectPath.value).toBe(cleanProjectPath);
+ });
+ });
+
+ describe('when the project path field is left empty', () => {
+ beforeEach(() => {
+ mockSubmit();
+ });
+
+ it('leaves the field empty', () => {
+ expect($projectPath.value).toBe('');
+ });
+ });
+ });
});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 21b37f5cfbc..ecd617ca44b 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -16,7 +16,7 @@ import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
-import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_deprecated.vue';
+import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import projectInfoQuery from '~/repository/queries/project_info.query.graphql';
import userInfoQuery from '~/repository/queries/user_info.query.graphql';
diff --git a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
index 19471f28fab..21e5220edd9 100644
--- a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
@@ -1,7 +1,8 @@
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { s__ } from '~/locale';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
import PinnedSection from '~/super_sidebar/components/pinned_section.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import MenuSection from '~/super_sidebar/components/menu_section.vue';
import { PANELS_WITH_PINS } from '~/super_sidebar/constants';
import { sidebarData } from '../mock_data';
@@ -12,181 +13,142 @@ const menuItems = [
{ id: 4, title: 'Also with subitems', items: [{ id: 41, title: 'Subitem' }] },
];
-describe('SidebarMenu component', () => {
+describe('Sidebar Menu', () => {
let wrapper;
- const createWrapper = (mockData) => {
- wrapper = mountExtended(SidebarMenu, {
+ const createWrapper = (extraProps = {}) => {
+ wrapper = shallowMountExtended(SidebarMenu, {
propsData: {
- items: mockData.current_menu_items,
- pinnedItemIds: mockData.pinned_items,
- panelType: mockData.panel_type,
- updatePinsUrl: mockData.update_pins_url,
+ items: sidebarData.current_menu_items,
+ pinnedItemIds: sidebarData.pinned_items,
+ panelType: sidebarData.panel_type,
+ updatePinsUrl: sidebarData.update_pins_url,
+ ...extraProps,
},
});
};
+ const findStaticItemsSection = () => wrapper.findByTestId('static-items-section');
+ const findStaticItems = () => findStaticItemsSection().findAllComponents(NavItem);
const findPinnedSection = () => wrapper.findComponent(PinnedSection);
const findMainMenuSeparator = () => wrapper.findByTestId('main-menu-separator');
-
- describe('computed', () => {
- describe('supportsPins', () => {
- it('is true for the project sidebar', () => {
- createWrapper({ ...sidebarData, panel_type: 'project' });
- expect(wrapper.vm.supportsPins).toBe(true);
- });
-
- it('is true for the group sidebar', () => {
- createWrapper({ ...sidebarData, panel_type: 'group' });
- expect(wrapper.vm.supportsPins).toBe(true);
- });
-
- it('is false for any other sidebar', () => {
- createWrapper({ ...sidebarData, panel_type: 'your_work' });
- expect(wrapper.vm.supportsPins).toEqual(false);
+ const findNonStaticItemsSection = () => wrapper.findByTestId('non-static-items-section');
+ const findNonStaticItems = () => findNonStaticItemsSection().findAllComponents(NavItem);
+ const findNonStaticSectionItems = () =>
+ findNonStaticItemsSection().findAllComponents(MenuSection);
+
+ describe('Static section', () => {
+ describe('when the sidebar supports pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ items: menuItems,
+ panelType: PANELS_WITH_PINS[0],
+ });
});
- });
- describe('flatPinnableItems', () => {
- it('returns all subitems in a flat array', () => {
- createWrapper({ ...sidebarData, current_menu_items: menuItems });
- expect(wrapper.vm.flatPinnableItems).toEqual([
- { id: 21, title: 'Pinned subitem' },
- { id: 41, title: 'Subitem' },
+ it('renders static items section', () => {
+ expect(findStaticItemsSection().exists()).toBe(true);
+ expect(findStaticItems().wrappers.map((w) => w.props('item').title)).toEqual([
+ 'No subitems',
+ 'Empty subitems array',
]);
});
});
- describe('staticItems', () => {
- describe('when the sidebar supports pins', () => {
- beforeEach(() => {
- createWrapper({
- ...sidebarData,
- current_menu_items: menuItems,
- panel_type: PANELS_WITH_PINS[0],
- });
+ describe('when the sidebar does not support pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ items: menuItems,
+ panelType: 'explore',
});
+ });
- it('makes everything that has no subitems a static item', () => {
- expect(wrapper.vm.staticItems.map((i) => i.title)).toEqual([
- 'No subitems',
- 'Empty subitems array',
- ]);
- });
+ it('does not render static items section', () => {
+ expect(findStaticItemsSection().exists()).toBe(false);
});
+ });
+ });
- describe('when the sidebar does not support pins', () => {
- beforeEach(() => {
- createWrapper({
- ...sidebarData,
- current_menu_items: menuItems,
- panel_type: 'explore',
- });
- });
+ describe('Pinned section', () => {
+ it('is rendered in a project sidebar', () => {
+ createWrapper({ panelType: 'project' });
+ expect(findPinnedSection().exists()).toBe(true);
+ });
- it('returns an empty array', () => {
- expect(wrapper.vm.staticItems.map((i) => i.title)).toEqual([]);
- });
- });
+ it('is rendered in a group sidebar', () => {
+ createWrapper({ panelType: 'group' });
+ expect(findPinnedSection().exists()).toBe(true);
});
- describe('nonStaticItems', () => {
- describe('when the sidebar supports pins', () => {
- beforeEach(() => {
- createWrapper({
- ...sidebarData,
- current_menu_items: menuItems,
- panel_type: PANELS_WITH_PINS[0],
- });
- });
+ it('is not rendered in other sidebars', () => {
+ createWrapper({ panelType: 'your_work' });
+ expect(findPinnedSection().exists()).toBe(false);
+ });
+ });
- it('keeps items that have subitems (aka "sections") as non-static', () => {
- expect(wrapper.vm.nonStaticItems.map((i) => i.title)).toEqual([
- 'With subitems',
- 'Also with subitems',
- ]);
+ describe('Non static items section', () => {
+ describe('when the sidebar supports pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ items: menuItems,
+ panelType: PANELS_WITH_PINS[0],
});
});
- describe('when the sidebar does not support pins', () => {
- beforeEach(() => {
- createWrapper({
- ...sidebarData,
- current_menu_items: menuItems,
- panel_type: 'explore',
- });
- });
-
- it('keeps all items as non-static', () => {
- expect(wrapper.vm.nonStaticItems).toEqual(menuItems);
- });
+ it('keeps items that have subitems (aka "sections") as non-static', () => {
+ expect(findNonStaticSectionItems().wrappers.map((w) => w.props('item').title)).toEqual([
+ 'With subitems',
+ 'Also with subitems',
+ ]);
});
});
- describe('pinnedItems', () => {
- describe('when user has no pinned item ids stored', () => {
- beforeEach(() => {
- createWrapper({
- ...sidebarData,
- current_menu_items: menuItems,
- pinned_items: [],
- });
- });
-
- it('returns an empty array', () => {
- expect(wrapper.vm.pinnedItems).toEqual([]);
+ describe('when the sidebar does not support pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ items: menuItems,
+ panelType: 'explore',
});
});
- describe('when user has some pinned item ids stored', () => {
- beforeEach(() => {
- createWrapper({
- ...sidebarData,
- current_menu_items: menuItems,
- pinned_items: [21],
- });
- });
-
- it('returns the items matching the pinned ids', () => {
- expect(wrapper.vm.pinnedItems).toEqual([{ id: 21, title: 'Pinned subitem' }]);
- });
+ it('keeps all items as non-static', () => {
+ expect(findNonStaticSectionItems().length + findNonStaticItems().length).toBe(
+ menuItems.length,
+ );
});
});
});
- describe('Menu separators', () => {
+ describe('Separators', () => {
it('should add the separator above pinned section', () => {
createWrapper({
- ...sidebarData,
- current_menu_items: menuItems,
- panel_type: 'project',
+ items: menuItems,
+ panelType: 'project',
});
expect(findPinnedSection().props('separated')).toBe(true);
});
it('should add the separator above main menu items when there is a pinned section', () => {
createWrapper({
- ...sidebarData,
- current_menu_items: menuItems,
- panel_type: PANELS_WITH_PINS[0],
+ items: menuItems,
+ panelType: PANELS_WITH_PINS[0],
});
expect(findMainMenuSeparator().exists()).toBe(true);
});
it('should NOT add the separator above main menu items when there is no pinned section', () => {
createWrapper({
- ...sidebarData,
- current_menu_items: menuItems,
- panel_type: 'explore',
+ items: menuItems,
+ panelType: 'explore',
});
expect(findMainMenuSeparator().exists()).toBe(false);
});
});
- describe('template', () => {
+ describe('ARIA attributes', () => {
it('adds aria-label attribute to nav element', () => {
- createWrapper({ ...sidebarData });
- expect(wrapper.find('nav').attributes('aria-label')).toBe(s__('Navigation|Main navigation'));
+ createWrapper();
+ expect(wrapper.find('nav').attributes('aria-label')).toBe('Main navigation');
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_preparing_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_preparing_spec.js
new file mode 100644
index 00000000000..a54591cdb16
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_preparing_spec.js
@@ -0,0 +1,29 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+
+import Preparing from '~/vue_merge_request_widget/components/states/mr_widget_preparing.vue';
+import { MR_WIDGET_PREPARING_ASYNCHRONOUSLY } from '~/vue_merge_request_widget/i18n';
+
+function createComponent() {
+ return shallowMount(Preparing);
+}
+
+function findSpinnerIcon(wrapper) {
+ return wrapper.findComponent(GlLoadingIcon);
+}
+
+describe('Preparing', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('should render a spinner', () => {
+ expect(findSpinnerIcon(wrapper).exists()).toBe(true);
+ });
+
+ it('should render the correct text', () => {
+ expect(wrapper.text()).toBe(MR_WIDGET_PREPARING_ASYNCHRONOUSLY);
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 656203b1d25..daa45a9e876 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -3,8 +3,8 @@ import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import * as Sentry from '@sentry/browser';
import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
+import * as Sentry from '@sentry/browser';
import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
@@ -26,10 +26,13 @@ import { STATE_QUERY_POLLING_INTERVAL_BACKOFF } from '~/vue_merge_request_widget
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
+import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
+import Preparing from '~/vue_merge_request_widget/components/states/mr_widget_preparing.vue';
import WidgetContainer from '~/vue_merge_request_widget/components/widget/app.vue';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
+import getStateSubscription from '~/vue_merge_request_widget/queries/get_state.subscription.graphql';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import approvalsQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql';
import approvedBySubscription from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql';
@@ -65,13 +68,14 @@ jest.mock('@sentry/browser', () => ({
Vue.use(VueApollo);
describe('MrWidgetOptions', () => {
- let mockedApprovalsSubscription;
let stateQueryHandler;
let queryResponse;
let wrapper;
let mock;
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
+ const findApprovalsWidget = () => wrapper.findComponent(Approvals);
+ const findPreparingWidget = () => wrapper.findComponent(Preparing);
const findWidgetContainer = () => wrapper.findComponent(WidgetContainer);
const findExtensionToggleButton = () =>
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]');
@@ -97,7 +101,7 @@ describe('MrWidgetOptions', () => {
});
const createComponent = (mrData = mockData, options = {}, data = {}, fullMount = true) => {
- mockedApprovalsSubscription = createMockApolloSubscription();
+ const mockedApprovalsSubscription = createMockApolloSubscription();
queryResponse = {
data: {
project: {
@@ -105,6 +109,9 @@ describe('MrWidgetOptions', () => {
mergeRequest: {
...getStateQueryResponse.data.project.mergeRequest,
mergeError: mrData.mergeError || null,
+ detailedMergeStatus:
+ mrData.detailedMergeStatus ||
+ getStateQueryResponse.data.project.mergeRequest.detailedMergeStatus,
},
},
},
@@ -128,7 +135,10 @@ describe('MrWidgetOptions', () => {
],
...(options.apolloMock || []),
];
- const subscriptionHandlers = [[approvedBySubscription, () => mockedApprovalsSubscription]];
+ const subscriptionHandlers = [
+ [approvedBySubscription, () => mockedApprovalsSubscription],
+ ...(options.apolloSubscriptions || []),
+ ];
const apolloProvider = createMockApollo(queryHandlers);
subscriptionHandlers.forEach(([query, stream]) => {
@@ -1275,4 +1285,86 @@ describe('MrWidgetOptions', () => {
});
});
});
+
+ describe('async preparation for a newly opened MR', () => {
+ beforeEach(() => {
+ mock
+ .onGet(mockData.merge_request_widget_path)
+ .reply(() => [HTTP_STATUS_OK, { ...mockData, state: 'opened' }]);
+ });
+
+ it('does not render the Preparing state component by default', async () => {
+ await createComponent();
+
+ expect(findApprovalsWidget().exists()).toBe(true);
+ expect(findPreparingWidget().exists()).toBe(false);
+ });
+
+ it('renders the Preparing state component when the MR state is initially "preparing"', async () => {
+ await createComponent({
+ ...mockData,
+ state: 'opened',
+ detailedMergeStatus: 'PREPARING',
+ });
+
+ expect(findApprovalsWidget().exists()).toBe(false);
+ expect(findPreparingWidget().exists()).toBe(true);
+ });
+
+ describe('when the MR is updated by observing its status', () => {
+ let stateSubscription;
+
+ beforeEach(() => {
+ window.gon.features.realtimeMrStatusChange = true;
+ stateSubscription = createMockApolloSubscription();
+ });
+
+ it("shows the Preparing widget when the MR reports it's not ready yet", async () => {
+ await createComponent(
+ {
+ ...mockData,
+ state: 'opened',
+ detailedMergeStatus: 'PREPARING',
+ },
+ {
+ apolloSubscriptions: [[getStateSubscription, () => stateSubscription]],
+ },
+ {},
+ false,
+ );
+
+ expect(wrapper.html()).toContain('mr-widget-preparing-stub');
+ });
+
+ it('removes the Preparing widget when the MR indicates it has been prepared', async () => {
+ await createComponent(
+ {
+ ...mockData,
+ state: 'opened',
+ detailedMergeStatus: 'PREPARING',
+ },
+ {
+ apolloSubscriptions: [[getStateSubscription, () => stateSubscription]],
+ },
+ {},
+ false,
+ );
+
+ expect(wrapper.html()).toContain('mr-widget-preparing-stub');
+
+ stateSubscription.next({
+ data: {
+ mergeRequestMergeStatusUpdated: {
+ preparedAt: 'non-null value',
+ },
+ },
+ });
+
+ // Wait for batched DOM updates
+ await nextTick();
+
+ expect(wrapper.html()).not.toContain('mr-widget-preparing-stub');
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
index a6288b9c725..ca5c9084a62 100644
--- a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
@@ -16,10 +16,14 @@ describe('getStateKey', () => {
commitsCount: 2,
hasConflicts: false,
draft: false,
- detailedMergeStatus: null,
+ detailedMergeStatus: 'PREPARING',
};
const bound = getStateKey.bind(context);
+ expect(bound()).toEqual('preparing');
+
+ context.detailedMergeStatus = null;
+
expect(bound()).toEqual('checking');
context.detailedMergeStatus = 'MERGEABLE';
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap
index 26c9a6f8d5a..26c9a6f8d5a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap
+++ b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
deleted file mode 100644
index 395ba92d4c6..00000000000
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import { nextTick } from 'vue';
-import { GlIntersectionObserver } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import Chunk from '~/vue_shared/components/source_viewer/components/chunk_deprecated.vue';
-import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
-import LineHighlighter from '~/blob/line_highlighter';
-
-const lineHighlighter = new LineHighlighter();
-jest.mock('~/blob/line_highlighter', () =>
- jest.fn().mockReturnValue({
- highlightHash: jest.fn(),
- }),
-);
-
-const DEFAULT_PROPS = {
- chunkIndex: 2,
- isHighlighted: false,
- content: '// Line 1 content \n // Line 2 content',
- startingFrom: 140,
- totalLines: 50,
- language: 'javascript',
- blamePath: 'blame/file.js',
-};
-
-const hash = '#L142';
-
-describe('Chunk component', () => {
- let wrapper;
- let idleCallbackSpy;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(Chunk, {
- mocks: { $route: { hash } },
- propsData: { ...DEFAULT_PROPS, ...props },
- });
- };
-
- const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
- const findChunkLines = () => wrapper.findAllComponents(ChunkLine);
- const findLineNumbers = () => wrapper.findAllByTestId('line-number');
- const findContent = () => wrapper.findByTestId('content');
-
- beforeEach(() => {
- idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
- createComponent();
- });
-
- describe('Intersection observer', () => {
- it('renders an Intersection observer component', () => {
- expect(findIntersectionObserver().exists()).toBe(true);
- });
-
- it('emits an appear event when intersection-observer appears', () => {
- findIntersectionObserver().vm.$emit('appear');
-
- expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]);
- });
-
- it('does not emit an appear event is isHighlighted is true', () => {
- createComponent({ isHighlighted: true });
- findIntersectionObserver().vm.$emit('appear');
-
- expect(wrapper.emitted('appear')).toEqual(undefined);
- });
- });
-
- describe('rendering', () => {
- it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
- jest.clearAllMocks();
- createComponent({ isFirstChunk: true });
-
- expect(window.requestIdleCallback).not.toHaveBeenCalled();
- expect(findContent().exists()).toBe(true);
- });
-
- it('does not render a Chunk Line component if isHighlighted is false', () => {
- expect(findChunkLines().length).toBe(0);
- });
-
- it('does not render simplified line numbers and content if browser is not in idle state', () => {
- idleCallbackSpy.mockRestore();
- createComponent();
-
- expect(findLineNumbers()).toHaveLength(0);
- expect(findContent().exists()).toBe(false);
- });
-
- it('renders simplified line numbers and content if isHighlighted is false', () => {
- expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
-
- expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
-
- expect(findContent().text()).toBe(DEFAULT_PROPS.content);
- });
-
- it('renders Chunk Line components if isHighlighted is true', () => {
- const splitContent = DEFAULT_PROPS.content.split('\n');
- createComponent({ isHighlighted: true });
-
- expect(findChunkLines().length).toBe(splitContent.length);
-
- expect(findChunkLines().at(0).props()).toMatchObject({
- number: DEFAULT_PROPS.startingFrom + 1,
- content: splitContent[0],
- language: DEFAULT_PROPS.language,
- blamePath: DEFAULT_PROPS.blamePath,
- });
- });
-
- it('does not scroll to route hash if last chunk is not loaded', () => {
- expect(LineHighlighter).not.toHaveBeenCalled();
- });
-
- it('scrolls to route hash if last chunk is loaded', async () => {
- createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
- await nextTick();
- expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
- expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
new file mode 100644
index 00000000000..919abc26e05
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
@@ -0,0 +1,84 @@
+import { nextTick } from 'vue';
+import { GlIntersectionObserver } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue';
+import { CHUNK_1, CHUNK_2 } from '../mock_data';
+
+describe('Chunk component', () => {
+ let wrapper;
+ let idleCallbackSpy;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(Chunk, {
+ propsData: { ...CHUNK_1, ...props },
+ });
+ };
+
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findLineNumbers = () => wrapper.findAllByTestId('line-numbers');
+ const findContent = () => wrapper.findByTestId('content');
+
+ beforeEach(() => {
+ idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
+ createComponent();
+ });
+
+ describe('Intersection observer', () => {
+ it('renders an Intersection observer component', () => {
+ expect(findIntersectionObserver().exists()).toBe(true);
+ });
+
+ it('renders highlighted content if appear event is emitted', async () => {
+ createComponent({ chunkIndex: 1, isHighlighted: false });
+ findIntersectionObserver().vm.$emit('appear');
+
+ await nextTick();
+
+ expect(findContent().exists()).toBe(true);
+ });
+ });
+
+ describe('rendering', () => {
+ it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
+ jest.clearAllMocks();
+
+ expect(window.requestIdleCallback).not.toHaveBeenCalled();
+ expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
+ });
+
+ it('does not render content if browser is not in idle state', () => {
+ idleCallbackSpy.mockRestore();
+ createComponent({ chunkIndex: 1, ...CHUNK_2 });
+
+ expect(findLineNumbers()).toHaveLength(0);
+ expect(findContent().exists()).toBe(false);
+ });
+
+ describe('isHighlighted is false', () => {
+ beforeEach(() => createComponent(CHUNK_2));
+
+ it('does not render line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(0);
+ });
+
+ it('renders raw content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.rawContent);
+ });
+ });
+
+ describe('isHighlighted is true', () => {
+ beforeEach(() => createComponent({ ...CHUNK_2, isHighlighted: true }));
+
+ it('renders line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(CHUNK_2.totalLines);
+
+ // Opted for a snapshot test here since the output is simple and verifies native HTML elements
+ expect(findLineNumbers().at(0).element).toMatchSnapshot();
+ });
+
+ it('renders highlighted content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.highlightedContent);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
index ff50326917f..9e43aa1d707 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -2,7 +2,27 @@ import { nextTick } from 'vue';
import { GlIntersectionObserver } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
-import { CHUNK_1, CHUNK_2 } from '../mock_data';
+import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
+import LineHighlighter from '~/blob/line_highlighter';
+
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () =>
+ jest.fn().mockReturnValue({
+ highlightHash: jest.fn(),
+ }),
+);
+
+const DEFAULT_PROPS = {
+ chunkIndex: 2,
+ isHighlighted: false,
+ content: '// Line 1 content \n // Line 2 content',
+ startingFrom: 140,
+ totalLines: 50,
+ language: 'javascript',
+ blamePath: 'blame/file.js',
+};
+
+const hash = '#L142';
describe('Chunk component', () => {
let wrapper;
@@ -10,12 +30,14 @@ describe('Chunk component', () => {
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(Chunk, {
- propsData: { ...CHUNK_1, ...props },
+ mocks: { $route: { hash } },
+ propsData: { ...DEFAULT_PROPS, ...props },
});
};
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
- const findLineNumbers = () => wrapper.findAllByTestId('line-numbers');
+ const findChunkLines = () => wrapper.findAllComponents(ChunkLine);
+ const findLineNumbers = () => wrapper.findAllByTestId('line-number');
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
@@ -28,57 +50,72 @@ describe('Chunk component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
- it('renders highlighted content if appear event is emitted', async () => {
- createComponent({ chunkIndex: 1, isHighlighted: false });
+ it('emits an appear event when intersection-observer appears', () => {
findIntersectionObserver().vm.$emit('appear');
- await nextTick();
+ expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]);
+ });
- expect(findContent().exists()).toBe(true);
+ it('does not emit an appear event is isHighlighted is true', () => {
+ createComponent({ isHighlighted: true });
+ findIntersectionObserver().vm.$emit('appear');
+
+ expect(wrapper.emitted('appear')).toEqual(undefined);
});
});
describe('rendering', () => {
- it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
+ it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
jest.clearAllMocks();
+ createComponent({ isFirstChunk: true });
expect(window.requestIdleCallback).not.toHaveBeenCalled();
- expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
+ expect(findContent().exists()).toBe(true);
+ });
+
+ it('does not render a Chunk Line component if isHighlighted is false', () => {
+ expect(findChunkLines().length).toBe(0);
});
- it('does not render content if browser is not in idle state', () => {
+ it('does not render simplified line numbers and content if browser is not in idle state', () => {
idleCallbackSpy.mockRestore();
- createComponent({ chunkIndex: 1, ...CHUNK_2 });
+ createComponent();
expect(findLineNumbers()).toHaveLength(0);
expect(findContent().exists()).toBe(false);
});
- describe('isHighlighted is false', () => {
- beforeEach(() => createComponent(CHUNK_2));
+ it('renders simplified line numbers and content if isHighlighted is false', () => {
+ expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
- it('does not render line numbers', () => {
- expect(findLineNumbers()).toHaveLength(0);
- });
+ expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
- it('renders raw content', () => {
- expect(findContent().text()).toBe(CHUNK_2.rawContent);
- });
+ expect(findContent().text()).toBe(DEFAULT_PROPS.content);
});
- describe('isHighlighted is true', () => {
- beforeEach(() => createComponent({ ...CHUNK_2, isHighlighted: true }));
+ it('renders Chunk Line components if isHighlighted is true', () => {
+ const splitContent = DEFAULT_PROPS.content.split('\n');
+ createComponent({ isHighlighted: true });
- it('renders line numbers', () => {
- expect(findLineNumbers()).toHaveLength(CHUNK_2.totalLines);
+ expect(findChunkLines().length).toBe(splitContent.length);
- // Opted for a snapshot test here since the output is simple and verifies native HTML elements
- expect(findLineNumbers().at(0).element).toMatchSnapshot();
+ expect(findChunkLines().at(0).props()).toMatchObject({
+ number: DEFAULT_PROPS.startingFrom + 1,
+ content: splitContent[0],
+ language: DEFAULT_PROPS.language,
+ blamePath: DEFAULT_PROPS.blamePath,
});
+ });
- it('renders highlighted content', () => {
- expect(findContent().text()).toBe(CHUNK_2.highlightedContent);
- });
+ it('does not scroll to route hash if last chunk is not loaded', () => {
+ expect(LineHighlighter).not.toHaveBeenCalled();
+ });
+
+ it('scrolls to route hash if last chunk is loaded', async () => {
+ createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
+ await nextTick();
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
deleted file mode 100644
index e46c30d7c49..00000000000
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
+++ /dev/null
@@ -1,192 +0,0 @@
-import hljs from 'highlight.js/lib/core';
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_deprecated.vue';
-import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
-import Chunk from '~/vue_shared/components/source_viewer/components/chunk_deprecated.vue';
-import {
- EVENT_ACTION,
- EVENT_LABEL_VIEWER,
- EVENT_LABEL_FALLBACK,
- ROUGE_TO_HLJS_LANGUAGE_MAP,
- LINES_PER_CHUNK,
- LEGACY_FALLBACKS,
- CODEOWNERS_FILE_NAME,
- CODEOWNERS_LANGUAGE,
-} from '~/vue_shared/components/source_viewer/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import LineHighlighter from '~/blob/line_highlighter';
-import eventHub from '~/notes/event_hub';
-import Tracking from '~/tracking';
-
-jest.mock('~/blob/line_highlighter');
-jest.mock('highlight.js/lib/core');
-jest.mock('~/vue_shared/components/source_viewer/plugins/index');
-Vue.use(VueRouter);
-const router = new VueRouter();
-
-const generateContent = (content, totalLines = 1, delimiter = '\n') => {
- let generatedContent = '';
- for (let i = 0; i < totalLines; i += 1) {
- generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
- }
- return generatedContent;
-};
-
-const execImmediately = (callback) => callback();
-
-describe('Source Viewer component', () => {
- let wrapper;
- const language = 'docker';
- const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
- const chunk1 = generateContent('// Some source code 1', 70);
- const chunk2 = generateContent('// Some source code 2', 70);
- const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
- const chunk3Result = generateContent('// Some source code 3', 70, '\n');
- const content = chunk1 + chunk2 + chunk3;
- const path = 'some/path.js';
- const blamePath = 'some/blame/path.js';
- const fileType = 'javascript';
- const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
- const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
-
- const createComponent = async (blob = {}) => {
- wrapper = shallowMountExtended(SourceViewer, {
- router,
- propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
- });
- await waitForPromises();
- };
-
- const findChunks = () => wrapper.findAllComponents(Chunk);
-
- beforeEach(() => {
- hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
- hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
- jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
- jest.spyOn(eventHub, '$emit');
- jest.spyOn(Tracking, 'event');
-
- return createComponent();
- });
-
- describe('event tracking', () => {
- it('fires a tracking event when the component is created', () => {
- const eventData = { label: EVENT_LABEL_VIEWER, property: language };
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- });
-
- it('does not emit an error event when the language is supported', () => {
- expect(wrapper.emitted('error')).toBeUndefined();
- });
-
- it('fires a tracking event and emits an error when the language is not supported', () => {
- const unsupportedLanguage = 'apex';
- const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
- createComponent({ language: unsupportedLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
- });
-
- describe('legacy fallbacks', () => {
- it.each(LEGACY_FALLBACKS)(
- 'tracks a fallback event and emits an error when viewing %s files',
- (fallbackLanguage) => {
- const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
- createComponent({ language: fallbackLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- },
- );
- });
-
- describe('highlight.js', () => {
- beforeEach(() => createComponent({ language: mappedLanguage }));
-
- it('registers our plugins for Highlight.js', () => {
- expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
- });
-
- it('registers the language definition', async () => {
- const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- mappedLanguage,
- languageDefinition.default,
- );
- });
-
- it('registers json language definition if fileType is package_json', async () => {
- await createComponent({ language: 'json', fileType: 'package_json' });
- const languageDefinition = await import(`highlight.js/lib/languages/json`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
- });
-
- it('correctly maps languages starting with uppercase', async () => {
- await createComponent({ language: 'Ruby' });
- const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
- });
-
- it('registers codeowners language definition if file name is CODEOWNERS', async () => {
- await createComponent({ name: CODEOWNERS_FILE_NAME });
- const languageDefinition = await import(
- '~/vue_shared/components/source_viewer/languages/codeowners'
- );
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- CODEOWNERS_LANGUAGE,
- languageDefinition.default,
- );
- });
-
- it('highlights the first chunk', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
- expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
- });
-
- describe('auto-detects if a language cannot be loaded', () => {
- beforeEach(() => createComponent({ language: 'some_unknown_language' }));
-
- it('highlights the content with auto-detection', () => {
- expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim());
- });
- });
- });
-
- describe('rendering', () => {
- it.each`
- chunkIndex | chunkContent | totalChunks
- ${0} | ${chunk1} | ${0}
- ${1} | ${chunk2} | ${3}
- ${2} | ${chunk3Result} | ${3}
- `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
- const chunk = findChunks().at(chunkIndex);
-
- expect(chunk.props('content')).toContain(chunkContent.trim());
-
- expect(chunk.props()).toMatchObject({
- totalLines: LINES_PER_CHUNK,
- startingFrom: LINES_PER_CHUNK * chunkIndex,
- totalChunks,
- });
- });
-
- it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
- findChunks().at(0).vm.$emit('appear');
- expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
- });
- });
-
- describe('LineHighlighter', () => {
- it('instantiates the lineHighlighter class', () => {
- expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
new file mode 100644
index 00000000000..715234e56fd
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
@@ -0,0 +1,45 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_new.vue';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
+import Tracking from '~/tracking';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
+
+jest.mock('~/blob/blob_links_tracking');
+
+describe('Source Viewer component', () => {
+ let wrapper;
+ const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(SourceViewer, {
+ propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
+ });
+ };
+
+ const findChunks = () => wrapper.findAllComponents(Chunk);
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ return createComponent();
+ });
+
+ describe('event tracking', () => {
+ it('fires a tracking event when the component is created', () => {
+ const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ });
+
+ it('adds blob links tracking', () => {
+ expect(addBlobLinksTracking).toHaveBeenCalled();
+ });
+ });
+
+ describe('rendering', () => {
+ it('renders a Chunk component for each chunk', () => {
+ expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
+ expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 46b582c3668..6b1d65c5a6a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -1,45 +1,192 @@
+import hljs from 'highlight.js/lib/core';
+import Vue from 'vue';
+import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
-import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
+import {
+ EVENT_ACTION,
+ EVENT_LABEL_VIEWER,
+ EVENT_LABEL_FALLBACK,
+ ROUGE_TO_HLJS_LANGUAGE_MAP,
+ LINES_PER_CHUNK,
+ LEGACY_FALLBACKS,
+ CODEOWNERS_FILE_NAME,
+ CODEOWNERS_LANGUAGE,
+} from '~/vue_shared/components/source_viewer/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import LineHighlighter from '~/blob/line_highlighter';
+import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
-import addBlobLinksTracking from '~/blob/blob_links_tracking';
-import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
-jest.mock('~/blob/blob_links_tracking');
+jest.mock('~/blob/line_highlighter');
+jest.mock('highlight.js/lib/core');
+jest.mock('~/vue_shared/components/source_viewer/plugins/index');
+Vue.use(VueRouter);
+const router = new VueRouter();
+
+const generateContent = (content, totalLines = 1, delimiter = '\n') => {
+ let generatedContent = '';
+ for (let i = 0; i < totalLines; i += 1) {
+ generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
+ }
+ return generatedContent;
+};
+
+const execImmediately = (callback) => callback();
describe('Source Viewer component', () => {
let wrapper;
- const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
+ const language = 'docker';
+ const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
+ const chunk1 = generateContent('// Some source code 1', 70);
+ const chunk2 = generateContent('// Some source code 2', 70);
+ const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
+ const chunk3Result = generateContent('// Some source code 3', 70, '\n');
+ const content = chunk1 + chunk2 + chunk3;
+ const path = 'some/path.js';
+ const blamePath = 'some/blame/path.js';
+ const fileType = 'javascript';
+ const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
+ const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
- const createComponent = () => {
+ const createComponent = async (blob = {}) => {
wrapper = shallowMountExtended(SourceViewer, {
- propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
+ router,
+ propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
});
+ await waitForPromises();
};
const findChunks = () => wrapper.findAllComponents(Chunk);
beforeEach(() => {
+ hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
+ hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
+ jest.spyOn(eventHub, '$emit');
jest.spyOn(Tracking, 'event');
+
return createComponent();
});
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
- const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
+ const eventData = { label: EVENT_LABEL_VIEWER, property: language };
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ });
+
+ it('does not emit an error event when the language is supported', () => {
+ expect(wrapper.emitted('error')).toBeUndefined();
+ });
+
+ it('fires a tracking event and emits an error when the language is not supported', () => {
+ const unsupportedLanguage = 'apex';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
+ createComponent({ language: unsupportedLanguage });
+
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+
+ describe('legacy fallbacks', () => {
+ it.each(LEGACY_FALLBACKS)(
+ 'tracks a fallback event and emits an error when viewing %s files',
+ (fallbackLanguage) => {
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
+ createComponent({ language: fallbackLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ },
+ );
+ });
+
+ describe('highlight.js', () => {
+ beforeEach(() => createComponent({ language: mappedLanguage }));
+
+ it('registers our plugins for Highlight.js', () => {
+ expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
+ });
+
+ it('registers the language definition', async () => {
+ const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ mappedLanguage,
+ languageDefinition.default,
+ );
});
- it('adds blob links tracking', () => {
- expect(addBlobLinksTracking).toHaveBeenCalled();
+ it('registers json language definition if fileType is package_json', async () => {
+ await createComponent({ language: 'json', fileType: 'package_json' });
+ const languageDefinition = await import(`highlight.js/lib/languages/json`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
+ });
+
+ it('correctly maps languages starting with uppercase', async () => {
+ await createComponent({ language: 'Ruby' });
+ const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
+ });
+
+ it('registers codeowners language definition if file name is CODEOWNERS', async () => {
+ await createComponent({ name: CODEOWNERS_FILE_NAME });
+ const languageDefinition = await import(
+ '~/vue_shared/components/source_viewer/languages/codeowners'
+ );
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ CODEOWNERS_LANGUAGE,
+ languageDefinition.default,
+ );
+ });
+
+ it('highlights the first chunk', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
+ expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
+ });
+
+ describe('auto-detects if a language cannot be loaded', () => {
+ beforeEach(() => createComponent({ language: 'some_unknown_language' }));
+
+ it('highlights the content with auto-detection', () => {
+ expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim());
+ });
});
});
describe('rendering', () => {
- it('renders a Chunk component for each chunk', () => {
- expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
- expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
+ it.each`
+ chunkIndex | chunkContent | totalChunks
+ ${0} | ${chunk1} | ${0}
+ ${1} | ${chunk2} | ${3}
+ ${2} | ${chunk3Result} | ${3}
+ `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
+ const chunk = findChunks().at(chunkIndex);
+
+ expect(chunk.props('content')).toContain(chunkContent.trim());
+
+ expect(chunk.props()).toMatchObject({
+ totalLines: LINES_PER_CHUNK,
+ startingFrom: LINES_PER_CHUNK * chunkIndex,
+ totalChunks,
+ });
+ });
+
+ it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
+ findChunks().at(0).vm.$emit('appear');
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
+ });
+ });
+
+ describe('LineHighlighter', () => {
+ it('instantiates the lineHighlighter class', () => {
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 7acbd769b6e..c3a98dcffb1 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -4,7 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { stubComponent } from 'helpers/stub_component';
+import { RENDER_ALL_SLOTS_TEMPLATE, stubComponent } from 'helpers/stub_component';
import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { resolvers } from '~/graphql_shared/issuable_client';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
@@ -68,11 +68,12 @@ describe('WorkItemLinks', () => {
show: showModal,
},
}),
+ WidgetWrapper: stubComponent(WidgetWrapper, {
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
},
});
- wrapper.vm.$refs.wrapper.show = jest.fn();
-
await waitForPromises();
};
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index c75f437a313..3a91b798bbd 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -170,15 +170,13 @@ RSpec.shared_examples 'variable list' do
expect(find('[data-testid="alert-danger"]').text).to have_content('(key) has already been taken')
end
- it 'prevents a variable to be added if no values are provided when a variable is set to masked' do
+ it 'allows variable to be added even if no value is provided' do
click_button('Add variable')
page.within('#add-ci-variable') do
find('[data-testid="pipeline-form-ci-variable-key"] input').set('empty_mask_key')
- find('[data-testid="ci-variable-protected-checkbox"]').click
- find('[data-testid="ci-variable-masked-checkbox"]').click
- expect(find_button('Add variable', disabled: true)).to be_present
+ expect(find_button('Add variable', disabled: false)).to be_present
end
end