summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.markdownlint.json2
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue67
-rw-r--r--app/assets/javascripts/alert_management/details.js19
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/details.query.graphql17
-rw-r--r--app/assets/javascripts/diffs/store/actions.js20
-rw-r--r--app/assets/javascripts/helpers/event_hub_factory.js11
-rw-r--r--app/assets/javascripts/ide/eventhub.js4
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue11
-rw-r--r--app/assets/javascripts/lib/graphql.js1
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js31
-rw-r--r--app/assets/javascripts/notes/index.js50
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/index.js25
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/typedefs.graphql7
-rw-r--r--app/assets/javascripts/static_site_editor/index.js15
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue29
-rw-r--r--app/controllers/projects/alert_management_controller.rb10
-rw-r--r--app/controllers/projects/static_site_editor_controller.rb4
-rw-r--r--app/helpers/projects/alert_management_helper.rb7
-rw-r--r--app/models/ci/build.rb8
-rw-r--r--app/models/ci/daily_build_group_report_result.rb14
-rw-r--r--app/models/ci/daily_report_result.rb22
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/group.rb10
-rw-r--r--app/models/project.rb2
-rw-r--r--app/policies/group_policy.rb34
-rw-r--r--app/services/ci/daily_build_group_report_result_service.rb (renamed from app/services/ci/daily_report_result_service.rb)9
-rw-r--r--app/services/groups/import_export/export_service.rb2
-rw-r--r--app/services/groups/import_export/import_service.rb2
-rw-r--r--app/views/projects/alert_management/details.html.haml2
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/ci/daily_build_group_report_results_worker.rb (renamed from app/workers/ci/daily_report_results_worker.rb)4
-rw-r--r--config/initializers/action_cable.rb6
-rw-r--r--db/migrate/20200421111005_create_daily_build_group_report_results.rb21
-rw-r--r--db/structure.sql35
-rw-r--r--doc/api/labels.md2
-rw-r--r--doc/ci/large_repositories/index.md6
-rw-r--r--doc/development/contributing/merge_request_workflow.md2
-rw-r--r--doc/development/documentation/styleguide.md2
-rw-r--r--doc/development/fe_guide/vue.md34
-rw-r--r--doc/development/fe_guide/vue3_migration.md19
-rw-r--r--locale/gitlab.pot2
-rw-r--r--package.json3
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/resource/pipeline.rb53
-rw-r--r--qa/qa/resource/project.rb8
-rw-r--r--spec/controllers/projects/alert_management_controller_spec.rb16
-rw-r--r--spec/factories/ci/builds.rb6
-rw-r--r--spec/factories/ci/daily_build_group_report_results.rb (renamed from spec/factories/ci/daily_report_results.rb)9
-rw-r--r--spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb32
-rw-r--r--spec/frontend/__mocks__/@toast-ui/vue-editor/index.js3
-rw-r--r--spec/frontend/alert_management/components/alert_management_detail_spec.js56
-rw-r--r--spec/frontend/diffs/store/actions_spec.js41
-rw-r--r--spec/frontend/helpers/event_hub_factory_spec.js36
-rw-r--r--spec/frontend/issue_show/components/app_spec.js (renamed from spec/javascripts/issue_show/components/app_spec.js)398
-rw-r--r--spec/frontend/issue_show/components/description_spec.js (renamed from spec/javascripts/issue_show/components/description_spec.js)135
-rw-r--r--spec/frontend/issue_show/components/edited_spec.js (renamed from spec/javascripts/issue_show/components/edited_spec.js)0
-rw-r--r--spec/frontend/issue_show/components/fields/description_template_spec.js (renamed from spec/javascripts/issue_show/components/fields/description_template_spec.js)4
-rw-r--r--spec/frontend/issue_show/components/form_spec.js (renamed from spec/javascripts/issue_show/components/form_spec.js)19
-rw-r--r--spec/frontend/issue_show/components/title_spec.js (renamed from spec/javascripts/issue_show/components/title_spec.js)48
-rw-r--r--spec/frontend/static_site_editor/pages/home_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js4
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb15
-rw-r--r--spec/javascripts/issue_show/helpers.js1
-rw-r--r--spec/javascripts/issue_show/mock_data.js1
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml4
-rw-r--r--spec/models/ci/build_spec.rb55
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb (renamed from spec/models/ci/daily_report_result_spec.rb)25
-rw-r--r--spec/models/ci/pipeline_spec.rb4
-rw-r--r--spec/policies/group_policy_spec.rb22
-rw-r--r--spec/policies/project_policy_spec.rb1
-rw-r--r--spec/services/ci/daily_build_group_report_result_service_spec.rb (renamed from spec/services/ci/daily_report_result_service_spec.rb)41
-rw-r--r--spec/services/groups/import_export/export_service_spec.rb4
-rw-r--r--spec/services/groups/import_export/import_service_spec.rb8
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb7
-rw-r--r--spec/support/shared_examples/policies/wiki_policies_shared_examples.rb226
-rw-r--r--spec/workers/ci/daily_build_group_report_results_worker_spec.rb (renamed from spec/workers/ci/daily_report_results_worker_spec.rb)6
-rw-r--r--yarn.lock5
79 files changed, 1136 insertions, 771 deletions
diff --git a/.markdownlint.json b/.markdownlint.json
index 1ad75f229f4..ddf5105d7f2 100644
--- a/.markdownlint.json
+++ b/.markdownlint.json
@@ -116,6 +116,7 @@
"SMTP",
"SSH",
"Tiller",
+ "TOML",
"Trello",
"Trello Power-Ups",
"TypeScript",
@@ -125,6 +126,7 @@
"Unicorn",
"unicorn-worker-killer",
"WebdriverIO",
+ "YAML",
"YouTrack"
],
"code_blocks": false
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index 21622d8f750..e15ade142d7 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -1,6 +1,8 @@
<script>
import { GlNewDropdown, GlNewDropdownItem, GlTabs, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
+import query from '../graphql/queries/details.query.graphql';
+import { fetchPolicies } from '~/lib/graphql';
export default {
statuses: {
@@ -18,11 +20,40 @@ export default {
GlTab,
GlTabs,
},
+ props: {
+ alertId: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ alert: {
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ query,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ alertId: this.alertId,
+ };
+ },
+ update(data) {
+ return data?.project?.alertManagementAlerts?.nodes?.[0] ?? null;
+ },
+ },
+ },
+ data() {
+ return { alert: null };
+ },
};
</script>
<template>
<div>
- <div class="d-flex justify-content-between border-bottom pb-2 pt-1">
+ <div v-if="alert" class="d-flex justify-content-between border-bottom pb-2 pt-1">
+ <div></div>
<gl-new-dropdown class="align-self-center" right>
<gl-new-dropdown-item
v-for="(label, field) in $options.statuses"
@@ -33,23 +64,21 @@ export default {
</gl-new-dropdown-item>
</gl-new-dropdown>
</div>
- <div class="d-flex">
- <gl-tabs>
- <gl-tab data-testid="overviewTab" :title="$options.i18n.overviewTitle">
- <ul class="pl-3">
- <li data-testid="startTimeItem" class="font-weight-bold mb-3 mt-2">
- {{ s__('AlertManagement|Start time:') }}
- </li>
- <li class="font-weight-bold my-3">
- {{ s__('AlertManagement|End time:') }}
- </li>
- <li class="font-weight-bold my-3">
- {{ s__('AlertManagement|Events:') }}
- </li>
- </ul>
- </gl-tab>
- <gl-tab data-testid="fullDetailsTab" :title="$options.i18n.fullAlertDetailsTitle" />
- </gl-tabs>
- </div>
+ <gl-tabs v-if="alert" data-testid="alertDetailsTabs">
+ <gl-tab data-testid="overviewTab" :title="$options.i18n.overviewTitle">
+ <ul class="pl-3">
+ <li data-testid="startTimeItem" class="font-weight-bold mb-3 mt-2">
+ {{ s__('AlertManagement|Start time:') }}
+ </li>
+ <li class="font-weight-bold my-3">
+ {{ s__('AlertManagement|End time:') }}
+ </li>
+ <li class="font-weight-bold my-3">
+ {{ s__('AlertManagement|Events:') }}
+ </li>
+ </ul>
+ </gl-tab>
+ <gl-tab data-testid="fullDetailsTab" :title="$options.i18n.fullAlertDetailsTitle" />
+ </gl-tabs>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/alert_management/details.js
index 23c3b8a742a..0fb11595a04 100644
--- a/app/assets/javascripts/alert_management/details.js
+++ b/app/assets/javascripts/alert_management/details.js
@@ -1,15 +1,32 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import AlertDetails from './components/alert_details.vue';
+Vue.use(VueApollo);
+
export default selector => {
+ const domEl = document.querySelector(selector);
+ const { alertId, projectPath } = domEl.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: selector,
+ apolloProvider,
components: {
AlertDetails,
},
render(createElement) {
- return createElement('alert-details', {});
+ return createElement('alert-details', {
+ props: {
+ alertId,
+ projectPath,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql
new file mode 100644
index 00000000000..81e95500e05
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql
@@ -0,0 +1,17 @@
+query alertDetails($fullPath: ID!, $alertId: String) {
+ project(fullPath: $fullPath) {
+ alertManagementAlerts(iid: $alertId) {
+ nodes {
+ iid
+ endedAt
+ eventCount
+ monitoringTool
+ service
+ severity
+ startedAt
+ status
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 4e44cb5e6f4..a6185746bb1 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -665,5 +665,25 @@ export function changeCurrentCommit({ dispatch, commit, state }, { commitId }) {
return dispatch('fetchDiffFilesMeta');
}
+export function moveToNeighboringCommit({ dispatch, state }, { direction }) {
+ const previousCommitId = state.commit?.prev_commit_id;
+ const nextCommitId = state.commit?.next_commit_id;
+ const canMove = {
+ next: !state.isLoading && nextCommitId,
+ previous: !state.isLoading && previousCommitId,
+ };
+ let commitId;
+
+ if (direction === 'next' && canMove.next) {
+ commitId = nextCommitId;
+ } else if (direction === 'previous' && canMove.previous) {
+ commitId = previousCommitId;
+ }
+
+ if (commitId) {
+ dispatch('changeCurrentCommit', { commitId });
+ }
+}
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/helpers/event_hub_factory.js b/app/assets/javascripts/helpers/event_hub_factory.js
new file mode 100644
index 00000000000..4bd390c3535
--- /dev/null
+++ b/app/assets/javascripts/helpers/event_hub_factory.js
@@ -0,0 +1,11 @@
+import mitt from 'mitt';
+
+export default () => {
+ const emitter = mitt();
+
+ emitter.$on = emitter.on;
+ emitter.$off = emitter.off;
+ emitter.$emit = emitter.emit;
+
+ return emitter;
+};
diff --git a/app/assets/javascripts/ide/eventhub.js b/app/assets/javascripts/ide/eventhub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/ide/eventhub.js
+++ b/app/assets/javascripts/ide/eventhub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index b8b3a4f44fd..6edcb974670 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -329,7 +329,7 @@ export default {
},
deleteIssuable(payload) {
- this.service
+ return this.service
.deleteIssuable(payload)
.then(res => res.data)
.then(data => {
@@ -340,7 +340,7 @@ export default {
})
.catch(() => {
createFlash(
- sprintf(s__('Error deleting %{issuableType}'), { issuableType: this.issuableType }),
+ sprintf(s__('Error deleting %{issuableType}'), { issuableType: this.issuableType }),
);
});
},
@@ -365,7 +365,12 @@ export default {
:issuable-type="issuableType"
/>
- <recaptcha-modal v-show="showRecaptcha" :html="recaptchaHTML" @close="closeRecaptchaModal" />
+ <recaptcha-modal
+ v-show="showRecaptcha"
+ ref="recaptchaModal"
+ :html="recaptchaHTML"
+ @close="closeRecaptchaModal"
+ />
</div>
<div v-else>
<title-component
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 8d3b87d5cc0..b6c41ffa7ab 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -33,6 +33,7 @@ export default (resolvers = {}, config = {}) => {
};
return new ApolloClient({
+ typeDefs: config.typeDefs,
link: ApolloLink.split(
operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index 2580f8e86b1..ec9c800b7a2 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -15,19 +15,6 @@ export default () => {
notesApp,
},
store,
- data() {
- const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
- const noteableData = JSON.parse(notesDataset.noteableData);
- noteableData.noteableType = notesDataset.noteableType;
- noteableData.targetType = notesDataset.targetType;
-
- return {
- noteableData,
- currentUserData: JSON.parse(notesDataset.currentUserData),
- notesData: JSON.parse(notesDataset.notesData),
- helpPagePath: notesDataset.helpPagePath,
- };
- },
computed: {
...mapGetters(['discussionTabCounter']),
...mapState({
@@ -67,6 +54,19 @@ export default () => {
updateDiscussionTabCounter() {
this.notesCountBadge.text(this.discussionTabCounter);
},
+ dataset() {
+ const data = this.$el.dataset;
+ const noteableData = JSON.parse(data.noteableData);
+ noteableData.noteableType = data.noteableType;
+ noteableData.targetType = data.targetType;
+
+ return {
+ noteableData,
+ notesData: JSON.parse(data.notesData),
+ userData: JSON.parse(data.currentUserData),
+ helpPagePath: data.helpPagePath,
+ };
+ },
},
render(createElement) {
// NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`,
@@ -76,11 +76,8 @@ export default () => {
return createElement(discussionKeyboardNavigator, [
createElement('notes-app', {
props: {
- noteableData: this.noteableData,
- notesData: this.notesData,
- userData: this.currentUserData,
+ ...this.dataset(),
shouldShow: this.isShowTabActive,
- helpPagePath: this.helpPagePath,
},
}),
]);
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 8f9e2359e0d..6fd3cee5340 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -14,38 +14,36 @@ document.addEventListener('DOMContentLoaded', () => {
notesApp,
},
store,
- data() {
- const notesDataset = document.getElementById('js-vue-notes').dataset;
- const parsedUserData = JSON.parse(notesDataset.currentUserData);
- const noteableData = JSON.parse(notesDataset.noteableData);
- let currentUserData = {};
+ methods: {
+ setData() {
+ const notesDataset = this.$el.dataset;
+ const parsedUserData = JSON.parse(notesDataset.currentUserData);
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ let currentUserData = {};
- noteableData.noteableType = notesDataset.noteableType;
- noteableData.targetType = notesDataset.targetType;
+ noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
- if (parsedUserData) {
- currentUserData = {
- id: parsedUserData.id,
- name: parsedUserData.name,
- username: parsedUserData.username,
- avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
- path: parsedUserData.path,
- };
- }
+ if (parsedUserData) {
+ currentUserData = {
+ id: parsedUserData.id,
+ name: parsedUserData.name,
+ username: parsedUserData.username,
+ avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
+ path: parsedUserData.path,
+ };
+ }
- return {
- noteableData,
- currentUserData,
- notesData: JSON.parse(notesDataset.notesData),
- };
+ return {
+ noteableData,
+ userData: currentUserData,
+ notesData: JSON.parse(notesDataset.notesData),
+ };
+ },
},
render(createElement) {
return createElement('notes-app', {
- props: {
- noteableData: this.noteableData,
- notesData: this.notesData,
- userData: this.currentUserData,
- },
+ props: { ...this.setData() },
});
},
});
diff --git a/app/assets/javascripts/static_site_editor/graphql/index.js b/app/assets/javascripts/static_site_editor/graphql/index.js
new file mode 100644
index 00000000000..129e75b5bf6
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import typeDefs from './typedefs.graphql';
+
+Vue.use(VueApollo);
+
+const createApolloProvider = data => {
+ const defaultClient = createDefaultClient(
+ {},
+ {
+ typeDefs,
+ },
+ );
+
+ defaultClient.cache.writeData({
+ data,
+ });
+
+ return new VueApollo({
+ defaultClient,
+ });
+};
+
+export default createApolloProvider;
diff --git a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
new file mode 100644
index 00000000000..efff746a3a9
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
@@ -0,0 +1,7 @@
+extend type Query {
+ isSupportedContent: Boolean!
+ projectId: String!
+ returnUrl: String
+ sourcePath: String!
+ username: String!
+}
diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js
index b53d461f5a9..01b01762391 100644
--- a/app/assets/javascripts/static_site_editor/index.js
+++ b/app/assets/javascripts/static_site_editor/index.js
@@ -3,9 +3,12 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/app.vue';
import createStore from './store';
import createRouter from './router';
+import createApolloProvider from './graphql';
const initStaticSiteEditor = el => {
- const { isSupportedContent, projectId, path: sourcePath, returnUrl, baseUrl } = el.dataset;
+ const { isSupportedContent, projectId, path: sourcePath, baseUrl } = el.dataset;
+ const { current_username: username } = window.gon;
+ const returnUrl = el.dataset.returnUrl || null;
const store = createStore({
initialState: {
@@ -13,15 +16,23 @@ const initStaticSiteEditor = el => {
projectId,
returnUrl,
sourcePath,
- username: window.gon.current_username,
+ username,
},
});
const router = createRouter(baseUrl);
+ const apolloProvider = createApolloProvider({
+ isSupportedContent: parseBoolean(isSupportedContent),
+ projectId,
+ returnUrl,
+ sourcePath,
+ username,
+ });
return new Vue({
el,
store,
router,
+ apolloProvider,
components: {
App,
},
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
index 3de4a4a27cf..8a9fff47623 100644
--- a/app/assets/javascripts/static_site_editor/pages/home.vue
+++ b/app/assets/javascripts/static_site_editor/pages/home.vue
@@ -2,6 +2,8 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlSkeletonLoader } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import EditArea from '../components/edit_area.vue';
import EditHeader from '../components/edit_header.vue';
import SavedChangesMessage from '../components/saved_changes_message.vue';
@@ -11,6 +13,7 @@ import SubmitChangesError from '../components/submit_changes_error.vue';
export default {
components: {
+ RichContentEditor,
EditArea,
EditHeader,
InvalidContentMessage,
@@ -19,6 +22,7 @@ export default {
PublishToolbar,
SubmitChangesError,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
...mapState([
'content',
@@ -76,7 +80,14 @@ export default {
@dismiss="dismissSubmitChangesError"
/>
<edit-header class="w-75 align-self-center py-2" :title="title" />
+ <rich-content-editor
+ v-if="glFeatures.richContentEditor"
+ class="w-75 gl-align-self-center"
+ :value="content"
+ @input="setContent"
+ />
<edit-area
+ v-else
class="w-75 h-100 shadow-none align-self-center"
:value="content"
@input="setContent"
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
index 01c5329b4d7..3e8f3dd548f 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
@@ -23,3 +23,5 @@ export const EDITOR_OPTIONS = {
export const EDITOR_TYPES = {
wysiwyg: 'wysiwyg',
};
+
+export const EDITOR_HEIGHT = '100%';
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
index e02d8661ceb..0b10424ad1e 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
@@ -2,7 +2,7 @@
import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css';
-import { EDITOR_OPTIONS, EDITOR_TYPES } from './constants';
+import { EDITOR_OPTIONS, EDITOR_TYPES, EDITOR_HEIGHT } from './constants';
export default {
components: {
@@ -16,6 +16,26 @@ export default {
type: String,
required: true,
},
+ options: {
+ type: Object,
+ required: false,
+ default: () => EDITOR_OPTIONS,
+ },
+ initialEditType: {
+ type: String,
+ required: false,
+ default: EDITOR_TYPES.wysiwyg,
+ },
+ height: {
+ type: String,
+ required: false,
+ default: EDITOR_HEIGHT,
+ },
+ },
+ computed: {
+ editorOptions() {
+ return { ...EDITOR_OPTIONS, ...this.options };
+ },
},
methods: {
onContentChanged() {
@@ -25,16 +45,15 @@ export default {
return this.$refs.editor.invoke('getMarkdown');
},
},
- editorOptions: EDITOR_OPTIONS,
- initialEditType: EDITOR_TYPES.wysiwyg,
};
</script>
<template>
<toast-editor
ref="editor"
- :initial-edit-type="$options.initialEditType"
:initial-value="value"
- :options="$options.editorOptions"
+ :options="editorOptions"
+ :initial-edit-type="initialEditType"
+ :height="height"
@change="onContentChanged"
/>
</template>
diff --git a/app/controllers/projects/alert_management_controller.rb b/app/controllers/projects/alert_management_controller.rb
index 8bbcecaa0af..fd85b4346c0 100644
--- a/app/controllers/projects/alert_management_controller.rb
+++ b/app/controllers/projects/alert_management_controller.rb
@@ -1,17 +1,23 @@
# frozen_string_literal: true
class Projects::AlertManagementController < Projects::ApplicationController
- before_action :ensure_feature_enabled
+ before_action :ensure_list_feature_enabled, only: :index
+ before_action :ensure_detail_feature_enabled, only: :details
def index
end
def details
+ @alert_id = params[:id]
end
private
- def ensure_feature_enabled
+ def ensure_list_feature_enabled
render_404 unless Feature.enabled?(:alert_management_minimal, project)
end
+
+ def ensure_detail_feature_enabled
+ render_404 unless Feature.enabled?(:alert_management_detail, project)
+ end
end
diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb
index 74f28c3da67..c91eece9983 100644
--- a/app/controllers/projects/static_site_editor_controller.rb
+++ b/app/controllers/projects/static_site_editor_controller.rb
@@ -10,6 +10,10 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
before_action :assign_ref_and_path, only: [:show]
before_action :authorize_edit_tree!, only: [:show]
+ before_action do
+ push_frontend_feature_flag(:rich_content_editor)
+ end
+
def show
@config = Gitlab::StaticSiteEditor::Config.new(@repository, @ref, @path, params[:return_url])
end
diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb
index 54184d99294..07c8168fc24 100644
--- a/app/helpers/projects/alert_management_helper.rb
+++ b/app/helpers/projects/alert_management_helper.rb
@@ -10,4 +10,11 @@ module Projects::AlertManagementHelper
'alert-management-enabled' => Feature.enabled?(:alert_management_minimal, project).to_s
}
end
+
+ def alert_management_detail_data(project_path, alert_id)
+ {
+ 'alert-id' => alert_id,
+ 'project-path' => project_path
+ }
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index abf75a21404..d51ffa70035 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -869,6 +869,14 @@ module Ci
end
end
+ def collect_accessibility_reports!(accessibility_report)
+ each_report(Ci::JobArtifact::ACCESSIBILITY_REPORT_FILE_TYPES) do |file_type, blob|
+ Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, accessibility_report)
+ end
+
+ accessibility_report
+ end
+
def collect_coverage_reports!(coverage_report)
each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob|
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, coverage_report)
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
new file mode 100644
index 00000000000..22ad08b8238
--- /dev/null
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ci
+ class DailyBuildGroupReportResult < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id
+ belongs_to :project
+
+ def self.upsert_reports(data)
+ upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
+ end
+ end
+end
diff --git a/app/models/ci/daily_report_result.rb b/app/models/ci/daily_report_result.rb
deleted file mode 100644
index 3c1c5f11ed4..00000000000
--- a/app/models/ci/daily_report_result.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class DailyReportResult < ApplicationRecord
- extend Gitlab::Ci::Model
-
- belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id
- belongs_to :project
-
- # TODO: Refactor this out when BuildReportResult is implemented.
- # They both need to share the same enum values for param.
- REPORT_PARAMS = {
- coverage: 0
- }.freeze
-
- enum param_type: REPORT_PARAMS
-
- def self.upsert_reports(data)
- upsert_all(data, unique_by: :index_daily_report_results_unique_columns) if data.any?
- end
- end
-end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 1f3d47997f7..e2ef9e3dd5f 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -82,7 +82,7 @@ module Ci
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
- has_many :daily_report_results, class_name: 'Ci::DailyReportResult', foreign_key: :last_pipeline_id
+ has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult', foreign_key: :last_pipeline_id
accepts_nested_attributes_for :variables, reject_if: :persisted?
@@ -194,7 +194,7 @@ module Ci
# We wait a little bit to ensure that all BuildFinishedWorkers finish first
# because this is where some metrics like code coverage is parsed and stored
# in CI build records which the daily build metrics worker relies on.
- pipeline.run_after_commit { Ci::DailyReportResultsWorker.perform_in(10.minutes, pipeline.id) }
+ pipeline.run_after_commit { Ci::DailyBuildGroupReportResultsWorker.perform_in(10.minutes, pipeline.id) }
end
after_transition do |pipeline, transition|
diff --git a/app/models/group.rb b/app/models/group.rb
index fd0f9cc223c..ea3c4c969cc 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -478,16 +478,6 @@ class Group < Namespace
false
end
- def wiki_access_level
- # TODO: Remove this method once we implement group-level features.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
- if Feature.enabled?(:group_wiki, self)
- ProjectFeature::ENABLED
- else
- ProjectFeature::DISABLED
- end
- end
-
private
def update_two_factor_requirement
diff --git a/app/models/project.rb b/app/models/project.rb
index 815124360e2..d76c1ee343a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -322,7 +322,7 @@ class Project < ApplicationRecord
has_many :import_failures, inverse_of: :project
has_many :jira_imports, -> { order 'jira_imports.created_at' }, class_name: 'JiraImportState', inverse_of: :project
- has_many :daily_report_results, class_name: 'Ci::DailyReportResult'
+ has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult'
has_many :repository_storage_moves, class_name: 'ProjectRepositoryStorageMove'
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 7d503b49c14..136ac4cce63 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class GroupPolicy < BasePolicy
- include CrudPolicyHelpers
include FindGroupProjects
desc "Group is public"
@@ -43,23 +42,15 @@ class GroupPolicy < BasePolicy
@subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS
end
- desc "Group has wiki disabled"
- condition(:wiki_disabled, score: 32) { !feature_available?(:wiki) }
-
rule { public_group }.policy do
enable :read_group
enable :read_package
- enable :read_wiki
end
- rule { logged_in_viewable }.policy do
- enable :read_group
- enable :read_wiki
- end
+ rule { logged_in_viewable }.enable :read_group
rule { guest }.policy do
enable :read_group
- enable :read_wiki
enable :upload_file
end
@@ -87,13 +78,11 @@ class GroupPolicy < BasePolicy
enable :create_metrics_dashboard_annotation
enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation
- enable :create_wiki
end
rule { reporter }.policy do
enable :reporter_access
enable :read_container_image
- enable :download_wiki_code
enable :admin_label
enable :admin_list
enable :admin_issue
@@ -112,7 +101,6 @@ class GroupPolicy < BasePolicy
enable :destroy_deploy_token
enable :read_deploy_token
enable :create_deploy_token
- enable :admin_wiki
end
rule { owner }.policy do
@@ -159,11 +147,6 @@ class GroupPolicy < BasePolicy
rule { maintainer & can?(:create_projects) }.enable :transfer_projects
- rule { wiki_disabled }.policy do
- prevent(*create_read_update_admin_destroy(:wiki))
- prevent(:download_wiki_code)
- end
-
def access_level
return GroupMember::NO_ACCESS if @user.nil?
@@ -173,21 +156,6 @@ class GroupPolicy < BasePolicy
def lookup_access_level!
@subject.max_member_access_for_user(@user)
end
-
- # TODO: Extract this into a helper shared with ProjectPolicy, once we implement group-level features.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
- def feature_available?(feature)
- return false unless feature == :wiki
-
- case @subject.wiki_access_level
- when ProjectFeature::DISABLED
- false
- when ProjectFeature::PRIVATE
- admin? || access_level >= ProjectFeature.required_minimum_access_level(feature)
- else
- true
- end
- end
end
GroupPolicy.prepend_if_ee('EE::GroupPolicy')
diff --git a/app/services/ci/daily_report_result_service.rb b/app/services/ci/daily_build_group_report_result_service.rb
index b774a806203..bacf96627f7 100644
--- a/app/services/ci/daily_report_result_service.rb
+++ b/app/services/ci/daily_build_group_report_result_service.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
module Ci
- class DailyReportResultService
+ class DailyBuildGroupReportResultService
def execute(pipeline)
return unless Feature.enabled?(:ci_daily_code_coverage, pipeline.project, default_enabled: true)
- DailyReportResult.upsert_reports(coverage_reports(pipeline))
+ DailyBuildGroupReportResult.upsert_reports(coverage_reports(pipeline))
end
private
@@ -14,15 +14,14 @@ module Ci
base_attrs = {
project_id: pipeline.project_id,
ref_path: pipeline.source_ref_path,
- param_type: DailyReportResult.param_types[:coverage],
date: pipeline.created_at.to_date,
last_pipeline_id: pipeline.id
}
aggregate(pipeline.builds.with_coverage).map do |group_name, group|
base_attrs.merge(
- title: group_name,
- value: average_coverage(group)
+ group_name: group_name,
+ data: { coverage: average_coverage(group) }
)
end
end
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
index c0de1c7c961..6e82a39ffd8 100644
--- a/app/services/groups/import_export/export_service.rb
+++ b/app/services/groups/import_export/export_service.rb
@@ -65,7 +65,7 @@ module Groups
end
def tree_exporter_class
- if ::Feature.enabled?(:group_import_export_ndjson, @group&.parent)
+ if ::Feature.enabled?(:group_export_ndjson, @group&.parent)
Gitlab::ImportExport::Group::TreeSaver
else
Gitlab::ImportExport::Group::LegacyTreeSaver
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index c122da389ac..5e00ce9ccc0 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -53,7 +53,7 @@ module Groups
end
def ndjson?
- ::Feature.enabled?(:group_import_export_ndjson, @group&.parent) &&
+ ::Feature.enabled?(:group_import_ndjson, @group&.parent) &&
File.exist?(File.join(@shared.export_path, 'tree/groups/_all.ndjson'))
end
diff --git a/app/views/projects/alert_management/details.html.haml b/app/views/projects/alert_management/details.html.haml
index 766dbf7c128..1f2ea5e3a4f 100644
--- a/app/views/projects/alert_management/details.html.haml
+++ b/app/views/projects/alert_management/details.html.haml
@@ -1,4 +1,4 @@
- add_to_breadcrumbs s_('AlertManagement|Alerts'), project_alert_management_index_path(@project)
- page_title s_('AlertManagement|Alert detail')
-#js-alert_details
+#js-alert_details{ data: alert_management_detail_data(@project.full_path, @alert_id) }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index fa205f4f3d0..ccac944605d 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -682,7 +682,7 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
-- :name: pipeline_background:ci_daily_report_results
+- :name: pipeline_background:ci_daily_build_group_report_results
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
diff --git a/app/workers/ci/daily_report_results_worker.rb b/app/workers/ci/daily_build_group_report_results_worker.rb
index 314fd44f86c..a6d3c485e24 100644
--- a/app/workers/ci/daily_report_results_worker.rb
+++ b/app/workers/ci/daily_build_group_report_results_worker.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Ci
- class DailyReportResultsWorker
+ class DailyBuildGroupReportResultsWorker
include ApplicationWorker
include PipelineBackgroundQueue
@@ -9,7 +9,7 @@ module Ci
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
- Ci::DailyReportResultService.new.execute(pipeline)
+ Ci::DailyBuildGroupReportResultService.new.execute(pipeline)
end
end
end
diff --git a/config/initializers/action_cable.rb b/config/initializers/action_cable.rb
index 79a6ca5e18a..eb44ff00d09 100644
--- a/config/initializers/action_cable.rb
+++ b/config/initializers/action_cable.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
Rails.application.configure do
- # Prevents the default engine from being mounted because
- # we're running ActionCable as a standalone server
- config.action_cable.mount_path = nil
+ # We only mount the ActionCable engine in tests where we run it in-app
+ # For other environments, we run it on a standalone Puma server
+ config.action_cable.mount_path = Rails.env.test? ? '/-/cable' : nil
config.action_cable.url = Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/cable')
config.action_cable.worker_pool_size = Gitlab.config.action_cable.worker_pool_size
end
diff --git a/db/migrate/20200421111005_create_daily_build_group_report_results.rb b/db/migrate/20200421111005_create_daily_build_group_report_results.rb
new file mode 100644
index 00000000000..12d1c7531d5
--- /dev/null
+++ b/db/migrate/20200421111005_create_daily_build_group_report_results.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class CreateDailyBuildGroupReportResults < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ create_table :ci_daily_build_group_report_results do |t|
+ t.date :date, null: false
+ t.bigint :project_id, null: false
+ t.bigint :last_pipeline_id, null: false
+ t.text :ref_path, null: false # rubocop:disable Migration/AddLimitToTextColumns
+ t.text :group_name, null: false # rubocop:disable Migration/AddLimitToTextColumns
+ t.jsonb :data, null: false
+
+ t.index :last_pipeline_id
+ t.index [:project_id, :ref_path, :date, :group_name], name: 'index_daily_build_group_report_results_unique_columns', unique: true
+ t.foreign_key :projects, on_delete: :cascade
+ t.foreign_key :ci_pipelines, column: :last_pipeline_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index b5bc2d0d361..73edcc371ce 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1025,6 +1025,25 @@ CREATE SEQUENCE public.ci_builds_runner_session_id_seq
ALTER SEQUENCE public.ci_builds_runner_session_id_seq OWNED BY public.ci_builds_runner_session.id;
+CREATE TABLE public.ci_daily_build_group_report_results (
+ id bigint NOT NULL,
+ date date NOT NULL,
+ project_id bigint NOT NULL,
+ last_pipeline_id bigint NOT NULL,
+ ref_path text NOT NULL,
+ group_name text NOT NULL,
+ data jsonb NOT NULL
+);
+
+CREATE SEQUENCE public.ci_daily_build_group_report_results_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.ci_daily_build_group_report_results_id_seq OWNED BY public.ci_daily_build_group_report_results.id;
+
CREATE TABLE public.ci_daily_report_results (
id bigint NOT NULL,
date date NOT NULL,
@@ -7285,6 +7304,8 @@ ALTER TABLE ONLY public.ci_builds_metadata ALTER COLUMN id SET DEFAULT nextval('
ALTER TABLE ONLY public.ci_builds_runner_session ALTER COLUMN id SET DEFAULT nextval('public.ci_builds_runner_session_id_seq'::regclass);
+ALTER TABLE ONLY public.ci_daily_build_group_report_results ALTER COLUMN id SET DEFAULT nextval('public.ci_daily_build_group_report_results_id_seq'::regclass);
+
ALTER TABLE ONLY public.ci_daily_report_results ALTER COLUMN id SET DEFAULT nextval('public.ci_daily_report_results_id_seq'::regclass);
ALTER TABLE ONLY public.ci_group_variables ALTER COLUMN id SET DEFAULT nextval('public.ci_group_variables_id_seq'::regclass);
@@ -7954,6 +7975,9 @@ ALTER TABLE ONLY public.ci_builds
ALTER TABLE ONLY public.ci_builds_runner_session
ADD CONSTRAINT ci_builds_runner_session_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.ci_daily_build_group_report_results
+ ADD CONSTRAINT ci_daily_build_group_report_results_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.ci_daily_report_results
ADD CONSTRAINT ci_daily_report_results_pkey PRIMARY KEY (id);
@@ -9148,6 +9172,8 @@ CREATE INDEX index_ci_builds_project_id_and_status_for_live_jobs_partial2 ON pub
CREATE UNIQUE INDEX index_ci_builds_runner_session_on_build_id ON public.ci_builds_runner_session USING btree (build_id);
+CREATE INDEX index_ci_daily_build_group_report_results_on_last_pipeline_id ON public.ci_daily_build_group_report_results USING btree (last_pipeline_id);
+
CREATE INDEX index_ci_daily_report_results_on_last_pipeline_id ON public.ci_daily_report_results USING btree (last_pipeline_id);
CREATE UNIQUE INDEX index_ci_group_variables_on_group_id_and_key ON public.ci_group_variables USING btree (group_id, key);
@@ -9356,6 +9382,8 @@ CREATE UNIQUE INDEX index_container_repositories_on_project_id_and_name ON publi
CREATE INDEX index_container_repository_on_name_trigram ON public.container_repositories USING gin (name public.gin_trgm_ops);
+CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON public.ci_daily_build_group_report_results USING btree (project_id, ref_path, date, group_name);
+
CREATE UNIQUE INDEX index_daily_report_results_unique_columns ON public.ci_daily_report_results USING btree (project_id, ref_path, param_type, date, title);
CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON public.dependency_proxy_blobs USING btree (group_id, file_name);
@@ -11469,6 +11497,9 @@ ALTER TABLE ONLY public.events
ALTER TABLE ONLY public.ip_restrictions
ADD CONSTRAINT fk_rails_04a93778d5 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.ci_daily_build_group_report_results
+ ADD CONSTRAINT fk_rails_0667f7608c FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.ci_subscriptions_projects
ADD CONSTRAINT fk_rails_0818751483 FOREIGN KEY (downstream_project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@@ -12414,6 +12445,9 @@ ALTER TABLE ONLY public.ci_daily_report_results
ALTER TABLE ONLY public.cluster_providers_aws
ADD CONSTRAINT fk_rails_ed1fdfaeb2 FOREIGN KEY (created_by_user_id) REFERENCES public.users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY public.ci_daily_build_group_report_results
+ ADD CONSTRAINT fk_rails_ee072d13b3 FOREIGN KEY (last_pipeline_id) REFERENCES public.ci_pipelines(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.label_priorities
ADD CONSTRAINT fk_rails_ef916d14fa FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@@ -13656,6 +13690,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200420172927
20200420201933
20200421092907
+20200421111005
20200421233150
20200422091541
20200422213749
diff --git a/doc/api/labels.md b/doc/api/labels.md
index e3f367daaca..eb8ec906ec1 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -109,7 +109,7 @@ GET /projects/:id/labels/:label_id
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `label_id` | integer or string | yes | The ID or title of a group's label. |
+| `label_id` | integer or string | yes | The ID or title of a project's label. |
| `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. |
```shell
diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md
index 6ac3fa2c92d..b4059fc252b 100644
--- a/doc/ci/large_repositories/index.md
+++ b/doc/ci/large_repositories/index.md
@@ -130,7 +130,7 @@ other using `docker` executor.
### `shell` executor example
-Let's assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+Let's assume that you have the following [`config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
```toml
concurrent = 4
@@ -155,7 +155,7 @@ This `config.toml`:
### `docker` executor example
-Let's assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+Let's assume that you have the following [`config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
```toml
concurrent = 4
@@ -216,7 +216,7 @@ but this brings administrative overhead as the `.gitlab-ci.yml` needs to be upda
In such cases, it might be desirable to keep the `.gitlab-ci.yml` clone path agnostic, but make it
a configuration of Runner.
-We can extend our [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
+We can extend our [`config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
with the following specification that will be used by Runner if `.gitlab-ci.yml` will not override it:
```toml
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index e37c2ba3766..c7ecf1ab3d4 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -244,7 +244,7 @@ request:
1. The [CI environment preparation](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/prepare_build.sh).
1. The [Omnibus package creator](https://gitlab.com/gitlab-org/omnibus-gitlab).
-### Incremental improvements
+## Incremental improvements
We allow engineering time to fix small problems (with or without an
issue) that are incremental improvements, such as:
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index e478234df1d..34ec8c09708 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -1008,7 +1008,7 @@ of language classes available.
| `ruby` | Alias: `rb`. |
| `shell` | Aliases: `bash` or `sh`. |
| `sql` | |
-| `toml` | Runner configuration examples, and other toml formatted configuration files. |
+| `toml` | Runner configuration examples, and other TOML-formatted configuration files. |
| `typescript` | Alias: `ts`. |
| `xml` | |
| `yaml` | Alias: `yml`. |
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 7e2d4b08767..83871f5c98e 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -280,6 +280,40 @@ const vm = mountComponent(Component, data);
The main return value of a Vue component is the rendered output. In order to test the component we
need to test the rendered output. [Vue](https://vuejs.org/v2/guide/unit-testing.html) guide's to unit test show us exactly that:
+### Events
+
+We should test for events emitted in response to an action within our component, this is useful to verify the correct events are being fired with the correct arguments.
+
+For any DOM events we should use [`trigger`](https://vue-test-utils.vuejs.org/api/wrapper/#trigger) to fire out event.
+
+```javascript
+// Assuming SomeButton renders: <button>Some button</button>
+wrapper = mount(SomeButton);
+
+...
+it('should fire the click event', () => {
+ const btn = wrapper.find('button')
+
+ btn.trigger('click');
+ ...
+})
+```
+
+When we need to fire a Vue event, we should use [`emit`](https://vuejs.org/v2/guide/components-custom-events.html) to fire our event.
+
+```javascript
+wrapper = shallowMount(DropdownItem);
+
+...
+
+it('should fire the itemClicked event', () => {
+ DropdownItem.vm.$emit('itemClicked');
+ ...
+})
+```
+
+We should verify an event has been fired by asserting against the result of the [`emitted()`](https://vue-test-utils.vuejs.org/api/wrapper/#emitted) method
+
## Vue.js Expert Role
One should apply to be a Vue.js expert by opening an MR when the Merge Request's they create and review show:
diff --git a/doc/development/fe_guide/vue3_migration.md b/doc/development/fe_guide/vue3_migration.md
index 1292926d951..65b6b460de5 100644
--- a/doc/development/fe_guide/vue3_migration.md
+++ b/doc/development/fe_guide/vue3_migration.md
@@ -12,11 +12,11 @@ Filters [are removed](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0015
Component's computed properties / methods or external helpers.
-## Event bus
+## Event hub
**Why?**
-`$on` and `$off` methods [are removed](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0020-events-api-change.md) from the Vue instance, so in Vue 3 it can't be used to create an event bus.
+`$on` and `$off` methods [are removed](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0020-events-api-change.md) from the Vue instance, so in Vue 3 it can't be used to create an event hub.
**What to use instead**
@@ -43,6 +43,21 @@ emitter.on('foo', onFoo) // listen
emitter.off('foo', onFoo) // unlisten
```
+**Event hub factory**
+
+To make it easier for you to migrate existing event hubs to the new recommended approach, or simply
+to create new ones, we have created a factory that you can use to instantiate a new mitt-based
+event hub.
+
+```javascript
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
+```
+
+Event hubs created with the factory expose the same methods as Vue 2 event hubs (`$on`, `$off` and
+`$emit`), making them backward compatible with our previous approach.
+
## <template functional>
**Why?**
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f1c6e1569dd..1f29126109d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8395,7 +8395,7 @@ msgstr ""
msgid "Error creating label."
msgstr ""
-msgid "Error deleting %{issuableType}"
+msgid "Error deleting %{issuableType}"
msgstr ""
msgid "Error deleting project. Check logs for error details."
diff --git a/package.json b/package.json
index dd03299964b..5b145853ac7 100644
--- a/package.json
+++ b/package.json
@@ -100,6 +100,7 @@
"lodash": "^4.17.15",
"marked": "^0.3.12",
"mermaid": "^8.4.8",
+ "mitt": "^1.2.0",
"monaco-editor": "^0.18.1",
"monaco-editor-webpack-plugin": "^1.7.0",
"mousetrap": "^1.4.6",
@@ -217,4 +218,4 @@
"node": ">=10.13.0",
"yarn": "^1.10.0"
}
-} \ No newline at end of file
+}
diff --git a/qa/qa.rb b/qa/qa.rb
index ac0886dbd90..130c7b2dae8 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -72,6 +72,7 @@ module QA
autoload :DeployKey, 'qa/resource/deploy_key'
autoload :DeployToken, 'qa/resource/deploy_token'
autoload :ProtectedBranch, 'qa/resource/protected_branch'
+ autoload :Pipeline, 'qa/resource/pipeline'
autoload :CiVariable, 'qa/resource/ci_variable'
autoload :Runner, 'qa/resource/runner'
autoload :PersonalAccessToken, 'qa/resource/personal_access_token'
diff --git a/qa/qa/resource/pipeline.rb b/qa/qa/resource/pipeline.rb
new file mode 100644
index 00000000000..a115de3e825
--- /dev/null
+++ b/qa/qa/resource/pipeline.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Pipeline < Base
+ attribute :project do
+ Resource::Project.fabricate! do |project|
+ project.name = 'project-with-pipeline'
+ end
+ end
+
+ attribute :id
+ attribute :status
+ attribute :ref
+ attribute :sha
+
+ # array in form
+ # [
+ # { key: 'UPLOAD_TO_S3', variable_type: 'file', value: true },
+ # { key: 'SOMETHING', variable_type: 'env_var', value: 'yes' }
+ # ]
+ attribute :variables
+
+ def initialize
+ @ref = 'master'
+ @variables = []
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_ci_cd_pipelines)
+ Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
+ Page::Project::Pipeline::New.perform(&:click_run_pipeline_button)
+ end
+
+ def api_get_path
+ "/projects/#{project.id}/pipelines/#{id}"
+ end
+
+ def api_post_path
+ "/projects/#{project.id}/pipeline"
+ end
+
+ def api_post_body
+ {
+ ref: ref,
+ variables: variables
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index 1b671a076de..f41328c2d82 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -100,6 +100,10 @@ module QA
"#{api_get_path}/runners"
end
+ def api_pipelines_path
+ "#{api_get_path}/pipelines"
+ end
+
def api_put_path
"/projects/#{id}"
end
@@ -161,6 +165,10 @@ module QA
parse_body(response)
end
+ def pipelines
+ parse_body(get(Runtime::API::Request.new(api_client, api_pipelines_path).url))
+ end
+
def share_with_group(invitee, access_level = Resource::Members::AccessLevel::DEVELOPER)
post Runtime::API::Request.new(api_client, "/projects/#{id}/share").url, { group_id: invitee.id, group_access: access_level }
end
diff --git a/spec/controllers/projects/alert_management_controller_spec.rb b/spec/controllers/projects/alert_management_controller_spec.rb
index 4a6ebc46311..ccb9bfc3001 100644
--- a/spec/controllers/projects/alert_management_controller_spec.rb
+++ b/spec/controllers/projects/alert_management_controller_spec.rb
@@ -40,9 +40,9 @@ describe Projects::AlertManagementController do
end
describe 'GET #details' do
- context 'when alert_management_minimal is enabled' do
+ context 'when alert_management_detail is enabled' do
before do
- stub_feature_flags(alert_management_minimal: true)
+ stub_feature_flags(alert_management_detail: true)
end
it 'shows the page' do
@@ -52,9 +52,9 @@ describe Projects::AlertManagementController do
end
end
- context 'when alert_management_minimal is disabled' do
+ context 'when alert_management_detail is disabled' do
before do
- stub_feature_flags(alert_management_minimal: false)
+ stub_feature_flags(alert_management_detail: false)
end
it 'shows 404' do
@@ -64,4 +64,12 @@ describe Projects::AlertManagementController do
end
end
end
+
+ describe 'set_alert_id' do
+ it 'sets alert id from the route' do
+ get :details, params: { namespace_id: project.namespace, project_id: project, id: id }
+
+ expect(assigns(:alert_id)).to eq(id.to_s)
+ end
+ end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 875371d26c9..26786aab12c 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -320,6 +320,12 @@ FactoryBot.define do
end
end
+ trait :accessibility_reports do
+ after(:build) do |build|
+ build.job_artifacts << create(:ci_job_artifact, :accessibility, job: build)
+ end
+ end
+
trait :coverage_reports do
after(:build) do |build|
build.job_artifacts << create(:ci_job_artifact, :cobertura, job: build)
diff --git a/spec/factories/ci/daily_report_results.rb b/spec/factories/ci/daily_build_group_report_results.rb
index e2255e8a134..7f72991b3eb 100644
--- a/spec/factories/ci/daily_report_results.rb
+++ b/spec/factories/ci/daily_build_group_report_results.rb
@@ -1,13 +1,14 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :ci_daily_report_result, class: 'Ci::DailyReportResult' do
+ factory :ci_daily_build_group_report_result, class: 'Ci::DailyBuildGroupReportResult' do
ref_path { Gitlab::Git::BRANCH_REF_PREFIX + 'master' }
date { Time.zone.now.to_date }
project
last_pipeline factory: :ci_pipeline
- param_type { Ci::DailyReportResult.param_types[:coverage] }
- title { 'rspec' }
- value { 77.0 }
+ group_name { 'rspec' }
+ data do
+ { coverage: 77.0 }
+ end
end
end
diff --git a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
new file mode 100644
index 00000000000..c3f17227701
--- /dev/null
+++ b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Issues > Real-time sidebar', :js do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ project.add_developer(user)
+ end
+
+ it 'updates the assignee in real-time' do
+ Capybara::Session.new(:other_session)
+
+ using_session :other_session do
+ visit project_issue_path(project, issue)
+ expect(page.find('.assignee')).to have_content 'None'
+ end
+
+ gitlab_sign_in(user)
+ visit project_issue_path(project, issue)
+ expect(page.find('.assignee')).to have_content 'None'
+
+ click_button 'assign yourself'
+
+ using_session :other_session do
+ expect(page.find('.assignee')).to have_content user.name
+ end
+ end
+end
diff --git a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
index 03279fc56a4..91bcef5cb62 100644
--- a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
+++ b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
@@ -10,6 +10,9 @@ export const Editor = {
initialEditType: {
type: String,
},
+ height: {
+ type: String,
+ },
},
render(h) {
return h('div');
diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js
index 994de8d4d39..f5b5d669742 100644
--- a/spec/frontend/alert_management/components/alert_management_detail_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js
@@ -4,14 +4,18 @@ import AlertDetails from '~/alert_management/components/alert_details.vue';
describe('AlertDetails', () => {
let wrapper;
- function mountComponent() {
- wrapper = shallowMount(AlertDetails);
+ function mountComponent(alert = {}) {
+ wrapper = shallowMount(AlertDetails, {
+ propsData: {
+ alertId: 'alertId',
+ projectPath: 'projectPath',
+ },
+ data() {
+ return { alert };
+ },
+ });
}
- beforeEach(() => {
- mountComponent();
- });
-
afterEach(() => {
if (wrapper) {
wrapper.destroy();
@@ -19,20 +23,42 @@ describe('AlertDetails', () => {
});
describe('Alert details', () => {
- it('renders a tab with overview information', () => {
- expect(wrapper.find('[data-testid="overviewTab"]').exists()).toBe(true);
- });
+ describe('when alert is null', () => {
+ beforeEach(() => {
+ mountComponent(null);
+ });
- it('renders a tab with full alert information', () => {
- expect(wrapper.find('[data-testid="fullDetailsTab"]').exists()).toBe(true);
+ describe('when alert is null', () => {
+ beforeEach(() => {
+ mountComponent(null);
+ });
+
+ it('shows an empty state', () => {
+ expect(wrapper.find('[data-testid="alertDetailsTabs"]').exists()).toBe(false);
+ });
+ });
});
- it('renders alert details', () => {
- expect(wrapper.find('[data-testid="startTimeItem"]').exists()).toBe(true);
+ describe('when alert is present', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders a tab with overview information', () => {
+ expect(wrapper.find('[data-testid="overviewTab"]').exists()).toBe(true);
+ });
+
+ it('renders a tab with full alert information', () => {
+ expect(wrapper.find('[data-testid="fullDetailsTab"]').exists()).toBe(true);
+ });
+
+ it('renders alert details', () => {
+ expect(wrapper.find('[data-testid="startTimeItem"]').exists()).toBe(true);
+ });
});
- it('renders a status dropdown', () => {
- expect(wrapper.find('[data-testid="statusDropdownItem"]').exists()).toBe(true);
+ it('renders a status dropdown containing three items', () => {
+ expect(wrapper.findAll('[data-testid="statusDropdownItem"]').length).toBe(3);
});
});
});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 3b6021c8014..970bc99f8ff 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -44,6 +44,7 @@ import {
setExpandedDiffLines,
setSuggestPopoverDismissed,
changeCurrentCommit,
+ moveToNeighboringCommit,
} from '~/diffs/store/actions';
import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
@@ -1406,4 +1407,44 @@ describe('DiffsStoreActions', () => {
},
);
});
+
+ describe('moveToNeighboringCommit', () => {
+ it.each`
+ direction | expected | currentCommit
+ ${'next'} | ${'NEXTSHA'} | ${{ next_commit_id: 'NEXTSHA' }}
+ ${'previous'} | ${'PREVIOUSSHA'} | ${{ prev_commit_id: 'PREVIOUSSHA' }}
+ `(
+ 'for the direction "$direction", dispatches the action to move to the SHA "$expected"',
+ ({ direction, expected, currentCommit }) => {
+ return testAction(
+ moveToNeighboringCommit,
+ { direction },
+ { commit: currentCommit },
+ [],
+ [{ type: 'changeCurrentCommit', payload: { commitId: expected } }],
+ );
+ },
+ );
+
+ it.each`
+ direction | diffsAreLoading | currentCommit
+ ${'next'} | ${false} | ${{ prev_commit_id: 'PREVIOUSSHA' }}
+ ${'next'} | ${true} | ${{ prev_commit_id: 'PREVIOUSSHA' }}
+ ${'next'} | ${false} | ${undefined}
+ ${'previous'} | ${false} | ${{ next_commit_id: 'NEXTSHA' }}
+ ${'previous'} | ${true} | ${{ next_commit_id: 'NEXTSHA' }}
+ ${'previous'} | ${false} | ${undefined}
+ `(
+ 'given `{ "isloading": $diffsAreLoading, "commit": $currentCommit }` in state, no actions are dispatched',
+ ({ direction, diffsAreLoading, currentCommit }) => {
+ return testAction(
+ moveToNeighboringCommit,
+ { direction },
+ { commit: currentCommit, isLoading: diffsAreLoading },
+ [],
+ [],
+ );
+ },
+ );
+ });
});
diff --git a/spec/frontend/helpers/event_hub_factory_spec.js b/spec/frontend/helpers/event_hub_factory_spec.js
new file mode 100644
index 00000000000..ff00e29a40a
--- /dev/null
+++ b/spec/frontend/helpers/event_hub_factory_spec.js
@@ -0,0 +1,36 @@
+import createEventHub from '~/helpers/event_hub_factory';
+import mitt from 'mitt';
+
+jest.mock('mitt');
+
+mitt.mockReturnValue({
+ on: () => {},
+ off: () => {},
+ emit: () => {},
+});
+
+describe('event bus factory', () => {
+ let eventBus;
+
+ beforeEach(() => {
+ eventBus = createEventHub();
+ });
+
+ afterEach(() => {
+ eventBus = null;
+ });
+
+ it('creates an emitter', () => {
+ expect(mitt).toHaveBeenCalled();
+ });
+
+ it.each`
+ method
+ ${'on'}
+ ${'off'}
+ ${'emit'}
+ `('binds $$method to $method ', ({ method }) => {
+ expect(typeof eventBus[method]).toBe('function');
+ expect(eventBus[method]).toBe(eventBus[`$${method}`]);
+ });
+});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index 2d4d3ea28ff..42076e8da5c 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -1,9 +1,8 @@
-/* eslint-disable no-unused-vars */
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
-import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
-import GLDropdown from '~/gl_dropdown';
+import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
import '~/behaviors/markdown/render_gfm';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
@@ -13,6 +12,9 @@ function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
}
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/issue_show/event_hub');
+
const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
describe('Issuable output', () => {
@@ -20,9 +22,10 @@ describe('Issuable output', () => {
let realtimeRequestCount = 0;
let vm;
- beforeEach(done => {
+ beforeEach(() => {
setFixtures(`
<div>
+ <title>Title</title>
<div class="detail-page-description content-block">
<details open>
<summary>One</summary>
@@ -35,7 +38,6 @@ describe('Issuable output', () => {
<span id="task_status"></span>
</div>
`);
- spyOn(eventHub, '$emit');
const IssuableDescriptionComponent = Vue.extend(issuableApp);
@@ -53,7 +55,7 @@ describe('Issuable output', () => {
canUpdate: true,
canDestroy: true,
endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes',
- updateEndpoint: gl.TEST_HOST,
+ updateEndpoint: TEST_HOST,
issuableRef: '#1',
initialTitleHtml: '',
initialTitleText: '',
@@ -67,8 +69,6 @@ describe('Issuable output', () => {
issuableTemplateNamesPath: '/issuable-templates-path',
},
}).$mount();
-
- setTimeout(done);
});
afterEach(() => {
@@ -79,9 +79,10 @@ describe('Issuable output', () => {
vm.$destroy();
});
- it('should render a title/description/edited and update title/description/edited on update', done => {
+ it('should render a title/description/edited and update title/description/edited on update', () => {
let editedText;
- Vue.nextTick()
+ return axios
+ .waitForAll()
.then(() => {
editedText = vm.$el.querySelector('.edited-text');
})
@@ -100,8 +101,8 @@ describe('Issuable output', () => {
})
.then(() => {
vm.poll.makeRequest();
+ return axios.waitForAll();
})
- .then(() => new Promise(resolve => setTimeout(resolve)))
.then(() => {
expect(document.querySelector('title').innerText).toContain('2 (#1)');
expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
@@ -115,312 +116,239 @@ describe('Issuable output', () => {
expect(editedText.querySelector('.author-link').href).toMatch(/\/other_user$/);
expect(editedText.querySelector('time')).toBeTruthy();
expect(vm.state.lock_version).toEqual(2);
- })
- .then(done)
- .catch(done.fail);
+ });
});
- it('shows actions if permissions are correct', done => {
+ it('shows actions if permissions are correct', () => {
vm.showForm = true;
- Vue.nextTick(() => {
+ return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.btn')).not.toBeNull();
-
- done();
});
});
- it('does not show actions if permissions are incorrect', done => {
+ it('does not show actions if permissions are incorrect', () => {
vm.showForm = true;
vm.canUpdate = false;
- Vue.nextTick(() => {
+ return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.btn')).toBeNull();
-
- done();
});
});
- it('does not update formState if form is already open', done => {
+ it('does not update formState if form is already open', () => {
vm.updateAndShowForm();
vm.state.titleText = 'testing 123';
vm.updateAndShowForm();
- Vue.nextTick(() => {
+ return vm.$nextTick().then(() => {
expect(vm.store.formState.title).not.toBe('testing 123');
+ });
+ });
+
+ it('opens reCAPTCHA modal if update rejected as spam', () => {
+ let modal;
- done();
+ jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: {
+ recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ },
});
+
+ vm.canUpdate = true;
+ vm.showForm = true;
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ vm.$refs.recaptchaModal.scriptSrc = '//scriptsrc';
+ return vm.updateIssuable();
+ })
+ .then(() => {
+ modal = vm.$el.querySelector('.js-recaptcha-modal');
+
+ expect(modal.style.display).not.toEqual('none');
+ expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
+ expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
+ })
+ .then(() => {
+ modal.querySelector('.close').click();
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(modal.style.display).toEqual('none');
+ expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
+ });
});
describe('updateIssuable', () => {
- it('fetches new data after update', done => {
- spyOn(vm, 'updateStoreState').and.callThrough();
- spyOn(vm.service, 'getData').and.callThrough();
- spyOn(vm.service, 'updateIssuable').and.returnValue(
- Promise.resolve({
- data: { web_url: window.location.pathname },
- }),
- );
-
- vm.updateIssuable()
- .then(() => {
- expect(vm.updateStoreState).toHaveBeenCalled();
- expect(vm.service.getData).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ it('fetches new data after update', () => {
+ const updateStoreSpy = jest.spyOn(vm, 'updateStoreState');
+ const getDataSpy = jest.spyOn(vm.service, 'getData');
+ jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: { web_url: window.location.pathname },
+ });
+
+ return vm.updateIssuable().then(() => {
+ expect(updateStoreSpy).toHaveBeenCalled();
+ expect(getDataSpy).toHaveBeenCalled();
+ });
});
- it('correctly updates issuable data', done => {
- spyOn(vm.service, 'updateIssuable').and.returnValue(
- Promise.resolve({
- data: { web_url: window.location.pathname },
- }),
- );
+ it('correctly updates issuable data', () => {
+ const spy = jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: { web_url: window.location.pathname },
+ });
- vm.updateIssuable()
- .then(() => {
- expect(vm.service.updateIssuable).toHaveBeenCalledWith(vm.formState);
- expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
- })
- .then(done)
- .catch(done.fail);
+ return vm.updateIssuable().then(() => {
+ expect(spy).toHaveBeenCalledWith(vm.formState);
+ expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
+ });
});
- it('does not redirect if issue has not moved', done => {
- const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
- spyOn(vm.service, 'updateIssuable').and.returnValue(
- Promise.resolve({
- data: {
- web_url: window.location.pathname,
- confidential: vm.isConfidential,
- },
- }),
- );
-
- vm.updateIssuable();
+ it('does not redirect if issue has not moved', () => {
+ jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: {
+ web_url: window.location.pathname,
+ confidential: vm.isConfidential,
+ },
+ });
- setTimeout(() => {
+ return vm.updateIssuable().then(() => {
expect(visitUrl).not.toHaveBeenCalled();
- done();
});
});
- it('redirects if returned web_url has changed', done => {
- const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
- spyOn(vm.service, 'updateIssuable').and.returnValue(
- Promise.resolve({
- data: {
- web_url: '/testing-issue-move',
- confidential: vm.isConfidential,
- },
- }),
- );
+ it('redirects if returned web_url has changed', () => {
+ jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: {
+ web_url: '/testing-issue-move',
+ confidential: vm.isConfidential,
+ },
+ });
vm.updateIssuable();
- setTimeout(() => {
+ return vm.updateIssuable().then(() => {
expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
- done();
});
});
describe('shows dialog when issue has unsaved changed', () => {
- it('confirms on title change', done => {
+ it('confirms on title change', () => {
vm.showForm = true;
vm.state.titleText = 'title has changed';
const e = { returnValue: null };
vm.handleBeforeUnloadEvent(e);
- Vue.nextTick(() => {
+ return vm.$nextTick().then(() => {
expect(e.returnValue).not.toBeNull();
-
- done();
});
});
- it('confirms on description change', done => {
+ it('confirms on description change', () => {
vm.showForm = true;
vm.state.descriptionText = 'description has changed';
const e = { returnValue: null };
vm.handleBeforeUnloadEvent(e);
- Vue.nextTick(() => {
+ return vm.$nextTick().then(() => {
expect(e.returnValue).not.toBeNull();
-
- done();
});
});
- it('does nothing when nothing has changed', done => {
+ it('does nothing when nothing has changed', () => {
const e = { returnValue: null };
vm.handleBeforeUnloadEvent(e);
- Vue.nextTick(() => {
+ return vm.$nextTick().then(() => {
expect(e.returnValue).toBeNull();
-
- done();
});
});
});
describe('error when updating', () => {
- it('closes form on error', done => {
- spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject());
- vm.updateIssuable();
-
- setTimeout(() => {
+ it('closes form on error', () => {
+ jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue();
+ return vm.updateIssuable().then(() => {
expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
`Error updating issue`,
);
-
- done();
});
});
- it('returns the correct error message for issuableType', done => {
- spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject());
+ it('returns the correct error message for issuableType', () => {
+ jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue();
vm.issuableType = 'merge request';
- Vue.nextTick(() => {
- vm.updateIssuable();
-
- setTimeout(() => {
+ return vm
+ .$nextTick()
+ .then(vm.updateIssuable)
+ .then(() => {
expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
`Error updating merge request`,
);
-
- done();
});
- });
});
- it('shows error message from backend if exists', done => {
+ it('shows error message from backend if exists', () => {
const msg = 'Custom error message from backend';
- spyOn(vm.service, 'updateIssuable').and.callFake(
- // eslint-disable-next-line prefer-promise-reject-errors
- () => Promise.reject({ response: { data: { errors: [msg] } } }),
- );
+ jest
+ .spyOn(vm.service, 'updateIssuable')
+ .mockRejectedValue({ response: { data: { errors: [msg] } } });
- vm.updateIssuable();
- setTimeout(() => {
+ return vm.updateIssuable().then(() => {
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
`${vm.defaultErrorMessage}. ${msg}`,
);
-
- done();
});
});
});
});
- it('opens reCAPTCHA modal if update rejected as spam', done => {
- function mockScriptSrc() {
- const recaptchaChild = vm.$children.find(
- // eslint-disable-next-line no-underscore-dangle
- child => child.$options._componentTag === 'recaptcha-modal',
- );
-
- recaptchaChild.scriptSrc = '//scriptsrc';
- }
-
- let modal;
- const promise = new Promise(resolve => {
- resolve({
+ describe('deleteIssuable', () => {
+ it('changes URL when deleted', () => {
+ jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({
data: {
- recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ web_url: '/test',
},
});
- });
-
- spyOn(vm.service, 'updateIssuable').and.returnValue(promise);
-
- vm.canUpdate = true;
- vm.showForm = true;
-
- vm.$nextTick()
- .then(() => mockScriptSrc())
- .then(() => vm.updateIssuable())
- .then(promise)
- .then(() => setTimeoutPromise())
- .then(() => {
- modal = vm.$el.querySelector('.js-recaptcha-modal');
- expect(modal.style.display).not.toEqual('none');
- expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
- expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
- })
- .then(() => modal.querySelector('.close').click())
- .then(() => vm.$nextTick())
- .then(() => {
- expect(modal.style.display).toEqual('none');
- expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
-
- describe('deleteIssuable', () => {
- it('changes URL when deleted', done => {
- const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
- spyOn(vm.service, 'deleteIssuable').and.returnValue(
- Promise.resolve({
- data: {
- web_url: '/test',
- },
- }),
- );
-
- vm.deleteIssuable();
-
- setTimeout(() => {
+ return vm.deleteIssuable().then(() => {
expect(visitUrl).toHaveBeenCalledWith('/test');
-
- done();
});
});
- it('stops polling when deleting', done => {
- spyOnDependency(issuableApp, 'visitUrl');
- spyOn(vm.poll, 'stop').and.callThrough();
- spyOn(vm.service, 'deleteIssuable').and.returnValue(
- Promise.resolve({
- data: {
- web_url: '/test',
- },
- }),
- );
-
- vm.deleteIssuable();
-
- setTimeout(() => {
- expect(vm.poll.stop).toHaveBeenCalledWith();
+ it('stops polling when deleting', () => {
+ const spy = jest.spyOn(vm.poll, 'stop');
+ jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({
+ data: {
+ web_url: '/test',
+ },
+ });
- done();
+ return vm.deleteIssuable().then(() => {
+ expect(spy).toHaveBeenCalledWith();
});
});
- it('closes form on error', done => {
- spyOn(vm.service, 'deleteIssuable').and.returnValue(Promise.reject());
+ it('closes form on error', () => {
+ jest.spyOn(vm.service, 'deleteIssuable').mockRejectedValue();
- vm.deleteIssuable();
-
- setTimeout(() => {
+ return vm.deleteIssuable().then(() => {
expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
'Error deleting issue',
);
-
- done();
});
});
});
describe('updateAndShowForm', () => {
- it('shows locked warning if form is open & data is different', done => {
- vm.$nextTick()
+ it('shows locked warning if form is open & data is different', () => {
+ return vm
+ .$nextTick()
.then(() => {
vm.updateAndShowForm();
@@ -436,44 +364,38 @@ describe('Issuable output', () => {
expect(vm.formState.lockedWarningVisible).toEqual(true);
expect(vm.formState.lock_version).toEqual(1);
expect(vm.$el.querySelector('.alert')).not.toBeNull();
- })
- .then(done)
- .catch(done.fail);
+ });
});
});
describe('requestTemplatesAndShowForm', () => {
+ let formSpy;
+
beforeEach(() => {
- spyOn(vm, 'updateAndShowForm');
+ formSpy = jest.spyOn(vm, 'updateAndShowForm');
});
- it('shows the form if template names request is successful', done => {
+ it('shows the form if template names request is successful', () => {
const mockData = [{ name: 'Bug' }];
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
- vm.requestTemplatesAndShowForm()
- .then(() => {
- expect(vm.updateAndShowForm).toHaveBeenCalledWith(mockData);
- })
- .then(done)
- .catch(done.fail);
+ return vm.requestTemplatesAndShowForm().then(() => {
+ expect(formSpy).toHaveBeenCalledWith(mockData);
+ });
});
- it('shows the form if template names request failed', done => {
+ it('shows the form if template names request failed', () => {
mock
.onGet('/issuable-templates-path')
.reply(() => Promise.reject(new Error('something went wrong')));
- vm.requestTemplatesAndShowForm()
- .then(() => {
- expect(document.querySelector('.flash-container .flash-text').textContent).toContain(
- 'Error updating issue',
- );
+ return vm.requestTemplatesAndShowForm().then(() => {
+ expect(document.querySelector('.flash-container .flash-text').textContent).toContain(
+ 'Error updating issue',
+ );
- expect(vm.updateAndShowForm).toHaveBeenCalledWith();
- })
- .then(done)
- .catch(done.fail);
+ expect(formSpy).toHaveBeenCalledWith();
+ });
});
});
@@ -490,32 +412,26 @@ describe('Issuable output', () => {
});
describe('updateStoreState', () => {
- it('should make a request and update the state of the store', done => {
+ it('should make a request and update the state of the store', () => {
const data = { foo: 1 };
- spyOn(vm.store, 'updateState');
- spyOn(vm.service, 'getData').and.returnValue(Promise.resolve({ data }));
+ const getDataSpy = jest.spyOn(vm.service, 'getData').mockResolvedValue({ data });
+ const updateStateSpy = jest.spyOn(vm.store, 'updateState').mockImplementation(jest.fn);
- vm.updateStoreState()
- .then(() => {
- expect(vm.service.getData).toHaveBeenCalled();
- expect(vm.store.updateState).toHaveBeenCalledWith(data);
- })
- .then(done)
- .catch(done.fail);
+ return vm.updateStoreState().then(() => {
+ expect(getDataSpy).toHaveBeenCalled();
+ expect(updateStateSpy).toHaveBeenCalledWith(data);
+ });
});
- it('should show error message if store update fails', done => {
- spyOn(vm.service, 'getData').and.returnValue(Promise.reject());
+ it('should show error message if store update fails', () => {
+ jest.spyOn(vm.service, 'getData').mockRejectedValue();
vm.issuableType = 'merge request';
- vm.updateStoreState()
- .then(() => {
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating ${vm.issuableType}`,
- );
- })
- .then(done)
- .catch(done.fail);
+ return vm.updateStoreState().then(() => {
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ `Error updating ${vm.issuableType}`,
+ );
+ });
});
});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js
index 194f177d837..9c448c498e2 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/frontend/issue_show/components/description_spec.js
@@ -1,8 +1,12 @@
import $ from 'jquery';
import Vue from 'vue';
import '~/behaviors/markdown/render_gfm';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import Description from '~/issue_show/components/description.vue';
+import TaskList from '~/task_list';
+
+jest.mock('~/task_list');
describe('Description component', () => {
let vm;
@@ -13,7 +17,7 @@ describe('Description component', () => {
descriptionText: 'test',
updatedAt: new Date().toString(),
taskStatus: '',
- updateUrl: gl.TEST_HOST,
+ updateUrl: TEST_HOST,
};
beforeEach(() => {
@@ -39,25 +43,26 @@ describe('Description component', () => {
$('.issuable-meta .flash-container').remove();
});
- it('animates description changes', done => {
+ it('animates description changes', () => {
vm.descriptionHtml = 'changed';
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
- ).toBeTruthy();
-
- setTimeout(() => {
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(
+ vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
+ ).toBeTruthy();
+ jest.runAllTimers();
+ return vm.$nextTick();
+ })
+ .then(() => {
expect(
vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'),
).toBeTruthy();
-
- done();
});
- });
});
- it('opens reCAPTCHA dialog if update rejected as spam', done => {
+ it('opens reCAPTCHA dialog if update rejected as spam', () => {
let modal;
const recaptchaChild = vm.$children.find(
// eslint-disable-next-line no-underscore-dangle
@@ -70,7 +75,8 @@ describe('Description component', () => {
recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
});
- vm.$nextTick()
+ return vm
+ .$nextTick()
.then(() => {
modal = vm.$el.querySelector('.js-recaptcha-modal');
@@ -83,128 +89,105 @@ describe('Description component', () => {
.then(() => {
expect(modal.style.display).toEqual('none');
expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
- })
- .then(done)
- .catch(done.fail);
+ });
});
- describe('TaskList', () => {
- let TaskList;
+ it('applies syntax highlighting and math when description changed', () => {
+ const vmSpy = jest.spyOn(vm, 'renderGFM');
+ const prototypeSpy = jest.spyOn($.prototype, 'renderGFM');
+ vm.descriptionHtml = 'changed';
+ return vm.$nextTick().then(() => {
+ expect(vm.$refs['gfm-content']).toBeDefined();
+ expect(vmSpy).toHaveBeenCalled();
+ expect(prototypeSpy).toHaveBeenCalled();
+ expect($.prototype.renderGFM).toHaveBeenCalled();
+ });
+ });
+
+ it('sets data-update-url', () => {
+ expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(TEST_HOST);
+ });
+
+ describe('TaskList', () => {
beforeEach(() => {
vm.$destroy();
+ TaskList.mockClear();
vm = mountComponent(
DescriptionComponent,
Object.assign({}, props, {
issuableType: 'issuableType',
}),
);
- TaskList = spyOnDependency(Description, 'TaskList');
});
- it('re-inits the TaskList when description changed', done => {
+ it('re-inits the TaskList when description changed', () => {
vm.descriptionHtml = 'changed';
- setTimeout(() => {
- expect(TaskList).toHaveBeenCalled();
- done();
- });
+ expect(TaskList).toHaveBeenCalled();
});
- it('does not re-init the TaskList when canUpdate is false', done => {
+ it('does not re-init the TaskList when canUpdate is false', () => {
vm.canUpdate = false;
vm.descriptionHtml = 'changed';
- setTimeout(() => {
- expect(TaskList).not.toHaveBeenCalled();
- done();
- });
+ expect(TaskList).toHaveBeenCalledTimes(1);
});
- it('calls with issuableType dataType', done => {
+ it('calls with issuableType dataType', () => {
vm.descriptionHtml = 'changed';
- setTimeout(() => {
- expect(TaskList).toHaveBeenCalledWith({
- dataType: 'issuableType',
- fieldName: 'description',
- selector: '.detail-page-description',
- onSuccess: jasmine.any(Function),
- onError: jasmine.any(Function),
- lockVersion: 0,
- });
-
- done();
+ expect(TaskList).toHaveBeenCalledWith({
+ dataType: 'issuableType',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ onSuccess: expect.any(Function),
+ onError: expect.any(Function),
+ lockVersion: 0,
});
});
});
describe('taskStatus', () => {
- it('adds full taskStatus', done => {
+ it('adds full taskStatus', () => {
vm.taskStatus = '1 of 1';
- setTimeout(() => {
+ return vm.$nextTick().then(() => {
expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(
'1 of 1',
);
-
- done();
});
});
- it('adds short taskStatus', done => {
+ it('adds short taskStatus', () => {
vm.taskStatus = '1 of 1';
- setTimeout(() => {
+ return vm.$nextTick().then(() => {
expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe(
'1/1 task',
);
-
- done();
});
});
- it('clears task status text when no tasks are present', done => {
+ it('clears task status text when no tasks are present', () => {
vm.taskStatus = '0 of 0';
- setTimeout(() => {
+ return vm.$nextTick().then(() => {
expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe('');
-
- done();
});
});
});
- it('applies syntax highlighting and math when description changed', done => {
- spyOn(vm, 'renderGFM').and.callThrough();
- spyOn($.prototype, 'renderGFM').and.callThrough();
- vm.descriptionHtml = 'changed';
-
- Vue.nextTick(() => {
- setTimeout(() => {
- expect(vm.$refs['gfm-content']).toBeDefined();
- expect(vm.renderGFM).toHaveBeenCalled();
- expect($.prototype.renderGFM).toHaveBeenCalled();
-
- done();
- });
- });
- });
-
- it('sets data-update-url', () => {
- expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(gl.TEST_HOST);
- });
-
describe('taskListUpdateError', () => {
it('should create flash notification and emit an event to parent', () => {
const msg =
'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.';
- spyOn(vm, '$emit');
+ const spy = jest.spyOn(vm, '$emit');
vm.taskListUpdateError();
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg);
- expect(vm.$emit).toHaveBeenCalledWith('taskListUpdateFailed');
+ expect(spy).toHaveBeenCalledWith('taskListUpdateFailed');
});
});
});
diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/frontend/issue_show/components/edited_spec.js
index a1683f060c0..a1683f060c0 100644
--- a/spec/javascripts/issue_show/components/edited_spec.js
+++ b/spec/frontend/issue_show/components/edited_spec.js
diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/frontend/issue_show/components/fields/description_template_spec.js
index 8d77a620d76..9ebab31f1ad 100644
--- a/spec/javascripts/issue_show/components/fields/description_template_spec.js
+++ b/spec/frontend/issue_show/components/fields/description_template_spec.js
@@ -5,7 +5,7 @@ describe('Issue description template component', () => {
let vm;
let formState;
- beforeEach(done => {
+ beforeEach(() => {
const Component = Vue.extend(descriptionTemplate);
formState = {
description: 'test',
@@ -19,8 +19,6 @@ describe('Issue description template component', () => {
projectNamespace: '/',
},
}).$mount();
-
- Vue.nextTick(done);
});
it('renders templates as JSON array in data attribute', () => {
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/frontend/issue_show/components/form_spec.js
index a111333ac80..b06a3a89d3b 100644
--- a/spec/javascripts/issue_show/components/form_spec.js
+++ b/spec/frontend/issue_show/components/form_spec.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import formComponent from '~/issue_show/components/form.vue';
+import Autosave from '~/autosave';
import eventHub from '~/issue_show/event_hub';
+jest.mock('~/autosave');
+
describe('Inline edit form component', () => {
let vm;
const defaultProps = {
@@ -65,18 +68,16 @@ describe('Inline edit form component', () => {
});
describe('autosave', () => {
- let autosaveObj;
- let autosave;
+ let spy;
beforeEach(() => {
- autosaveObj = { reset: jasmine.createSpy() };
- autosave = spyOnDependency(formComponent, 'Autosave').and.returnValue(autosaveObj);
+ spy = jest.spyOn(Autosave.prototype, 'reset');
});
it('initialized Autosave on mount', () => {
createComponent();
- expect(autosave).toHaveBeenCalledTimes(2);
+ expect(Autosave).toHaveBeenCalledTimes(2);
});
it('calls reset on autosave when eventHub emits appropriate events', () => {
@@ -84,15 +85,15 @@ describe('Inline edit form component', () => {
eventHub.$emit('close.form');
- expect(autosaveObj.reset).toHaveBeenCalledTimes(2);
+ expect(spy).toHaveBeenCalledTimes(2);
eventHub.$emit('delete.issuable');
- expect(autosaveObj.reset).toHaveBeenCalledTimes(4);
+ expect(spy).toHaveBeenCalledTimes(4);
eventHub.$emit('update.issuable');
- expect(autosaveObj.reset).toHaveBeenCalledTimes(6);
+ expect(spy).toHaveBeenCalledTimes(6);
});
});
});
diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/frontend/issue_show/components/title_spec.js
index 9754c8a6755..c274048fdd5 100644
--- a/spec/javascripts/issue_show/components/title_spec.js
+++ b/spec/frontend/issue_show/components/title_spec.js
@@ -5,8 +5,9 @@ import eventHub from '~/issue_show/event_hub';
describe('Title component', () => {
let vm;
-
beforeEach(() => {
+ setFixtures(`<title />`);
+
const Component = Vue.extend(titleComponent);
const store = new Store({
titleHtml: '',
@@ -28,51 +29,39 @@ describe('Title component', () => {
expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
});
- it('updates page title when changing titleHtml', done => {
- spyOn(vm, 'setPageTitle');
+ it('updates page title when changing titleHtml', () => {
+ const spy = jest.spyOn(vm, 'setPageTitle');
vm.titleHtml = 'test';
- Vue.nextTick(() => {
- expect(vm.setPageTitle).toHaveBeenCalled();
-
- done();
+ return vm.$nextTick().then(() => {
+ expect(spy).toHaveBeenCalled();
});
});
- it('animates title changes', done => {
+ it('animates title changes', () => {
vm.titleHtml = 'test';
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.title').classList.contains('issue-realtime-pre-pulse'),
- ).toBeTruthy();
-
- setTimeout(() => {
- expect(
- vm.$el.querySelector('.title').classList.contains('issue-realtime-trigger-pulse'),
- ).toBeTruthy();
-
- done();
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse');
+ jest.runAllTimers();
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse');
});
- });
});
- it('updates page title after changing title', done => {
+ it('updates page title after changing title', () => {
vm.titleHtml = 'changed';
vm.titleText = 'changed';
- Vue.nextTick(() => {
+ return vm.$nextTick().then(() => {
expect(document.querySelector('title').textContent.trim()).toContain('changed');
-
- done();
});
});
describe('inline edit button', () => {
- beforeEach(() => {
- spyOn(eventHub, '$emit');
- });
-
it('should not show by default', () => {
expect(vm.$el.querySelector('.btn-edit')).toBeNull();
});
@@ -92,6 +81,7 @@ describe('Title component', () => {
});
it('should trigger open.form event when clicked', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
vm.showInlineEditButton = true;
vm.canUpdate = true;
diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js
index 82e39447ae6..ca03abfe6bb 100644
--- a/spec/frontend/static_site_editor/pages/home_spec.js
+++ b/spec/frontend/static_site_editor/pages/home_spec.js
@@ -5,7 +5,7 @@ import { GlSkeletonLoader } from '@gitlab/ui';
import createState from '~/static_site_editor/store/state';
import Home from '~/static_site_editor/pages/home.vue';
-import EditArea from '~/static_site_editor/components/edit_area.vue';
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import EditHeader from '~/static_site_editor/components/edit_header.vue';
import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
@@ -71,10 +71,13 @@ describe('static_site_editor/pages/home', () => {
wrapper = shallowMount(Home, {
localVue,
store,
+ provide: {
+ glFeatures: { richContentEditor: true },
+ },
});
};
- const findEditArea = () => wrapper.find(EditArea);
+ const findRichContentEditor = () => wrapper.find(RichContentEditor);
const findEditHeader = () => wrapper.find(EditHeader);
const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
const findPublishToolbar = () => wrapper.find(PublishToolbar);
@@ -103,8 +106,8 @@ describe('static_site_editor/pages/home', () => {
});
describe('when content is not loaded', () => {
- it('does not render edit area', () => {
- expect(findEditArea().exists()).toBe(false);
+ it('does not render rich content editor', () => {
+ expect(findRichContentEditor().exists()).toBe(false);
});
it('does not render edit header', () => {
@@ -129,8 +132,8 @@ describe('static_site_editor/pages/home', () => {
buildWrapper();
});
- it('renders the edit area', () => {
- expect(findEditArea().exists()).toBe(true);
+ it('renders the rich content editor', () => {
+ expect(findRichContentEditor().exists()).toBe(true);
});
it('renders the edit header', () => {
@@ -141,8 +144,8 @@ describe('static_site_editor/pages/home', () => {
expect(findSkeletonLoader().exists()).toBe(false);
});
- it('passes page content to edit area', () => {
- expect(findEditArea().props('value')).toBe(content);
+ it('passes page content to the rich content editor', () => {
+ expect(findRichContentEditor().props('value')).toBe(content);
});
it('passes page title to edit header', () => {
@@ -228,11 +231,11 @@ describe('static_site_editor/pages/home', () => {
expect(loadContentActionMock).toHaveBeenCalled();
});
- it('dispatches setContent action when edit area emits input event', () => {
+ it('dispatches setContent action when rich content editor emits input event', () => {
buildContentLoadedStore();
buildWrapper();
- findEditArea().vm.$emit('input', sourceContent);
+ findRichContentEditor().vm.$emit('input', sourceContent);
expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined);
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
index 933609c3072..774fe25387a 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
@@ -50,6 +50,10 @@ describe('Rich Content Editor', () => {
it('has the correct initial edit type', () => {
expect(findEditor().props().initialEditType).toBe('wysiwyg');
});
+
+ it('has the correct height', () => {
+ expect(findEditor().props().height).toBe('100%');
+ });
});
describe('when content is changed', () => {
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index 177dcb4ec2e..41f0f54b5ac 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -7,11 +7,11 @@ describe Projects::AlertManagementHelper do
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:current_user) { create(:user) }
+ let_it_be(:project_path) { project.full_path }
describe '#alert_management_data' do
let(:user_can_enable_alert_management) { false }
let(:setting_path) { project_settings_operations_path(project) }
- let(:project_path) { project.full_path }
before do
allow(helper)
@@ -21,7 +21,7 @@ describe Projects::AlertManagementHelper do
end
context 'without alert_managements_setting' do
- it 'returns frontend configuration' do
+ it 'returns index page configuration' do
expect(alert_management_data(current_user, project)).to eq(
'project-path' => project_path,
'enable-alert-management-path' => setting_path,
@@ -32,4 +32,15 @@ describe Projects::AlertManagementHelper do
end
end
end
+
+ describe '#alert_management_detail_data' do
+ let(:alert_id) { 1 }
+
+ it 'returns detail page configuration' do
+ expect(alert_management_detail_data(project_path, alert_id)).to eq(
+ 'alert-id' => alert_id,
+ 'project-path' => project_path
+ )
+ end
+ end
end
diff --git a/spec/javascripts/issue_show/helpers.js b/spec/javascripts/issue_show/helpers.js
deleted file mode 100644
index 951acfd4e10..00000000000
--- a/spec/javascripts/issue_show/helpers.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from '../../frontend/issue_show/helpers.js';
diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js
deleted file mode 100644
index 1b391bd1588..00000000000
--- a/spec/javascripts/issue_show/mock_data.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from '../../frontend/issue_show/mock_data';
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index d2790a6b858..03930c6c1a7 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -217,7 +217,7 @@ ci_pipelines:
- vulnerability_findings
- pipeline_config
- security_scans
-- daily_report_results
+- daily_build_group_report_results
pipeline_variables:
- pipeline
stages:
@@ -484,7 +484,7 @@ project:
- status_page_setting
- requirements
- export_jobs
-- daily_report_results
+- daily_build_group_report_results
- jira_imports
- compliance_framework_setting
- metrics_users_starred_dashboards
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 266d39a0dfb..3a608391b2b 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3834,6 +3834,61 @@ describe Ci::Build do
end
end
+ describe '#collect_accessibility_reports!' do
+ subject { build.collect_accessibility_reports!(accessibility_report) }
+
+ let(:accessibility_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
+
+ it { expect(accessibility_report.urls).to eq({}) }
+
+ context 'when build has an accessibility report' do
+ context 'when there is an accessibility report with errors' do
+ before do
+ create(:ci_job_artifact, :accessibility, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls.keys).to match_array(['https://about.gitlab.com/'])
+ expect(accessibility_report.errors_count).to eq(10)
+ expect(accessibility_report.scans_count).to eq(1)
+ expect(accessibility_report.passes_count).to eq(0)
+ end
+ end
+
+ context 'when there is an accessibility report without errors' do
+ before do
+ create(:ci_job_artifact, :accessibility_without_errors, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls.keys).to match_array(['https://pa11y.org/'])
+ expect(accessibility_report.errors_count).to eq(0)
+ expect(accessibility_report.scans_count).to eq(1)
+ expect(accessibility_report.passes_count).to eq(1)
+ end
+ end
+
+ context 'when there is an accessibility report with an invalid url' do
+ before do
+ create(:ci_job_artifact, :accessibility_with_invalid_url, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls).to be_empty
+ expect(accessibility_report.errors_count).to eq(0)
+ expect(accessibility_report.scans_count).to eq(0)
+ expect(accessibility_report.passes_count).to eq(0)
+ end
+ end
+ end
+ end
+
describe '#collect_coverage_reports!' do
subject { build.collect_coverage_reports!(coverage_report) }
diff --git a/spec/models/ci/daily_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb
index 61aa58c6692..d4c305c649a 100644
--- a/spec/models/ci/daily_report_result_spec.rb
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -2,14 +2,14 @@
require 'spec_helper'
-describe Ci::DailyReportResult do
+describe Ci::DailyBuildGroupReportResult do
describe '.upsert_reports' do
let!(:rspec_coverage) do
create(
- :ci_daily_report_result,
- title: 'rspec',
+ :ci_daily_build_group_report_result,
+ group_name: 'rspec',
date: '2020-03-09',
- value: 71.2
+ data: { coverage: 71.2 }
)
end
let!(:new_pipeline) { create(:ci_pipeline) }
@@ -19,20 +19,18 @@ describe Ci::DailyReportResult do
{
project_id: rspec_coverage.project_id,
ref_path: rspec_coverage.ref_path,
- param_type: described_class.param_types[rspec_coverage.param_type],
last_pipeline_id: new_pipeline.id,
date: rspec_coverage.date,
- title: 'rspec',
- value: 81.0
+ group_name: 'rspec',
+ data: { 'coverage' => 81.0 }
},
{
project_id: rspec_coverage.project_id,
ref_path: rspec_coverage.ref_path,
- param_type: described_class.param_types[rspec_coverage.param_type],
last_pipeline_id: new_pipeline.id,
date: rspec_coverage.date,
- title: 'karma',
- value: 87.0
+ group_name: 'karma',
+ data: { 'coverage' => 87.0 }
}
])
@@ -40,16 +38,15 @@ describe Ci::DailyReportResult do
expect(rspec_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
- value: 81.0
+ data: { 'coverage' => 81.0 }
)
- expect(described_class.find_by_title('karma')).to have_attributes(
+ expect(described_class.find_by_group_name('karma')).to have_attributes(
project_id: rspec_coverage.project_id,
ref_path: rspec_coverage.ref_path,
- param_type: rspec_coverage.param_type,
last_pipeline_id: new_pipeline.id,
date: rspec_coverage.date,
- value: 87.0
+ data: { 'coverage' => 87.0 }
)
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 3df87e5d2b4..b8e10f43ef4 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1163,8 +1163,8 @@ describe Ci::Pipeline, :mailer do
context "from #{status}" do
let(:from_status) { status }
- it 'schedules pipeline success worker' do
- expect(Ci::DailyReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id)
+ it 'schedules daily build group report results worker' do
+ expect(Ci::DailyBuildGroupReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id)
pipeline.succeed
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 13f1bcb389a..5a9ca9f7b7e 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -655,26 +655,4 @@ describe GroupPolicy do
end
end
end
-
- it_behaves_like 'model with wiki policies' do
- let(:container) { create(:group) }
-
- def set_access_level(access_level)
- allow(container).to receive(:wiki_access_level).and_return(access_level)
- end
-
- before do
- stub_feature_flags(group_wiki: true)
- end
-
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(group_wiki: false)
- end
-
- it 'does not include the wiki permissions' do
- expect_disallowed(*permissions)
- end
- end
- end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index f214b1ccf17..4e15af7e0b5 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -124,6 +124,7 @@ describe ProjectPolicy do
it_behaves_like 'model with wiki policies' do
let(:container) { project }
+ let_it_be(:user) { owner }
def set_access_level(access_level)
project.project_feature.update_attribute(:wiki_access_level, access_level)
diff --git a/spec/services/ci/daily_report_result_service_spec.rb b/spec/services/ci/daily_build_group_report_result_service_spec.rb
index 240709bab0b..f0b72b8fd86 100644
--- a/spec/services/ci/daily_report_result_service_spec.rb
+++ b/spec/services/ci/daily_build_group_report_result_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe Ci::DailyReportResultService, '#execute' do
+describe Ci::DailyBuildGroupReportResultService, '#execute' do
let!(:pipeline) { create(:ci_pipeline, created_at: '2020-02-06 00:01:10') }
let!(:rspec_job) { create(:ci_build, pipeline: pipeline, name: '3/3 rspec', coverage: 80) }
let!(:karma_job) { create(:ci_build, pipeline: pipeline, name: '2/2 karma', coverage: 90) }
@@ -11,31 +11,29 @@ describe Ci::DailyReportResultService, '#execute' do
it 'creates daily code coverage record for each job in the pipeline that has coverage value' do
described_class.new.execute(pipeline)
- Ci::DailyReportResult.find_by(title: 'rspec').tap do |coverage|
+ Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec').tap do |coverage|
expect(coverage).to have_attributes(
project_id: pipeline.project.id,
last_pipeline_id: pipeline.id,
ref_path: pipeline.source_ref_path,
- param_type: 'coverage',
- title: rspec_job.group_name,
- value: rspec_job.coverage,
+ group_name: rspec_job.group_name,
+ data: { 'coverage' => rspec_job.coverage },
date: pipeline.created_at.to_date
)
end
- Ci::DailyReportResult.find_by(title: 'karma').tap do |coverage|
+ Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma').tap do |coverage|
expect(coverage).to have_attributes(
project_id: pipeline.project.id,
last_pipeline_id: pipeline.id,
ref_path: pipeline.source_ref_path,
- param_type: 'coverage',
- title: karma_job.group_name,
- value: karma_job.coverage,
+ group_name: karma_job.group_name,
+ data: { 'coverage' => karma_job.coverage },
date: pipeline.created_at.to_date
)
end
- expect(Ci::DailyReportResult.find_by(title: 'extra')).to be_nil
+ expect(Ci::DailyBuildGroupReportResult.find_by(group_name: 'extra')).to be_nil
end
context 'when there are multiple builds with the same group name that report coverage' do
@@ -45,14 +43,13 @@ describe Ci::DailyReportResultService, '#execute' do
it 'creates daily code coverage record with the average as the value' do
described_class.new.execute(pipeline)
- Ci::DailyReportResult.find_by(title: 'test').tap do |coverage|
+ Ci::DailyBuildGroupReportResult.find_by(group_name: 'test').tap do |coverage|
expect(coverage).to have_attributes(
project_id: pipeline.project.id,
last_pipeline_id: pipeline.id,
ref_path: pipeline.source_ref_path,
- param_type: 'coverage',
- title: test_job_2.group_name,
- value: 75,
+ group_name: test_job_2.group_name,
+ data: { 'coverage' => 75.0 },
date: pipeline.created_at.to_date
)
end
@@ -77,8 +74,8 @@ describe Ci::DailyReportResultService, '#execute' do
end
it "updates the existing record's coverage value and last_pipeline_id" do
- rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec')
- karma_coverage = Ci::DailyReportResult.find_by(title: 'karma')
+ rspec_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec')
+ karma_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma')
# Bump up the coverage values
described_class.new.execute(new_pipeline)
@@ -88,12 +85,12 @@ describe Ci::DailyReportResultService, '#execute' do
expect(rspec_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
- value: new_rspec_job.coverage
+ data: { 'coverage' => new_rspec_job.coverage }
)
expect(karma_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
- value: new_karma_job.coverage
+ data: { 'coverage' => new_karma_job.coverage }
)
end
end
@@ -117,8 +114,8 @@ describe Ci::DailyReportResultService, '#execute' do
end
it 'updates the existing daily code coverage records' do
- rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec')
- karma_coverage = Ci::DailyReportResult.find_by(title: 'karma')
+ rspec_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec')
+ karma_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma')
# Run another one but for the older pipeline.
# This simulates the scenario wherein the success worker
@@ -135,12 +132,12 @@ describe Ci::DailyReportResultService, '#execute' do
expect(rspec_coverage).to have_attributes(
last_pipeline_id: pipeline.id,
- value: rspec_job.coverage
+ data: { 'coverage' => rspec_job.coverage }
)
expect(karma_coverage).to have_attributes(
last_pipeline_id: pipeline.id,
- value: karma_job.coverage
+ data: { 'coverage' => karma_job.coverage }
)
end
end
diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb
index f77b5a2e5b9..e9e356ab4f6 100644
--- a/spec/services/groups/import_export/export_service_spec.rb
+++ b/spec/services/groups/import_export/export_service_spec.rb
@@ -50,7 +50,7 @@ describe Groups::ImportExport::ExportService do
end
it 'saves the models using ndjson tree saver' do
- stub_feature_flags(group_import_export_ndjson: true)
+ stub_feature_flags(group_export_ndjson: true)
expect(Gitlab::ImportExport::Group::TreeSaver).to receive(:new).and_call_original
@@ -58,7 +58,7 @@ describe Groups::ImportExport::ExportService do
end
it 'saves the models using legacy tree saver' do
- stub_feature_flags(group_import_export_ndjson: false)
+ stub_feature_flags(group_export_ndjson: false)
expect(Gitlab::ImportExport::Group::LegacyTreeSaver).to receive(:new).and_call_original
diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb
index cd7ad1a1cfa..256e0a1b3c5 100644
--- a/spec/services/groups/import_export/import_service_spec.rb
+++ b/spec/services/groups/import_export/import_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Groups::ImportExport::ImportService do
- context 'with group_import_export_ndjson feature flag disabled' do
+ context 'with group_import_ndjson feature flag disabled' do
let(:user) { create(:admin) }
let(:group) { create(:group) }
let(:import_logger) { instance_double(Gitlab::Import::Logger) }
@@ -11,7 +11,7 @@ describe Groups::ImportExport::ImportService do
subject(:service) { described_class.new(group: group, user: user) }
before do
- stub_feature_flags(group_import_export_ndjson: false)
+ stub_feature_flags(group_import_ndjson: false)
ImportExportUpload.create(group: group, import_file: import_file)
@@ -39,9 +39,9 @@ describe Groups::ImportExport::ImportService do
end
end
- context 'with group_import_export_ndjson feature flag enabled' do
+ context 'with group_import_ndjson feature flag enabled' do
before do
- stub_feature_flags(group_import_export_ndjson: true)
+ stub_feature_flags(group_import_ndjson: true)
end
context 'when importing a ndjson export' do
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index de64cea6474..a0d54666dff 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -14,17 +14,16 @@ RSpec.shared_context 'GroupPolicy context' do
%i[
read_label read_group upload_file read_namespace read_group_activity
read_group_issues read_group_boards read_group_labels read_group_milestones
- read_group_merge_requests read_wiki
+ read_group_merge_requests
]
end
let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] }
- let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation download_wiki_code] }
- let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation create_wiki] }
+ let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation] }
+ let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] }
let(:maintainer_permissions) do
%i[
create_projects
read_cluster create_cluster update_cluster admin_cluster add_cluster
- admin_wiki
]
end
let(:owner_permissions) do
diff --git a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb
index b91500ffd9c..bd9e3a26f1e 100644
--- a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb
+++ b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb
@@ -1,152 +1,114 @@
# frozen_string_literal: true
RSpec.shared_examples 'model with wiki policies' do
- let(:container) { raise NotImplementedError }
- let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) }
-
- # TODO: Remove this helper once we implement group features
- # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
- def set_access_level(access_level)
- raise NotImplementedError
- end
-
- subject { described_class.new(owner, container) }
-
- context 'when the feature is disabled' do
- before do
- set_access_level(ProjectFeature::DISABLED)
- end
+ include ProjectHelpers
- it 'does not include the wiki permissions' do
- expect_disallowed(*permissions)
- end
+ let(:container) { raise NotImplementedError }
+ let(:user) { raise NotImplementedError }
- context 'when there is an external wiki' do
- it 'does not include the wiki permissions' do
- allow(container).to receive(:has_external_wiki?).and_return(true)
+ subject { described_class.new(user, container) }
- expect_disallowed(*permissions)
- end
+ let_it_be(:wiki_permissions) do
+ {}.tap do |permissions|
+ permissions[:guest] = %i[read_wiki]
+ permissions[:reporter] = permissions[:guest] + %i[download_wiki_code]
+ permissions[:developer] = permissions[:reporter] + %i[create_wiki]
+ permissions[:maintainer] = permissions[:developer] + %i[admin_wiki]
+ permissions[:all] = permissions[:maintainer]
end
end
- describe 'read_wiki' do
- subject { described_class.new(user, container) }
-
- member_roles = %i[guest developer]
- stranger_roles = %i[anonymous non_member]
-
- user_roles = stranger_roles + member_roles
+ using RSpec::Parameterized::TableSyntax
+
+ where(:container_level, :access_level, :membership, :access) do
+ :public | :enabled | :admin | :all
+ :public | :enabled | :maintainer | :maintainer
+ :public | :enabled | :developer | :developer
+ :public | :enabled | :reporter | :reporter
+ :public | :enabled | :guest | :guest
+ :public | :enabled | :non_member | :guest
+ :public | :enabled | :anonymous | :guest
+
+ :public | :private | :admin | :all
+ :public | :private | :maintainer | :maintainer
+ :public | :private | :developer | :developer
+ :public | :private | :reporter | :reporter
+ :public | :private | :guest | :guest
+ :public | :private | :non_member | nil
+ :public | :private | :anonymous | nil
+
+ :public | :disabled | :admin | nil
+ :public | :disabled | :maintainer | nil
+ :public | :disabled | :developer | nil
+ :public | :disabled | :reporter | nil
+ :public | :disabled | :guest | nil
+ :public | :disabled | :non_member | nil
+ :public | :disabled | :anonymous | nil
+
+ :internal | :enabled | :admin | :all
+ :internal | :enabled | :maintainer | :maintainer
+ :internal | :enabled | :developer | :developer
+ :internal | :enabled | :reporter | :reporter
+ :internal | :enabled | :guest | :guest
+ :internal | :enabled | :non_member | :guest
+ :internal | :enabled | :anonymous | nil
+
+ :internal | :private | :admin | :all
+ :internal | :private | :maintainer | :maintainer
+ :internal | :private | :developer | :developer
+ :internal | :private | :reporter | :reporter
+ :internal | :private | :guest | :guest
+ :internal | :private | :non_member | nil
+ :internal | :private | :anonymous | nil
+
+ :internal | :disabled | :admin | nil
+ :internal | :disabled | :maintainer | nil
+ :internal | :disabled | :developer | nil
+ :internal | :disabled | :reporter | nil
+ :internal | :disabled | :guest | nil
+ :internal | :disabled | :non_member | nil
+ :internal | :disabled | :anonymous | nil
+
+ :private | :private | :admin | :all
+ :private | :private | :maintainer | :maintainer
+ :private | :private | :developer | :developer
+ :private | :private | :reporter | :reporter
+ :private | :private | :guest | :guest
+ :private | :private | :non_member | nil
+ :private | :private | :anonymous | nil
+
+ :private | :disabled | :admin | nil
+ :private | :disabled | :maintainer | nil
+ :private | :disabled | :developer | nil
+ :private | :disabled | :reporter | nil
+ :private | :disabled | :guest | nil
+ :private | :disabled | :non_member | nil
+ :private | :disabled | :anonymous | nil
+ end
- # When a user is anonymous, their `current_user == nil`
- let(:user) { create(:user) unless user_role == :anonymous }
+ with_them do
+ let(:user) { create_user_from_membership(container, membership) }
+ let(:allowed_permissions) { wiki_permissions[access].dup || [] }
+ let(:disallowed_permissions) { wiki_permissions[:all] - allowed_permissions }
before do
- container.visibility = container_visibility
- set_access_level(wiki_access_level)
- container.add_user(user, user_role) if member_roles.include?(user_role)
- end
-
- title = ->(container_visibility, wiki_access_level, user_role) do
- [
- "container is #{Gitlab::VisibilityLevel.level_name container_visibility}",
- "wiki is #{ProjectFeature.str_from_access_level wiki_access_level}",
- "user is #{user_role}"
- ].join(', ')
- end
-
- describe 'Situations where :read_wiki is always false' do
- where(case_names: title,
- container_visibility: Gitlab::VisibilityLevel.options.values,
- wiki_access_level: [ProjectFeature::DISABLED],
- user_role: user_roles)
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
-
- describe 'Situations where :read_wiki is always true' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PUBLIC],
- wiki_access_level: [ProjectFeature::ENABLED],
- user_role: user_roles)
+ container.visibility = container_level.to_s
+ set_access_level(ProjectFeature.access_level_from_str(access_level.to_s))
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
+ if allowed_permissions.any? && [container_level, access_level, membership] != [:private, :private, :guest]
+ allowed_permissions << :download_wiki_code
end
end
- describe 'Situations where :read_wiki requires membership' do
- context 'the wiki is private, and the user is a member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PUBLIC,
- Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::PRIVATE],
- user_role: member_roles)
-
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
- end
- end
-
- context 'the wiki is private, and the user is not member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PUBLIC,
- Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::PRIVATE],
- user_role: stranger_roles)
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
-
- context 'the wiki is enabled, and the user is a member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PRIVATE],
- wiki_access_level: [ProjectFeature::ENABLED],
- user_role: member_roles)
-
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
- end
- end
-
- context 'the wiki is enabled, and the user is not a member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PRIVATE],
- wiki_access_level: [ProjectFeature::ENABLED],
- user_role: stranger_roles)
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
+ it 'allows actions based on membership' do
+ expect_allowed(*allowed_permissions)
+ expect_disallowed(*disallowed_permissions)
end
+ end
- describe 'Situations where :read_wiki prohibits anonymous access' do
- context 'the user is not anonymous' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
- user_role: user_roles.reject { |u| u == :anonymous })
-
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
- end
- end
-
- context 'the user is anonymous' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
- user_role: %i[anonymous])
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
- end
+ # TODO: Remove this helper once we implement group features
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
+ def set_access_level(access_level)
+ raise NotImplementedError
end
end
diff --git a/spec/workers/ci/daily_report_results_worker_spec.rb b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb
index b6543b32b09..d9706982a62 100644
--- a/spec/workers/ci/daily_report_results_worker_spec.rb
+++ b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe Ci::DailyReportResultsWorker do
+describe Ci::DailyBuildGroupReportResultsWorker do
describe '#perform' do
let!(:pipeline) { create(:ci_pipeline) }
@@ -12,7 +12,7 @@ describe Ci::DailyReportResultsWorker do
let(:pipeline_id) { pipeline.id }
it 'executes service' do
- expect_any_instance_of(Ci::DailyReportResultService)
+ expect_any_instance_of(Ci::DailyBuildGroupReportResultService)
.to receive(:execute).with(pipeline)
subject
@@ -23,7 +23,7 @@ describe Ci::DailyReportResultsWorker do
let(:pipeline_id) { 123 }
it 'does not execute service' do
- expect_any_instance_of(Ci::DailyReportResultService)
+ expect_any_instance_of(Ci::DailyBuildGroupReportResultService)
.not_to receive(:execute)
expect { subject }
diff --git a/yarn.lock b/yarn.lock
index 1fa766a6274..bbbe8a2eb71 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7969,6 +7969,11 @@ mississippi@^3.0.0:
stream-each "^1.1.0"
through2 "^2.0.0"
+mitt@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d"
+ integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==
+
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"