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
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap8
-rw-r--r--spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap47
-rw-r--r--spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap (renamed from spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap)8
-rw-r--r--spec/frontend/alerts_settings/alerts_integrations_list_spec.js3
-rw-r--r--spec/frontend/alerts_settings/alerts_settings_form_old_spec.js204
-rw-r--r--spec/frontend/alerts_settings/alerts_settings_form_spec.js (renamed from spec/frontend/alerts_settings/alerts_settings_form_new_spec.js)2
-rw-r--r--spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js75
-rw-r--r--spec/frontend/blob/components/mock_data.js2
-rw-r--r--spec/frontend/boards/board_list_new_spec.js74
-rw-r--r--spec/frontend/boards/components/board_assignee_dropdown_spec.js70
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js152
-rw-r--r--spec/frontend/boards/mock_data.js6
-rw-r--r--spec/frontend/boards/stores/actions_spec.js68
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js18
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap2
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js3
-rw-r--r--spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap2
-rw-r--r--spec/frontend/diffs/components/app_spec.js105
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js7
-rw-r--r--spec/frontend/diffs/components/diff_expansion_cell_spec.js5
-rw-r--r--spec/frontend/diffs/store/actions_spec.js8
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js292
-rw-r--r--spec/frontend/diffs/store/utils_spec.js217
-rw-r--r--spec/frontend/environments/environment_actions_spec.js131
-rw-r--r--spec/frontend/fixtures/raw.rb15
-rw-r--r--spec/frontend/groups/components/app_spec.js2
-rw-r--r--spec/frontend/groups/components/group_item_spec.js49
-rw-r--r--spec/frontend/groups/members/components/app_spec.js7
-rw-r--r--spec/frontend/groups/store/groups_store_spec.js27
-rw-r--r--spec/frontend/groups/store/utils_spec.js44
-rw-r--r--spec/frontend/ide/components/terminal/session_spec.js11
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js16
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap2
-rw-r--r--spec/frontend/issuable/related_issues/components/issue_token_spec.js2
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap2
-rw-r--r--spec/frontend/jobs/components/log/line_spec.js63
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js1
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js (renamed from spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js)8
-rw-r--r--spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js (renamed from spec/frontend/vue_shared/components/members/action_buttons/approve_access_request_button_spec.js)2
-rw-r--r--spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js (renamed from spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js)8
-rw-r--r--spec/frontend/members/components/action_buttons/leave_button_spec.js (renamed from spec/frontend/vue_shared/components/members/action_buttons/leave_button_spec.js)8
-rw-r--r--spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js (renamed from spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js)4
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js (renamed from spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js)2
-rw-r--r--spec/frontend/members/components/action_buttons/resend_invite_button_spec.js (renamed from spec/frontend/vue_shared/components/members/action_buttons/resend_invite_button_spec.js)2
-rw-r--r--spec/frontend/members/components/action_buttons/user_action_buttons_spec.js (renamed from spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js)8
-rw-r--r--spec/frontend/members/components/avatars/group_avatar_spec.js (renamed from spec/frontend/vue_shared/components/members/avatars/group_avatar_spec.js)4
-rw-r--r--spec/frontend/members/components/avatars/invite_avatar_spec.js (renamed from spec/frontend/vue_shared/components/members/avatars/invite_avatar_spec.js)4
-rw-r--r--spec/frontend/members/components/avatars/user_avatar_spec.js (renamed from spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js)4
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js (renamed from spec/frontend/vue_shared/components/members/modals/leave_modal_spec.js)6
-rw-r--r--spec/frontend/members/components/modals/remove_group_link_modal_spec.js (renamed from spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js)6
-rw-r--r--spec/frontend/members/components/table/created_at_spec.js (renamed from spec/frontend/vue_shared/components/members/table/created_at_spec.js)2
-rw-r--r--spec/frontend/members/components/table/expiration_datepicker_spec.js (renamed from spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js)4
-rw-r--r--spec/frontend/members/components/table/expires_at_spec.js (renamed from spec/frontend/vue_shared/components/members/table/expires_at_spec.js)2
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js (renamed from spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js)14
-rw-r--r--spec/frontend/members/components/table/member_avatar_spec.js (renamed from spec/frontend/vue_shared/components/members/table/member_avatar_spec.js)12
-rw-r--r--spec/frontend/members/components/table/member_source_spec.js (renamed from spec/frontend/vue_shared/components/members/table/member_source_spec.js)2
-rw-r--r--spec/frontend/members/components/table/members_table_cell_spec.js (renamed from spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js)8
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js (renamed from spec/frontend/vue_shared/components/members/table/members_table_spec.js)20
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js (renamed from spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js)4
-rw-r--r--spec/frontend/members/mock_data.js (renamed from spec/frontend/vue_shared/components/members/mock_data.js)0
-rw-r--r--spec/frontend/members/store/actions_spec.js (renamed from spec/frontend/vuex_shared/modules/members/actions_spec.js)6
-rw-r--r--spec/frontend/members/store/mutations_spec.js (renamed from spec/frontend/vuex_shared/modules/members/mutations_spec.js)6
-rw-r--r--spec/frontend/members/store/utils_spec.js (renamed from spec/frontend/vuex_shared/modules/members/utils_spec.js)4
-rw-r--r--spec/frontend/members/utils_spec.js (renamed from spec/frontend/vue_shared/components/members/utils_spec.js)2
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js5
-rw-r--r--spec/frontend/notes/stores/actions_spec.js36
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js10
-rw-r--r--spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap61
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_form_spec.js116
-rw-r--r--spec/frontend/pipeline_editor/components/text_editor_spec.js17
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js3
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js363
-rw-r--r--spec/frontend/pipelines/test_reports/stores/getters_spec.js33
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js13
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js24
-rw-r--r--spec/frontend/registry/settings/components/expiration_dropdown_spec.js83
-rw-r--r--spec/frontend/registry/settings/components/expiration_run_text_spec.js66
-rw-r--r--spec/frontend/registry/settings/components/expiration_textarea_spec.js169
-rw-r--r--spec/frontend/registry/settings/components/expiration_toggle_spec.js80
-rw-r--r--spec/frontend/registry/settings/mock_data.js24
-rw-r--r--spec/frontend/registry/shared/stubs.js20
-rw-r--r--spec/frontend/sidebar/__snapshots__/todo_spec.js.snap5
-rw-r--r--spec/frontend/sidebar/issuable_assignees_spec.js12
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js51
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_spec.js5
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js3
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js68
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js18
-rw-r--r--spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap191
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap47
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js (renamed from spec/frontend/vue_shared/components/gl_mentions_spec.js)8
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js344
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap144
-rw-r--r--spec/frontend/vue_shared/components/security_reports/security_summary_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js239
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js318
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js373
-rw-r--r--spec/frontend/vue_shared/security_reports/store/getters_spec.js182
106 files changed, 3820 insertions, 1424 deletions
diff --git a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
index 5fad0d07f97..8948a9926bb 100644
--- a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
+++ b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
@@ -18,7 +18,9 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
theme="indigo"
value="0"
>
- <gl-tab-stub>
+ <gl-tab-stub
+ titlelinkclass=""
+ >
<div
class="mt-2"
@@ -37,7 +39,9 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
</div>
</gl-tab-stub>
- <gl-tab-stub>
+ <gl-tab-stub
+ titlelinkclass=""
+ >
<review-tab-container-stub
commits=""
diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap
deleted file mode 100644
index 9306bf24baf..00000000000
--- a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap
+++ /dev/null
@@ -1,47 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AlertsSettingsFormOld with default values renders the initial template 1`] = `
-"<gl-form-stub>
- <h5 class=\\"gl-font-lg gl-my-5\\"></h5>
- <!---->
- <div data-testid=\\"alert-settings-description\\">
- <p>
- <gl-sprintf-stub message=\\"You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.\\"></gl-sprintf-stub>
- </p>
- <p>
- <gl-sprintf-stub message=\\"Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.\\"></gl-sprintf-stub>
- </p>
- </div>
- <gl-form-group-stub label-for=\\"integration-type\\" label=\\"Integration\\">
- <gl-form-select-stub id=\\"integration-type\\" options=\\"[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"HTTP\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span>
- </gl-form-group-stub>
- <gl-form-group-stub label=\\"Active\\" label-for=\\"active\\">
- <toggle-button-stub id=\\"active\\"></toggle-button-stub>
- </gl-form-group-stub>
- <!---->
- <gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\">
- <gl-form-input-group-stub value=\\"/alerts/notify.json\\" predefinedoptions=\\"[object Object]\\" id=\\"url\\" readonly=\\"\\"></gl-form-input-group-stub> <span class=\\"gl-text-gray-500\\">
-
- </span>
- </gl-form-group-stub>
- <gl-form-group-stub label-for=\\"authorization-key\\">
- <gl-form-input-group-stub value=\\"\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub>
- <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
- <gl-modal-stub modalid=\\"tokenModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\">
- Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.
- </gl-modal-stub>
- </gl-form-group-stub>
- <gl-form-group-stub label=\\"Alert test payload\\" label-for=\\"alert-json\\">
- <gl-form-textarea-stub noresize=\\"true\\" id=\\"alert-json\\" disabled=\\"true\\" state=\\"true\\" placeholder=\\"Enter test alert JSON....\\" rows=\\"6\\" max-rows=\\"10\\"></gl-form-textarea-stub>
- </gl-form-group-stub>
- <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub>
- <div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between\\">
- <gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">
- Save changes
- </gl-button-stub>
- <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">
- Cancel
- </gl-button-stub>
- </div>
-</gl-form-stub>"
-`;
diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap
index e2ef7483316..a1ced8910b3 100644
--- a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap
+++ b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap
@@ -28,7 +28,7 @@ exports[`AlertsSettingsFormNew with default values renders the initial template
<div id=\\"integration-webhook\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-webhook__BV_label_\\" for=\\"integration-webhook\\" class=\\"d-block col-form-label\\">3. Set up webhook</label>
<div class=\\"bv-no-focus-ring\\"><span>Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the <a rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html\\" class=\\"gl-link gl-display-inline-block\\">GitLab documentation</a> to learn more about configuring your endpoint.</span> <label class=\\"gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal\\">
<div class=\\"gl-toggle-wrapper\\"><span class=\\"gl-toggle-label\\">Active</span>
- <!----> <button aria-label=\\"Active\\" type=\\"button\\" class=\\"gl-toggle\\"><span class=\\"toggle-icon\\"><svg data-testid=\\"close-icon\\" class=\\"gl-icon s16\\"><use href=\\"#close\\"></use></svg></span></button></div>
+ <!----> <button aria-label=\\"Active\\" type=\\"button\\" class=\\"gl-toggle\\"><span class=\\"toggle-icon\\"><svg data-testid=\\"close-icon\\" aria-hidden=\\"true\\" class=\\"gl-icon s16\\"><use href=\\"#close\\"></use></svg></span></button></div>
<!---->
</label>
<!---->
@@ -40,7 +40,7 @@ exports[`AlertsSettingsFormNew with default values renders the initial template
<!---->
<!----> <input id=\\"url\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\">
<div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\">
- <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" class=\\"gl-button-icon gl-icon s16\\">
+ <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" aria-hidden=\\"true\\" class=\\"gl-button-icon gl-icon s16\\">
<use href=\\"#copy-to-clipboard\\"></use>
</svg>
<!----></button></div>
@@ -56,7 +56,7 @@ exports[`AlertsSettingsFormNew with default values renders the initial template
<!---->
<!----> <input id=\\"authorization-key\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\">
<div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\">
- <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" class=\\"gl-button-icon gl-icon s16\\">
+ <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" aria-hidden=\\"true\\" class=\\"gl-button-icon gl-icon s16\\">
<use href=\\"#copy-to-clipboard\\"></use>
</svg>
<!----></button></div>
@@ -87,7 +87,7 @@ exports[`AlertsSettingsFormNew with default values renders the initial template
<div class=\\"gl-display-flex gl-justify-content-start gl-py-3\\"><button data-testid=\\"integration-form-submit\\" type=\\"submit\\" class=\\"btn js-no-auto-disable btn-success btn-md gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">Save integration
- </span></button> <button data-testid=\\"integration-test-and-submit\\" type=\\"button\\" class=\\"btn gl-mx-3 js-no-auto-disable btn-success btn-md gl-button btn-success-secondary\\">
+ </span></button> <button data-testid=\\"integration-test-and-submit\\" type=\\"button\\" disabled=\\"disabled\\" class=\\"btn gl-mx-3 js-no-auto-disable btn-success btn-md disabled gl-button btn-success-secondary\\">
<!---->
<!----> <span class=\\"gl-button-text\\">Save and test payload</span></button> <button type=\\"reset\\" class=\\"btn js-no-auto-disable btn-default btn-md gl-button\\">
<!---->
diff --git a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/alerts_integrations_list_spec.js
index 90bb38f0c2b..3a7392f64f7 100644
--- a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js
+++ b/spec/frontend/alerts_settings/alerts_integrations_list_spec.js
@@ -35,9 +35,6 @@ describe('AlertIntegrationsList', () => {
integrations: mockIntegrations,
...props,
},
- provide: {
- glFeatures: { httpIntegrationsList: true },
- },
stubs: {
GlIcon: true,
GlButton: true,
diff --git a/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js
deleted file mode 100644
index 3d0dfb44d63..00000000000
--- a/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js
+++ /dev/null
@@ -1,204 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlModal, GlAlert } from '@gitlab/ui';
-import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_old.vue';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-import { i18n } from '~/alerts_settings/constants';
-import service from '~/alerts_settings/services';
-import { defaultAlertSettingsConfig } from './util';
-
-jest.mock('~/alerts_settings/services');
-
-describe('AlertsSettingsFormOld', () => {
- let wrapper;
-
- const createComponent = ({ methods } = {}, data) => {
- wrapper = shallowMount(AlertsSettingsForm, {
- data() {
- return { ...data };
- },
- provide: {
- ...defaultAlertSettingsConfig,
- },
- methods,
- });
- };
-
- const findSelect = () => wrapper.find('[data-testid="alert-settings-select"]');
- const findJsonInput = () => wrapper.find('#alert-json');
- const findUrl = () => wrapper.find('#url');
- const findAuthorizationKey = () => wrapper.find('#authorization-key');
- const findApiUrl = () => wrapper.find('#api-url');
-
- beforeEach(() => {
- setFixtures(`
- <div>
- <span class="js-service-active-status fa fa-circle" data-value="true"></span>
- <span class="js-service-active-status fa fa-power-off" data-value="false"></span>
- </div>`);
- });
-
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
- describe('with default values', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders the initial template', () => {
- expect(wrapper.html()).toMatchSnapshot();
- });
- });
-
- describe('reset key', () => {
- it('triggers resetKey method', () => {
- const resetKey = jest.fn();
- const methods = { resetKey };
- createComponent({ methods });
-
- wrapper.find(GlModal).vm.$emit('ok');
-
- expect(resetKey).toHaveBeenCalled();
- });
-
- it('updates the authorization key on success', () => {
- createComponent(
- {},
- {
- token: 'newToken',
- },
- );
-
- expect(findAuthorizationKey().attributes('value')).toBe('newToken');
- });
-
- it('shows a alert message on error', () => {
- service.updateGenericKey.mockRejectedValueOnce({});
-
- createComponent();
-
- return wrapper.vm.resetKey().then(() => {
- expect(wrapper.find(GlAlert).exists()).toBe(true);
- });
- });
- });
-
- describe('activate toggle', () => {
- it('triggers toggleActivated method', () => {
- const toggleService = jest.fn();
- const methods = { toggleService };
- createComponent({ methods });
-
- wrapper.find(ToggleButton).vm.$emit('change', true);
- expect(toggleService).toHaveBeenCalled();
- });
-
- describe('error is encountered', () => {
- it('restores previous value', () => {
- service.updateGenericKey.mockRejectedValueOnce({});
- createComponent();
- return wrapper.vm.resetKey().then(() => {
- expect(wrapper.find(ToggleButton).props('value')).toBe(false);
- });
- });
- });
- });
-
- describe('prometheus is active', () => {
- beforeEach(() => {
- createComponent(
- {},
- {
- selectedIntegration: 'PROMETHEUS',
- },
- );
- });
-
- it('renders a valid "select"', () => {
- expect(findSelect().exists()).toBe(true);
- });
-
- it('shows the API URL input', () => {
- expect(findApiUrl().exists()).toBe(true);
- });
-
- it('shows the correct default API URL', () => {
- expect(findUrl().attributes('value')).toBe(defaultAlertSettingsConfig.prometheus.url);
- });
- });
-
- describe('Opsgenie is active', () => {
- beforeEach(() => {
- createComponent(
- {},
- {
- selectedIntegration: 'OPSGENIE',
- },
- );
- });
-
- it('shows a input for the Opsgenie target URL', () => {
- expect(findApiUrl().exists()).toBe(true);
- });
- });
-
- describe('trigger test alert', () => {
- beforeEach(() => {
- createComponent({});
- });
-
- it('should enable the JSON input', () => {
- expect(findJsonInput().exists()).toBe(true);
- expect(findJsonInput().props('value')).toBe(null);
- });
-
- it('should validate JSON input', async () => {
- createComponent(true, {
- testAlertJson: '{ "value": "test" }',
- });
-
- findJsonInput().vm.$emit('change');
-
- await wrapper.vm.$nextTick();
-
- expect(findJsonInput().attributes('state')).toBe('true');
- });
-
- describe('alert service is toggled', () => {
- describe('error handling', () => {
- const toggleService = true;
-
- it('should show generic error', async () => {
- service.updateGenericActive.mockRejectedValueOnce({});
-
- createComponent();
-
- await wrapper.vm.toggleActivated(toggleService);
- expect(wrapper.vm.active).toBe(false);
- expect(wrapper.find(GlAlert).attributes('variant')).toBe('danger');
- expect(wrapper.find(GlAlert).text()).toBe(i18n.errorMsg);
- });
-
- it('should show first field specific error when available', async () => {
- const err1 = "can't be blank";
- const err2 = 'is not a valid URL';
- const key = 'api_url';
- service.updateGenericActive.mockRejectedValueOnce({
- response: { data: { errors: { [key]: [err1, err2] } } },
- });
-
- createComponent();
-
- await wrapper.vm.toggleActivated(toggleService);
-
- expect(wrapper.find(GlAlert).text()).toContain(i18n.errorMsg);
- expect(wrapper.find(GlAlert).text()).toContain(`${key} ${err1}`);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_spec.js
index fbd482b1906..dd7d32c5e75 100644
--- a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js
+++ b/spec/frontend/alerts_settings/alerts_settings_form_spec.js
@@ -8,7 +8,7 @@ import {
GlFormTextarea,
} from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
-import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_new.vue';
+import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
import { defaultAlertSettingsConfig } from './util';
import { typeSet } from '~/alerts_settings/constants';
diff --git a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js
index 7384cf9a095..fe187d9e8f9 100644
--- a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js
@@ -1,12 +1,13 @@
import VueApollo from 'vue-apollo';
import { mount, createLocalVue } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
-import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue';
-import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue';
+import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
@@ -75,7 +76,6 @@ describe('AlertsSettingsWrapper', () => {
},
provide: {
...defaultAlertSettingsConfig,
- glFeatures: { httpIntegrationsList: false },
...provide,
},
mocks: {
@@ -110,39 +110,25 @@ describe('AlertsSettingsWrapper', () => {
apolloProvider: fakeApollo,
provide: {
...defaultAlertSettingsConfig,
- glFeatures: { httpIntegrationsList: true },
},
});
}
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
+ wrapper = null;
});
- describe('with httpIntegrationsList feature flag disabled', () => {
- it('renders data driven alerts integrations list and old form by default', () => {
- createComponent();
- expect(wrapper.find(IntegrationsList).exists()).toBe(true);
- expect(wrapper.find(AlertsSettingsFormOld).exists()).toBe(true);
- expect(wrapper.find(AlertsSettingsFormNew).exists()).toBe(false);
- });
- });
-
- describe('with httpIntegrationsList feature flag enabled', () => {
+ describe('rendered via default permissions', () => {
it('renders the GraphQL alerts integrations list and new form', () => {
- createComponent({ provide: { glFeatures: { httpIntegrationsList: true } } });
+ createComponent();
expect(wrapper.find(IntegrationsList).exists()).toBe(true);
- expect(wrapper.find(AlertsSettingsFormOld).exists()).toBe(false);
- expect(wrapper.find(AlertsSettingsFormNew).exists()).toBe(true);
+ expect(wrapper.find(AlertsSettingsForm).exists()).toBe(true);
});
it('uses a loading state inside the IntegrationsList table', () => {
createComponent({
data: { integrations: {} },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: true,
});
expect(wrapper.find(IntegrationsList).exists()).toBe(true);
@@ -152,7 +138,6 @@ describe('AlertsSettingsWrapper', () => {
it('renders the IntegrationsList table using the API data', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
expect(findLoader().exists()).toBe(false);
@@ -162,14 +147,13 @@ describe('AlertsSettingsWrapper', () => {
it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { createHttpIntegrationMutation: { integration: { id: '1' } } },
});
- wrapper.find(AlertsSettingsFormNew).vm.$emit('create-new-integration', {
+ wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {
type: typeSet.http,
variables: createHttpVariables,
});
@@ -185,14 +169,13 @@ describe('AlertsSettingsWrapper', () => {
it('calls `$apollo.mutate` with `updateHttpIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { updateHttpIntegrationMutation: { integration: { id: '1' } } },
});
- wrapper.find(AlertsSettingsFormNew).vm.$emit('update-integration', {
+ wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {
type: typeSet.http,
variables: updateHttpVariables,
});
@@ -206,14 +189,13 @@ describe('AlertsSettingsWrapper', () => {
it('calls `$apollo.mutate` with `resetHttpTokenMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { resetHttpTokenMutation: { integration: { id: '1' } } },
});
- wrapper.find(AlertsSettingsFormNew).vm.$emit('reset-token', {
+ wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
type: typeSet.http,
variables: { id: ID },
});
@@ -229,14 +211,13 @@ describe('AlertsSettingsWrapper', () => {
it('calls `$apollo.mutate` with `createPrometheusIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } },
});
- wrapper.find(AlertsSettingsFormNew).vm.$emit('create-new-integration', {
+ wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {
type: typeSet.prometheus,
variables: createPrometheusVariables,
});
@@ -252,14 +233,13 @@ describe('AlertsSettingsWrapper', () => {
it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { updatePrometheusIntegrationMutation: { integration: { id: '2' } } },
});
- wrapper.find(AlertsSettingsFormNew).vm.$emit('update-integration', {
+ wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {
type: typeSet.prometheus,
variables: updatePrometheusVariables,
});
@@ -273,14 +253,13 @@ describe('AlertsSettingsWrapper', () => {
it('calls `$apollo.mutate` with `resetPrometheusTokenMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { resetPrometheusTokenMutation: { integration: { id: '1' } } },
});
- wrapper.find(AlertsSettingsFormNew).vm.$emit('reset-token', {
+ wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
type: typeSet.prometheus,
variables: { id: ID },
});
@@ -296,12 +275,11 @@ describe('AlertsSettingsWrapper', () => {
it('shows an error alert when integration creation fails ', async () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR);
- wrapper.find(AlertsSettingsFormNew).vm.$emit('create-new-integration', {});
+ wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {});
await waitForPromises();
@@ -311,13 +289,12 @@ describe('AlertsSettingsWrapper', () => {
it('shows an error alert when integration token reset fails ', async () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR);
- wrapper.find(AlertsSettingsFormNew).vm.$emit('reset-token', {});
+ wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {});
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR });
@@ -326,30 +303,30 @@ describe('AlertsSettingsWrapper', () => {
it('shows an error alert when integration update fails ', async () => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg);
- wrapper.find(AlertsSettingsFormNew).vm.$emit('update-integration', {});
+ wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {});
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR });
});
it('shows an error alert when integration test payload fails ', async () => {
+ const mock = new AxiosMockAdapter(axios);
+ mock.onPost(/(.*)/).replyOnce(403);
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
- wrapper.find(AlertsSettingsFormNew).vm.$emit('test-payload-failure');
-
- await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
- expect(createFlash).toHaveBeenCalledTimes(1);
+ return wrapper.vm.validateAlertPayload({ endpoint: '', data: '', token: '' }).then(() => {
+ expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ mock.restore();
+ });
});
});
@@ -405,7 +382,7 @@ describe('AlertsSettingsWrapper', () => {
it.each([true, false])('it shows/hides the alert when opsgenie is %s', active => {
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
- provide: { glFeatures: { httpIntegrationsList: true }, opsgenie: { active } },
+ provide: { opsgenie: { active } },
loading: false,
});
diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js
index 8cfcec2693c..95789ca13cb 100644
--- a/spec/frontend/blob/components/mock_data.js
+++ b/spec/frontend/blob/components/mock_data.js
@@ -55,5 +55,3 @@ export const SimpleBlobContentMock = {
path: 'foo.js',
plainData: 'Plain',
};
-
-export default {};
diff --git a/spec/frontend/boards/board_list_new_spec.js b/spec/frontend/boards/board_list_new_spec.js
index 55516e3fd56..585c0f37329 100644
--- a/spec/frontend/boards/board_list_new_spec.js
+++ b/spec/frontend/boards/board_list_new_spec.js
@@ -1,5 +1,4 @@
/* global List */
-/* global ListIssue */
import Vuex from 'vuex';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
@@ -7,9 +6,8 @@ import { createLocalVue, mount } from '@vue/test-utils';
import eventHub from '~/boards/eventhub';
import BoardList from '~/boards/components/board_list_new.vue';
import BoardCard from '~/boards/components/board_card.vue';
-import '~/boards/models/issue';
import '~/boards/models/list';
-import { listObj, mockIssuesByListId, issues } from './mock_data';
+import { listObj, mockIssuesByListId, issues, mockIssues } from './mock_data';
import defaultState from '~/boards/stores/state';
const localVue = createLocalVue();
@@ -52,7 +50,7 @@ const createComponent = ({
...listProps,
doNotFetchIssues: true,
});
- const issue = new ListIssue({
+ const issue = {
title: 'Testing',
id: 1,
iid: 1,
@@ -60,7 +58,7 @@ const createComponent = ({
labels: [],
assignees: [],
...listIssueProps,
- });
+ };
if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) {
list.issuesSize = 1;
}
@@ -71,6 +69,7 @@ const createComponent = ({
disabled: false,
list,
issues: [issue],
+ canAdminList: true,
...componentProps,
},
store,
@@ -87,17 +86,19 @@ const createComponent = ({
describe('Board list component', () => {
let wrapper;
+ const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
useFakeRequestAnimationFrame();
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
describe('When Expanded', () => {
beforeEach(() => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders component', () => {
expect(wrapper.find('.board-list-component').exists()).toBe(true);
});
@@ -107,7 +108,7 @@ describe('Board list component', () => {
state: { listsFlags: { 'gid://gitlab/List/1': { isLoading: true } } },
});
- expect(wrapper.find('[data-testid="board_list_loading"').exists()).toBe(true);
+ expect(findByTestId('board_list_loading').exists()).toBe(true);
});
it('renders issues', () => {
@@ -171,19 +172,15 @@ describe('Board list component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('loads more issues after scrolling', () => {
- wrapper.vm.$refs.list.dispatchEvent(new Event('scroll'));
+ wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
expect(actions.fetchIssuesForList).toHaveBeenCalled();
});
it('does not load issues if already loading', () => {
- wrapper.vm.$refs.list.dispatchEvent(new Event('scroll'));
- wrapper.vm.$refs.list.dispatchEvent(new Event('scroll'));
+ wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
+ wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
expect(actions.fetchIssuesForList).toHaveBeenCalledTimes(1);
});
@@ -204,10 +201,6 @@ describe('Board list component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when issue count exceeds max issue count', () => {
it('sets background to bg-danger-100', async () => {
wrapper.setProps({ list: { issuesSize: 4, maxIssueCount: 3 } });
@@ -233,4 +226,43 @@ describe('Board list component', () => {
});
});
});
+
+ describe('drag & drop issue', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('handleDragOnStart', () => {
+ it('adds a class `is-dragging` to document body', () => {
+ expect(document.body.classList.contains('is-dragging')).toBe(false);
+
+ findByTestId('tree-root-wrapper').vm.$emit('start');
+
+ expect(document.body.classList.contains('is-dragging')).toBe(true);
+ });
+ });
+
+ describe('handleDragOnEnd', () => {
+ it('removes class `is-dragging` from document body', () => {
+ jest.spyOn(wrapper.vm, 'moveIssue').mockImplementation(() => {});
+ document.body.classList.add('is-dragging');
+
+ findByTestId('tree-root-wrapper').vm.$emit('end', {
+ oldIndex: 1,
+ newIndex: 0,
+ item: {
+ dataset: {
+ issueId: mockIssues[0].id,
+ issueIid: mockIssues[0].iid,
+ issuePath: mockIssues[0].referencePath,
+ },
+ },
+ to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } },
+ from: { dataset: { listId: 'gid://gitlab/List/2' } },
+ });
+
+ expect(document.body.classList.contains('is-dragging')).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
index e185a6d5419..5ddd00f7fd4 100644
--- a/spec/frontend/boards/components/board_assignee_dropdown_spec.js
+++ b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
@@ -1,5 +1,11 @@
import { mount, createLocalVue } from '@vue/test-utils';
-import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlDropdownItem,
+ GlAvatarLink,
+ GlAvatarLabeled,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue';
@@ -20,12 +26,13 @@ describe('BoardCardAssigneeDropdown', () => {
let fakeApollo;
let getIssueParticipantsSpy;
let getSearchUsersSpy;
+ let dispatchSpy;
const iid = '111';
const activeIssueName = 'test';
const anotherIssueName = 'hello';
- const createComponent = (search = '') => {
+ const createComponent = (search = '', loading = false) => {
wrapper = mount(BoardAssigneeDropdown, {
data() {
return {
@@ -39,6 +46,15 @@ describe('BoardCardAssigneeDropdown', () => {
canUpdate: true,
rootPath: '',
},
+ mocks: {
+ $apollo: {
+ queries: {
+ participants: {
+ loading,
+ },
+ },
+ },
+ },
});
};
@@ -82,6 +98,8 @@ describe('BoardCardAssigneeDropdown', () => {
return wrapper.findAll(GlDropdownItem).wrappers.find(node => node.text().indexOf(text) === 0);
};
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
beforeEach(() => {
store.state.activeId = '1';
store.state.issues = {
@@ -91,10 +109,11 @@ describe('BoardCardAssigneeDropdown', () => {
},
};
- jest.spyOn(store, 'dispatch').mockResolvedValue();
+ dispatchSpy = jest.spyOn(store, 'dispatch').mockResolvedValue();
});
afterEach(() => {
+ window.gon = {};
jest.restoreAllMocks();
});
@@ -243,6 +262,30 @@ describe('BoardCardAssigneeDropdown', () => {
},
);
+ describe('when participants is loading', () => {
+ beforeEach(() => {
+ createComponent('', true);
+ });
+
+ it('finds a loading icon in the dropdown', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when participants is loading is false', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not find GlLoading icon in the dropdown', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('finds at least 1 GlDropdownItem', () => {
+ expect(wrapper.findAll(GlDropdownItem).length).toBeGreaterThan(0);
+ });
+ });
+
describe('Apollo', () => {
beforeEach(() => {
getIssueParticipantsSpy = jest.fn().mockResolvedValue({
@@ -305,4 +348,25 @@ describe('BoardCardAssigneeDropdown', () => {
expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true);
});
+
+ describe('when assign-self is emitted from IssuableAssignees', () => {
+ const currentUser = { username: 'self', name: '', id: '' };
+
+ beforeEach(() => {
+ window.gon = { current_username: currentUser.username };
+
+ dispatchSpy.mockResolvedValue([currentUser]);
+ createComponent();
+
+ wrapper.find(IssuableAssignees).vm.$emit('assign-self');
+ });
+
+ it('calls setAssignees with currentUser', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('setAssignees', currentUser.username);
+ });
+
+ it('adds the user to the selected list', async () => {
+ expect(findByText(currentUser.username).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
new file mode 100644
index 00000000000..74d88d9f34c
--- /dev/null
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
@@ -0,0 +1,152 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mockMilestone as TEST_MILESTONE } from 'jest/boards/mock_data';
+import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
+import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import { createStore } from '~/boards/stores';
+import createFlash from '~/flash';
+
+const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, referencePath: 'h/b#2' };
+
+jest.mock('~/flash');
+
+describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => {
+ let wrapper;
+ let store;
+
+ afterEach(() => {
+ wrapper.destroy();
+ store = null;
+ wrapper = null;
+ });
+
+ const createWrapper = ({ milestone = null } = {}) => {
+ store = createStore();
+ store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } };
+ store.state.activeId = TEST_ISSUE.id;
+
+ wrapper = shallowMount(BoardSidebarMilestoneSelect, {
+ store,
+ provide: {
+ canUpdate: true,
+ },
+ data: () => ({
+ milestones: [TEST_MILESTONE],
+ }),
+ stubs: {
+ 'board-editable-item': BoardEditableItem,
+ },
+ mocks: {
+ $apollo: {
+ loading: false,
+ },
+ },
+ });
+ };
+
+ const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findDropdownItem = () => wrapper.find('[data-testid="milestone-item"]');
+ const findUnsetMilestoneItem = () => wrapper.find('[data-testid="no-milestone-item"]');
+ const findNoMilestonesFoundItem = () => wrapper.find('[data-testid="no-milestones-found"]');
+
+ it('renders "None" when no milestone is selected', () => {
+ createWrapper();
+
+ expect(findCollapsed().text()).toBe('None');
+ });
+
+ it('renders milestone title when set', () => {
+ createWrapper({ milestone: TEST_MILESTONE });
+
+ expect(findCollapsed().text()).toContain(TEST_MILESTONE.title);
+ });
+
+ it('shows loader while Apollo is loading', async () => {
+ createWrapper({ milestone: TEST_MILESTONE });
+
+ expect(findLoader().exists()).toBe(false);
+
+ wrapper.vm.$apollo.loading = true;
+ await wrapper.vm.$nextTick();
+
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('shows message when error or no milestones found', async () => {
+ createWrapper();
+
+ wrapper.setData({ milestones: [] });
+ await wrapper.vm.$nextTick();
+
+ expect(findNoMilestonesFoundItem().text()).toBe('No milestones found');
+ });
+
+ describe('when milestone is selected', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
+ store.state.issues[TEST_ISSUE.id].milestone = TEST_MILESTONE;
+ });
+ findDropdownItem().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders selected milestone', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toContain(TEST_MILESTONE.title);
+ });
+
+ it('commits change to the server', () => {
+ expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({
+ milestoneId: TEST_MILESTONE.id,
+ projectPath: 'h/b',
+ });
+ });
+ });
+
+ describe('when milestone is set to "None"', () => {
+ beforeEach(async () => {
+ createWrapper({ milestone: TEST_MILESTONE });
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
+ store.state.issues[TEST_ISSUE.id].milestone = null;
+ });
+ findUnsetMilestoneItem().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders "None"', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toBe('None');
+ });
+
+ it('commits change to the server', () => {
+ expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({
+ milestoneId: null,
+ projectPath: 'h/b',
+ });
+ });
+ });
+
+ describe('when the mutation fails', () => {
+ const testMilestone = { id: '1', title: 'Former milestone' };
+
+ beforeEach(async () => {
+ createWrapper({ milestone: testMilestone });
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
+ throw new Error(['failed mutation']);
+ });
+ findDropdownItem().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders former milestone', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toContain(testMilestone.title);
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 58f67231d55..a81db9c8404 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,10 +1,8 @@
-/* global ListIssue */
/* global List */
import Vue from 'vue';
import { keyBy } from 'lodash';
import '~/boards/models/list';
-import '~/boards/models/issue';
import boardsStore from '~/boards/stores/boards_store';
export const boardObj = {
@@ -184,8 +182,6 @@ export const mockActiveIssue = {
emailsDisabled: false,
};
-export const mockIssueWithModel = new ListIssue(mockIssue);
-
export const mockIssue2 = {
id: 'gid://gitlab/Issue/437',
iid: 28,
@@ -203,8 +199,6 @@ export const mockIssue2 = {
},
};
-export const mockIssue2WithModel = new ListIssue(mockIssue2);
-
export const mockIssue3 = {
id: 'gid://gitlab/Issue/438',
iid: 29,
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 4d529580a7a..f168e427431 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -4,10 +4,10 @@ import {
mockLists,
mockListsById,
mockIssue,
- mockIssueWithModel,
- mockIssue2WithModel,
+ mockIssue2,
rawIssue,
mockIssues,
+ mockMilestone,
labels,
mockActiveIssue,
} from '../mock_data';
@@ -499,8 +499,8 @@ describe('moveIssue', () => {
};
const issues = {
- '436': mockIssueWithModel,
- '437': mockIssue2WithModel,
+ '436': mockIssue,
+ '437': mockIssue2,
};
const state = {
@@ -536,7 +536,7 @@ describe('moveIssue', () => {
{
type: types.MOVE_ISSUE,
payload: {
- originalIssue: mockIssueWithModel,
+ originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
},
@@ -611,7 +611,7 @@ describe('moveIssue', () => {
{
type: types.MOVE_ISSUE,
payload: {
- originalIssue: mockIssueWithModel,
+ originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
},
@@ -619,7 +619,7 @@ describe('moveIssue', () => {
{
type: types.MOVE_ISSUE_FAILURE,
payload: {
- originalIssue: mockIssueWithModel,
+ originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
originalIndex: 0,
@@ -885,6 +885,60 @@ describe('setActiveIssueSubscribed', () => {
});
});
+describe('setActiveIssueMilestone', () => {
+ const state = { issues: { [mockIssue.id]: mockIssue } };
+ const getters = { activeIssue: mockIssue };
+ const testMilestone = {
+ ...mockMilestone,
+ id: 'gid://gitlab/Milestone/1',
+ };
+ const input = {
+ milestoneId: testMilestone.id,
+ projectPath: 'h/b',
+ };
+
+ it('should commit milestone after setting the issue', done => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ updateIssue: {
+ issue: {
+ milestone: testMilestone,
+ },
+ errors: [],
+ },
+ },
+ });
+
+ const payload = {
+ issueId: getters.activeIssue.id,
+ prop: 'milestone',
+ value: testMilestone,
+ };
+
+ testAction(
+ actions.setActiveIssueMilestone,
+ input,
+ { ...state, ...getters },
+ [
+ {
+ type: types.UPDATE_ISSUE_BY_ID,
+ payload,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('throws error if fails', async () => {
+ jest
+ .spyOn(gqlClient, 'mutate')
+ .mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
+
+ await expect(actions.setActiveIssueMilestone({ getters }, input)).rejects.toThrow(Error);
+ });
+});
+
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index e1e57a8fd43..65b37007b5c 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,15 +1,7 @@
import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state';
-import {
- mockListsWithModel,
- mockLists,
- rawIssue,
- mockIssue,
- mockIssue2,
- mockIssueWithModel,
- mockIssue2WithModel,
-} from '../mock_data';
+import { mockListsWithModel, mockLists, rawIssue, mockIssue, mockIssue2 } from '../mock_data';
const expectNotImplemented = action => {
it('is not implemented', () => {
@@ -355,8 +347,8 @@ describe('Board Store Mutations', () => {
};
const issues = {
- '1': mockIssueWithModel,
- '2': mockIssue2WithModel,
+ '1': mockIssue,
+ '2': mockIssue2,
};
state = {
@@ -367,7 +359,7 @@ describe('Board Store Mutations', () => {
};
mutations.MOVE_ISSUE(state, {
- originalIssue: mockIssue2WithModel,
+ originalIssue: mockIssue2,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
});
@@ -396,7 +388,7 @@ describe('Board Store Mutations', () => {
issue: rawIssue,
});
- expect(state.issues).toEqual({ '436': { ...mockIssueWithModel, id: 436 } });
+ expect(state.issues).toEqual({ '436': { ...mockIssue, id: 436 } });
});
});
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index de40e03b598..6f28573c808 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -53,6 +53,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
type="button"
>
<svg
+ aria-hidden="true"
class="gl-icon s16 gl-new-dropdown-item-check-icon"
data-testid="mobile-issue-close-icon"
>
@@ -107,6 +108,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
type="button"
>
<svg
+ aria-hidden="true"
class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden"
data-testid="mobile-issue-close-icon"
>
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index ed862818c7b..381a4717127 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -50,6 +50,7 @@ describe('Clusters Store', () => {
expect(store.state).toEqual({
helpPath: null,
+ helmHelpPath: null,
ingressHelpPath: null,
environmentsHelpPath: null,
clustersHelpPath: null,
@@ -62,7 +63,7 @@ describe('Clusters Store', () => {
rbac: false,
applications: {
helm: {
- title: 'Helm Tiller',
+ title: 'Legacy Helm Tiller server',
status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason,
requestReason: null,
diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
index 62e527a2c5f..b59d1597a12 100644
--- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
+++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
@@ -17,6 +17,7 @@ exports[`Code navigation popover component renders popover 1`] = `
>
<gl-tab-stub
title="Definition"
+ titlelinkclass=""
>
<div
class="overflow-auto code-navigation-popover-container"
@@ -76,6 +77,7 @@ exports[`Code navigation popover component renders popover 1`] = `
<gl-tab-stub
class="py-2"
data-testid="references-tab"
+ titlelinkclass=""
>
<p
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 225710eab63..3f7d4bfe9f1 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -12,7 +12,6 @@ import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import CommitWidget from '~/diffs/components/commit_widget.vue';
import TreeList from '~/diffs/components/tree_list.vue';
-import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
import createDiffsStore from '../create_diffs_store';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
@@ -75,12 +74,6 @@ describe('diffs/components/app', () => {
});
}
- function getOppositeViewType(currentViewType) {
- return currentViewType === INLINE_DIFF_VIEW_TYPE
- ? PARALLEL_DIFF_VIEW_TYPE
- : INLINE_DIFF_VIEW_TYPE;
- }
-
beforeEach(() => {
// setup globals (needed for component to mount :/)
window.mrTabs = {
@@ -125,104 +118,6 @@ describe('diffs/components/app', () => {
wrapper.vm.$nextTick(done);
});
- describe('when the diff view type changes and it should load a single diff view style', () => {
- const noLinesDiff = {
- highlighted_diff_lines: [],
- parallel_diff_lines: [],
- };
- const parallelLinesDiff = {
- highlighted_diff_lines: [],
- parallel_diff_lines: ['line'],
- };
- const inlineLinesDiff = {
- highlighted_diff_lines: ['line'],
- parallel_diff_lines: [],
- };
- const fullDiff = {
- highlighted_diff_lines: ['line'],
- parallel_diff_lines: ['line'],
- };
-
- function expectFetchToOccur({ vueInstance, done = () => {}, existingFiles = 1 } = {}) {
- vueInstance.$nextTick(() => {
- expect(vueInstance.diffFiles.length).toEqual(existingFiles);
- expect(vueInstance.fetchDiffFilesBatch).toHaveBeenCalled();
-
- done();
- });
- }
-
- it('fetches diffs if it has none', done => {
- wrapper.vm.isLatestVersion = () => false;
-
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expectFetchToOccur({ vueInstance: wrapper.vm, existingFiles: 0, done });
- });
-
- it('fetches diffs if it has both view styles, but no lines in either', done => {
- wrapper.vm.isLatestVersion = () => false;
-
- store.state.diffs.diffFiles.push(noLinesDiff);
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expectFetchToOccur({ vueInstance: wrapper.vm, done });
- });
-
- it('fetches diffs if it only has inline view style', done => {
- wrapper.vm.isLatestVersion = () => false;
-
- store.state.diffs.diffFiles.push(inlineLinesDiff);
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expectFetchToOccur({ vueInstance: wrapper.vm, done });
- });
-
- it('fetches diffs if it only has parallel view style', done => {
- wrapper.vm.isLatestVersion = () => false;
-
- store.state.diffs.diffFiles.push(parallelLinesDiff);
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expectFetchToOccur({ vueInstance: wrapper.vm, done });
- });
-
- it('fetches batch diffs if it has none', done => {
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expectFetchToOccur({ vueInstance: wrapper.vm, existingFiles: 0, done });
- });
-
- it('fetches batch diffs if it has both view styles, but no lines in either', done => {
- store.state.diffs.diffFiles.push(noLinesDiff);
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expectFetchToOccur({ vueInstance: wrapper.vm, done });
- });
-
- it('fetches batch diffs if it only has inline view style', done => {
- store.state.diffs.diffFiles.push(inlineLinesDiff);
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expectFetchToOccur({ vueInstance: wrapper.vm, done });
- });
-
- it('fetches batch diffs if it only has parallel view style', done => {
- store.state.diffs.diffFiles.push(parallelLinesDiff);
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expectFetchToOccur({ vueInstance: wrapper.vm, done });
- });
-
- it('does not fetch batch diffs if it has already fetched both styles of diff', () => {
- store.state.diffs.diffFiles.push(fullDiff);
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expect(wrapper.vm.diffFiles.length).toEqual(1);
- expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
- });
- });
-
it('calls batch methods if diffsBatchLoad is enabled, and not latest version', done => {
expect(wrapper.vm.diffFilesLength).toEqual(0);
wrapper.vm.isLatestVersion = () => false;
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 9e4fcddd1b4..8a7eb6aaca6 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -84,7 +84,7 @@ describe('diffs/components/commit_item', () => {
it('renders commit sha', () => {
const shaElement = getShaElement();
- const labelElement = shaElement.find('[data-testid="commit-sha-group"] button');
+ const labelElement = shaElement.find('[data-testid="commit-sha-short-id"]');
const buttonElement = shaElement.find('button.input-group-text');
expect(labelElement.text()).toEqual(commit.short_id);
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index e3a6f7f16a9..c0532e392d1 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -12,7 +12,6 @@ import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import diffFileMockData from '../mock_data/diff_file';
import { diffViewerModes } from '~/ide/constants';
-import { diffLines } from '~/diffs/store/getters';
import DiffView from '~/diffs/components/diff_view.vue';
const localVue = createLocalVue();
@@ -74,7 +73,7 @@ describe('DiffContent', () => {
isInlineView: isInlineViewGetterMock,
isParallelView: isParallelViewGetterMock,
getCommentFormForDiffFile: getCommentFormForDiffFileGetterMock,
- diffLines,
+ diffLines: () => () => [...diffFileMockData.parallel_diff_lines],
},
actions: {
saveDiffDiscussion: saveDiffDiscussionMock,
@@ -122,11 +121,11 @@ describe('DiffContent', () => {
expect(wrapper.find(ParallelDiffView).exists()).toBe(true);
});
- it('should render diff view if `unifiedDiffLines` & `unifiedDiffComponents` are true', () => {
+ it('should render diff view if `unifiedDiffComponents` are true', () => {
isParallelViewGetterMock.mockReturnValue(true);
createComponent({
props: { diffFile: textDiffFile },
- provide: { glFeatures: { unifiedDiffLines: true, unifiedDiffComponents: true } },
+ provide: { glFeatures: { unifiedDiffComponents: true } },
});
expect(wrapper.find(DiffView).exists()).toBe(true);
diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
index 81e08f09f62..a3b4b5c3abb 100644
--- a/spec/frontend/diffs/components/diff_expansion_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
@@ -5,18 +5,16 @@ import { getByText } from '@testing-library/dom';
import { createStore } from '~/mr_notes/stores';
import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue';
import { getPreviousLineIndex } from '~/diffs/store/utils';
-import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
+import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
import diffFileMockData from '../mock_data/diff_file';
const EXPAND_UP_CLASS = '.js-unfold';
const EXPAND_DOWN_CLASS = '.js-unfold-down';
const lineSources = {
[INLINE_DIFF_VIEW_TYPE]: 'highlighted_diff_lines',
- [PARALLEL_DIFF_VIEW_TYPE]: 'parallel_diff_lines',
};
const lineHandlers = {
[INLINE_DIFF_VIEW_TYPE]: line => line,
- [PARALLEL_DIFF_VIEW_TYPE]: line => line.right || line.left,
};
function makeLoadMoreLinesPayload({
@@ -126,7 +124,6 @@ describe('DiffExpansionCell', () => {
describe('any row', () => {
[
{ diffViewType: INLINE_DIFF_VIEW_TYPE, lineIndex: 8, file: { parallel_diff_lines: [] } },
- { diffViewType: PARALLEL_DIFF_VIEW_TYPE, lineIndex: 7, file: { highlighted_diff_lines: [] } },
].forEach(({ diffViewType, file, lineIndex }) => {
describe(`with diffViewType (${diffViewType})`, () => {
beforeEach(() => {
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 0af5ddd9764..f010e88a1a7 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -1236,10 +1236,6 @@ describe('DiffsStoreActions', () => {
{ diffViewType: 'inline' },
[
{
- type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES',
- payload: { filePath: 'path', lines: ['test'] },
- },
- {
type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES',
payload: { filePath: 'path', lines: ['test'] },
},
@@ -1259,10 +1255,6 @@ describe('DiffsStoreActions', () => {
{ diffViewType: 'inline' },
[
{
- type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES',
- payload: { filePath: 'path', lines },
- },
- {
type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES',
payload: { filePath: 'path', lines: lines.slice(0, 200) },
},
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index c0645faf89e..ae23cea157d 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -1,7 +1,7 @@
import createState from '~/diffs/store/modules/diff_state';
import mutations from '~/diffs/store/mutations';
import * as types from '~/diffs/store/mutation_types';
-import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
+import { INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
import diffFileMockData from '../mock_data/diff_file';
import * as utils from '~/diffs/store/utils';
@@ -74,7 +74,7 @@ describe('DiffsStoreMutations', () => {
{
content_sha: diffFileMockData.content_sha,
file_hash: diffFileMockData.file_hash,
- highlighted_diff_lines: [],
+ [INLINE_DIFF_LINES_KEY]: [],
},
],
};
@@ -84,11 +84,11 @@ describe('DiffsStoreMutations', () => {
mutations[types.SET_DIFF_DATA](state, diffMock);
- expect(state.diffFiles[0].parallel_diff_lines).toBeUndefined();
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY]).toEqual([]);
});
});
- describe('SET_DIFFSET_DIFF_DATA_BATCH_DATA', () => {
+ describe('SET_DIFF_DATA_BATCH_DATA', () => {
it('should set diff data batch type properly', () => {
const state = { diffFiles: [] };
const diffMock = {
@@ -97,9 +97,6 @@ describe('DiffsStoreMutations', () => {
mutations[types.SET_DIFF_DATA_BATCH](state, diffMock);
- const firstLine = state.diffFiles[0].parallel_diff_lines[0];
-
- expect(firstLine.right.text).toBeUndefined();
expect(state.diffFiles[0].renderIt).toEqual(true);
expect(state.diffFiles[0].collapsed).toEqual(false);
});
@@ -142,8 +139,7 @@ describe('DiffsStoreMutations', () => {
};
const diffFile = {
file_hash: options.fileHash,
- highlighted_diff_lines: [],
- parallel_diff_lines: [],
+ [INLINE_DIFF_LINES_KEY]: [],
};
const state = { diffFiles: [diffFile], diffViewType: 'viewType' };
const lines = [{ old_line: 1, new_line: 1 }];
@@ -171,9 +167,7 @@ describe('DiffsStoreMutations', () => {
);
expect(utils.addContextLines).toHaveBeenCalledWith({
- inlineLines: diffFile.highlighted_diff_lines,
- parallelLines: diffFile.parallel_diff_lines,
- diffViewType: 'viewType',
+ inlineLines: diffFile[INLINE_DIFF_LINES_KEY],
contextLines: options.contextLines,
bottom: options.params.bottom,
lineNumbers: options.lineNumbers,
@@ -225,19 +219,7 @@ describe('DiffsStoreMutations', () => {
diffFiles: [
{
file_hash: 'ABC',
- parallel_diff_lines: [
- {
- left: {
- line_code: 'ABC_1',
- discussions: [],
- },
- right: {
- line_code: 'ABC_2',
- discussions: [],
- },
- },
- ],
- highlighted_diff_lines: [
+ [INLINE_DIFF_LINES_KEY]: [
{
line_code: 'ABC_1',
discussions: [],
@@ -267,12 +249,8 @@ describe('DiffsStoreMutations', () => {
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]);
-
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1);
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions[0].id).toEqual(1);
});
it('should not duplicate discussions on line', () => {
@@ -291,19 +269,7 @@ describe('DiffsStoreMutations', () => {
diffFiles: [
{
file_hash: 'ABC',
- parallel_diff_lines: [
- {
- left: {
- line_code: 'ABC_1',
- discussions: [],
- },
- right: {
- line_code: 'ABC_2',
- discussions: [],
- },
- },
- ],
- highlighted_diff_lines: [
+ [INLINE_DIFF_LINES_KEY]: [
{
line_code: 'ABC_1',
discussions: [],
@@ -333,24 +299,16 @@ describe('DiffsStoreMutations', () => {
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]);
-
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1);
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions[0].id).toEqual(1);
mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
discussion,
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]);
-
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1);
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions[0].id).toEqual(1);
});
it('updates existing discussion', () => {
@@ -369,19 +327,7 @@ describe('DiffsStoreMutations', () => {
diffFiles: [
{
file_hash: 'ABC',
- parallel_diff_lines: [
- {
- left: {
- line_code: 'ABC_1',
- discussions: [],
- },
- right: {
- line_code: 'ABC_2',
- discussions: [],
- },
- },
- ],
- highlighted_diff_lines: [
+ [INLINE_DIFF_LINES_KEY]: [
{
line_code: 'ABC_1',
discussions: [],
@@ -411,12 +357,8 @@ describe('DiffsStoreMutations', () => {
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]);
-
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1);
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions[0].id).toEqual(1);
mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
discussion: {
@@ -427,11 +369,8 @@ describe('DiffsStoreMutations', () => {
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].notes.length).toBe(1);
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].notes.length).toBe(1);
-
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].resolved).toBe(true);
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].resolved).toBe(true);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions[0].notes.length).toBe(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions[0].resolved).toBe(true);
});
it('should not duplicate inline diff discussions', () => {
@@ -450,7 +389,7 @@ describe('DiffsStoreMutations', () => {
diffFiles: [
{
file_hash: 'ABC',
- highlighted_diff_lines: [
+ [INLINE_DIFF_LINES_KEY]: [
{
line_code: 'ABC_1',
discussions: [
@@ -472,7 +411,6 @@ describe('DiffsStoreMutations', () => {
discussions: [],
},
],
- parallel_diff_lines: [],
},
],
};
@@ -497,7 +435,7 @@ describe('DiffsStoreMutations', () => {
diffPositionByLineCode,
});
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toBe(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions.length).toBe(1);
});
it('should add legacy discussions to the given line', () => {
@@ -517,19 +455,7 @@ describe('DiffsStoreMutations', () => {
diffFiles: [
{
file_hash: 'ABC',
- parallel_diff_lines: [
- {
- left: {
- line_code: 'ABC_1',
- discussions: [],
- },
- right: {
- line_code: 'ABC_1',
- discussions: [],
- },
- },
- ],
- highlighted_diff_lines: [
+ [INLINE_DIFF_LINES_KEY]: [
{
line_code: 'ABC_1',
discussions: [],
@@ -557,11 +483,8 @@ describe('DiffsStoreMutations', () => {
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1);
-
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1);
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions[0].id).toEqual(1);
});
it('should add discussions by line_codes and positions attributes', () => {
@@ -580,19 +503,7 @@ describe('DiffsStoreMutations', () => {
diffFiles: [
{
file_hash: 'ABC',
- parallel_diff_lines: [
- {
- left: {
- line_code: 'ABC_1',
- discussions: [],
- },
- right: {
- line_code: 'ABC_1',
- discussions: [],
- },
- },
- ],
- highlighted_diff_lines: [
+ [INLINE_DIFF_LINES_KEY]: [
{
line_code: 'ABC_1',
discussions: [],
@@ -624,11 +535,8 @@ describe('DiffsStoreMutations', () => {
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions).toHaveLength(1);
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toBe(1);
-
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions).toHaveLength(1);
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toBe(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions).toHaveLength(1);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions[0].id).toBe(1);
});
it('should add discussion to file', () => {
@@ -638,8 +546,7 @@ describe('DiffsStoreMutations', () => {
{
file_hash: 'ABC',
discussions: [],
- parallel_diff_lines: [],
- highlighted_diff_lines: [],
+ [INLINE_DIFF_LINES_KEY]: [],
},
],
};
@@ -668,30 +575,7 @@ describe('DiffsStoreMutations', () => {
diffFiles: [
{
file_hash: 'ABC',
- parallel_diff_lines: [
- {
- left: {
- line_code: 'ABC_1',
- discussions: [
- {
- id: 1,
- line_code: 'ABC_1',
- notes: [],
- },
- {
- id: 2,
- line_code: 'ABC_1',
- notes: [],
- },
- ],
- },
- right: {
- line_code: 'ABC_1',
- discussions: [],
- },
- },
- ],
- highlighted_diff_lines: [
+ [INLINE_DIFF_LINES_KEY]: [
{
line_code: 'ABC_1',
discussions: [
@@ -717,8 +601,7 @@ describe('DiffsStoreMutations', () => {
lineCode: 'ABC_1',
});
- expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(0);
- expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(0);
+ expect(state.diffFiles[0][INLINE_DIFF_LINES_KEY][0].discussions.length).toEqual(0);
});
});
@@ -776,11 +659,7 @@ describe('DiffsStoreMutations', () => {
it('sets hasForm on lines', () => {
const file = {
file_hash: 'hash',
- parallel_diff_lines: [
- { left: { line_code: '123', hasForm: false }, right: {} },
- { left: {}, right: { line_code: '124', hasForm: false } },
- ],
- highlighted_diff_lines: [
+ [INLINE_DIFF_LINES_KEY]: [
{ line_code: '123', hasForm: false },
{ line_code: '124', hasForm: false },
],
@@ -795,11 +674,8 @@ describe('DiffsStoreMutations', () => {
fileHash: 'hash',
});
- expect(file.highlighted_diff_lines[0].hasForm).toBe(true);
- expect(file.highlighted_diff_lines[1].hasForm).toBe(false);
-
- expect(file.parallel_diff_lines[0].left.hasForm).toBe(true);
- expect(file.parallel_diff_lines[1].right.hasForm).toBe(false);
+ expect(file[INLINE_DIFF_LINES_KEY][0].hasForm).toBe(true);
+ expect(file[INLINE_DIFF_LINES_KEY][1].hasForm).toBe(false);
});
});
@@ -885,8 +761,7 @@ describe('DiffsStoreMutations', () => {
file_path: 'test',
isLoadingFullFile: true,
isShowingFullFile: false,
- highlighted_diff_lines: [],
- parallel_diff_lines: [],
+ [INLINE_DIFF_LINES_KEY]: [],
},
],
};
@@ -903,8 +778,7 @@ describe('DiffsStoreMutations', () => {
file_path: 'test',
isLoadingFullFile: true,
isShowingFullFile: false,
- highlighted_diff_lines: [],
- parallel_diff_lines: [],
+ [INLINE_DIFF_LINES_KEY]: [],
},
],
};
@@ -927,80 +801,42 @@ describe('DiffsStoreMutations', () => {
});
});
- describe('SET_HIDDEN_VIEW_DIFF_FILE_LINES', () => {
- [
- { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' },
- { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' },
- ].forEach(({ current, hidden, diffViewType }) => {
- it(`sets the ${hidden} lines when diff view is ${diffViewType}`, () => {
- const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] };
- const state = {
- diffFiles: [file],
- diffViewType,
- };
-
- mutations[types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, {
- filePath: 'test',
- lines: ['test'],
- });
-
- expect(file[`${current}_diff_lines`]).toEqual([]);
- expect(file[`${hidden}_diff_lines`]).toEqual(['test']);
- });
- });
- });
-
describe('SET_CURRENT_VIEW_DIFF_FILE_LINES', () => {
- [
- { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' },
- { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' },
- ].forEach(({ current, hidden, diffViewType }) => {
- it(`sets the ${current} lines when diff view is ${diffViewType}`, () => {
- const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] };
- const state = {
- diffFiles: [file],
- diffViewType,
- };
-
- mutations[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, {
- filePath: 'test',
- lines: ['test'],
- });
-
- expect(file[`${current}_diff_lines`]).toEqual(['test']);
- expect(file[`${hidden}_diff_lines`]).toEqual([]);
+ it(`sets the highlighted lines`, () => {
+ const file = { file_path: 'test', [INLINE_DIFF_LINES_KEY]: [] };
+ const state = {
+ diffFiles: [file],
+ };
+
+ mutations[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ lines: ['test'],
});
+
+ expect(file[INLINE_DIFF_LINES_KEY]).toEqual(['test']);
});
});
describe('ADD_CURRENT_VIEW_DIFF_FILE_LINES', () => {
- [
- { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' },
- { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' },
- ].forEach(({ current, hidden, diffViewType }) => {
- it(`pushes to ${current} lines when diff view is ${diffViewType}`, () => {
- const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] };
- const state = {
- diffFiles: [file],
- diffViewType,
- };
-
- mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, {
- filePath: 'test',
- line: 'test',
- });
-
- expect(file[`${current}_diff_lines`]).toEqual(['test']);
- expect(file[`${hidden}_diff_lines`]).toEqual([]);
-
- mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, {
- filePath: 'test',
- line: 'test2',
- });
-
- expect(file[`${current}_diff_lines`]).toEqual(['test', 'test2']);
- expect(file[`${hidden}_diff_lines`]).toEqual([]);
+ it('pushes to inline lines', () => {
+ const file = { file_path: 'test', [INLINE_DIFF_LINES_KEY]: [] };
+ const state = {
+ diffFiles: [file],
+ };
+
+ mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ line: 'test',
});
+
+ expect(file[INLINE_DIFF_LINES_KEY]).toEqual(['test']);
+
+ mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ line: 'test2',
+ });
+
+ expect(file[INLINE_DIFF_LINES_KEY]).toEqual(['test', 'test2']);
});
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 866be0abd22..181d562661f 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -10,7 +10,7 @@ import {
OLD_LINE_TYPE,
MATCH_LINE_TYPE,
INLINE_DIFF_VIEW_TYPE,
- PARALLEL_DIFF_VIEW_TYPE,
+ INLINE_DIFF_LINES_KEY,
} from '~/diffs/constants';
import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants';
import diffFileMockData from '../mock_data/diff_file';
@@ -20,14 +20,6 @@ import { noteableDataMock } from '../../notes/mock_data';
const getDiffFileMock = () => JSON.parse(JSON.stringify(diffFileMockData));
const getDiffMetadataMock = () => JSON.parse(JSON.stringify(diffMetadata));
-function extractLinesFromFile(file) {
- const unpackedParallel = file.parallel_diff_lines
- .flatMap(({ left, right }) => [left, right])
- .filter(Boolean);
-
- return [...file.highlighted_diff_lines, ...unpackedParallel];
-}
-
describe('DiffsStoreUtils', () => {
describe('findDiffFile', () => {
const files = [{ file_hash: 1, name: 'one' }];
@@ -45,7 +37,7 @@ describe('DiffsStoreUtils', () => {
});
});
- describe('findIndexInInlineLines and findIndexInParallelLines', () => {
+ describe('findIndexInInlineLines', () => {
const expectSet = (method, lines, invalidLines) => {
expect(method(lines, { oldLineNumber: 3, newLineNumber: 5 })).toEqual(4);
expect(method(invalidLines || lines, { oldLineNumber: 32, newLineNumber: 53 })).toEqual(-1);
@@ -53,44 +45,26 @@ describe('DiffsStoreUtils', () => {
describe('findIndexInInlineLines', () => {
it('should return correct index for given line numbers', () => {
- expectSet(utils.findIndexInInlineLines, getDiffFileMock().highlighted_diff_lines);
- });
- });
-
- describe('findIndexInParallelLines', () => {
- it('should return correct index for given line numbers', () => {
- expectSet(utils.findIndexInParallelLines, getDiffFileMock().parallel_diff_lines, []);
+ expectSet(utils.findIndexInInlineLines, getDiffFileMock()[INLINE_DIFF_LINES_KEY]);
});
});
});
describe('getPreviousLineIndex', () => {
- [
- { diffViewType: INLINE_DIFF_VIEW_TYPE, file: { parallel_diff_lines: [] } },
- { diffViewType: PARALLEL_DIFF_VIEW_TYPE, file: { highlighted_diff_lines: [] } },
- ].forEach(({ diffViewType, file }) => {
- describe(`with diffViewType (${diffViewType}) in split diffs`, () => {
- let diffFile;
-
- beforeEach(() => {
- diffFile = { ...clone(diffFileMockData), ...file };
- });
+ describe(`with diffViewType (inline) in split diffs`, () => {
+ let diffFile;
- it('should return the correct previous line number', () => {
- const emptyLines =
- diffViewType === INLINE_DIFF_VIEW_TYPE
- ? diffFile.parallel_diff_lines
- : diffFile.highlighted_diff_lines;
-
- // This expectation asserts that we cannot possibly be using the opposite view type lines in the next expectation
- expect(emptyLines.length).toBe(0);
- expect(
- utils.getPreviousLineIndex(diffViewType, diffFile, {
- oldLineNumber: 3,
- newLineNumber: 5,
- }),
- ).toBe(4);
- });
+ beforeEach(() => {
+ diffFile = { ...clone(diffFileMockData) };
+ });
+
+ it('should return the correct previous line number', () => {
+ expect(
+ utils.getPreviousLineIndex(INLINE_DIFF_VIEW_TYPE, diffFile, {
+ oldLineNumber: 3,
+ newLineNumber: 5,
+ }),
+ ).toBe(4);
});
});
});
@@ -100,82 +74,50 @@ describe('DiffsStoreUtils', () => {
const diffFile = getDiffFileMock();
const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
const inlineIndex = utils.findIndexInInlineLines(
- diffFile.highlighted_diff_lines,
- lineNumbers,
- );
- const parallelIndex = utils.findIndexInParallelLines(
- diffFile.parallel_diff_lines,
+ diffFile[INLINE_DIFF_LINES_KEY],
lineNumbers,
);
- const atInlineIndex = diffFile.highlighted_diff_lines[inlineIndex];
- const atParallelIndex = diffFile.parallel_diff_lines[parallelIndex];
+ const atInlineIndex = diffFile[INLINE_DIFF_LINES_KEY][inlineIndex];
utils.removeMatchLine(diffFile, lineNumbers, false);
- expect(diffFile.highlighted_diff_lines[inlineIndex]).not.toEqual(atInlineIndex);
- expect(diffFile.parallel_diff_lines[parallelIndex]).not.toEqual(atParallelIndex);
+ expect(diffFile[INLINE_DIFF_LINES_KEY][inlineIndex]).not.toEqual(atInlineIndex);
utils.removeMatchLine(diffFile, lineNumbers, true);
- expect(diffFile.highlighted_diff_lines[inlineIndex + 1]).not.toEqual(atInlineIndex);
- expect(diffFile.parallel_diff_lines[parallelIndex + 1]).not.toEqual(atParallelIndex);
+ expect(diffFile[INLINE_DIFF_LINES_KEY][inlineIndex + 1]).not.toEqual(atInlineIndex);
});
});
describe('addContextLines', () => {
- [INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE].forEach(diffViewType => {
- it(`should add context lines for ${diffViewType}`, () => {
- const diffFile = getDiffFileMock();
- const inlineLines = diffFile.highlighted_diff_lines;
- const parallelLines = diffFile.parallel_diff_lines;
- const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
- const contextLines = [{ lineNumber: 42, line_code: '123' }];
- const options = { inlineLines, parallelLines, contextLines, lineNumbers, diffViewType };
- const inlineIndex = utils.findIndexInInlineLines(inlineLines, lineNumbers);
- const parallelIndex = utils.findIndexInParallelLines(parallelLines, lineNumbers);
- const normalizedParallelLine = {
- left: options.contextLines[0],
- right: options.contextLines[0],
- line_code: '123',
- };
+ it(`should add context lines`, () => {
+ const diffFile = getDiffFileMock();
+ const inlineLines = diffFile[INLINE_DIFF_LINES_KEY];
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
+ const contextLines = [{ lineNumber: 42, line_code: '123' }];
+ const options = { inlineLines, contextLines, lineNumbers };
+ const inlineIndex = utils.findIndexInInlineLines(inlineLines, lineNumbers);
- utils.addContextLines(options);
+ utils.addContextLines(options);
- if (diffViewType === INLINE_DIFF_VIEW_TYPE) {
- expect(inlineLines[inlineIndex]).toEqual(contextLines[0]);
- } else {
- expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine);
- }
- });
+ expect(inlineLines[inlineIndex]).toEqual(contextLines[0]);
+ });
- it(`should add context lines properly with bottom parameter for ${diffViewType}`, () => {
- const diffFile = getDiffFileMock();
- const inlineLines = diffFile.highlighted_diff_lines;
- const parallelLines = diffFile.parallel_diff_lines;
- const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
- const contextLines = [{ lineNumber: 42, line_code: '123' }];
- const options = {
- inlineLines,
- parallelLines,
- contextLines,
- lineNumbers,
- bottom: true,
- diffViewType,
- };
- const normalizedParallelLine = {
- left: options.contextLines[0],
- right: options.contextLines[0],
- line_code: '123',
- };
+ it(`should add context lines properly with bottom parameter`, () => {
+ const diffFile = getDiffFileMock();
+ const inlineLines = diffFile[INLINE_DIFF_LINES_KEY];
+ const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
+ const contextLines = [{ lineNumber: 42, line_code: '123' }];
+ const options = {
+ inlineLines,
+ contextLines,
+ lineNumbers,
+ bottom: true,
+ };
- utils.addContextLines(options);
+ utils.addContextLines(options);
- if (diffViewType === INLINE_DIFF_VIEW_TYPE) {
- expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]);
- } else {
- expect(parallelLines[parallelLines.length - 1]).toEqual(normalizedParallelLine);
- }
- });
+ expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]);
});
});
@@ -195,7 +137,6 @@ describe('DiffsStoreUtils', () => {
new_line: 3,
old_line: 1,
},
- diffViewType: PARALLEL_DIFF_VIEW_TYPE,
linePosition: LINE_POSITION_LEFT,
lineRange: { start_line_code: 'abc_1_1', end_line_code: 'abc_2_2' },
};
@@ -256,7 +197,6 @@ describe('DiffsStoreUtils', () => {
new_line: 3,
old_line: 1,
},
- diffViewType: PARALLEL_DIFF_VIEW_TYPE,
linePosition: LINE_POSITION_LEFT,
};
@@ -424,20 +364,6 @@ describe('DiffsStoreUtils', () => {
expect(preppedLine).toEqual(correctLine);
});
- it('returns a nested object with "left" and "right" lines + the line code for `parallel` lines', () => {
- preppedLine = utils.prepareLineForRenamedFile({
- diffViewType: PARALLEL_DIFF_VIEW_TYPE,
- line: sourceLine,
- index: lineIndex,
- diffFile,
- });
-
- expect(Object.keys(preppedLine)).toEqual(['left', 'right', 'line_code']);
- expect(preppedLine.left).toEqual(correctLine);
- expect(preppedLine.right).toEqual(correctLine);
- expect(preppedLine.line_code).toEqual(correctLine.line_code);
- });
-
it.each`
brokenSymlink
${false}
@@ -474,13 +400,13 @@ describe('DiffsStoreUtils', () => {
preparedDiff = { diff_files: [mock] };
splitInlineDiff = {
- diff_files: [{ ...mock, parallel_diff_lines: undefined }],
+ diff_files: [{ ...mock }],
};
splitParallelDiff = {
- diff_files: [{ ...mock, highlighted_diff_lines: undefined }],
+ diff_files: [{ ...mock, [INLINE_DIFF_LINES_KEY]: undefined }],
};
completedDiff = {
- diff_files: [{ ...mock, highlighted_diff_lines: undefined }],
+ diff_files: [{ ...mock, [INLINE_DIFF_LINES_KEY]: undefined }],
};
preparedDiff.diff_files = utils.prepareDiffData(preparedDiff);
@@ -490,19 +416,7 @@ describe('DiffsStoreUtils', () => {
});
it('sets the renderIt and collapsed attribute on files', () => {
- const firstParallelDiffLine = preparedDiff.diff_files[0].parallel_diff_lines[2];
-
- expect(firstParallelDiffLine.left.discussions.length).toBe(0);
- expect(firstParallelDiffLine.left).not.toHaveAttr('text');
- expect(firstParallelDiffLine.right.discussions.length).toBe(0);
- expect(firstParallelDiffLine.right).not.toHaveAttr('text');
- const firstParallelChar = firstParallelDiffLine.right.rich_text.charAt(0);
-
- expect(firstParallelChar).not.toBe(' ');
- expect(firstParallelChar).not.toBe('+');
- expect(firstParallelChar).not.toBe('-');
-
- const checkLine = preparedDiff.diff_files[0].highlighted_diff_lines[0];
+ const checkLine = preparedDiff.diff_files[0][INLINE_DIFF_LINES_KEY][0];
expect(checkLine.discussions.length).toBe(0);
expect(checkLine).not.toHaveAttr('text');
@@ -516,29 +430,14 @@ describe('DiffsStoreUtils', () => {
expect(preparedDiff.diff_files[0].collapsed).toBeFalsy();
});
- it('adds line_code to all lines', () => {
- expect(
- preparedDiff.diff_files[0].parallel_diff_lines.filter(line => !line.line_code),
- ).toHaveLength(0);
- });
-
- it('uses right line code if left has none', () => {
- const firstLine = preparedDiff.diff_files[0].parallel_diff_lines[0];
-
- expect(firstLine.line_code).toEqual(firstLine.right.line_code);
- });
-
it('guarantees an empty array for both diff styles', () => {
- expect(splitInlineDiff.diff_files[0].parallel_diff_lines.length).toEqual(0);
- expect(splitInlineDiff.diff_files[0].highlighted_diff_lines.length).toBeGreaterThan(0);
- expect(splitParallelDiff.diff_files[0].parallel_diff_lines.length).toBeGreaterThan(0);
- expect(splitParallelDiff.diff_files[0].highlighted_diff_lines.length).toEqual(0);
+ expect(splitInlineDiff.diff_files[0][INLINE_DIFF_LINES_KEY].length).toBeGreaterThan(0);
+ expect(splitParallelDiff.diff_files[0][INLINE_DIFF_LINES_KEY].length).toEqual(0);
});
it('merges existing diff files with newly loaded diff files to ensure split diffs are eventually completed', () => {
expect(completedDiff.diff_files.length).toEqual(1);
- expect(completedDiff.diff_files[0].parallel_diff_lines.length).toBeGreaterThan(0);
- expect(completedDiff.diff_files[0].highlighted_diff_lines.length).toBeGreaterThan(0);
+ expect(completedDiff.diff_files[0][INLINE_DIFF_LINES_KEY].length).toBeGreaterThan(0);
});
it('leaves files in the existing state', () => {
@@ -555,11 +454,11 @@ describe('DiffsStoreUtils', () => {
it('completes an existing split diff without overwriting existing diffs', () => {
// The current state has a file that has only loaded inline lines
- const priorFiles = [{ ...mock, parallel_diff_lines: [] }];
+ const priorFiles = [{ ...mock }];
// The next (batch) load loads two files: the other half of that file, and a new file
const fakeBatch = [
- { ...mock, highlighted_diff_lines: undefined },
- { ...mock, highlighted_diff_lines: undefined, content_sha: 'ABC', file_hash: 'DEF' },
+ { ...mock, [INLINE_DIFF_LINES_KEY]: undefined },
+ { ...mock, [INLINE_DIFF_LINES_KEY]: undefined, content_sha: 'ABC', file_hash: 'DEF' },
];
const updatedFilesList = utils.prepareDiffData({ diff_files: fakeBatch }, priorFiles);
@@ -584,7 +483,7 @@ describe('DiffsStoreUtils', () => {
...splitInlineDiff.diff_files,
...splitParallelDiff.diff_files,
...completedDiff.diff_files,
- ].flatMap(file => extractLinesFromFile(file));
+ ].flatMap(file => [...file[INLINE_DIFF_LINES_KEY]]);
lines.forEach(line => {
expect(line.commentsDisabled).toBe(false);
@@ -608,8 +507,7 @@ describe('DiffsStoreUtils', () => {
});
it('guarantees an empty array of lines for both diff styles', () => {
- expect(preparedDiffFiles[0].parallel_diff_lines.length).toEqual(0);
- expect(preparedDiffFiles[0].highlighted_diff_lines.length).toEqual(0);
+ expect(preparedDiffFiles[0][INLINE_DIFF_LINES_KEY].length).toEqual(0);
});
it('leaves files in the existing state', () => {
@@ -647,8 +545,7 @@ describe('DiffsStoreUtils', () => {
fileMock,
{
...metaMock.diff_files[0],
- highlighted_diff_lines: [],
- parallel_diff_lines: [],
+ [INLINE_DIFF_LINES_KEY]: [],
},
]);
});
@@ -1217,7 +1114,7 @@ describe('DiffsStoreUtils', () => {
it('converts inline diff lines to parallel diff lines', () => {
const file = getDiffFileMock();
- expect(utils.parallelizeDiffLines(file.highlighted_diff_lines)).toEqual(
+ expect(utils.parallelizeDiffLines(file[INLINE_DIFF_LINES_KEY])).toEqual(
file.parallel_diff_lines,
);
});
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index d305f5e90bd..cc5153d6eba 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,51 +1,69 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import eventHub from '~/environments/event_hub';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
+const scheduledJobAction = {
+ name: 'scheduled action',
+ playPath: `${TEST_HOST}/scheduled/job/action`,
+ playable: true,
+ scheduledAt: '2063-04-05T00:42:00Z',
+};
+
+const expiredJobAction = {
+ name: 'expired action',
+ playPath: `${TEST_HOST}/expired/job/action`,
+ playable: true,
+ scheduledAt: '2018-10-05T08:23:00Z',
+};
+
describe('EnvironmentActions Component', () => {
- let vm;
+ let wrapper;
- const findEnvironmentActionsButton = () => vm.find('[data-testid="environment-actions-button"]');
+ const findEnvironmentActionsButton = () =>
+ wrapper.find('[data-testid="environment-actions-button"]');
- beforeEach(() => {
- vm = shallowMount(EnvironmentActions, {
- propsData: { actions: [] },
+ function createComponent(props, { mountFn = shallowMount } = {}) {
+ wrapper = mountFn(EnvironmentActions, {
+ propsData: { actions: [], ...props },
directives: {
GlTooltip: createMockDirective(),
},
});
- });
+ }
+
+ function createComponentWithScheduledJobs(opts = {}) {
+ return createComponent({ actions: [scheduledJobAction, expiredJobAction] }, opts);
+ }
+
+ const findDropdownItem = action => {
+ const buttons = wrapper.findAll(GlDropdownItem);
+ return buttons.filter(button => button.text().startsWith(action.name)).at(0);
+ };
afterEach(() => {
- vm.destroy();
+ wrapper.destroy();
+ wrapper = null;
});
it('should render a dropdown button with 2 icons', () => {
- expect(vm.find('.dropdown-new').findAll(GlIcon).length).toBe(2);
+ createComponent({}, { mountFn: mount });
+ expect(wrapper.find(GlDropdown).findAll(GlIcon).length).toBe(2);
});
it('should render a dropdown button with aria-label description', () => {
- expect(vm.find('.dropdown-new').attributes('aria-label')).toEqual('Deploy to...');
+ createComponent();
+ expect(wrapper.find(GlDropdown).attributes('aria-label')).toBe('Deploy to...');
});
it('should render a tooltip', () => {
+ createComponent();
const tooltip = getBinding(findEnvironmentActionsButton().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
});
- describe('is loading', () => {
- beforeEach(() => {
- vm.setData({ isLoading: true });
- });
-
- it('should render a dropdown button with a loading icon', () => {
- expect(vm.findAll(GlLoadingIcon).length).toBe(1);
- });
- });
-
describe('manual actions', () => {
const actions = [
{
@@ -64,68 +82,71 @@ describe('EnvironmentActions Component', () => {
];
beforeEach(() => {
- vm.setProps({ actions });
+ createComponent({ actions });
});
it('should render a dropdown with the provided list of actions', () => {
- expect(vm.findAll('.dropdown-menu li').length).toEqual(actions.length);
+ expect(wrapper.findAll(GlDropdownItem)).toHaveLength(actions.length);
});
it("should render a disabled action when it's not playable", () => {
- expect(vm.find('.dropdown-menu li:last-child gl-button-stub').props('disabled')).toBe(true);
+ const dropdownItems = wrapper.findAll(GlDropdownItem);
+ const lastDropdownItem = dropdownItems.at(dropdownItems.length - 1);
+ expect(lastDropdownItem.attributes('disabled')).toBe('true');
});
});
describe('scheduled jobs', () => {
- const scheduledJobAction = {
- name: 'scheduled action',
- playPath: `${TEST_HOST}/scheduled/job/action`,
- playable: true,
- scheduledAt: '2063-04-05T00:42:00Z',
- };
- const expiredJobAction = {
- name: 'expired action',
- playPath: `${TEST_HOST}/expired/job/action`,
- playable: true,
- scheduledAt: '2018-10-05T08:23:00Z',
- };
- const findDropdownItem = action => {
- const buttons = vm.findAll('.dropdown-menu li gl-button-stub');
- return buttons.filter(button => button.text().startsWith(action.name)).at(0);
+ let emitSpy;
+
+ const clickAndConfirm = async ({ confirm = true } = {}) => {
+ jest.spyOn(window, 'confirm').mockImplementation(() => confirm);
+
+ findDropdownItem(scheduledJobAction).vm.$emit('click');
+ await wrapper.vm.$nextTick();
};
beforeEach(() => {
+ emitSpy = jest.fn();
+ eventHub.$on('postAction', emitSpy);
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
- vm.setProps({ actions: [scheduledJobAction, expiredJobAction] });
});
- it('emits postAction event after confirming', () => {
- const emitSpy = jest.fn();
- eventHub.$on('postAction', emitSpy);
- jest.spyOn(window, 'confirm').mockImplementation(() => true);
+ describe('when postAction event is confirmed', () => {
+ beforeEach(async () => {
+ createComponentWithScheduledJobs({ mountFn: mount });
+ clickAndConfirm();
+ });
- findDropdownItem(scheduledJobAction).vm.$emit('click');
+ it('emits postAction event', () => {
+ expect(window.confirm).toHaveBeenCalled();
+ expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
+ });
- expect(window.confirm).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
+ it('should render a dropdown button with a loading icon', () => {
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ });
});
- it('does not emit postAction event if confirmation is cancelled', () => {
- const emitSpy = jest.fn();
- eventHub.$on('postAction', emitSpy);
- jest.spyOn(window, 'confirm').mockImplementation(() => false);
-
- findDropdownItem(scheduledJobAction).vm.$emit('click');
+ describe('when postAction event is denied', () => {
+ beforeEach(() => {
+ createComponentWithScheduledJobs({ mountFn: mount });
+ clickAndConfirm({ confirm: false });
+ });
- expect(window.confirm).toHaveBeenCalled();
- expect(emitSpy).not.toHaveBeenCalled();
+ it('does not emit postAction event if confirmation is cancelled', () => {
+ expect(window.confirm).toHaveBeenCalled();
+ expect(emitSpy).not.toHaveBeenCalled();
+ });
});
it('displays the remaining time in the dropdown', () => {
+ createComponentWithScheduledJobs();
expect(findDropdownItem(scheduledJobAction).text()).toContain('24:00:00');
});
it('displays 00:00:00 for expired jobs in the dropdown', () => {
+ createComponentWithScheduledJobs();
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
});
});
diff --git a/spec/frontend/fixtures/raw.rb b/spec/frontend/fixtures/raw.rb
index 337067121d0..7e42fa61e8e 100644
--- a/spec/frontend/fixtures/raw.rb
+++ b/spec/frontend/fixtures/raw.rb
@@ -13,6 +13,9 @@ RSpec.describe 'Raw files', '(JavaScript fixtures)' do
clean_frontend_fixtures('blob/balsamiq/')
clean_frontend_fixtures('blob/notebook/')
clean_frontend_fixtures('blob/pdf/')
+ clean_frontend_fixtures('blob/text/')
+ clean_frontend_fixtures('blob/binary/')
+ clean_frontend_fixtures('blob/images/')
end
after do
@@ -38,4 +41,16 @@ RSpec.describe 'Raw files', '(JavaScript fixtures)' do
it 'blob/pdf/test.pdf' do
@blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf')
end
+
+ it 'blob/text/README.md' do
+ @blob = project.repository.blob_at('e774ebd33', 'README.md')
+ end
+
+ it 'blob/images/logo-white.png' do
+ @blob = project.repository.blob_at('e774ebd33', 'files/images/logo-white.png')
+ end
+
+ it 'blob/binary/Gemfile.zip' do
+ @blob = project.repository.blob_at('e774ebd33', 'Gemfile.zip')
+ end
end
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 691f8896d74..72d8e23f28b 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -35,7 +35,7 @@ describe('AppComponent', () => {
let mock;
let getGroupsSpy;
- const store = new GroupsStore(false);
+ const store = new GroupsStore({ hideProjects: false });
const service = new GroupsService(mockEndpoint);
const createShallowComponent = (hideProjects = false) => {
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 83acbb152b5..32bae812c86 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import groupItemComponent from '~/groups/components/group_item.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
+import { getGroupItemMicrodata } from '~/groups/store/utils';
import eventHub from '~/groups/event_hub';
import * as urlUtilities from '~/lib/utils/url_utility';
import { mockParentGroupItem, mockChildren } from '../mock_data';
@@ -30,6 +31,11 @@ describe('GroupItemComponent', () => {
vm.$destroy();
});
+ const withMicrodata = group => ({
+ ...group,
+ microdata: getGroupItemMicrodata(group),
+ });
+
describe('computed', () => {
describe('groupDomId', () => {
it('should return ID string suffixed with group ID', () => {
@@ -212,4 +218,47 @@ describe('GroupItemComponent', () => {
expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
});
});
+ describe('schema.org props', () => {
+ describe('when showSchemaMarkup is disabled on the group', () => {
+ it.each(['itemprop', 'itemtype', 'itemscope'], 'it does not set %s', attr => {
+ expect(vm.$el.getAttribute(attr)).toBeNull();
+ });
+ it.each(
+ ['.js-group-avatar', '.js-group-name', '.js-group-description'],
+ 'it does not set `itemprop` on sub-nodes',
+ selector => {
+ expect(vm.$el.querySelector(selector).getAttribute('itemprop')).toBeNull();
+ },
+ );
+ });
+ describe('when group has microdata', () => {
+ beforeEach(() => {
+ const group = withMicrodata({
+ ...mockParentGroupItem,
+ avatarUrl: 'http://foo.bar',
+ description: 'Foo Bar',
+ });
+
+ vm = createComponent(group);
+ });
+
+ it.each`
+ attr | value
+ ${'itemscope'} | ${'itemscope'}
+ ${'itemtype'} | ${'https://schema.org/Organization'}
+ ${'itemprop'} | ${'subOrganization'}
+ `('it does set correct $attr', ({ attr, value } = {}) => {
+ expect(vm.$el.getAttribute(attr)).toBe(value);
+ });
+
+ it.each`
+ selector | propValue
+ ${'[data-testid="group-avatar"]'} | ${'logo'}
+ ${'[data-testid="group-name"]'} | ${'name'}
+ ${'[data-testid="group-description"]'} | ${'description'}
+ `('it does set correct $selector', ({ selector, propValue } = {}) => {
+ expect(vm.$el.querySelector(selector).getAttribute('itemprop')).toBe(propValue);
+ });
+ });
+ });
});
diff --git a/spec/frontend/groups/members/components/app_spec.js b/spec/frontend/groups/members/components/app_spec.js
index de9f30649e9..0db3fc18c34 100644
--- a/spec/frontend/groups/members/components/app_spec.js
+++ b/spec/frontend/groups/members/components/app_spec.js
@@ -4,11 +4,8 @@ import Vuex from 'vuex';
import { GlAlert } from '@gitlab/ui';
import App from '~/groups/members/components/app.vue';
import * as commonUtils from '~/lib/utils/common_utils';
-import {
- RECEIVE_MEMBER_ROLE_ERROR,
- HIDE_ERROR,
-} from '~/vuex_shared/modules/members/mutation_types';
-import mutations from '~/vuex_shared/modules/members/mutations';
+import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
+import mutations from '~/members/store/mutations';
describe('GroupMembersApp', () => {
const localVue = createLocalVue();
diff --git a/spec/frontend/groups/store/groups_store_spec.js b/spec/frontend/groups/store/groups_store_spec.js
index 7d12f73d270..8ac5d7099f1 100644
--- a/spec/frontend/groups/store/groups_store_spec.js
+++ b/spec/frontend/groups/store/groups_store_spec.js
@@ -1,4 +1,5 @@
import GroupsStore from '~/groups/store/groups_store';
+import { getGroupItemMicrodata } from '~/groups/store/utils';
import {
mockGroups,
mockSearchedGroups,
@@ -17,9 +18,9 @@ describe('ProjectsStore', () => {
expect(Object.keys(store.state).length).toBe(2);
expect(Array.isArray(store.state.groups)).toBeTruthy();
expect(Object.keys(store.state.pageInfo).length).toBe(0);
- expect(store.hideProjects).not.toBeDefined();
+ expect(store.hideProjects).toBeFalsy();
- store = new GroupsStore(true);
+ store = new GroupsStore({ hideProjects: true });
expect(store.hideProjects).toBeTruthy();
});
@@ -86,22 +87,30 @@ describe('ProjectsStore', () => {
describe('formatGroupItem', () => {
it('should parse group item object and return updated object', () => {
- let store;
- let updatedGroupItem;
-
- store = new GroupsStore();
- updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
+ const store = new GroupsStore();
+ const updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
expect(Object.keys(updatedGroupItem).indexOf('fullName')).toBeGreaterThan(-1);
expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].children_count);
expect(updatedGroupItem.isChildrenLoading).toBe(false);
expect(updatedGroupItem.isBeingRemoved).toBe(false);
+ expect(updatedGroupItem.microdata).toEqual({});
+ });
- store = new GroupsStore(true);
- updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
+ it('with hideProjects', () => {
+ const store = new GroupsStore({ hideProjects: true });
+ const updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
expect(Object.keys(updatedGroupItem).indexOf('fullName')).toBeGreaterThan(-1);
expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].subgroup_count);
+ expect(updatedGroupItem.microdata).toEqual({});
+ });
+
+ it('with showSchemaMarkup', () => {
+ const store = new GroupsStore({ showSchemaMarkup: true });
+ const updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
+
+ expect(updatedGroupItem.microdata).toEqual(getGroupItemMicrodata(mockRawChildren[0]));
});
});
diff --git a/spec/frontend/groups/store/utils_spec.js b/spec/frontend/groups/store/utils_spec.js
new file mode 100644
index 00000000000..0961d4c72b4
--- /dev/null
+++ b/spec/frontend/groups/store/utils_spec.js
@@ -0,0 +1,44 @@
+import { getGroupItemMicrodata } from '~/groups/store/utils';
+
+describe('~/groups/store/utils', () => {
+ describe('getGroupItemMetadata', () => {
+ it('has default type', () => {
+ expect(getGroupItemMicrodata({ type: 'silly' })).toMatchInlineSnapshot(`
+ Object {
+ "descriptionItemprop": "description",
+ "imageItemprop": "image",
+ "itemprop": "owns",
+ "itemscope": true,
+ "itemtype": "https://schema.org/Thing",
+ "nameItemprop": "name",
+ }
+ `);
+ });
+
+ it('has group props', () => {
+ expect(getGroupItemMicrodata({ type: 'group' })).toMatchInlineSnapshot(`
+ Object {
+ "descriptionItemprop": "description",
+ "imageItemprop": "logo",
+ "itemprop": "subOrganization",
+ "itemscope": true,
+ "itemtype": "https://schema.org/Organization",
+ "nameItemprop": "name",
+ }
+ `);
+ });
+
+ it('has project props', () => {
+ expect(getGroupItemMicrodata({ type: 'project' })).toMatchInlineSnapshot(`
+ Object {
+ "descriptionItemprop": "description",
+ "imageItemprop": "image",
+ "itemprop": "owns",
+ "itemscope": true,
+ "itemtype": "https://schema.org/SoftwareSourceCode",
+ "nameItemprop": "name",
+ }
+ `);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/terminal/session_spec.js b/spec/frontend/ide/components/terminal/session_spec.js
index ce61a31691a..3ca37166ac4 100644
--- a/spec/frontend/ide/components/terminal/session_spec.js
+++ b/spec/frontend/ide/components/terminal/session_spec.js
@@ -1,4 +1,5 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import Vuex from 'vuex';
import TerminalSession from '~/ide/components/terminal/session.vue';
import Terminal from '~/ide/components/terminal/terminal.vue';
@@ -38,6 +39,8 @@ describe('IDE TerminalSession', () => {
});
};
+ const findButton = () => wrapper.find(GlButton);
+
beforeEach(() => {
state = {
session: { status: RUNNING, terminalPath: TEST_TERMINAL_PATH },
@@ -69,8 +72,8 @@ describe('IDE TerminalSession', () => {
state.session = { status };
factory();
- const button = wrapper.find('button');
- button.trigger('click');
+ const button = findButton();
+ button.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(button.text()).toEqual('Stop Terminal');
@@ -84,8 +87,8 @@ describe('IDE TerminalSession', () => {
state.session = { status };
factory();
- const button = wrapper.find('button');
- button.trigger('click');
+ const button = findButton();
+ button.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(button.text()).toEqual('Restart Terminal');
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index cc290fc526e..744ac086b5f 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -510,8 +510,6 @@ describe('IDE store file actions', () => {
describe('changeFileContent', () => {
let tmpFile;
- const callAction = (content = 'content\n') =>
- store.dispatch('changeFileContent', { path: tmpFile.path, content });
beforeEach(() => {
tmpFile = file('tmpFile');
@@ -521,11 +519,23 @@ describe('IDE store file actions', () => {
});
it('updates file content', () => {
- return callAction().then(() => {
+ const content = 'content\n';
+
+ return store.dispatch('changeFileContent', { path: tmpFile.path, content }).then(() => {
expect(tmpFile.content).toBe('content\n');
});
});
+ it('does nothing if path does not exist', () => {
+ const content = 'content\n';
+
+ return store
+ .dispatch('changeFileContent', { path: 'not/a/real_file.txt', content })
+ .then(() => {
+ expect(tmpFile.content).toBe('\n');
+ });
+ });
+
it('adds file into stagedFiles array', () => {
return store
.dispatch('changeFileContent', {
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
index 072e611b9a4..2b3c803be08 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
@@ -43,6 +43,7 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
>
<gl-tab-stub
title="Alert integration"
+ titlelinkclass=""
>
<alertssettingsform-stub
class="gl-pt-3"
@@ -51,6 +52,7 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
</gl-tab-stub>
<gl-tab-stub
title="PagerDuty integration"
+ titlelinkclass=""
>
<pagerdutysettingsform-stub
class="gl-pt-3"
diff --git a/spec/frontend/issuable/related_issues/components/issue_token_spec.js b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
index 1b4c6b548e2..d5181d4a17a 100644
--- a/spec/frontend/issuable/related_issues/components/issue_token_spec.js
+++ b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
@@ -100,7 +100,7 @@ describe('IssueToken', () => {
state,
});
- expect(findReferenceIcon().attributes('aria-label')).toBe(state);
+ expect(findReferenceIcon().props('ariaLabel')).toBe(state);
expect(findReference().text()).toBe(displayReference);
expect(findTitle().text()).toBe(title);
});
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index fe6d9a34078..c40b7c90c72 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -120,6 +120,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
class="gl-search-box-by-type"
>
<svg
+ aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
@@ -234,6 +235,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
class="gl-search-box-by-type"
>
<svg
+ aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
diff --git a/spec/frontend/jobs/components/log/line_spec.js b/spec/frontend/jobs/components/log/line_spec.js
index 314b23ec29b..354bc62de29 100644
--- a/spec/frontend/jobs/components/log/line_spec.js
+++ b/spec/frontend/jobs/components/log/line_spec.js
@@ -4,6 +4,7 @@ import LineNumber from '~/jobs/components/log/line_number.vue';
const httpUrl = 'http://example.com';
const httpsUrl = 'https://example.com';
+const queryUrl = 'https://example.com?param=val';
const mockProps = ({ text = 'Running with gitlab-runner 12.1.0 (de7731dd)' } = {}) => ({
line: {
@@ -33,10 +34,11 @@ describe('Job Log Line', () => {
const findLine = () => wrapper.find('span');
const findLink = () => findLine().find('a');
- const findLinksAt = i =>
- findLine()
- .findAll('a')
- .at(i);
+ const findLinks = () => findLine().findAll('a');
+ const findLinkAttributeByIndex = i =>
+ findLinks()
+ .at(i)
+ .attributes();
beforeEach(() => {
originalGon = window.gon;
@@ -116,15 +118,6 @@ describe('Job Log Line', () => {
expect(findLink().attributes().href).toBe(httpsUrl);
});
- it('renders a multiple links surrounded by text', () => {
- createComponent(mockProps({ text: `My HTTP url: ${httpUrl} and my HTTPS url: ${httpsUrl}` }));
- expect(findLine().text()).toBe(
- 'My HTTP url: http://example.com and my HTTPS url: https://example.com',
- );
- expect(findLinksAt(0).attributes().href).toBe(httpUrl);
- expect(findLinksAt(1).attributes().href).toBe(httpsUrl);
- });
-
it('renders a link with rel nofollow and noopener', () => {
createComponent(mockProps({ text: httpsUrl }));
@@ -137,15 +130,49 @@ describe('Job Log Line', () => {
expect(findLink().classes()).toEqual(['gl-reset-color!', 'gl-text-decoration-underline']);
});
- it('render links surrounded by text', () => {
+ it('renders a links with queries, surrounded by questions marks', () => {
+ createComponent(mockProps({ text: `Did you see my url ${queryUrl}??` }));
+
+ expect(findLine().text()).toBe('Did you see my url https://example.com?param=val??');
+ expect(findLinkAttributeByIndex(0).href).toBe(queryUrl);
+ });
+
+ it('renders a links with queries, surrounded by exclamation marks', () => {
+ createComponent(mockProps({ text: `No! The ${queryUrl}!?` }));
+
+ expect(findLine().text()).toBe('No! The https://example.com?param=val!?');
+ expect(findLinkAttributeByIndex(0).href).toBe(queryUrl);
+ });
+
+ it('renders a multiple links surrounded by text', () => {
createComponent(
- mockProps({ text: `My HTTP url: ${httpUrl} and my HTTPS url: ${httpsUrl} are here.` }),
+ mockProps({ text: `Well, my HTTP url: ${httpUrl} and my HTTPS url: ${httpsUrl}` }),
);
expect(findLine().text()).toBe(
- 'My HTTP url: http://example.com and my HTTPS url: https://example.com are here.',
+ 'Well, my HTTP url: http://example.com and my HTTPS url: https://example.com',
);
- expect(findLinksAt(0).attributes().href).toBe(httpUrl);
- expect(findLinksAt(1).attributes().href).toBe(httpsUrl);
+
+ expect(findLinks()).toHaveLength(2);
+
+ expect(findLinkAttributeByIndex(0).href).toBe(httpUrl);
+ expect(findLinkAttributeByIndex(1).href).toBe(httpsUrl);
+ });
+
+ it('renders a multiple links surrounded by text, with other symbols', () => {
+ createComponent(
+ mockProps({ text: `${httpUrl}, ${httpUrl}: ${httpsUrl}; ${httpsUrl}. ${httpsUrl}...` }),
+ );
+ expect(findLine().text()).toBe(
+ 'http://example.com, http://example.com: https://example.com; https://example.com. https://example.com...',
+ );
+
+ expect(findLinks()).toHaveLength(5);
+
+ expect(findLinkAttributeByIndex(0).href).toBe(httpUrl);
+ expect(findLinkAttributeByIndex(1).href).toBe(httpUrl);
+ expect(findLinkAttributeByIndex(2).href).toBe(httpsUrl);
+ expect(findLinkAttributeByIndex(3).href).toBe(httpsUrl);
+ expect(findLinkAttributeByIndex(4).href).toBe(httpsUrl);
});
const jshref = 'javascript:doEvil();'; // eslint-disable-line no-script-url
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 09eb362c77e..a0282e838cd 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -220,6 +220,7 @@ describe('common_utils', () => {
beforeEach(() => {
elem = document.createElement('div');
window.innerHeight = windowHeight;
+ window.mrTabs = { currentAction: 'show' };
jest.spyOn($.fn, 'animate');
jest.spyOn($.fn, 'offset').mockReturnValue({ top: elemTop });
});
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
index 58cb8ef61d1..9a8434a1222 100644
--- a/spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import AccessRequestActionButtons from '~/vue_shared/components/members/action_buttons/access_request_action_buttons.vue';
-import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
-import ApproveAccessRequestButton from '~/vue_shared/components/members/action_buttons/approve_access_request_button.vue';
-import { accessRequest as member } from '../mock_data';
+import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
+import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
+import ApproveAccessRequestButton from '~/members/components/action_buttons/approve_access_request_button.vue';
+import { accessRequest as member } from '../../mock_data';
describe('AccessRequestActionButtons', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/approve_access_request_button_spec.js b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
index 93edaaa400d..7ce2c633bb3 100644
--- a/spec/frontend/vue_shared/components/members/action_buttons/approve_access_request_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
@@ -2,7 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlButton, GlForm } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ApproveAccessRequestButton from '~/vue_shared/components/members/action_buttons/approve_access_request_button.vue';
+import ApproveAccessRequestButton from '~/members/components/action_buttons/approve_access_request_button.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
index 1374cdc6aef..887b21dc1d0 100644
--- a/spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import InviteActionButtons from '~/vue_shared/components/members/action_buttons/invite_action_buttons.vue';
-import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
-import ResendInviteButton from '~/vue_shared/components/members/action_buttons/resend_invite_button.vue';
-import { invite as member } from '../mock_data';
+import InviteActionButtons from '~/members/components/action_buttons/invite_action_buttons.vue';
+import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
+import ResendInviteButton from '~/members/components/action_buttons/resend_invite_button.vue';
+import { invite as member } from '../../mock_data';
describe('InviteActionButtons', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/leave_button_spec.js b/spec/frontend/members/components/action_buttons/leave_button_spec.js
index 00896b23b95..2afe112c74b 100644
--- a/spec/frontend/vue_shared/components/members/action_buttons/leave_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/leave_button_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import LeaveButton from '~/vue_shared/components/members/action_buttons/leave_button.vue';
-import LeaveModal from '~/vue_shared/components/members/modals/leave_modal.vue';
-import { LEAVE_MODAL_ID } from '~/vue_shared/components/members/constants';
-import { member } from '../mock_data';
+import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
+import LeaveModal from '~/members/components/modals/leave_modal.vue';
+import { LEAVE_MODAL_ID } from '~/members/constants';
+import { member } from '../../mock_data';
describe('LeaveButton', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
index 84fe1c51773..45283788676 100644
--- a/spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
@@ -2,8 +2,8 @@ import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import RemoveGroupLinkButton from '~/vue_shared/components/members/action_buttons/remove_group_link_button.vue';
-import { group } from '../mock_data';
+import RemoveGroupLinkButton from '~/members/components/action_buttons/remove_group_link_button.vue';
+import { group } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index 7aa30494234..437b3e705a4 100644
--- a/spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -1,7 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
+import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/resend_invite_button_spec.js b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
index 859fdd01043..a48942dd277 100644
--- a/spec/frontend/vue_shared/components/members/action_buttons/resend_invite_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
@@ -2,7 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ResendInviteButton from '~/vue_shared/components/members/action_buttons/resend_invite_button.vue';
+import ResendInviteButton from '~/members/components/action_buttons/resend_invite_button.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
diff --git a/spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
index f766ad5b0d1..b03e80a537d 100644
--- a/spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import UserActionButtons from '~/vue_shared/components/members/action_buttons/user_action_buttons.vue';
-import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue';
-import LeaveButton from '~/vue_shared/components/members/action_buttons/leave_button.vue';
-import { member, orphanedMember } from '../mock_data';
+import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
+import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
+import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
+import { member, orphanedMember } from '../../mock_data';
describe('UserActionButtons', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js
index d6f5773295c..658bb9462b0 100644
--- a/spec/frontend/vue_shared/components/members/avatars/group_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/group_avatar_spec.js
@@ -1,8 +1,8 @@
import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { GlAvatarLink } from '@gitlab/ui';
-import { group as member } from '../mock_data';
-import GroupAvatar from '~/vue_shared/components/members/avatars/group_avatar.vue';
+import { group as member } from '../../mock_data';
+import GroupAvatar from '~/members/components/avatars/group_avatar.vue';
describe('MemberList', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/avatars/invite_avatar_spec.js b/spec/frontend/members/components/avatars/invite_avatar_spec.js
index 7948da7eb40..13ee727528b 100644
--- a/spec/frontend/vue_shared/components/members/avatars/invite_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/invite_avatar_spec.js
@@ -1,7 +1,7 @@
import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
-import { invite as member } from '../mock_data';
-import InviteAvatar from '~/vue_shared/components/members/avatars/invite_avatar.vue';
+import { invite as member } from '../../mock_data';
+import InviteAvatar from '~/members/components/avatars/invite_avatar.vue';
describe('MemberList', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 93d8e640968..7d6a9065975 100644
--- a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -1,8 +1,8 @@
import { mount, createWrapper } from '@vue/test-utils';
import { within } from '@testing-library/dom';
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
-import { member as memberMock, orphanedMember } from '../mock_data';
-import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
+import { member as memberMock, orphanedMember } from '../../mock_data';
+import UserAvatar from '~/members/components/avatars/user_avatar.vue';
describe('UserAvatar', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index 63de355a3c8..d7acf12212c 100644
--- a/spec/frontend/vue_shared/components/members/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -3,9 +3,9 @@ import { GlModal, GlForm } from '@gitlab/ui';
import { nextTick } from 'vue';
import { within } from '@testing-library/dom';
import Vuex from 'vuex';
-import LeaveModal from '~/vue_shared/components/members/modals/leave_modal.vue';
-import { LEAVE_MODAL_ID } from '~/vue_shared/components/members/constants';
-import { member } from '../mock_data';
+import LeaveModal from '~/members/components/modals/leave_modal.vue';
+import { LEAVE_MODAL_ID } from '~/members/constants';
+import { member } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
diff --git a/spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
index 84da051792d..593dbcd28ba 100644
--- a/spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
@@ -3,9 +3,9 @@ import { GlModal, GlForm } from '@gitlab/ui';
import { nextTick } from 'vue';
import { within } from '@testing-library/dom';
import Vuex from 'vuex';
-import RemoveGroupLinkModal from '~/vue_shared/components/members/modals/remove_group_link_modal.vue';
-import { REMOVE_GROUP_LINK_MODAL_ID } from '~/vue_shared/components/members/constants';
-import { group } from '../mock_data';
+import RemoveGroupLinkModal from '~/members/components/modals/remove_group_link_modal.vue';
+import { REMOVE_GROUP_LINK_MODAL_ID } from '~/members/constants';
+import { group } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
diff --git a/spec/frontend/vue_shared/components/members/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js
index cf3821baf44..a9f809cd805 100644
--- a/spec/frontend/vue_shared/components/members/table/created_at_spec.js
+++ b/spec/frontend/members/components/table/created_at_spec.js
@@ -1,7 +1,7 @@
import { mount, createWrapper } from '@vue/test-utils';
import { within } from '@testing-library/dom';
import { useFakeDate } from 'helpers/fake_date';
-import CreatedAt from '~/vue_shared/components/members/table/created_at.vue';
+import CreatedAt from '~/members/components/table/created_at.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('CreatedAt', () => {
diff --git a/spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js
index a1afdbc2b49..ba1b2256e76 100644
--- a/spec/frontend/vue_shared/components/members/table/expiration_datepicker_spec.js
+++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js
@@ -4,8 +4,8 @@ import { nextTick } from 'vue';
import { GlDatepicker } from '@gitlab/ui';
import { useFakeDate } from 'helpers/fake_date';
import waitForPromises from 'helpers/wait_for_promises';
-import ExpirationDatepicker from '~/vue_shared/components/members/table/expiration_datepicker.vue';
-import { member } from '../mock_data';
+import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
+import { member } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/vue_shared/components/members/table/expires_at_spec.js b/spec/frontend/members/components/table/expires_at_spec.js
index 95ae251b0fd..cf0fc78656e 100644
--- a/spec/frontend/vue_shared/components/members/table/expires_at_spec.js
+++ b/spec/frontend/members/components/table/expires_at_spec.js
@@ -2,7 +2,7 @@ import { mount, createWrapper } from '@vue/test-utils';
import { within } from '@testing-library/dom';
import { useFakeDate } from 'helpers/fake_date';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ExpiresAt from '~/vue_shared/components/members/table/expires_at.vue';
+import ExpiresAt from '~/members/components/table/expires_at.vue';
describe('ExpiresAt', () => {
// March 15th, 2020
diff --git a/spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js
index e55d9b6be2a..b7a6df3d054 100644
--- a/spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import { MEMBER_TYPES } from '~/vue_shared/components/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../mock_data';
-import MemberActionButtons from '~/vue_shared/components/members/table/member_action_buttons.vue';
-import UserActionButtons from '~/vue_shared/components/members/action_buttons/user_action_buttons.vue';
-import GroupActionButtons from '~/vue_shared/components/members/action_buttons/group_action_buttons.vue';
-import InviteActionButtons from '~/vue_shared/components/members/action_buttons/invite_action_buttons.vue';
-import AccessRequestActionButtons from '~/vue_shared/components/members/action_buttons/access_request_action_buttons.vue';
+import { MEMBER_TYPES } from '~/members/constants';
+import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
+import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
+import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
+import GroupActionButtons from '~/members/components/action_buttons/group_action_buttons.vue';
+import InviteActionButtons from '~/members/components/action_buttons/invite_action_buttons.vue';
+import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
describe('MemberActionButtons', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js b/spec/frontend/members/components/table/member_avatar_spec.js
index a171dd830c1..98177893c18 100644
--- a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js
+++ b/spec/frontend/members/components/table/member_avatar_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { MEMBER_TYPES } from '~/vue_shared/components/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../mock_data';
-import MemberAvatar from '~/vue_shared/components/members/table/member_avatar.vue';
-import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
-import GroupAvatar from '~/vue_shared/components/members/avatars/group_avatar.vue';
-import InviteAvatar from '~/vue_shared/components/members/avatars/invite_avatar.vue';
+import { MEMBER_TYPES } from '~/members/constants';
+import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
+import MemberAvatar from '~/members/components/table/member_avatar.vue';
+import UserAvatar from '~/members/components/avatars/user_avatar.vue';
+import GroupAvatar from '~/members/components/avatars/group_avatar.vue';
+import InviteAvatar from '~/members/components/avatars/invite_avatar.vue';
describe('MemberList', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js
index 8b914d76674..48ac06f32f6 100644
--- a/spec/frontend/vue_shared/components/members/table/member_source_spec.js
+++ b/spec/frontend/members/components/table/member_source_spec.js
@@ -1,7 +1,7 @@
import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import MemberSource from '~/vue_shared/components/members/table/member_source.vue';
+import MemberSource from '~/members/components/table/member_source.vue';
describe('MemberSource', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js
index ba693975a88..117c9255c00 100644
--- a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js
+++ b/spec/frontend/members/components/table/members_table_cell_spec.js
@@ -1,10 +1,10 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { MEMBER_TYPES } from '~/vue_shared/components/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../mock_data';
-import MembersTableCell from '~/vue_shared/components/members/table/members_table_cell.vue';
+import { MEMBER_TYPES } from '~/members/constants';
+import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
+import MembersTableCell from '~/members/components/table/members_table_cell.vue';
-describe('MemberList', () => {
+describe('MembersTableCell', () => {
const WrappedComponent = {
props: {
memberType: {
diff --git a/spec/frontend/vue_shared/components/members/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index e593e88438c..9945cc7ee57 100644
--- a/spec/frontend/vue_shared/components/members/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -6,21 +6,21 @@ import {
within,
} from '@testing-library/dom';
import { GlBadge, GlTable } from '@gitlab/ui';
-import MembersTable from '~/vue_shared/components/members/table/members_table.vue';
-import MemberAvatar from '~/vue_shared/components/members/table/member_avatar.vue';
-import MemberSource from '~/vue_shared/components/members/table/member_source.vue';
-import ExpiresAt from '~/vue_shared/components/members/table/expires_at.vue';
-import CreatedAt from '~/vue_shared/components/members/table/created_at.vue';
-import RoleDropdown from '~/vue_shared/components/members/table/role_dropdown.vue';
-import ExpirationDatepicker from '~/vue_shared/components/members/table/expiration_datepicker.vue';
-import MemberActionButtons from '~/vue_shared/components/members/table/member_action_buttons.vue';
+import MembersTable from '~/members/components/table/members_table.vue';
+import MemberAvatar from '~/members/components/table/member_avatar.vue';
+import MemberSource from '~/members/components/table/member_source.vue';
+import ExpiresAt from '~/members/components/table/expires_at.vue';
+import CreatedAt from '~/members/components/table/created_at.vue';
+import RoleDropdown from '~/members/components/table/role_dropdown.vue';
+import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
+import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import * as initUserPopovers from '~/user_popovers';
-import { member as memberMock, invite, accessRequest } from '../mock_data';
+import { member as memberMock, invite, accessRequest } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
-describe('MemberList', () => {
+describe('MembersTable', () => {
let wrapper;
const createStore = (state = {}) => {
diff --git a/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 55ec7000693..8ff1d2c1259 100644
--- a/spec/frontend/vue_shared/components/members/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -5,8 +5,8 @@ import { within } from '@testing-library/dom';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import waitForPromises from 'helpers/wait_for_promises';
-import RoleDropdown from '~/vue_shared/components/members/table/role_dropdown.vue';
-import { member } from '../mock_data';
+import RoleDropdown from '~/members/components/table/role_dropdown.vue';
+import { member } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/vue_shared/components/members/mock_data.js b/spec/frontend/members/mock_data.js
index 5674929716d..5674929716d 100644
--- a/spec/frontend/vue_shared/components/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
diff --git a/spec/frontend/vuex_shared/modules/members/actions_spec.js b/spec/frontend/members/store/actions_spec.js
index c7048a9c421..5424fee0750 100644
--- a/spec/frontend/vuex_shared/modules/members/actions_spec.js
+++ b/spec/frontend/members/store/actions_spec.js
@@ -1,17 +1,17 @@
import { noop } from 'lodash';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { members, group } from 'jest/vue_shared/components/members/mock_data';
+import { members, group } from 'jest/members/mock_data';
import testAction from 'helpers/vuex_action_helper';
import { useFakeDate } from 'helpers/fake_date';
import httpStatusCodes from '~/lib/utils/http_status';
-import * as types from '~/vuex_shared/modules/members/mutation_types';
+import * as types from '~/members/store/mutation_types';
import {
updateMemberRole,
showRemoveGroupLinkModal,
hideRemoveGroupLinkModal,
updateMemberExpiration,
-} from '~/vuex_shared/modules/members/actions';
+} from '~/members/store/actions';
describe('Vuex members actions', () => {
describe('update member actions', () => {
diff --git a/spec/frontend/vuex_shared/modules/members/mutations_spec.js b/spec/frontend/members/store/mutations_spec.js
index 710d43b8990..488bfdf15fd 100644
--- a/spec/frontend/vuex_shared/modules/members/mutations_spec.js
+++ b/spec/frontend/members/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import { members, group } from 'jest/vue_shared/components/members/mock_data';
-import mutations from '~/vuex_shared/modules/members/mutations';
-import * as types from '~/vuex_shared/modules/members/mutation_types';
+import { members, group } from 'jest/members/mock_data';
+import mutations from '~/members/store/mutations';
+import * as types from '~/members/store/mutation_types';
describe('Vuex members mutations', () => {
describe('update member mutations', () => {
diff --git a/spec/frontend/vuex_shared/modules/members/utils_spec.js b/spec/frontend/members/store/utils_spec.js
index 4fc3445dac0..e3cde38269c 100644
--- a/spec/frontend/vuex_shared/modules/members/utils_spec.js
+++ b/spec/frontend/members/store/utils_spec.js
@@ -1,5 +1,5 @@
-import { members } from 'jest/vue_shared/components/members/mock_data';
-import { findMember } from '~/vuex_shared/modules/members/utils';
+import { members } from 'jest/members/mock_data';
+import { findMember } from '~/members/store/utils';
describe('Members Vuex utils', () => {
describe('findMember', () => {
diff --git a/spec/frontend/vue_shared/components/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 3f2b2097133..a598b51d8dd 100644
--- a/spec/frontend/vue_shared/components/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -7,7 +7,7 @@ import {
canResend,
canUpdate,
canOverride,
-} from '~/vue_shared/components/members/utils';
+} from '~/members/utils';
import { member as memberMock, group, invite } from './mock_data';
const DIRECT_MEMBER_ID = 178;
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index d203435e7bf..4114df618e5 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -42,6 +42,7 @@ describe('Discussion navigation mixin', () => {
);
jest.spyOn(utils, 'scrollToElementWithContext');
+ jest.spyOn(utils, 'scrollToElement');
expandDiscussion = jest.fn();
const { actions, ...notesRest } = notesModule();
@@ -133,7 +134,7 @@ describe('Discussion navigation mixin', () => {
});
it('scrolls to element', () => {
- expect(utils.scrollToElementWithContext).toHaveBeenCalledWith(
+ expect(utils.scrollToElement).toHaveBeenCalledWith(
findDiscussion('div.discussion', expected),
);
});
@@ -200,7 +201,7 @@ describe('Discussion navigation mixin', () => {
});
it('scrolls to discussion', () => {
- expect(utils.scrollToElementWithContext).toHaveBeenCalledWith(
+ expect(utils.scrollToElement).toHaveBeenCalledWith(
findDiscussion('div.discussion', expected),
);
});
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 920959f41e7..4a1d42647f8 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -944,10 +944,16 @@ describe('Actions Notes Store', () => {
it('when service success, commits and resolves discussion', done => {
testSubmitSuggestion(done, () => {
expect(commit.mock.calls).toEqual([
+ [mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(dispatch.mock.calls).toEqual([['resolveDiscussion', { discussionId }]]);
+ expect(dispatch.mock.calls).toEqual([
+ ['stopPolling'],
+ ['resolveDiscussion', { discussionId }],
+ ['restartPolling'],
+ ]);
expect(Flash).not.toHaveBeenCalled();
});
});
@@ -958,8 +964,11 @@ describe('Actions Notes Store', () => {
Api.applySuggestion.mockReturnValue(Promise.reject(response));
testSubmitSuggestion(done, () => {
- expect(commit).not.toHaveBeenCalled();
- expect(dispatch).not.toHaveBeenCalled();
+ expect(commit.mock.calls).toEqual([
+ [mutationTypes.SET_RESOLVING_DISCUSSION, true],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, false],
+ ]);
+ expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
expect(Flash).toHaveBeenCalledWith(TEST_ERROR_MESSAGE, 'alert', flashContainer);
});
});
@@ -970,8 +979,11 @@ describe('Actions Notes Store', () => {
Api.applySuggestion.mockReturnValue(Promise.reject(response));
testSubmitSuggestion(done, () => {
- expect(commit).not.toHaveBeenCalled();
- expect(dispatch).not.toHaveBeenCalled();
+ expect(commit.mock.calls).toEqual([
+ [mutationTypes.SET_RESOLVING_DISCUSSION, true],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, false],
+ ]);
+ expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
expect(Flash).toHaveBeenCalledWith(
'Something went wrong while applying the suggestion. Please try again.',
'alert',
@@ -1015,15 +1027,19 @@ describe('Actions Notes Store', () => {
testSubmitSuggestionBatch(done, () => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.APPLY_SUGGESTION, batchSuggestionsInfo[0]],
[mutationTypes.APPLY_SUGGESTION, batchSuggestionsInfo[1]],
[mutationTypes.CLEAR_SUGGESTION_BATCH],
[mutationTypes.SET_APPLYING_BATCH_STATE, false],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
expect(dispatch.mock.calls).toEqual([
+ ['stopPolling'],
['resolveDiscussion', { discussionId: discussionIds[0] }],
['resolveDiscussion', { discussionId: discussionIds[1] }],
+ ['restartPolling'],
]);
expect(Flash).not.toHaveBeenCalled();
@@ -1038,10 +1054,12 @@ describe('Actions Notes Store', () => {
testSubmitSuggestionBatch(done, () => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.SET_APPLYING_BATCH_STATE, false],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(dispatch).not.toHaveBeenCalled();
+ expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
expect(Flash).toHaveBeenCalledWith(TEST_ERROR_MESSAGE, 'alert', flashContainer);
});
});
@@ -1054,10 +1072,12 @@ describe('Actions Notes Store', () => {
testSubmitSuggestionBatch(done, () => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.SET_APPLYING_BATCH_STATE, false],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(dispatch).not.toHaveBeenCalled();
+ expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
expect(Flash).toHaveBeenCalledWith(
'Something went wrong while applying the batch of suggestions. Please try again.',
'alert',
@@ -1072,10 +1092,12 @@ describe('Actions Notes Store', () => {
testSubmitSuggestionBatch(done, () => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.APPLY_SUGGESTION, batchSuggestionsInfo[0]],
[mutationTypes.APPLY_SUGGESTION, batchSuggestionsInfo[1]],
[mutationTypes.CLEAR_SUGGESTION_BATCH],
[mutationTypes.SET_APPLYING_BATCH_STATE, false],
+ [mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
expect(Flash).not.toHaveBeenCalled();
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index 2618c3a53b8..922918ef50b 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -377,6 +377,16 @@ describe('Notes Store mutations', () => {
});
});
+ describe('SET_RESOLVING_DISCUSSION', () => {
+ it('should set resolving discussion state', () => {
+ const state = {};
+
+ mutations.SET_RESOLVING_DISCUSSION(state, true);
+
+ expect(state.isResolvingDiscussion).toEqual(true);
+ });
+ });
+
describe('UPDATE_NOTE', () => {
it('should update a note', () => {
const state = {
diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
index d27038e765f..c51130dae00 100644
--- a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -202,6 +202,67 @@ exports[`packages_list_app renders 1`] = `
</b-tab-stub>
<b-tab-stub
tag="div"
+ title="Generic"
+ titlelinkclass="gl-tab-nav-item"
+ >
+ <template>
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no Generic packages yet"
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no Generic packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div>
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </template>
+ </b-tab-stub>
+ <b-tab-stub
+ tag="div"
title="Maven"
titlelinkclass="gl-tab-nav-item"
>
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
new file mode 100644
index 00000000000..ae2a9e5065d
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
@@ -0,0 +1,116 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import { GlFormInput, GlFormTextarea } from '@gitlab/ui';
+
+import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
+
+import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
+
+describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
+ let wrapper;
+
+ const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(CommitForm, {
+ propsData: {
+ defaultMessage: mockCommitMessage,
+ defaultBranch: mockDefaultBranch,
+ ...props,
+ },
+
+ // attachToDocument is required for input/submit events
+ attachToDocument: mountFn === mount,
+ });
+ };
+
+ const findCommitTextarea = () => wrapper.find(GlFormTextarea);
+ const findBranchInput = () => wrapper.find(GlFormInput);
+ const findNewMrCheckbox = () => wrapper.find('[data-testid="new-mr-checkbox"]');
+ const findSubmitBtn = () => wrapper.find('[type="submit"]');
+ const findCancelBtn = () => wrapper.find('[type="reset"]');
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when the form is displayed', () => {
+ beforeEach(async () => {
+ createComponent();
+ });
+
+ it('shows a default commit message', () => {
+ expect(findCommitTextarea().attributes('value')).toBe(mockCommitMessage);
+ });
+
+ it('shows a default branch', () => {
+ expect(findBranchInput().attributes('value')).toBe(mockDefaultBranch);
+ });
+
+ it('shows buttons', () => {
+ expect(findSubmitBtn().exists()).toBe(true);
+ expect(findCancelBtn().exists()).toBe(true);
+ });
+
+ it('does not show a new MR checkbox by default', () => {
+ expect(findNewMrCheckbox().exists()).toBe(false);
+ });
+ });
+
+ describe('when buttons are clicked', () => {
+ beforeEach(async () => {
+ createComponent({}, mount);
+ });
+
+ it('emits an event when the form submits', () => {
+ findSubmitBtn().trigger('click');
+
+ expect(wrapper.emitted('submit')[0]).toEqual([
+ {
+ message: mockCommitMessage,
+ branch: mockDefaultBranch,
+ openMergeRequest: false,
+ },
+ ]);
+ });
+
+ it('emits an event when the form resets', () => {
+ findCancelBtn().trigger('click');
+
+ expect(wrapper.emitted('cancel')).toHaveLength(1);
+ });
+ });
+
+ describe('when user inputs values', () => {
+ const anotherMessage = 'Another commit message';
+ const anotherBranch = 'my-branch';
+
+ beforeEach(() => {
+ createComponent({}, mount);
+
+ findCommitTextarea().setValue(anotherMessage);
+ findBranchInput().setValue(anotherBranch);
+ });
+
+ it('shows a new MR checkbox', () => {
+ expect(findNewMrCheckbox().exists()).toBe(true);
+ });
+
+ it('emits an event with values', async () => {
+ await findNewMrCheckbox().setChecked();
+ await findSubmitBtn().trigger('click');
+
+ expect(wrapper.emitted('submit')[0]).toEqual([
+ {
+ message: anotherMessage,
+ branch: anotherBranch,
+ openMergeRequest: true,
+ },
+ ]);
+ });
+
+ it('when the commit message is empty, submit button is disabled', async () => {
+ await findCommitTextarea().setValue('');
+
+ expect(findSubmitBtn().attributes('disabled')).toBe('disabled');
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/text_editor_spec.js b/spec/frontend/pipeline_editor/components/text_editor_spec.js
index 39d205839f4..18f71ebc95c 100644
--- a/spec/frontend/pipeline_editor/components/text_editor_spec.js
+++ b/spec/frontend/pipeline_editor/components/text_editor_spec.js
@@ -6,12 +6,16 @@ import TextEditor from '~/pipeline_editor/components/text_editor.vue';
describe('~/pipeline_editor/components/text_editor.vue', () => {
let wrapper;
+ const editorReadyListener = jest.fn();
- const createComponent = (props = {}, mountFn = shallowMount) => {
+ const createComponent = (attrs = {}, mountFn = shallowMount) => {
wrapper = mountFn(TextEditor, {
- propsData: {
+ attrs: {
value: mockCiYml,
- ...props,
+ ...attrs,
+ },
+ listeners: {
+ 'editor-ready': editorReadyListener,
},
});
};
@@ -28,14 +32,13 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
expect(findEditor().props('value')).toBe(mockCiYml);
});
- it('editor is readony and configured for .yml', () => {
- expect(findEditor().props('editorOptions')).toEqual({ readOnly: true });
+ it('editor is configured for .yml', () => {
expect(findEditor().props('fileName')).toBe('*.yml');
});
- it('bubbles up editor-ready event', () => {
+ it('bubbles up events', () => {
findEditor().vm.$emit('editor-ready');
- expect(wrapper.emitted('editor-ready')).toHaveLength(1);
+ expect(editorReadyListener).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index 96fa6e5e004..3073b748e50 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -1,5 +1,8 @@
export const mockProjectPath = 'user1/project1';
export const mockDefaultBranch = 'master';
+export const mockNewMergeRequestPath = '/-/merge_requests/new';
+export const mockCommitId = 'aabbccdd';
+export const mockCommitMessage = 'My commit message';
export const mockCiConfigPath = '.gitlab-ci.yml';
export const mockCiYml = `
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index 46523baadf3..c6dafc6e258 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,32 +1,82 @@
import { nextTick } from 'vue';
-import { shallowMount } from '@vue/test-utils';
-import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
+import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import {
+ GlAlert,
+ GlButton,
+ GlFormInput,
+ GlFormTextarea,
+ GlLoadingIcon,
+ GlTabs,
+ GlTab,
+} from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
+
+import { redirectTo, refreshCurrentPage, objectToQuery } from '~/lib/utils/url_utility';
+import {
+ mockCiConfigPath,
+ mockCiYml,
+ mockCommitId,
+ mockCommitMessage,
+ mockDefaultBranch,
+ mockProjectPath,
+ mockNewMergeRequestPath,
+} from './mock_data';
-import { mockProjectPath, mockDefaultBranch, mockCiConfigPath, mockCiYml } from './mock_data';
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
+import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+ refreshCurrentPage: jest.fn(),
+ objectToQuery: jest.requireActual('~/lib/utils/url_utility').objectToQuery,
+ mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
+}));
describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
let wrapper;
- const createComponent = (
- { props = {}, data = {}, loading = false } = {},
+ let mockMutate;
+ let mockApollo;
+ let mockBlobContentData;
+
+ const createComponent = ({
+ props = {},
+ loading = false,
+ options = {},
mountFn = shallowMount,
- ) => {
+ } = {}) => {
+ mockMutate = jest.fn().mockResolvedValue({
+ data: {
+ commitCreate: {
+ errors: [],
+ commit: {},
+ },
+ },
+ });
+
wrapper = mountFn(PipelineEditorApp, {
propsData: {
- projectPath: mockProjectPath,
- defaultBranch: mockDefaultBranch,
ciConfigPath: mockCiConfigPath,
+ commitId: mockCommitId,
+ defaultBranch: mockDefaultBranch,
+ projectPath: mockProjectPath,
+ newMergeRequestPath: mockNewMergeRequestPath,
...props,
},
- data() {
- return data;
- },
stubs: {
GlTabs,
+ GlButton,
+ CommitForm,
+ EditorLite: {
+ template: '<div/>',
+ },
TextEditor,
},
mocks: {
@@ -36,45 +86,73 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
loading,
},
},
+ mutate: mockMutate,
},
},
+ // attachToDocument is required for input/submit events
+ attachToDocument: mountFn === mount,
+ ...options,
});
};
+ const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
+ mockApollo = createMockApollo([], {
+ Query: {
+ blobContent() {
+ return {
+ __typename: 'BlobContent',
+ rawData: mockBlobContentData(),
+ };
+ },
+ },
+ });
+
+ const options = {
+ localVue,
+ mocks: {},
+ apolloProvider: mockApollo,
+ };
+
+ createComponent({ props, options }, mountFn);
+ };
+
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAlert = () => wrapper.find(GlAlert);
const findTabAt = i => wrapper.findAll(GlTab).at(i);
- const findEditorLite = () => wrapper.find(EditorLite);
+ const findTextEditor = () => wrapper.find(TextEditor);
+ const findCommitForm = () => wrapper.find(CommitForm);
+ const findCommitBtnLoadingIcon = () => wrapper.find('[type="submit"]').find(GlLoadingIcon);
beforeEach(() => {
- createComponent();
+ mockBlobContentData = jest.fn();
});
afterEach(() => {
+ mockBlobContentData.mockReset();
+ refreshCurrentPage.mockReset();
+ redirectTo.mockReset();
+ mockMutate.mockReset();
+
wrapper.destroy();
wrapper = null;
});
- it('displays content', () => {
- createComponent({ data: { content: mockCiYml } });
-
- expect(findLoadingIcon().exists()).toBe(false);
- expect(findEditorLite().props('value')).toBe(mockCiYml);
- });
-
it('displays a loading icon if the query is loading', () => {
createComponent({ loading: true });
expect(findLoadingIcon().exists()).toBe(true);
+ expect(findTextEditor().exists()).toBe(false);
});
describe('tabs', () => {
- it('displays tabs and their content', () => {
- createComponent({ data: { content: mockCiYml } });
+ beforeEach(() => {
+ createComponent();
+ });
+ it('displays tabs and their content', async () => {
expect(
findTabAt(0)
- .find(EditorLite)
+ .find(TextEditor)
.exists(),
).toBe(true);
expect(
@@ -85,55 +163,234 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
});
it('displays editor tab lazily, until editor is ready', async () => {
- createComponent({ data: { content: mockCiYml } });
-
expect(findTabAt(0).attributes('lazy')).toBe('true');
- findEditorLite().vm.$emit('editor-ready');
+ findTextEditor().vm.$emit('editor-ready');
+
await nextTick();
expect(findTabAt(0).attributes('lazy')).toBe(undefined);
});
});
- describe('when in error state', () => {
- class MockError extends Error {
- constructor(message, data) {
- super(message);
- if (data) {
- this.networkError = {
- response: { data },
- };
+ describe('when data is set', () => {
+ beforeEach(async () => {
+ createComponent({ mountFn: mount });
+
+ wrapper.setData({
+ content: mockCiYml,
+ contentModel: mockCiYml,
+ });
+
+ await nextTick();
+ });
+
+ it('displays content after the query loads', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findTextEditor().attributes('value')).toBe(mockCiYml);
+ });
+
+ describe('commit form', () => {
+ const mockVariables = {
+ content: mockCiYml,
+ filePath: mockCiConfigPath,
+ lastCommitId: mockCommitId,
+ message: mockCommitMessage,
+ projectPath: mockProjectPath,
+ startBranch: mockDefaultBranch,
+ };
+
+ const findInForm = selector => findCommitForm().find(selector);
+
+ const submitCommit = async ({
+ message = mockCommitMessage,
+ branch = mockDefaultBranch,
+ openMergeRequest = false,
+ } = {}) => {
+ await findInForm(GlFormTextarea).setValue(message);
+ await findInForm(GlFormInput).setValue(branch);
+ if (openMergeRequest) {
+ await findInForm('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest);
}
- }
- }
+ await findInForm('[type="submit"]').trigger('click');
+ };
+
+ const cancelCommitForm = async () => {
+ const findCancelBtn = () => wrapper.find('[type="reset"]');
+ await findCancelBtn().trigger('click');
+ };
+
+ describe('when the user commits changes to the current branch', () => {
+ beforeEach(async () => {
+ await submitCommit();
+ });
+
+ it('calls the mutation with the default branch', () => {
+ expect(mockMutate).toHaveBeenCalledWith({
+ mutation: expect.any(Object),
+ variables: {
+ ...mockVariables,
+ branch: mockDefaultBranch,
+ },
+ });
+ });
- it('shows a generic error', () => {
- const error = new MockError('An error message');
- createComponent({ data: { error } });
+ it('refreshes the page', () => {
+ expect(refreshCurrentPage).toHaveBeenCalled();
+ });
- expect(findAlert().text()).toBe('CI file could not be loaded: An error message');
+ it('shows no saving state', () => {
+ expect(findCommitBtnLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when the user commits changes to a new branch', () => {
+ const newBranch = 'new-branch';
+
+ beforeEach(async () => {
+ await submitCommit({
+ branch: newBranch,
+ });
+ });
+
+ it('calls the mutation with the new branch', () => {
+ expect(mockMutate).toHaveBeenCalledWith({
+ mutation: expect.any(Object),
+ variables: {
+ ...mockVariables,
+ branch: newBranch,
+ },
+ });
+ });
+
+ it('refreshes the page', () => {
+ expect(refreshCurrentPage).toHaveBeenCalledWith();
+ });
+ });
+
+ describe('when the user commits changes to open a new merge request', () => {
+ const newBranch = 'new-branch';
+
+ beforeEach(async () => {
+ await submitCommit({
+ branch: newBranch,
+ openMergeRequest: true,
+ });
+ });
+
+ it('redirects to the merge request page with source and target branches', () => {
+ const branchesQuery = objectToQuery({
+ 'merge_request[source_branch]': newBranch,
+ 'merge_request[target_branch]': mockDefaultBranch,
+ });
+
+ expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
+ });
+ });
+
+ describe('when the commit is ocurring', () => {
+ it('shows a saving state', async () => {
+ await mockMutate.mockImplementationOnce(() => {
+ expect(findCommitBtnLoadingIcon().exists()).toBe(true);
+ return Promise.resolve();
+ });
+
+ await submitCommit({
+ message: mockCommitMessage,
+ branch: mockDefaultBranch,
+ openMergeRequest: false,
+ });
+ });
+ });
+
+ describe('when the commit fails', () => {
+ it('shows a the error message', async () => {
+ mockMutate.mockRejectedValueOnce(new Error('commit failed'));
+
+ await submitCommit();
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toMatchInterpolatedText(
+ 'The GitLab CI configuration could not be updated. commit failed',
+ );
+ });
+
+ it('shows an unkown error', async () => {
+ mockMutate.mockRejectedValueOnce();
+
+ await submitCommit();
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toMatchInterpolatedText(
+ 'The GitLab CI configuration could not be updated.',
+ );
+ });
+ });
+
+ describe('when the commit form is cancelled', () => {
+ const otherContent = 'other content';
+
+ beforeEach(async () => {
+ findTextEditor().vm.$emit('input', otherContent);
+ await nextTick();
+ });
+
+ it('content is restored after cancel is called', async () => {
+ await cancelCommitForm();
+
+ expect(findTextEditor().attributes('value')).toBe(mockCiYml);
+ });
+ });
+ });
+ });
+
+ describe('displays fetch content errors', () => {
+ it('no error is show when data is set', async () => {
+ mockBlobContentData.mockResolvedValue(mockCiYml);
+ createComponentWithApollo();
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(false);
+ expect(findTextEditor().attributes('value')).toBe(mockCiYml);
});
- it('shows a ref missing error state', () => {
- const error = new MockError('Ref missing!', {
- error: 'ref is missing, ref is empty',
+ it('shows a 404 error message', async () => {
+ mockBlobContentData.mockRejectedValueOnce({
+ response: {
+ status: 404,
+ },
});
- createComponent({ data: { error } });
+ createComponentWithApollo();
- expect(findAlert().text()).toMatch(
- 'CI file could not be loaded: ref is missing, ref is empty',
- );
+ await waitForPromises();
+
+ expect(findAlert().text()).toMatch('No CI file found in this repository, please add one.');
});
- it('shows a file missing error state', async () => {
- const error = new MockError('File missing!', {
- message: 'file not found',
+ it('shows a 400 error message', async () => {
+ mockBlobContentData.mockRejectedValueOnce({
+ response: {
+ status: 400,
+ },
});
+ createComponentWithApollo();
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toMatch(
+ 'Repository does not have a default branch, please set one.',
+ );
+ });
- await wrapper.setData({ error });
+ it('shows a unkown error message', async () => {
+ mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
+ createComponentWithApollo();
+ await waitForPromises();
- expect(findAlert().text()).toMatch('CI file could not be loaded: file not found');
+ expect(findAlert().text()).toMatch('The CI configuration was not loaded, please try again.');
});
});
});
diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
index 58e8065033f..8cef499fdb9 100644
--- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
@@ -10,11 +10,19 @@ describe('Getters TestReports Store', () => {
const defaultState = {
testReports,
selectedSuiteIndex: 0,
+ pageInfo: {
+ page: 1,
+ perPage: 2,
+ },
};
const emptyState = {
testReports: {},
selectedSuite: null,
+ pageInfo: {
+ page: 1,
+ perPage: 2,
+ },
};
beforeEach(() => {
@@ -59,15 +67,17 @@ describe('Getters TestReports Store', () => {
});
describe('getSuiteTests', () => {
- it('should return the test cases inside the suite', () => {
+ it('should return the current page of test cases inside the suite', () => {
setupState();
const cases = getters.getSuiteTests(state);
- const expected = testReports.test_suites[0].test_cases.map(x => ({
- ...x,
- formattedTime: formattedTime(x.execution_time),
- icon: iconForTestStatus(x.status),
- }));
+ const expected = testReports.test_suites[0].test_cases
+ .map(x => ({
+ ...x,
+ formattedTime: formattedTime(x.execution_time),
+ icon: iconForTestStatus(x.status),
+ }))
+ .slice(0, state.pageInfo.perPage);
expect(cases).toEqual(expected);
});
@@ -78,4 +88,15 @@ describe('Getters TestReports Store', () => {
expect(getters.getSuiteTests(state)).toEqual([]);
});
});
+
+ describe('getSuiteTestCount', () => {
+ it('should return the total number of test cases', () => {
+ setupState();
+
+ const testCount = getters.getSuiteTestCount(state);
+ const expected = testReports.test_suites[0].test_cases.length;
+
+ expect(testCount).toEqual(expected);
+ });
+ });
});
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index b935029bc6a..191e9e7391c 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -12,12 +12,25 @@ describe('Mutations TestReports Store', () => {
testReports: {},
selectedSuite: null,
isLoading: false,
+ pageInfo: {
+ page: 1,
+ perPage: 2,
+ },
};
beforeEach(() => {
mockState = { ...defaultState };
});
+ describe('set page', () => {
+ it('should set the current page to display', () => {
+ const pageToDisplay = 3;
+ mutations[types.SET_PAGE](mockState, pageToDisplay);
+
+ expect(mockState.pageInfo.page).toEqual(pageToDisplay);
+ });
+ });
+
describe('set suite', () => {
it('should set the suite at the given index', () => {
mockState.testReports = testReports;
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index 284099b000b..0e00ca670a7 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
-import { GlButton, GlFriendlyWrap } from '@gitlab/ui';
+import { GlButton, GlFriendlyWrap, GlPagination } from '@gitlab/ui';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { TestStatus } from '~/pipelines/constants';
@@ -26,13 +26,17 @@ describe('Test reports suite table', () => {
const findCaseRowAtIndex = index => wrapper.findAll('.js-case-row').at(index);
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
- const createComponent = (suite = testSuite) => {
+ const createComponent = (suite = testSuite, perPage = 20) => {
store = new Vuex.Store({
state: {
testReports: {
test_suites: [suite],
},
selectedSuiteIndex: 0,
+ pageInfo: {
+ page: 1,
+ perPage,
+ },
},
getters,
});
@@ -86,4 +90,20 @@ describe('Test reports suite table', () => {
expect(button.attributes('data-clipboard-text')).toBe(file);
});
});
+
+ describe('when a test suite has more test cases than the pagination size', () => {
+ const perPage = 2;
+
+ beforeEach(() => {
+ createComponent(testSuite, perPage);
+ });
+
+ it('renders one page of test cases', () => {
+ expect(allCaseRows().length).toBe(perPage);
+ });
+
+ it('renders a pagination component', () => {
+ expect(wrapper.find(GlPagination).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/registry/settings/components/expiration_dropdown_spec.js b/spec/frontend/registry/settings/components/expiration_dropdown_spec.js
new file mode 100644
index 00000000000..e0cac317ad6
--- /dev/null
+++ b/spec/frontend/registry/settings/components/expiration_dropdown_spec.js
@@ -0,0 +1,83 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup, GlFormSelect } from 'jest/registry/shared/stubs';
+import component from '~/registry/settings/components/expiration_dropdown.vue';
+
+describe('ExpirationDropdown', () => {
+ let wrapper;
+
+ const defaultProps = {
+ name: 'foo',
+ label: 'label-bar',
+ formOptions: [{ key: 'foo', label: 'bar' }, { key: 'baz', label: 'zab' }],
+ };
+
+ const findFormSelect = () => wrapper.find(GlFormSelect);
+ const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findOptions = () => wrapper.findAll('[data-testid="option"]');
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlFormGroup,
+ GlFormSelect,
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has a form-select component', () => {
+ mountComponent();
+ expect(findFormSelect().exists()).toBe(true);
+ });
+
+ it('has the correct options', () => {
+ mountComponent();
+
+ expect(findOptions()).toHaveLength(defaultProps.formOptions.length);
+ });
+ });
+
+ describe('model', () => {
+ it('assign the right props to the form-select component', () => {
+ const value = 'foobar';
+ const disabled = true;
+
+ mountComponent({ value, disabled });
+
+ expect(findFormSelect().props()).toMatchObject({
+ value,
+ disabled,
+ });
+ expect(findFormSelect().attributes('id')).toBe(defaultProps.name);
+ });
+
+ it('assign the right props to the form-group component', () => {
+ mountComponent();
+
+ expect(findFormGroup().attributes()).toMatchObject({
+ id: `${defaultProps.name}-form-group`,
+ 'label-for': defaultProps.name,
+ label: defaultProps.label,
+ });
+ });
+
+ it('emits input event when form-select emits input', () => {
+ const emittedValue = 'barfoo';
+
+ mountComponent();
+
+ findFormSelect().vm.$emit('input', emittedValue);
+
+ expect(wrapper.emitted('input')).toEqual([[emittedValue]]);
+ });
+ });
+});
diff --git a/spec/frontend/registry/settings/components/expiration_run_text_spec.js b/spec/frontend/registry/settings/components/expiration_run_text_spec.js
new file mode 100644
index 00000000000..d023f1fd05a
--- /dev/null
+++ b/spec/frontend/registry/settings/components/expiration_run_text_spec.js
@@ -0,0 +1,66 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormInput } from '@gitlab/ui';
+import { GlFormGroup } from 'jest/registry/shared/stubs';
+import component from '~/registry/settings/components/expiration_run_text.vue';
+import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/constants';
+
+describe('ExpirationToggle', () => {
+ let wrapper;
+ const value = 'foo';
+
+ const findInput = () => wrapper.find(GlFormInput);
+ const findFormGroup = () => wrapper.find(GlFormGroup);
+
+ const mountComponent = propsData => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlFormGroup,
+ },
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has an input component', () => {
+ mountComponent();
+ expect(findInput().exists()).toBe(true);
+ });
+ });
+
+ describe('model', () => {
+ it('assigns the right props to the input component', () => {
+ mountComponent({ value, disabled: true });
+
+ expect(findInput().attributes()).toMatchObject({
+ value,
+ });
+ });
+
+ it('assigns the right props to the form-group component', () => {
+ mountComponent();
+
+ expect(findFormGroup().attributes()).toMatchObject({
+ label: NEXT_CLEANUP_LABEL,
+ });
+ });
+ });
+
+ describe('formattedValue', () => {
+ it('displays the values when it exists', () => {
+ mountComponent({ value });
+
+ expect(findInput().attributes('value')).toBe(value);
+ });
+
+ it('displays a placeholder when no value is present', () => {
+ mountComponent();
+
+ expect(findInput().attributes('value')).toBe(NOT_SCHEDULED_POLICY_TEXT);
+ });
+ });
+});
diff --git a/spec/frontend/registry/settings/components/expiration_textarea_spec.js b/spec/frontend/registry/settings/components/expiration_textarea_spec.js
new file mode 100644
index 00000000000..80464c61117
--- /dev/null
+++ b/spec/frontend/registry/settings/components/expiration_textarea_spec.js
@@ -0,0 +1,169 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlSprintf, GlFormTextarea, GlLink } from '@gitlab/ui';
+import { GlFormGroup } from 'jest/registry/shared/stubs';
+import component from '~/registry/settings/components/expiration_textarea.vue';
+import { NAME_REGEX_LENGTH } from '~/registry/shared/constants';
+
+describe('ExpirationTextarea', () => {
+ let wrapper;
+
+ const defaultProps = {
+ name: 'foo',
+ label: 'label-bar',
+ placeholder: 'placeholder-baz',
+ description: '%{linkStart}description-foo%{linkEnd}',
+ };
+
+ const tagsRegexHelpPagePath = 'fooPath';
+
+ const findTextArea = () => wrapper.find(GlFormTextarea);
+ const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findLabel = () => wrapper.find('[data-testid="label"]');
+ const findDescription = () => wrapper.find('[data-testid="description"]');
+ const findDescriptionLink = () => wrapper.find(GlLink);
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlSprintf,
+ GlFormGroup,
+ },
+ provide: {
+ tagsRegexHelpPagePath,
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has a label', () => {
+ mountComponent();
+
+ expect(findLabel().text()).toBe(defaultProps.label);
+ });
+
+ it('has a textarea component', () => {
+ mountComponent();
+
+ expect(findTextArea().exists()).toBe(true);
+ });
+
+ it('has a description', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(defaultProps.description);
+ });
+
+ it('has a description link', () => {
+ mountComponent();
+
+ const link = findDescriptionLink();
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(tagsRegexHelpPagePath);
+ });
+ });
+
+ describe('model', () => {
+ it('assigns the right props to the textarea component', () => {
+ const value = 'foobar';
+ const disabled = true;
+
+ mountComponent({ value, disabled });
+
+ expect(findTextArea().attributes()).toMatchObject({
+ id: defaultProps.name,
+ value,
+ placeholder: defaultProps.placeholder,
+ disabled: `${disabled}`,
+ trim: '',
+ });
+ });
+
+ it('emits input event when textarea emits input', () => {
+ const emittedValue = 'barfoo';
+
+ mountComponent();
+
+ findTextArea().vm.$emit('input', emittedValue);
+ expect(wrapper.emitted('input')).toEqual([[emittedValue]]);
+ });
+ });
+
+ describe('regex textarea validation', () => {
+ const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(',');
+
+ describe('when error contains an error message', () => {
+ const errorMessage = 'something went wrong';
+
+ it('shows the error message on the relevant field', () => {
+ mountComponent({ error: errorMessage });
+
+ expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage);
+ });
+
+ it('gives precedence to API errors compared to local ones', () => {
+ mountComponent({
+ error: errorMessage,
+ value: invalidString,
+ });
+
+ expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage);
+ });
+ });
+
+ describe('when error is empty', () => {
+ describe('if the user did not type', () => {
+ it('validation is not emitted', () => {
+ mountComponent();
+
+ expect(wrapper.emitted('validation')).toBeUndefined();
+ });
+
+ it('no error message is shown', () => {
+ mountComponent();
+
+ expect(findFormGroup().props('state')).toBe(true);
+ expect(findFormGroup().attributes('invalid-feedback')).toBe('');
+ });
+ });
+
+ describe('when the user typed something', () => {
+ describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => {
+ beforeEach(() => {
+ // since the component has no state we both emit the event and set the prop
+ mountComponent({ value: invalidString });
+
+ findTextArea().vm.$emit('input', invalidString);
+ });
+
+ it('textAreaValidation state is false', () => {
+ expect(findFormGroup().props('state')).toBe(false);
+ expect(findTextArea().attributes('state')).toBeUndefined();
+ });
+
+ it('emits the @validation event with false payload', () => {
+ expect(wrapper.emitted('validation')).toEqual([[false]]);
+ });
+ });
+
+ it(`when user input is less than ${NAME_REGEX_LENGTH} state is "true"`, () => {
+ mountComponent();
+
+ findTextArea().vm.$emit('input', 'foo');
+
+ expect(findFormGroup().props('state')).toBe(true);
+ expect(findTextArea().attributes('state')).toBe('true');
+ expect(wrapper.emitted('validation')).toEqual([[true]]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/registry/settings/components/expiration_toggle_spec.js b/spec/frontend/registry/settings/components/expiration_toggle_spec.js
new file mode 100644
index 00000000000..8b670c98dc1
--- /dev/null
+++ b/spec/frontend/registry/settings/components/expiration_toggle_spec.js
@@ -0,0 +1,80 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlToggle, GlSprintf } from '@gitlab/ui';
+import { GlFormGroup } from 'jest/registry/shared/stubs';
+import component from '~/registry/settings/components/expiration_toggle.vue';
+import {
+ ENABLE_TOGGLE_DESCRIPTION,
+ ENABLED_TEXT,
+ DISABLED_TEXT,
+} from '~/registry/settings/constants';
+
+describe('ExpirationToggle', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.find(GlToggle);
+ const findDescription = () => wrapper.find('[data-testid="description"]');
+
+ const mountComponent = propsData => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlFormGroup,
+ GlSprintf,
+ },
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('structure', () => {
+ it('has a toggle component', () => {
+ mountComponent();
+
+ expect(findToggle().exists()).toBe(true);
+ });
+
+ it('has a description', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toContain(
+ ENABLE_TOGGLE_DESCRIPTION.replace('%{toggleStatus}', ''),
+ );
+ });
+ });
+
+ describe('model', () => {
+ it('assigns the right props to the toggle component', () => {
+ mountComponent({ value: true, disabled: true });
+
+ expect(findToggle().props()).toMatchObject({
+ value: true,
+ disabled: true,
+ });
+ });
+
+ it('emits input event when toggle is updated', () => {
+ mountComponent();
+
+ findToggle().vm.$emit('change', false);
+
+ expect(wrapper.emitted('input')).toEqual([[false]]);
+ });
+ });
+
+ describe('toggle description', () => {
+ it('says enabled when the toggle is on', () => {
+ mountComponent({ value: true });
+
+ expect(findDescription().text()).toContain(ENABLED_TEXT);
+ });
+
+ it('says disabled when the toggle is off', () => {
+ mountComponent({ value: false });
+
+ expect(findDescription().text()).toContain(DISABLED_TEXT);
+ });
+ });
+});
diff --git a/spec/frontend/registry/settings/mock_data.js b/spec/frontend/registry/settings/mock_data.js
index 7f3772ce7fe..7cc645fcf55 100644
--- a/spec/frontend/registry/settings/mock_data.js
+++ b/spec/frontend/registry/settings/mock_data.js
@@ -1,13 +1,18 @@
+export const containerExpirationPolicyData = () => ({
+ cadence: 'EVERY_DAY',
+ enabled: true,
+ keepN: 'TEN_TAGS',
+ nameRegex: 'asdasdssssdfdf',
+ nameRegexKeep: 'sss',
+ olderThan: 'FOURTEEN_DAYS',
+ nextRunAt: '2020-11-19T07:37:03.941Z',
+});
+
export const expirationPolicyPayload = override => ({
data: {
project: {
containerExpirationPolicy: {
- cadence: 'EVERY_DAY',
- enabled: true,
- keepN: 'TEN_TAGS',
- nameRegex: 'asdasdssssdfdf',
- nameRegexKeep: 'sss',
- olderThan: 'FOURTEEN_DAYS',
+ ...containerExpirationPolicyData(),
...override,
},
},
@@ -26,12 +31,7 @@ export const expirationPolicyMutationPayload = ({ override, errors = [] } = {})
data: {
updateContainerExpirationPolicy: {
containerExpirationPolicy: {
- cadence: 'EVERY_DAY',
- enabled: true,
- keepN: 'TEN_TAGS',
- nameRegex: 'asdasdssssdfdf',
- nameRegexKeep: 'sss',
- olderThan: 'FOURTEEN_DAYS',
+ ...containerExpirationPolicyData(),
...override,
},
errors,
diff --git a/spec/frontend/registry/shared/stubs.js b/spec/frontend/registry/shared/stubs.js
index f6b88d70e49..ad41eb42df4 100644
--- a/spec/frontend/registry/shared/stubs.js
+++ b/spec/frontend/registry/shared/stubs.js
@@ -9,3 +9,23 @@ export const GlCard = {
</div>
`,
};
+
+export const GlFormGroup = {
+ name: 'gl-form-group-stub',
+ props: ['state'],
+ template: `
+ <div>
+ <slot name="label"></slot>
+ <slot></slot>
+ <slot name="description"></slot>
+ </div>`,
+};
+
+export const GlFormSelect = {
+ name: 'gl-form-select-stub',
+ props: ['disabled', 'value'],
+ template: `
+ <div>
+ <slot></slot>
+ </div>`,
+};
diff --git a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
index 6640c0844e2..e295c587d70 100644
--- a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
+++ b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
@@ -4,13 +4,8 @@ exports[`SidebarTodo template renders component container element with proper da
<button
aria-label="Mark as done"
class="btn btn-default btn-todo issuable-header-btn float-right"
- data-boundary="viewport"
- data-container="body"
data-issuable-id="1"
data-issuable-type="epic"
- data-original-title=""
- data-placement="left"
- title=""
type="button"
>
<gl-icon-stub
diff --git a/spec/frontend/sidebar/issuable_assignees_spec.js b/spec/frontend/sidebar/issuable_assignees_spec.js
index 076616de040..af4dc315aad 100644
--- a/spec/frontend/sidebar/issuable_assignees_spec.js
+++ b/spec/frontend/sidebar/issuable_assignees_spec.js
@@ -26,8 +26,8 @@ describe('IssuableAssignees', () => {
createComponent();
});
- it('renders "None"', () => {
- expect(findEmptyAssignee().text()).toBe('None');
+ it('renders "None - assign yourself"', () => {
+ expect(findEmptyAssignee().text()).toBe('None - assign yourself');
});
});
@@ -38,4 +38,12 @@ describe('IssuableAssignees', () => {
expect(findUncollapsedAssigneeList().exists()).toBe(true);
});
});
+
+ describe('when clicking "assign yourself"', () => {
+ it('emits "assign-self"', () => {
+ createComponent();
+ wrapper.find('[data-testid="assign-yourself"]').vm.$emit('click');
+ expect(wrapper.emitted('assign-self')).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
index 266c906ba60..f9b6ac721d2 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
@@ -164,9 +164,7 @@ describe('MRWidgetHeader', () => {
it('renders checkout branch button with modal trigger', () => {
const button = vm.$el.querySelector('.js-check-out-branch');
- expect(button.textContent.trim()).toEqual('Check out branch');
- expect(button.getAttribute('data-target')).toEqual('#modal_merge_info');
- expect(button.getAttribute('data-toggle')).toEqual('modal');
+ expect(button.textContent.trim()).toBe('Check out branch');
});
it('renders web ide button', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index 19f8a67d066..b8cd1469179 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -194,7 +194,7 @@ describe('MRWidgetConflicts', () => {
});
it('sets resolve button as disabled', () => {
- expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe('disabled');
+ expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe('true');
});
it('renders popover', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
index 5326d63cb8a..f9490ac77ff 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
@@ -1,4 +1,5 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlFormCheckbox } from '@gitlab/ui';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
import { SQUASH_BEFORE_MERGE } from '~/vue_merge_request_widget/i18n';
@@ -20,17 +21,15 @@ describe('Squash before merge component', () => {
wrapper.destroy();
});
- const findLabel = () => wrapper.find('[data-testid="squashLabel"]');
+ const findCheckbox = () => wrapper.find(GlFormCheckbox);
describe('checkbox', () => {
- const findCheckbox = () => wrapper.find('.js-squash-checkbox');
-
it('is unchecked if passed value prop is false', () => {
createComponent({
value: false,
});
- expect(findCheckbox().element.checked).toBeFalsy();
+ expect(findCheckbox().vm.$attrs.checked).toBe(false);
});
it('is checked if passed value prop is true', () => {
@@ -38,22 +37,7 @@ describe('Squash before merge component', () => {
value: true,
});
- expect(findCheckbox().element.checked).toBeTruthy();
- });
-
- it('changes value on click', done => {
- createComponent({
- value: false,
- });
-
- findCheckbox().element.checked = true;
-
- findCheckbox().trigger('change');
-
- wrapper.vm.$nextTick(() => {
- expect(findCheckbox().element.checked).toBeTruthy();
- done();
- });
+ expect(findCheckbox().vm.$attrs.checked).toBe(true);
});
it('is disabled if isDisabled prop is true', () => {
@@ -62,31 +46,12 @@ describe('Squash before merge component', () => {
isDisabled: true,
});
- expect(findCheckbox().attributes('disabled')).toBeTruthy();
- });
- });
-
- describe('label', () => {
- describe.each`
- isDisabled | expectation
- ${true} | ${'grays out text if it is true'}
- ${false} | ${'does not gray out text if it is false'}
- `('isDisabled prop', ({ isDisabled, expectation }) => {
- beforeEach(() => {
- createComponent({
- value: false,
- isDisabled,
- });
- });
-
- it(expectation, () => {
- expect(findLabel().classes('gl-text-gray-400')).toBe(isDisabled);
- });
+ expect(findCheckbox().vm.$attrs.disabled).toBe(true);
});
});
describe('tooltip', () => {
- const tooltipTitle = () => findLabel().attributes('title');
+ const tooltipTitle = () => findCheckbox().attributes('title');
it('does not render when isDisabled is false', () => {
createComponent({
@@ -114,7 +79,7 @@ describe('Squash before merge component', () => {
const aboutLink = wrapper.find('a');
- expect(aboutLink.exists()).toBeFalsy();
+ expect(aboutLink.exists()).toBe(false);
});
it('is rendered if help path is passed', () => {
@@ -125,7 +90,7 @@ describe('Squash before merge component', () => {
const aboutLink = wrapper.find('a');
- expect(aboutLink.exists()).toBeTruthy();
+ expect(aboutLink.exists()).toBe(true);
});
it('should have a correct help path if passed', () => {
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
index 17d7fcc4bff..19a5566c3b1 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
@@ -8,6 +8,7 @@ import {
SUCCESS,
FAILED,
CANCELED,
+ SKIPPED,
} from '~/vue_merge_request_widget/components/deployment/constants';
import { deploymentMockData, playDetails, retryDetails } from './deployment_mock_data';
@@ -77,6 +78,10 @@ describe('Deployment component', () => {
${CANCELED} | ${true} | ${noDetails} | ${'Canceled deployment to'} | ${defaultGroup}
${CANCELED} | ${false} | ${deployDetail} | ${'Canceled deployment to'} | ${noActions}
${CANCELED} | ${false} | ${noDetails} | ${'Canceled deployment to'} | ${noActions}
+ ${SKIPPED} | ${true} | ${deployDetail} | ${'Skipped deployment to'} | ${defaultGroup}
+ ${SKIPPED} | ${true} | ${noDetails} | ${'Skipped deployment to'} | ${defaultGroup}
+ ${SKIPPED} | ${false} | ${deployDetail} | ${'Skipped deployment to'} | ${noActions}
+ ${SKIPPED} | ${false} | ${noDetails} | ${'Skipped deployment to'} | ${noActions}
`(
'$status + previous: $previous + manual: $deploymentDetails.isManual',
({ status, previous, deploymentDetails, text, actionButtons }) => {
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index 144283dc507..8ee920f06a1 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -41,6 +41,7 @@ export default {
user_callouts_path: 'some/callout/path',
suggest_pipeline_feature_id: 'suggest_pipeline',
new_project_pipeline_path: '/group2/project2/pipelines/new',
+ source_project_default_url: '/gitlab-org/html5-boilerplate.git',
metrics: {
merged_by: {
name: 'Administrator',
@@ -263,6 +264,8 @@ export default {
merge_trains_count: 3,
merge_train_index: 1,
security_reports_docs_path: 'security-reports-docs-path',
+ sast_comparison_path: '/sast_comparison_path',
+ secret_scanning_comparison_path: '/secret_scanning_comparison_path',
};
export const mockStore = {
diff --git a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js b/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js
new file mode 100644
index 00000000000..aaaee3327a8
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js
@@ -0,0 +1,68 @@
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MrWidgetHowToMergeModal from '~/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue';
+
+describe('MRWidgetHowToMerge', () => {
+ let wrapper;
+
+ function mountComponent({ data = {}, props = {} } = {}) {
+ wrapper = shallowMount(MrWidgetHowToMergeModal, {
+ data() {
+ return { ...data };
+ },
+ propsData: {
+ ...props,
+ },
+ stubs: {},
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ const findModal = () => wrapper.find(GlModal);
+ const findInstructionsFields = () =>
+ wrapper.findAll('[ data-testid="how-to-merge-instructions"]');
+ const findTipLink = () => wrapper.find(GlSprintf);
+
+ it('renders a modal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('renders a selection of markdown fields', () => {
+ expect(findInstructionsFields().length).toBe(3);
+ });
+
+ it('renders a tip including a link to docs when a valid link is present', () => {
+ mountComponent({ props: { reviewingDocsPath: '/gitlab-org/help' } });
+ expect(findTipLink().exists()).toBe(true);
+ });
+
+ it('should not render a tip including a link to docs when a valid link is not present', () => {
+ expect(findTipLink().exists()).toBe(false);
+ });
+
+ it('should render different instructions based on if the user can merge', () => {
+ mountComponent({ props: { canMerge: true } });
+ expect(
+ findInstructionsFields()
+ .at(2)
+ .text(),
+ ).toContain('git push origin');
+ });
+
+ it('should render different instructions based on if the merge is based off a fork', () => {
+ mountComponent({ props: { isFork: true } });
+ expect(
+ findInstructionsFields()
+ .at(0)
+ .text(),
+ ).toContain('FETCH_HEAD');
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index d6f85dcfcc7..8c917e9339f 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -849,7 +849,7 @@ describe('mrWidgetOptions', () => {
});
});
- describe('suggestPipeline feature flag', () => {
+ describe('suggestPipeline', () => {
beforeEach(() => {
mock.onAny().reply(200);
@@ -860,8 +860,6 @@ describe('mrWidgetOptions', () => {
describe('given feature flag is enabled', () => {
beforeEach(() => {
- gon.features = { suggestPipeline: true };
-
createComponent();
vm.mr.hasCI = false;
@@ -891,19 +889,5 @@ describe('mrWidgetOptions', () => {
expect(findSuggestPipeline()).toBeNull();
});
});
-
- describe('given feature flag is not enabled', () => {
- beforeEach(() => {
- gon.features = { suggestPipeline: false };
-
- createComponent();
-
- vm.mr.hasCI = false;
- });
-
- it('should not suggest pipelines when none exist', () => {
- expect(findSuggestPipeline()).toBeNull();
- });
- });
});
});
diff --git a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
index f73f78d6f6e..8b2c10ec50a 100644
--- a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -1,3 +1,4 @@
+import { convertToCamelCase } from '~/lib/utils/text_utility';
import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from '../mock_data';
@@ -141,10 +142,29 @@ describe('MergeRequestStore', () => {
expect(store.newPipelinePath).toBe('/group2/project2/pipelines/new');
});
+ it('should set sourceProjectDefaultUrl', () => {
+ store.setPaths({ ...mockData });
+
+ expect(store.sourceProjectDefaultUrl).toBe('/gitlab-org/html5-boilerplate.git');
+ });
+
it('should set securityReportsDocsPath', () => {
store.setPaths({ ...mockData });
expect(store.securityReportsDocsPath).toBe('security-reports-docs-path');
});
+
+ it.each(['sast_comparison_path', 'secret_scanning_comparison_path'])(
+ 'should set %s path',
+ property => {
+ // Ensure something is set in the mock data
+ expect(property in mockData).toBe(true);
+ const expectedValue = mockData[property];
+
+ store.setPaths({ ...mockData });
+
+ expect(store[convertToCamelCase(property)]).toBe(expectedValue);
+ },
+ );
});
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
index 04ae2a0f34d..20ea897e29c 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
@@ -5,12 +5,17 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="awards js-awards-block"
>
<button
- class="btn award-control"
+ class="btn gl-mr-3 btn-default btn-md gl-button"
data-testid="award-button"
title="Ada, Leonardo, and Marie"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -23,18 +28,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 3
+
+ <span
+ class="js-counter"
+ >
+ 3
+ </span>
</span>
</button>
<button
- class="btn award-control active"
+ class="btn gl-mr-3 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You, Ada, and Marie"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -47,18 +62,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 3
+
+ <span
+ class="js-counter"
+ >
+ 3
+ </span>
</span>
</button>
<button
- class="btn award-control"
+ class="btn gl-mr-3 btn-default btn-md gl-button"
data-testid="award-button"
title="Ada and Jane"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -71,18 +96,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 2
+
+ <span
+ class="js-counter"
+ >
+ 2
+ </span>
</span>
</button>
<button
- class="btn award-control active"
+ class="btn gl-mr-3 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You, Ada, Jane, and Leonardo"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -95,18 +130,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 4
+
+ <span
+ class="js-counter"
+ >
+ 4
+ </span>
</span>
</button>
<button
- class="btn award-control active"
+ class="btn gl-mr-3 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -119,18 +164,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 1
+
+ <span
+ class="js-counter"
+ >
+ 1
+ </span>
</span>
</button>
<button
- class="btn award-control"
+ class="btn gl-mr-3 btn-default btn-md gl-button"
data-testid="award-button"
title="Marie"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -143,18 +198,28 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 1
+
+ <span
+ class="js-counter"
+ >
+ 1
+ </span>
</span>
</button>
<button
- class="btn award-control active"
+ class="btn gl-mr-3 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You"
type="button"
>
+ <!---->
+
+ <!---->
+
<span
+ class="award-emoji-block"
data-testid="award-html"
>
@@ -167,9 +232,14 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
<span
- class="award-control-text js-counter"
+ class="gl-button-text"
>
- 1
+
+ <span
+ class="js-counter"
+ >
+ 1
+ </span>
</span>
</button>
@@ -178,46 +248,59 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
>
<button
aria-label="Add reaction"
- class="award-control btn js-add-award js-test-add-button-class"
+ class="btn add-reaction-button js-add-award btn-default btn-md gl-button js-test-add-button-class"
title="Add reaction"
type="button"
>
- <span
- class="award-control-icon award-control-icon-neutral"
- >
- <gl-icon-stub
- aria-hidden="true"
- name="slight-smile"
- size="16"
- />
- </span>
+ <!---->
+ <!---->
+
<span
- class="award-control-icon award-control-icon-positive"
+ class="gl-button-text"
>
- <gl-icon-stub
- aria-hidden="true"
- name="smiley"
- size="16"
- />
+ <span
+ class="reaction-control-icon reaction-control-icon-neutral"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="slight-smile-icon"
+ >
+ <use
+ href="#slight-smile"
+ />
+ </svg>
+ </span>
+
+ <span
+ class="reaction-control-icon reaction-control-icon-positive"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="smiley-icon"
+ >
+ <use
+ href="#smiley"
+ />
+ </svg>
+ </span>
+
+ <span
+ class="reaction-control-icon reaction-control-icon-super-positive"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="smile-icon"
+ >
+ <use
+ href="#smile"
+ />
+ </svg>
+ </span>
</span>
-
- <span
- class="award-control-icon award-control-icon-super-positive"
- >
- <gl-icon-stub
- aria-hidden="true"
- name="smiley"
- size="16"
- />
- </span>
-
- <gl-loading-icon-stub
- class="award-control-icon-loading"
- color="dark"
- label="Loading"
- size="md"
- />
</button>
</div>
</div>
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 19a649089e0..adb6c935f96 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
@@ -11,6 +11,7 @@ exports[`Expand button on click when short text is provided renders button after
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
>
@@ -39,6 +40,7 @@ exports[`Expand button on click when short text is provided renders button after
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
>
@@ -62,6 +64,7 @@ exports[`Expand button when short text is provided renders button before text 1`
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
>
@@ -90,6 +93,7 @@ exports[`Expand button when short text is provided renders button before text 1`
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
>
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index 63fc8a5749d..d20de81c446 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
const createUser = (id, name) => ({ id, name });
@@ -41,6 +41,8 @@ const TEST_AWARDS = [
];
const TEST_ADD_BUTTON_CLASS = 'js-test-add-button-class';
+const REACTION_CONTROL_CLASSES = ['btn', 'gl-mr-3', 'btn-default', 'btn-md', 'gl-button'];
+
describe('vue_shared/components/awards_list', () => {
let wrapper;
@@ -54,16 +56,16 @@ describe('vue_shared/components/awards_list', () => {
throw new Error('There should only be one wrapper created per test');
}
- wrapper = shallowMount(AwardsList, { propsData: props });
+ wrapper = mount(AwardsList, { propsData: props });
};
const matchingEmojiTag = name => expect.stringMatching(`gl-emoji data-name="${name}"`);
- const findAwardButtons = () => wrapper.findAll('[data-testid="award-button"');
+ const findAwardButtons = () => wrapper.findAll('[data-testid="award-button"]');
const findAwardsData = () =>
findAwardButtons().wrappers.map(x => {
return {
classes: x.classes(),
title: x.attributes('title'),
- html: x.find('[data-testid="award-html"]').element.innerHTML,
+ html: x.find('[data-testid="award-html"]').html(),
count: Number(x.find('.js-counter').text()),
};
});
@@ -86,43 +88,43 @@ describe('vue_shared/components/awards_list', () => {
it('shows awards in correct order', () => {
expect(findAwardsData()).toEqual([
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: 'Ada, Leonardo, and Marie',
},
{
- classes: ['btn', 'award-control', 'active'],
+ classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: 'You, Ada, and Marie',
},
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 2,
html: matchingEmojiTag(EMOJI_SMILE),
title: 'Ada and Jane',
},
{
- classes: ['btn', 'award-control', 'active'],
+ classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 4,
html: matchingEmojiTag(EMOJI_OK),
title: 'You, Ada, Jane, and Leonardo',
},
{
- classes: ['btn', 'award-control', 'active'],
+ classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_CACTUS),
title: 'You',
},
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_A),
title: 'Marie',
},
{
- classes: ['btn', 'award-control', 'active'],
+ classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_B),
title: 'You',
@@ -135,7 +137,7 @@ describe('vue_shared/components/awards_list', () => {
findAwardButtons()
.at(2)
- .trigger('click');
+ .vm.$emit('click');
expect(wrapper.emitted().award).toEqual([[EMOJI_SMILE]]);
});
@@ -162,7 +164,7 @@ describe('vue_shared/components/awards_list', () => {
findAwardButtons()
.at(0)
- .trigger('click');
+ .vm.$emit('click');
expect(wrapper.emitted().award).toEqual([[Number(EMOJI_100)]]);
});
@@ -225,26 +227,26 @@ describe('vue_shared/components/awards_list', () => {
it('shows awards in correct order', () => {
expect(findAwardsData()).toEqual([
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: '',
},
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: '',
},
// We expect the EMOJI_100 before the EMOJI_SMILE because it was given as a defaultAward
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_100),
title: 'Marie',
},
{
- classes: ['btn', 'award-control'],
+ classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_SMILE),
title: 'Marie',
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
new file mode 100644
index 00000000000..d0fa2086fdc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
@@ -0,0 +1,47 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`gfm_autocomplete/utils issues config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context issue title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils issues config shows the reference and title in the menu item within a group context 1`] = `"<small>gitlab#987654</small> Group context issue title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils labels config shows the title in the menu item 1`] = `
+"
+ <span class=\\"dropdown-label-box\\" style=\\"background: #123456;\\"></span>
+ bug &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"
+`;
+
+exports[`gfm_autocomplete/utils members config shows an avatar character, name, parent name, and count in the menu item for a group 1`] = `
+"
+ <div class=\\"gl-display-flex gl-align-items-center\\">
+ <div class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-rounded-small
+ gl-display-flex gl-align-items-center gl-justify-content-center\\" aria-hidden=\\"true\\">
+ G</div>
+ <div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
+ <div>1-1s &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt; (2)</div>
+ <div class=\\"gl-text-gray-700\\">GitLab Support Team</div>
+ </div>
+
+ </div>
+ "
+`;
+
+exports[`gfm_autocomplete/utils members config shows the avatar, name and username in the menu item for a user 1`] = `
+"
+ <div class=\\"gl-display-flex gl-align-items-center\\">
+ <img class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" />
+ <div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
+ <div>My Name &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</div>
+ <div class=\\"gl-text-gray-700\\">@myusername</div>
+ </div>
+
+ </div>
+ "
+`;
+
+exports[`gfm_autocomplete/utils merge requests config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context merge request title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils merge requests config shows the reference and title in the menu item within a group context 1`] = `"<small>gitlab!456789</small> Group context merge request title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils milestones config shows the title in the menu item 1`] = `"13.2 &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
+
+exports[`gfm_autocomplete/utils snippets config shows the id and title in the menu item 1`] = `"<small>123456</small> Snippet title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
diff --git a/spec/frontend/vue_shared/components/gl_mentions_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js
index 32fc055a77d..d12faacca75 100644
--- a/spec/frontend/vue_shared/components/gl_mentions_spec.js
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js
@@ -1,15 +1,15 @@
import { shallowMount } from '@vue/test-utils';
import Tribute from 'tributejs';
-import GlMentions from '~/vue_shared/components/gl_mentions.vue';
+import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue';
-describe('GlMentions', () => {
+describe('GfmAutocomplete', () => {
let wrapper;
- describe('Tribute', () => {
+ describe('tribute', () => {
const mentions = '/gitlab-org/gitlab-test/-/autocomplete_sources/members?type=Issue&type_id=1';
beforeEach(() => {
- wrapper = shallowMount(GlMentions, {
+ wrapper = shallowMount(GfmAutocomplete, {
propsData: {
dataSources: {
mentions,
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js
new file mode 100644
index 00000000000..647f8c6e000
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js
@@ -0,0 +1,344 @@
+import { escape, last } from 'lodash';
+import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils';
+
+describe('gfm_autocomplete/utils', () => {
+ describe('issues config', () => {
+ const issuesConfig = tributeConfig[GfmAutocompleteType.Issues].config;
+ const groupContextIssue = {
+ iid: 987654,
+ reference: 'gitlab#987654',
+ title: "Group context issue title <script>alert('hi')</script>",
+ };
+ const projectContextIssue = {
+ id: null,
+ iid: 123456,
+ time_estimate: 0,
+ title: "Project context issue title <script>alert('hi')</script>",
+ };
+
+ it('uses # as the trigger', () => {
+ expect(issuesConfig.trigger).toBe('#');
+ });
+
+ it('searches using both the iid and title', () => {
+ expect(issuesConfig.lookup(projectContextIssue)).toBe(
+ `${projectContextIssue.iid}${projectContextIssue.title}`,
+ );
+ });
+
+ it('shows the reference and title in the menu item within a group context', () => {
+ expect(issuesConfig.menuItemTemplate({ original: groupContextIssue })).toMatchSnapshot();
+ });
+
+ it('shows the iid and title in the menu item within a project context', () => {
+ expect(issuesConfig.menuItemTemplate({ original: projectContextIssue })).toMatchSnapshot();
+ });
+
+ it('inserts the reference on autocomplete selection within a group context', () => {
+ expect(issuesConfig.selectTemplate({ original: groupContextIssue })).toBe(
+ groupContextIssue.reference,
+ );
+ });
+
+ it('inserts the iid on autocomplete selection within a project context', () => {
+ expect(issuesConfig.selectTemplate({ original: projectContextIssue })).toBe(
+ `#${projectContextIssue.iid}`,
+ );
+ });
+ });
+
+ describe('labels config', () => {
+ const labelsConfig = tributeConfig[GfmAutocompleteType.Labels].config;
+ const labelsFilter = tributeConfig[GfmAutocompleteType.Labels].filterValues;
+ const label = {
+ color: '#123456',
+ textColor: '#FFFFFF',
+ title: `bug <script>alert('hi')</script>`,
+ type: 'GroupLabel',
+ };
+ const singleWordLabel = {
+ color: '#456789',
+ textColor: '#DDD',
+ title: `bug`,
+ type: 'GroupLabel',
+ };
+ const numericalLabel = {
+ color: '#abcdef',
+ textColor: '#AAA',
+ title: 123456,
+ type: 'ProjectLabel',
+ };
+
+ it('uses ~ as the trigger', () => {
+ expect(labelsConfig.trigger).toBe('~');
+ });
+
+ it('searches using `title`', () => {
+ expect(labelsConfig.lookup).toBe('title');
+ });
+
+ it('shows the title in the menu item', () => {
+ expect(labelsConfig.menuItemTemplate({ original: label })).toMatchSnapshot();
+ });
+
+ it('inserts the title on autocomplete selection', () => {
+ expect(labelsConfig.selectTemplate({ original: singleWordLabel })).toBe(
+ `~${escape(singleWordLabel.title)}`,
+ );
+ });
+
+ it('inserts the title enclosed with quotes on autocomplete selection when the title is numerical', () => {
+ expect(labelsConfig.selectTemplate({ original: numericalLabel })).toBe(
+ `~"${escape(numericalLabel.title)}"`,
+ );
+ });
+
+ it('inserts the title enclosed with quotes on autocomplete selection when the title contains multiple words', () => {
+ expect(labelsConfig.selectTemplate({ original: label })).toBe(`~"${escape(label.title)}"`);
+ });
+
+ describe('filter', () => {
+ const collection = [label, singleWordLabel, { ...numericalLabel, set: true }];
+
+ describe('/label quick action', () => {
+ describe('when the line starts with `/label`', () => {
+ it('shows labels that are not currently selected', () => {
+ const fullText = '/label ~';
+ const selectionStart = 8;
+
+ expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([
+ collection[0],
+ collection[1],
+ ]);
+ });
+ });
+
+ describe('when the line does not start with `/label`', () => {
+ it('shows all labels', () => {
+ const fullText = '~';
+ const selectionStart = 1;
+
+ expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection);
+ });
+ });
+ });
+
+ describe('/unlabel quick action', () => {
+ describe('when the line starts with `/unlabel`', () => {
+ it('shows labels that are currently selected', () => {
+ const fullText = '/unlabel ~';
+ const selectionStart = 10;
+
+ expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([collection[2]]);
+ });
+ });
+
+ describe('when the line does not start with `/unlabel`', () => {
+ it('shows all labels', () => {
+ const fullText = '~';
+ const selectionStart = 1;
+
+ expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection);
+ });
+ });
+ });
+ });
+ });
+
+ describe('members config', () => {
+ const membersConfig = tributeConfig[GfmAutocompleteType.Members].config;
+ const membersFilter = tributeConfig[GfmAutocompleteType.Members].filterValues;
+ const userMember = {
+ type: 'User',
+ username: 'myusername',
+ name: "My Name <script>alert('hi')</script>",
+ avatar_url: '/uploads/-/system/user/avatar/123456/avatar.png',
+ availability: null,
+ };
+ const groupMember = {
+ type: 'Group',
+ username: 'gitlab-com/support/1-1s',
+ name: "GitLab.com / GitLab Support Team / 1-1s <script>alert('hi')</script>",
+ avatar_url: null,
+ count: 2,
+ mentionsDisabled: null,
+ };
+
+ it('uses @ as the trigger', () => {
+ expect(membersConfig.trigger).toBe('@');
+ });
+
+ it('inserts the username on autocomplete selection', () => {
+ expect(membersConfig.fillAttr).toBe('username');
+ });
+
+ it('searches using both the name and username for a user', () => {
+ expect(membersConfig.lookup(userMember)).toBe(`${userMember.name}${userMember.username}`);
+ });
+
+ it('searches using only its own name and not its ancestors for a group', () => {
+ expect(membersConfig.lookup(groupMember)).toBe(last(groupMember.name.split(' / ')));
+ });
+
+ it('shows the avatar, name and username in the menu item for a user', () => {
+ expect(membersConfig.menuItemTemplate({ original: userMember })).toMatchSnapshot();
+ });
+
+ it('shows an avatar character, name, parent name, and count in the menu item for a group', () => {
+ expect(membersConfig.menuItemTemplate({ original: groupMember })).toMatchSnapshot();
+ });
+
+ describe('filter', () => {
+ const assignees = [userMember.username];
+ const collection = [userMember, groupMember];
+
+ describe('/assign quick action', () => {
+ describe('when the line starts with `/assign`', () => {
+ it('shows members that are not currently selected', () => {
+ const fullText = '/assign @';
+ const selectionStart = 9;
+
+ expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([
+ collection[1],
+ ]);
+ });
+ });
+
+ describe('when the line does not start with `/assign`', () => {
+ it('shows all labels', () => {
+ const fullText = '@';
+ const selectionStart = 1;
+
+ expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual(
+ collection,
+ );
+ });
+ });
+ });
+
+ describe('/unassign quick action', () => {
+ describe('when the line starts with `/unassign`', () => {
+ it('shows members that are currently selected', () => {
+ const fullText = '/unassign @';
+ const selectionStart = 11;
+
+ expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([
+ collection[0],
+ ]);
+ });
+ });
+
+ describe('when the line does not start with `/unassign`', () => {
+ it('shows all members', () => {
+ const fullText = '@';
+ const selectionStart = 1;
+
+ expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual(
+ collection,
+ );
+ });
+ });
+ });
+ });
+ });
+
+ describe('merge requests config', () => {
+ const mergeRequestsConfig = tributeConfig[GfmAutocompleteType.MergeRequests].config;
+ const groupContextMergeRequest = {
+ iid: 456789,
+ reference: 'gitlab!456789',
+ title: "Group context merge request title <script>alert('hi')</script>",
+ };
+ const projectContextMergeRequest = {
+ id: null,
+ iid: 123456,
+ time_estimate: 0,
+ title: "Project context merge request title <script>alert('hi')</script>",
+ };
+
+ it('uses ! as the trigger', () => {
+ expect(mergeRequestsConfig.trigger).toBe('!');
+ });
+
+ it('searches using both the iid and title', () => {
+ expect(mergeRequestsConfig.lookup(projectContextMergeRequest)).toBe(
+ `${projectContextMergeRequest.iid}${projectContextMergeRequest.title}`,
+ );
+ });
+
+ it('shows the reference and title in the menu item within a group context', () => {
+ expect(
+ mergeRequestsConfig.menuItemTemplate({ original: groupContextMergeRequest }),
+ ).toMatchSnapshot();
+ });
+
+ it('shows the iid and title in the menu item within a project context', () => {
+ expect(
+ mergeRequestsConfig.menuItemTemplate({ original: projectContextMergeRequest }),
+ ).toMatchSnapshot();
+ });
+
+ it('inserts the reference on autocomplete selection within a group context', () => {
+ expect(mergeRequestsConfig.selectTemplate({ original: groupContextMergeRequest })).toBe(
+ groupContextMergeRequest.reference,
+ );
+ });
+
+ it('inserts the iid on autocomplete selection within a project context', () => {
+ expect(mergeRequestsConfig.selectTemplate({ original: projectContextMergeRequest })).toBe(
+ `!${projectContextMergeRequest.iid}`,
+ );
+ });
+ });
+
+ describe('milestones config', () => {
+ const milestonesConfig = tributeConfig[GfmAutocompleteType.Milestones].config;
+ const milestone = {
+ id: null,
+ iid: 49,
+ title: "13.2 <script>alert('hi')</script>",
+ };
+
+ it('uses % as the trigger', () => {
+ expect(milestonesConfig.trigger).toBe('%');
+ });
+
+ it('searches using the title', () => {
+ expect(milestonesConfig.lookup).toBe('title');
+ });
+
+ it('shows the title in the menu item', () => {
+ expect(milestonesConfig.menuItemTemplate({ original: milestone })).toMatchSnapshot();
+ });
+
+ it('inserts the title on autocomplete selection', () => {
+ expect(milestonesConfig.selectTemplate({ original: milestone })).toBe(
+ `%"${escape(milestone.title)}"`,
+ );
+ });
+ });
+
+ describe('snippets config', () => {
+ const snippetsConfig = tributeConfig[GfmAutocompleteType.Snippets].config;
+ const snippet = {
+ id: 123456,
+ title: "Snippet title <script>alert('hi')</script>",
+ };
+
+ it('uses $ as the trigger', () => {
+ expect(snippetsConfig.trigger).toBe('$');
+ });
+
+ it('inserts the id on autocomplete selection', () => {
+ expect(snippetsConfig.fillAttr).toBe('id');
+ });
+
+ it('searches using both the id and title', () => {
+ expect(snippetsConfig.lookup(snippet)).toBe(`${snippet.id}${snippet.title}`);
+ });
+
+ it('shows the id and title in the menu item', () => {
+ expect(snippetsConfig.menuItemTemplate({ original: snippet })).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
index ecea151fc8a..da49778f216 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -47,6 +47,7 @@ exports[`Package code instruction single line to match the default snapshot 1`]
<!---->
<svg
+ aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="copy-to-clipboard-icon"
>
diff --git a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
new file mode 100644
index 00000000000..0336f4c0325
--- /dev/null
+++ b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
@@ -0,0 +1,144 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Severity Summary given the message {"countMessage": "%{criticalStart}0 Critical%{criticalEnd} %{highStart}1 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 0, "high": 1, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
+<span>
+ Security scanning detected
+ <strong>
+ 1
+ </strong>
+ potential vulnerability
+ <span
+ class="gl-font-sm"
+ >
+ <span>
+ <span
+ class="gl-pl-4"
+ >
+
+ 0 Critical
+
+ </span>
+ </span>
+
+ <span>
+ <strong
+ class="text-danger-600 gl-px-2"
+ >
+
+ 1 High
+
+ </strong>
+ </span>
+ and
+ <span>
+ <span
+ class="gl-px-2"
+ >
+
+ 0 Others
+
+ </span>
+ </span>
+ </span>
+</span>
+`;
+
+exports[`Severity Summary given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}0 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 0, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
+<span>
+ Security scanning detected
+ <strong>
+ 1
+ </strong>
+ potential vulnerability
+ <span
+ class="gl-font-sm"
+ >
+ <span>
+ <strong
+ class="text-danger-800 gl-pl-4"
+ >
+
+ 1 Critical
+
+ </strong>
+ </span>
+
+ <span>
+ <span
+ class="gl-px-2"
+ >
+
+ 0 High
+
+ </span>
+ </span>
+ and
+ <span>
+ <span
+ class="gl-px-2"
+ >
+
+ 0 Others
+
+ </span>
+ </span>
+ </span>
+</span>
+`;
+
+exports[`Severity Summary given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}2 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 2, "message": "Security scanning detected %{totalStart}3%{totalEnd} potential vulnerabilities", "other": 0, "status": "", "total": 3} interpolates correctly 1`] = `
+<span>
+ Security scanning detected
+ <strong>
+ 3
+ </strong>
+ potential vulnerabilities
+ <span
+ class="gl-font-sm"
+ >
+ <span>
+ <strong
+ class="text-danger-800 gl-pl-4"
+ >
+
+ 1 Critical
+
+ </strong>
+ </span>
+
+ <span>
+ <strong
+ class="text-danger-600 gl-px-2"
+ >
+
+ 2 High
+
+ </strong>
+ </span>
+ and
+ <span>
+ <span
+ class="gl-px-2"
+ >
+
+ 0 Others
+
+ </span>
+ </span>
+ </span>
+</span>
+`;
+
+exports[`Severity Summary given the message {"message": ""} interpolates correctly 1`] = `
+<span>
+
+ <!---->
+</span>
+`;
+
+exports[`Severity Summary given the message {"message": "foo"} interpolates correctly 1`] = `
+<span>
+ foo
+ <!---->
+</span>
+`;
diff --git a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
new file mode 100644
index 00000000000..4217fef9fbf
--- /dev/null
+++ b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
@@ -0,0 +1,38 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue';
+import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
+
+describe('Severity Summary', () => {
+ let wrapper;
+
+ const createWrapper = message => {
+ wrapper = shallowMount(SecuritySummary, {
+ propsData: { message },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe.each([
+ { message: '' },
+ { message: 'foo' },
+ groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 0, total: 1 }),
+ groupedTextBuilder({ reportType: 'Security scanning', critical: 0, high: 1, total: 1 }),
+ groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 2, total: 3 }),
+ ])('given the message %p', message => {
+ beforeEach(() => {
+ createWrapper(message);
+ });
+
+ it('interpolates correctly', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
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 8c17a974b39..1206450bbeb 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
@@ -20,22 +20,24 @@ jest.mock('~/lib/utils/common_utils', () => ({
const localVue = createLocalVue();
localVue.use(Vuex);
-const createComponent = (config = mockConfig, slots = {}) =>
- shallowMount(LabelsSelectRoot, {
- localVue,
- slots,
- store: new Vuex.Store(labelsSelectModule()),
- propsData: config,
- stubs: {
- 'dropdown-contents': DropdownContents,
- },
- });
-
describe('LabelsSelectRoot', () => {
let wrapper;
+ let store;
+
+ const createComponent = (config = mockConfig, slots = {}) => {
+ wrapper = shallowMount(LabelsSelectRoot, {
+ localVue,
+ slots,
+ store,
+ propsData: config,
+ stubs: {
+ 'dropdown-contents': DropdownContents,
+ },
+ });
+ };
beforeEach(() => {
- wrapper = createComponent();
+ store = new Vuex.Store(labelsSelectModule());
});
afterEach(() => {
@@ -45,6 +47,7 @@ describe('LabelsSelectRoot', () => {
describe('methods', () => {
describe('handleVuexActionDispatch', () => {
it('calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`', () => {
+ createComponent();
jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
wrapper.vm.handleVuexActionDispatch(
@@ -67,7 +70,7 @@ describe('LabelsSelectRoot', () => {
});
it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => {
- wrapper = createComponent({
+ createComponent({
...mockConfig,
variant: 'embedded',
});
@@ -95,6 +98,10 @@ describe('LabelsSelectRoot', () => {
});
describe('handleDropdownClose', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty', () => {
wrapper.vm.handleDropdownClose([{ id: 1 }, { id: 2 }]);
@@ -112,6 +119,7 @@ describe('LabelsSelectRoot', () => {
describe('handleCollapsedValueClick', () => {
it('emits `toggleCollapse` event on component', () => {
+ createComponent();
wrapper.vm.handleCollapsedValueClick();
expect(wrapper.emitted().toggleCollapse).toBeTruthy();
@@ -121,6 +129,7 @@ describe('LabelsSelectRoot', () => {
describe('template', () => {
it('renders component with classes `labels-select-wrapper position-relative`', () => {
+ createComponent();
expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
});
@@ -131,7 +140,7 @@ describe('LabelsSelectRoot', () => {
`(
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
({ variant, cssClass }) => {
- wrapper = createComponent({
+ createComponent({
...mockConfig,
variant,
});
@@ -142,57 +151,58 @@ describe('LabelsSelectRoot', () => {
},
);
- it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', () => {
+ it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
+ createComponent();
+ await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
});
- it('renders `dropdown-title` component', () => {
+ it('renders `dropdown-title` component', async () => {
+ createComponent();
+ await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownTitle).exists()).toBe(true);
});
- it('renders `dropdown-value` component', () => {
- const wrapperDropdownValue = createComponent(mockConfig, {
+ it('renders `dropdown-value` component', async () => {
+ createComponent(mockConfig, {
default: 'None',
});
+ await wrapper.vm.$nextTick;
- return wrapperDropdownValue.vm.$nextTick(() => {
- const valueComp = wrapperDropdownValue.find(DropdownValue);
+ const valueComp = wrapper.find(DropdownValue);
- expect(valueComp.exists()).toBe(true);
- expect(valueComp.text()).toBe('None');
-
- wrapperDropdownValue.destroy();
- });
+ expect(valueComp.exists()).toBe(true);
+ expect(valueComp.text()).toBe('None');
});
- it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', () => {
+ it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => {
+ createComponent();
wrapper.vm.$store.dispatch('toggleDropdownButton');
-
+ await wrapper.vm.$nextTick;
expect(wrapper.find(DropdownButton).exists()).toBe(true);
});
- it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', () => {
+ it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => {
+ createComponent();
wrapper.vm.$store.dispatch('toggleDropdownContents');
-
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(DropdownContents).exists()).toBe(true);
- });
+ await wrapper.vm.$nextTick;
+ expect(wrapper.find(DropdownContents).exists()).toBe(true);
});
describe('sets content direction based on viewport', () => {
- it('does not set direction when `state.variant` is not "embedded"', () => {
- wrapper.vm.$store.dispatch('toggleDropdownContents');
+ it('does not set direction when `state.variant` is not "embedded"', async () => {
+ createComponent();
+ wrapper.vm.$store.dispatch('toggleDropdownContents');
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
+ await wrapper.vm.$nextTick;
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
- });
+ expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
});
describe('when `state.variant` is "embedded"', () => {
beforeEach(() => {
- wrapper = createComponent({ ...mockConfig, variant: 'embedded' });
+ createComponent({ ...mockConfig, variant: 'embedded' });
wrapper.vm.$store.dispatch('toggleDropdownContents');
});
@@ -216,4 +226,22 @@ describe('LabelsSelectRoot', () => {
});
});
});
+
+ it('calls toggleDropdownContents action when isEditing prop is changing to true', async () => {
+ createComponent();
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ await wrapper.setProps({ isEditing: true });
+
+ expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContents');
+ });
+
+ it('does not call toggleDropdownContents action when isEditing prop is changing to false', async () => {
+ createComponent();
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ await wrapper.setProps({ isEditing: false });
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
new file mode 100644
index 00000000000..175abf5aae0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
@@ -0,0 +1,239 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+const DUMMY_TEXT = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do';
+
+const createChildElement = () => `<a href="#">${DUMMY_TEXT}</a>`;
+
+jest.mock('~/lib/utils/dom_utils', () => ({
+ hasHorizontalOverflow: jest.fn(() => {
+ throw new Error('this needs to be mocked');
+ }),
+}));
+
+describe('TooltipOnTruncate component', () => {
+ let wrapper;
+ let parent;
+
+ const createComponent = ({ propsData, ...options } = {}) => {
+ wrapper = shallowMount(TooltipOnTruncate, {
+ attachToDocument: true,
+ propsData: {
+ ...propsData,
+ },
+ ...options,
+ });
+ };
+
+ const createWrappedComponent = ({ propsData, ...options }) => {
+ // set a parent around the tested component
+ parent = mount(
+ {
+ props: {
+ title: { default: '' },
+ },
+ template: `
+ <TooltipOnTruncate :title="title" truncate-target="child">
+ <div>{{title}}</div>
+ </TooltipOnTruncate>
+ `,
+ components: {
+ TooltipOnTruncate,
+ },
+ },
+ {
+ propsData: { ...propsData },
+ attachToDocument: true,
+ ...options,
+ },
+ );
+
+ wrapper = parent.find(TooltipOnTruncate);
+ };
+
+ const hasTooltip = () => wrapper.classes('js-show-tooltip');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with default target', () => {
+ it('renders tooltip if truncated', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createComponent({
+ propsData: {
+ title: DUMMY_TEXT,
+ },
+ slots: {
+ default: [DUMMY_TEXT],
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element);
+ expect(hasTooltip()).toBe(true);
+ expect(wrapper.attributes('data-original-title')).toEqual(DUMMY_TEXT);
+ expect(wrapper.attributes('data-placement')).toEqual('top');
+ });
+ });
+
+ it('does not render tooltip if normal', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(false);
+ createComponent({
+ propsData: {
+ title: DUMMY_TEXT,
+ },
+ slots: {
+ default: [DUMMY_TEXT],
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element);
+ expect(hasTooltip()).toBe(false);
+ });
+ });
+ });
+
+ describe('with child target', () => {
+ it('renders tooltip if truncated', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createComponent({
+ propsData: {
+ title: DUMMY_TEXT,
+ truncateTarget: 'child',
+ },
+ slots: {
+ default: createChildElement(),
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element.childNodes[0]);
+ expect(hasTooltip()).toBe(true);
+ });
+ });
+
+ it('does not render tooltip if normal', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(false);
+ createComponent({
+ propsData: {
+ truncateTarget: 'child',
+ },
+ slots: {
+ default: createChildElement(),
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element.childNodes[0]);
+ expect(hasTooltip()).toBe(false);
+ });
+ });
+ });
+
+ describe('with fn target', () => {
+ it('renders tooltip if truncated', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createComponent({
+ propsData: {
+ title: DUMMY_TEXT,
+ truncateTarget: el => el.childNodes[1],
+ },
+ slots: {
+ default: [createChildElement(), createChildElement()],
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element.childNodes[1]);
+ expect(hasTooltip()).toBe(true);
+ });
+ });
+ });
+
+ describe('placement', () => {
+ it('sets data-placement when tooltip is rendered', () => {
+ const placement = 'bottom';
+
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createComponent({
+ propsData: {
+ placement,
+ },
+ slots: {
+ default: DUMMY_TEXT,
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(hasTooltip()).toBe(true);
+ expect(wrapper.attributes('data-placement')).toEqual(placement);
+ });
+ });
+ });
+
+ describe('updates when title and slot content changes', () => {
+ describe('is initialized with a long text', () => {
+ beforeEach(() => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ createWrappedComponent({
+ propsData: { title: DUMMY_TEXT },
+ });
+ return parent.vm.$nextTick();
+ });
+
+ it('renders tooltip', () => {
+ expect(hasTooltip()).toBe(true);
+ expect(wrapper.attributes('data-original-title')).toEqual(DUMMY_TEXT);
+ expect(wrapper.attributes('data-placement')).toEqual('top');
+ });
+
+ it('does not render tooltip after updated to a short text', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(false);
+ parent.setProps({
+ title: 'new-text',
+ });
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot
+ .then(() => {
+ expect(hasTooltip()).toBe(false);
+ });
+ });
+ });
+
+ describe('is initialized with a short text', () => {
+ beforeEach(() => {
+ hasHorizontalOverflow.mockReturnValueOnce(false);
+ createWrappedComponent({
+ propsData: { title: DUMMY_TEXT },
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('does not render tooltip', () => {
+ expect(hasTooltip()).toBe(false);
+ });
+
+ it('renders tooltip after text is updated', () => {
+ hasHorizontalOverflow.mockReturnValueOnce(true);
+ const newText = 'new-text';
+ parent.setProps({
+ title: newText,
+ });
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot
+ .then(() => {
+ expect(hasTooltip()).toBe(true);
+ expect(wrapper.attributes('data-original-title')).toEqual(newText);
+ expect(wrapper.attributes('data-placement')).toEqual('top');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
new file mode 100644
index 00000000000..9061d4586c5
--- /dev/null
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -0,0 +1,318 @@
+export const mockFindings = [
+ {
+ id: null,
+ report_type: 'dependency_scanning',
+ name: 'Cross-site Scripting in serialize-javascript',
+ severity: 'critical',
+ scanner: {
+ external_id: 'gemnasium',
+ name: 'Gemnasium',
+ version: '1.1.1',
+ url: 'https://gitlab.com/gitlab-org/security-products/gemnasium',
+ },
+ identifiers: [
+ {
+ external_type: 'gemnasium',
+ external_id: '58caa017-9a9a-46d6-bab2-ec930f46833c',
+ name: 'Gemnasium-58caa017-9a9a-46d6-bab2-ec930f46833c',
+ url:
+ 'https://deps.sec.gitlab.com/packages/npm/serialize-javascript/versions/1.7.0/advisories',
+ },
+ {
+ external_type: 'cve',
+ external_id: 'CVE-2019-16769',
+ name: 'CVE-2019-16769',
+ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16769',
+ },
+ ],
+ project_fingerprint: '09df9f4d11c8deb93d81bdcc39f7667b44143298',
+ create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_merge_request_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ project: {
+ id: 7071551,
+ name: 'gitlab-ui',
+ full_path: '/gitlab-org/gitlab-ui',
+ full_name: 'GitLab.org / gitlab-ui',
+ },
+ dismissal_feedback: null,
+ issue_feedback: null,
+ merge_request_feedback: null,
+ description:
+ 'The serialize-javascript npm package is vulnerable to Cross-site Scripting (XSS). It does not properly mitigate against unsafe characters in serialized regular expressions. If serialized data of regular expression objects are used in an environment other than Node.js, it is affected by this vulnerability.',
+ links: [{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-16769' }],
+ location: {
+ file: 'yarn.lock',
+ dependency: { package: { name: 'serialize-javascript' }, version: '1.7.0' },
+ },
+ remediations: [null],
+ solution: 'Upgrade to version 2.1.1 or above.',
+ state: 'opened',
+ blob_path: '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/yarn.lock',
+ evidence: 'Credit Card Detected: Diners Card',
+ },
+ {
+ id: null,
+ report_type: 'dependency_scanning',
+ name: '3rd party CORS request may execute in jquery',
+ severity: 'high',
+ scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ identifiers: [
+ {
+ external_type: 'cve',
+ external_id: 'CVE-2015-9251',
+ name: 'CVE-2015-9251',
+ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-9251',
+ },
+ ],
+ project_fingerprint: '1ecd3b214cf39c0b9ad23a0a9679778d7cf55876',
+ create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_merge_request_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ project: {
+ id: 7071551,
+ name: 'gitlab-ui',
+ full_path: '/gitlab-org/gitlab-ui',
+ full_name: 'GitLab.org / gitlab-ui',
+ },
+ dismissal_feedback: {
+ id: 2528,
+ created_at: '2019-08-26T12:30:32.349Z',
+ project_id: 7071551,
+ author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ comment_details: {
+ comment: 'This particular jQuery version appears in a test path of tinycolor2.\n',
+ comment_timestamp: '2019-08-26T12:30:37.610Z',
+ comment_author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ },
+ pipeline: { id: 78375355, path: '/gitlab-org/gitlab-ui/pipelines/78375355' },
+ destroy_vulnerability_feedback_dismissal_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback/2528',
+ category: 'dependency_scanning',
+ feedback_type: 'dismissal',
+ branch: 'leipert-dogfood-secure',
+ project_fingerprint: '1ecd3b214cf39c0b9ad23a0a9679778d7cf55876',
+ },
+ issue_feedback: null,
+ merge_request_feedback: null,
+ description: null,
+ links: [
+ { url: 'https://github.com/jquery/jquery/issues/2432' },
+ { url: 'http://blog.jquery.com/2016/01/08/jquery-2-2-and-1-12-released/' },
+ { url: 'https://nvd.nist.gov/vuln/detail/CVE-2015-9251' },
+ { url: 'http://research.insecurelabs.org/jquery/test/' },
+ ],
+ location: {
+ file: 'node_modules/tinycolor2/demo/jquery-1.9.1.js',
+ dependency: { package: { name: 'jquery' }, version: '1.9.1' },
+ },
+ remediations: [null],
+ solution: null,
+ state: 'dismissed',
+ blob_path:
+ '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/tinycolor2/demo/jquery-1.9.1.js',
+ },
+ {
+ id: null,
+ report_type: 'dependency_scanning',
+ name:
+ 'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
+ severity: 'low',
+ scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ identifiers: [
+ {
+ external_type: 'cve',
+ external_id: 'CVE-2019-11358',
+ name: 'CVE-2019-11358',
+ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358',
+ },
+ ],
+ project_fingerprint: 'aeb4b2442d92d0ccf7023f0c220bda8b4ba910e3',
+ create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_merge_request_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ project: {
+ id: 7071551,
+ name: 'gitlab-ui',
+ full_path: '/gitlab-org/gitlab-ui',
+ full_name: 'GitLab.org / gitlab-ui',
+ },
+ dismissal_feedback: {
+ id: 4197,
+ created_at: '2019-11-14T11:03:18.472Z',
+ project_id: 7071551,
+ author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ comment_details: {
+ comment:
+ 'This is a false positive, as it just part of some documentation assets of sass-true.',
+ comment_timestamp: '2019-11-14T11:03:18.464Z',
+ comment_author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ },
+ destroy_vulnerability_feedback_dismissal_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback/4197',
+ category: 'dependency_scanning',
+ feedback_type: 'dismissal',
+ branch: null,
+ project_fingerprint: 'aeb4b2442d92d0ccf7023f0c220bda8b4ba910e3',
+ },
+ issue_feedback: null,
+ merge_request_feedback: null,
+ description: null,
+ links: [
+ { url: 'https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/' },
+ { url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-11358' },
+ { url: 'https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b' },
+ ],
+ location: {
+ file: 'node_modules/sass-true/docs/assets/webpack/common.min.js',
+ dependency: { package: { name: 'jquery' }, version: '3.3.1' },
+ },
+ remediations: [null],
+ solution: null,
+ state: 'dismissed',
+ blob_path:
+ '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/sass-true/docs/assets/webpack/common.min.js',
+ },
+ {
+ id: null,
+ report_type: 'dependency_scanning',
+ name:
+ 'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
+ severity: 'low',
+ scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ identifiers: [
+ {
+ external_type: 'cve',
+ external_id: 'CVE-2019-11358',
+ name: 'CVE-2019-11358',
+ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358',
+ },
+ ],
+ project_fingerprint: 'eb86aa13eb9d897a083ead6e134aa78aa9cadd52',
+ create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_merge_request_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
+ project: {
+ id: 7071551,
+ name: 'gitlab-ui',
+ full_path: '/gitlab-org/gitlab-ui',
+ full_name: 'GitLab.org / gitlab-ui',
+ },
+ dismissal_feedback: {
+ id: 2527,
+ created_at: '2019-08-26T12:29:43.624Z',
+ project_id: 7071551,
+ author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ comment_details: {
+ comment: 'This particular jQuery version appears in a test path of tinycolor2.',
+ comment_timestamp: '2019-08-26T12:30:14.840Z',
+ comment_author: {
+ id: 181229,
+ name: "Lukas 'Eipi' Eipert",
+ username: 'leipert',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/leipert',
+ status_tooltip_html: null,
+ path: '/leipert',
+ },
+ },
+ pipeline: { id: 78375355, path: '/gitlab-org/gitlab-ui/pipelines/78375355' },
+ destroy_vulnerability_feedback_dismissal_path:
+ '/gitlab-org/gitlab-ui/vulnerability_feedback/2527',
+ category: 'dependency_scanning',
+ feedback_type: 'dismissal',
+ branch: 'leipert-dogfood-secure',
+ project_fingerprint: 'eb86aa13eb9d897a083ead6e134aa78aa9cadd52',
+ },
+ issue_feedback: null,
+ merge_request_feedback: null,
+ description: null,
+ links: [
+ { url: 'https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/' },
+ { url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-11358' },
+ { url: 'https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b' },
+ ],
+ location: {
+ file: 'node_modules/tinycolor2/demo/jquery-1.9.1.js',
+ dependency: { package: { name: 'jquery' }, version: '1.9.1' },
+ },
+ remediations: [null],
+ solution: null,
+ state: 'dismissed',
+ blob_path:
+ '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/tinycolor2/demo/jquery-1.9.1.js',
+ },
+];
+
+export const sastDiffSuccessMock = {
+ added: [mockFindings[0]],
+ fixed: [mockFindings[1], mockFindings[2]],
+ existing: [mockFindings[3]],
+ base_report_created_at: '2020-01-01T10:00:00.000Z',
+ base_report_out_of_date: false,
+ head_report_created_at: '2020-01-10T10:00:00.000Z',
+};
+
+export const secretScanningDiffSuccessMock = {
+ added: [mockFindings[0], mockFindings[1]],
+ fixed: [mockFindings[2]],
+ base_report_created_at: '2020-01-01T10:00:00.000Z',
+ base_report_out_of_date: false,
+ head_report_created_at: '2020-01-10T10:00:00.000Z',
+};
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index ab87d80b291..d11b519e5ad 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -1,13 +1,32 @@
-import { mount } from '@vue/test-utils';
+import { mount, createLocalVue } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { merge } from 'lodash';
+import Vuex from 'vuex';
+import { trimText } from 'helpers/text_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ sastDiffSuccessMock,
+ secretScanningDiffSuccessMock,
+} from 'jest/vue_shared/security_reports/mock_data';
import Api from '~/api';
-import Flash from '~/flash';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_SECRET_DETECTION,
+} from '~/vue_shared/security_reports/constants';
import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
jest.mock('~/flash');
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const SAST_COMPARISON_PATH = '/sast.json';
+const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json';
+
describe('Security reports app', () => {
let wrapper;
- let mrTabsMock;
const props = {
pipelineId: 123,
@@ -15,148 +34,290 @@ describe('Security reports app', () => {
securityReportsDocsPath: '/docs',
};
- const createComponent = () => {
- wrapper = mount(SecurityReportsApp, {
- propsData: { ...props },
- });
+ const createComponent = options => {
+ wrapper = mount(
+ SecurityReportsApp,
+ merge(
+ {
+ localVue,
+ propsData: { ...props },
+ },
+ options,
+ ),
+ );
};
const anyParams = expect.any(Object);
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpLink = () => wrapper.find('[data-testid="help"]');
- const setupMrTabsMock = () => {
- mrTabsMock = { tabShown: jest.fn() };
- window.mrTabs = mrTabsMock;
- };
const setupMockJobArtifact = reportType => {
jest
.spyOn(Api, 'pipelineJobs')
.mockResolvedValue({ data: [{ artifacts: [{ file_type: reportType }] }] });
};
+ const expectPipelinesTabAnchor = () => {
+ const mrTabsMock = { tabShown: jest.fn() };
+ window.mrTabs = mrTabsMock;
+ findPipelinesTabAnchor().trigger('click');
+ expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]);
+ };
afterEach(() => {
wrapper.destroy();
delete window.mrTabs;
});
- describe.each(SecurityReportsApp.reportTypes)('given a report type %p', reportType => {
- beforeEach(() => {
- window.mrTabs = { tabShown: jest.fn() };
- setupMockJobArtifact(reportType);
- createComponent();
- return wrapper.vm.$nextTick();
- });
+ describe.each([false, true])(
+ 'given the coreSecurityMrWidgetCounts feature flag is %p',
+ coreSecurityMrWidgetCounts => {
+ const createComponentWithFlag = options =>
+ createComponent(
+ merge(
+ {
+ provide: {
+ glFeatures: {
+ coreSecurityMrWidgetCounts,
+ },
+ },
+ },
+ options,
+ ),
+ );
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
- });
+ describe.each(SecurityReportsApp.reportTypes)('given a report type %p', reportType => {
+ beforeEach(() => {
+ window.mrTabs = { tabShown: jest.fn() };
+ setupMockJobArtifact(reportType);
+ createComponentWithFlag();
+ return wrapper.vm.$nextTick();
+ });
- it('renders the expected message', () => {
- expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
- });
+ it('calls the pipelineJobs API correctly', () => {
+ expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
+ expect(Api.pipelineJobs).toHaveBeenCalledWith(
+ props.projectId,
+ props.pipelineId,
+ anyParams,
+ );
+ });
- describe('clicking the anchor to the pipelines tab', () => {
- beforeEach(() => {
- setupMrTabsMock();
- findPipelinesTabAnchor().trigger('click');
+ it('renders the expected message', () => {
+ expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
+ });
+
+ describe('clicking the anchor to the pipelines tab', () => {
+ it('calls the mrTabs.tabShown global', () => {
+ expectPipelinesTabAnchor();
+ });
+ });
+
+ it('renders a help link', () => {
+ expect(findHelpLink().attributes()).toMatchObject({
+ href: props.securityReportsDocsPath,
+ });
+ });
});
- it('calls the mrTabs.tabShown global', () => {
- expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]);
+ describe('given a report type "foo"', () => {
+ beforeEach(() => {
+ setupMockJobArtifact('foo');
+ createComponentWithFlag();
+ return wrapper.vm.$nextTick();
+ });
+
+ it('calls the pipelineJobs API correctly', () => {
+ expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
+ expect(Api.pipelineJobs).toHaveBeenCalledWith(
+ props.projectId,
+ props.pipelineId,
+ anyParams,
+ );
+ });
+
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
});
- });
- it('renders a help link', () => {
- expect(findHelpLink().attributes()).toMatchObject({
- href: props.securityReportsDocsPath,
+ describe('security artifacts on last page of multi-page response', () => {
+ const numPages = 3;
+
+ beforeEach(() => {
+ jest
+ .spyOn(Api, 'pipelineJobs')
+ .mockImplementation(async (projectId, pipelineId, { page }) => {
+ const requestedPage = parseInt(page, 10);
+ if (requestedPage < numPages) {
+ return {
+ // Some jobs with no relevant artifacts
+ data: [{}, {}],
+ headers: { 'x-next-page': String(requestedPage + 1) },
+ };
+ } else if (requestedPage === numPages) {
+ return {
+ data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }],
+ };
+ }
+
+ throw new Error('Test failed due to request of non-existent jobs page');
+ });
+
+ createComponentWithFlag();
+ return wrapper.vm.$nextTick();
+ });
+
+ it('fetches all pages', () => {
+ expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages);
+ });
+
+ it('renders the expected message', () => {
+ expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
+ });
});
- });
- });
- describe('given a report type "foo"', () => {
- beforeEach(() => {
- setupMockJobArtifact('foo');
- createComponent();
- return wrapper.vm.$nextTick();
- });
+ describe('given an error from the API', () => {
+ let error;
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
- });
+ beforeEach(() => {
+ error = new Error('an error');
+ jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
+ createComponentWithFlag();
+ return wrapper.vm.$nextTick();
+ });
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
+ it('calls the pipelineJobs API correctly', () => {
+ expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
+ expect(Api.pipelineJobs).toHaveBeenCalledWith(
+ props.projectId,
+ props.pipelineId,
+ anyParams,
+ );
+ });
+
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
+
+ it('calls createFlash correctly', () => {
+ expect(createFlash.mock.calls).toEqual([
+ [
+ {
+ message: SecurityReportsApp.i18n.apiError,
+ captureError: true,
+ error,
+ },
+ ],
+ ]);
+ });
+ });
+ },
+ );
- describe('security artifacts on last page of multi-page response', () => {
- const numPages = 3;
+ describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
+ let mock;
+
+ const createComponentWithFlagEnabled = options =>
+ createComponent(
+ merge(options, {
+ provide: {
+ glFeatures: {
+ coreSecurityMrWidgetCounts: true,
+ },
+ },
+ }),
+ );
beforeEach(() => {
- jest
- .spyOn(Api, 'pipelineJobs')
- .mockImplementation(async (projectId, pipelineId, { page }) => {
- const requestedPage = parseInt(page, 10);
- if (requestedPage < numPages) {
- return {
- // Some jobs with no relevant artifacts
- data: [{}, {}],
- headers: { 'x-next-page': String(requestedPage + 1) },
- };
- } else if (requestedPage === numPages) {
- return {
- data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }],
- };
- }
-
- throw new Error('Test failed due to request of non-existent jobs page');
- });
-
- createComponent();
- return wrapper.vm.$nextTick();
+ mock = new MockAdapter(axios);
});
- it('fetches all pages', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages);
+ afterEach(() => {
+ mock.restore();
});
- it('renders the expected message', () => {
- expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
- });
- });
+ const SAST_SUCCESS_MESSAGE =
+ 'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others';
+ const SECRET_SCANNING_SUCCESS_MESSAGE =
+ 'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others';
+ describe.each`
+ reportType | pathProp | path | successResponse | successMessage
+ ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
+ ${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE}
+ `(
+ 'given a $pathProp and $reportType artifact',
+ ({ reportType, pathProp, path, successResponse, successMessage }) => {
+ beforeEach(() => {
+ setupMockJobArtifact(reportType);
+ });
- describe('given an error from the API', () => {
- let error;
+ describe('when loading', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios, { delayResponse: 1 });
+ mock.onGet(path).replyOnce(200, successResponse);
- beforeEach(() => {
- error = new Error('an error');
- jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
- createComponent();
- return wrapper.vm.$nextTick();
- });
+ createComponentWithFlagEnabled({
+ propsData: {
+ [pathProp]: path,
+ },
+ });
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
- });
+ return waitForPromises();
+ });
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
+ it('should have loading message', () => {
+ expect(wrapper.text()).toBe('Security scanning is loading');
+ });
- it('calls Flash correctly', () => {
- expect(Flash.mock.calls).toEqual([
- [
- {
- message: SecurityReportsApp.i18n.apiError,
- captureError: true,
- error,
- },
- ],
- ]);
- });
+ it('should not render the pipeline tab anchor', () => {
+ expect(findPipelinesTabAnchor().exists()).toBe(false);
+ });
+ });
+
+ describe('when successfully loaded', () => {
+ beforeEach(() => {
+ mock.onGet(path).replyOnce(200, successResponse);
+
+ createComponentWithFlagEnabled({
+ propsData: {
+ [pathProp]: path,
+ },
+ });
+
+ return waitForPromises();
+ });
+
+ it('should show counts', () => {
+ expect(trimText(wrapper.text())).toContain(successMessage);
+ });
+
+ it('should render the pipeline tab anchor', () => {
+ expectPipelinesTabAnchor();
+ });
+ });
+
+ describe('when an error occurs', () => {
+ beforeEach(() => {
+ mock.onGet(path).replyOnce(500);
+
+ createComponentWithFlagEnabled({
+ propsData: {
+ [pathProp]: path,
+ },
+ });
+
+ return waitForPromises();
+ });
+
+ it('should show error message', () => {
+ expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
+ });
+
+ it('should render the pipeline tab anchor', () => {
+ expectPipelinesTabAnchor();
+ });
+ });
+ },
+ );
});
});
diff --git a/spec/frontend/vue_shared/security_reports/store/getters_spec.js b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
new file mode 100644
index 00000000000..8de704be455
--- /dev/null
+++ b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
@@ -0,0 +1,182 @@
+import createState from '~/vue_shared/security_reports/store/state';
+import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
+import createSecretScanningState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
+import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
+import {
+ groupedSummaryText,
+ allReportsHaveError,
+ areReportsLoading,
+ anyReportHasError,
+ areAllReportsLoading,
+ anyReportHasIssues,
+ summaryCounts,
+} from '~/vue_shared/security_reports/store/getters';
+import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants';
+
+const generateVuln = severity => ({ severity });
+
+describe('Security reports getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ state.sast = createSastState();
+ state.secretDetection = createSecretScanningState();
+ });
+
+ describe('summaryCounts', () => {
+ it('returns 0 count for empty state', () => {
+ expect(summaryCounts(state)).toEqual({
+ critical: 0,
+ high: 0,
+ other: 0,
+ });
+ });
+
+ describe('combines all reports', () => {
+ it('of the same severity', () => {
+ state.sast.newIssues = [generateVuln(CRITICAL)];
+ state.secretDetection.newIssues = [generateVuln(CRITICAL)];
+
+ expect(summaryCounts(state)).toEqual({
+ critical: 2,
+ high: 0,
+ other: 0,
+ });
+ });
+
+ it('of different severities', () => {
+ state.sast.newIssues = [generateVuln(CRITICAL)];
+ state.secretDetection.newIssues = [generateVuln(HIGH), generateVuln(LOW)];
+
+ expect(summaryCounts(state)).toEqual({
+ critical: 1,
+ high: 1,
+ other: 1,
+ });
+ });
+ });
+ });
+
+ describe('groupedSummaryText', () => {
+ it('returns failed text', () => {
+ expect(
+ groupedSummaryText(state, {
+ allReportsHaveError: true,
+ areReportsLoading: false,
+ summaryCounts: {},
+ }),
+ ).toEqual({ message: 'Security scanning failed loading any results' });
+ });
+
+ it('returns `is loading` as status text', () => {
+ expect(
+ groupedSummaryText(state, {
+ allReportsHaveError: false,
+ areReportsLoading: true,
+ summaryCounts: {},
+ }),
+ ).toEqual(
+ groupedTextBuilder({
+ reportType: 'Security scanning',
+ critical: 0,
+ high: 0,
+ other: 0,
+ status: 'is loading',
+ }),
+ );
+ });
+
+ it('returns no new status text if there are existing ones', () => {
+ expect(
+ groupedSummaryText(state, {
+ allReportsHaveError: false,
+ areReportsLoading: false,
+ summaryCounts: {},
+ }),
+ ).toEqual(
+ groupedTextBuilder({
+ reportType: 'Security scanning',
+ critical: 0,
+ high: 0,
+ other: 0,
+ status: '',
+ }),
+ );
+ });
+ });
+
+ describe('areReportsLoading', () => {
+ it('returns true when any report is loading', () => {
+ state.sast.isLoading = true;
+
+ expect(areReportsLoading(state)).toEqual(true);
+ });
+
+ it('returns false when none of the reports are loading', () => {
+ expect(areReportsLoading(state)).toEqual(false);
+ });
+ });
+
+ describe('areAllReportsLoading', () => {
+ it('returns true when all reports are loading', () => {
+ state.sast.isLoading = true;
+ state.secretDetection.isLoading = true;
+
+ expect(areAllReportsLoading(state)).toEqual(true);
+ });
+
+ it('returns false when some of the reports are loading', () => {
+ state.sast.isLoading = true;
+
+ expect(areAllReportsLoading(state)).toEqual(false);
+ });
+
+ it('returns false when none of the reports are loading', () => {
+ expect(areAllReportsLoading(state)).toEqual(false);
+ });
+ });
+
+ describe('allReportsHaveError', () => {
+ it('returns true when all reports have error', () => {
+ state.sast.hasError = true;
+ state.secretDetection.hasError = true;
+
+ expect(allReportsHaveError(state)).toEqual(true);
+ });
+
+ it('returns false when none of the reports have error', () => {
+ expect(allReportsHaveError(state)).toEqual(false);
+ });
+
+ it('returns false when one of the reports does not have error', () => {
+ state.secretDetection.hasError = true;
+
+ expect(allReportsHaveError(state)).toEqual(false);
+ });
+ });
+
+ describe('anyReportHasError', () => {
+ it('returns true when any of the reports has error', () => {
+ state.sast.hasError = true;
+
+ expect(anyReportHasError(state)).toEqual(true);
+ });
+
+ it('returns false when none of the reports has error', () => {
+ expect(anyReportHasError(state)).toEqual(false);
+ });
+ });
+
+ describe('anyReportHasIssues', () => {
+ it('returns true when any of the reports has new issues', () => {
+ state.sast.newIssues.push(generateVuln(LOW));
+
+ expect(anyReportHasIssues(state)).toEqual(true);
+ });
+
+ it('returns false when none of the reports has error', () => {
+ expect(anyReportHasIssues(state)).toEqual(false);
+ });
+ });
+});