Skip to content
Snippets Groups Projects
Commit 97aece04 authored by Thomas Randolph's avatar Thomas Randolph Committed by Phil Hughes
Browse files

Add a preparing state before any other MR states

Changelog: changed
parent 18456a89
No related branches found
No related tags found
2 merge requests!120770Update the Overview tab when the MR has finished async preparation,!119439Draft: Prevent file variable content expansion in downstream pipeline
Showing
with 171 additions and 7 deletions
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { MR_WIDGET_PREPARING_ASYNCHRONOUSLY } from '../../i18n';
export default {
name: 'MRWidgetPreparing',
i18n: {
preparing: MR_WIDGET_PREPARING_ASYNCHRONOUSLY,
},
components: {
GlLoadingIcon,
},
};
</script>
<template>
<div class="gl-w-full gl-display-flex gl-p-4">
<gl-loading-icon size="md" class="gl-pr-4" inline />
<div class="gl-display-flex gl-align-items-center">
{{ $options.i18n.preparing }}
</div>
</div>
</template>
......@@ -182,6 +182,7 @@ export const INVALID_RULES_DOCS_PATH = helpPagePath(
);
export const DETAILED_MERGE_STATUS = {
PREPARING: 'PREPARING',
MERGEABLE: 'MERGEABLE',
CHECKING: 'CHECKING',
NOT_OPEN: 'NOT_OPEN',
......
import { __, s__ } from '~/locale';
export const MR_WIDGET_PREPARING_ASYNCHRONOUSLY = s__(
'mrWidget|Your merge request is almost ready!',
);
export const MR_WIDGET_MISSING_BRANCH_WHICH = s__(
'mrWidget|The %{type} branch %{codeStart}%{name}%{codeEnd} does not exist.',
);
......
......@@ -26,6 +26,7 @@ import ArchivedState from './components/states/mr_widget_archived.vue';
import MrWidgetAutoMergeEnabled from './components/states/mr_widget_auto_merge_enabled.vue';
import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
import CheckingState from './components/states/mr_widget_checking.vue';
import PreparingState from './components/states/mr_widget_preparing.vue';
import ClosedState from './components/states/mr_widget_closed.vue';
import ConflictsState from './components/states/mr_widget_conflicts.vue';
import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
......@@ -88,6 +89,7 @@ export default {
MrWidgetReadyToMerge,
ShaMismatch,
MrWidgetChecking: CheckingState,
MrWidgetPreparing: PreparingState,
MrWidgetUnresolvedDiscussions: UnresolvedDiscussionsState,
MrWidgetPipelineBlocked: PipelineBlockedState,
MrWidgetPipelineFailed: PipelineFailedState,
......@@ -199,7 +201,7 @@ export default {
);
},
shouldRenderApprovals() {
return this.mr.state !== 'nothingToMerge';
return !['preparing', 'nothingToMerge'].includes(this.mr.state);
},
componentName() {
return stateToComponentMap[this.machineState] || classState[this.mr.state];
......
......@@ -3,6 +3,7 @@ subscription getStateSubscription($issuableId: IssuableID!) {
... on MergeRequest {
id
detailedMergeStatus
commitCount
}
}
}
......@@ -2,7 +2,9 @@ import { DETAILED_MERGE_STATUS } from '../constants';
import { stateKey } from './state_maps';
export default function deviseState() {
if (!this.commitsCount) {
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.PREPARING) {
return stateKey.preparing;
} else if (!this.commitsCount) {
return stateKey.nothingToMerge;
} else if (this.projectArchived) {
return stateKey.archived;
......
......@@ -212,6 +212,7 @@ export default class MergeRequestStore {
setGraphqlSubscriptionData(data) {
this.detailedMergeStatus = data.detailedMergeStatus;
this.commitsCount = data.commitCount;
this.setState();
}
......
......@@ -10,6 +10,7 @@ export const stateToComponentMap = {
notAllowedToMerge: 'mr-widget-not-allowed',
archived: 'mr-widget-archived',
checking: 'mr-widget-checking',
preparing: 'mr-widget-preparing',
unresolvedDiscussions: 'mr-widget-unresolved-discussions',
pipelineBlocked: 'mr-widget-pipeline-blocked',
pipelineFailed: 'mr-widget-pipeline-failed',
......@@ -38,6 +39,7 @@ export const stateKey = {
archived: 'archived',
missingBranch: 'missingBranch',
nothingToMerge: 'nothingToMerge',
preparing: 'preparing',
checking: 'checking',
conflicts: 'conflicts',
draft: 'draft',
......
......@@ -54737,6 +54737,9 @@ msgstr ""
msgid "mrWidget|What is a merge train?"
msgstr ""
 
msgid "mrWidget|Your merge request is almost ready!"
msgstr ""
msgid "mrWidget|Your password"
msgstr ""
 
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import Preparing from '~/vue_merge_request_widget/components/states/mr_widget_preparing.vue';
import { MR_WIDGET_PREPARING_ASYNCHRONOUSLY } from '~/vue_merge_request_widget/i18n';
function createComponent() {
return shallowMount(Preparing);
}
function findSpinnerIcon(wrapper) {
return wrapper.findComponent(GlLoadingIcon);
}
describe('Preparing', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
it('should render a spinner', () => {
expect(findSpinnerIcon(wrapper).exists()).toBe(true);
});
it('should render the correct text', () => {
expect(wrapper.text()).toBe(MR_WIDGET_PREPARING_ASYNCHRONOUSLY);
});
});
......@@ -3,8 +3,8 @@ import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import * as Sentry from '@sentry/browser';
import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
import * as Sentry from '@sentry/browser';
import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
......@@ -26,10 +26,13 @@ import { STATE_QUERY_POLLING_INTERVAL_BACKOFF } from '~/vue_merge_request_widget
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import Preparing from '~/vue_merge_request_widget/components/states/mr_widget_preparing.vue';
import WidgetContainer from '~/vue_merge_request_widget/components/widget/app.vue';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
import getStateSubscription from '~/vue_merge_request_widget/queries/get_state.subscription.graphql';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import approvalsQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql';
import approvedBySubscription from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql';
......@@ -65,13 +68,14 @@ jest.mock('@sentry/browser', () => ({
Vue.use(VueApollo);
describe('MrWidgetOptions', () => {
let mockedApprovalsSubscription;
let stateQueryHandler;
let queryResponse;
let wrapper;
let mock;
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
const findApprovalsWidget = () => wrapper.findComponent(Approvals);
const findPreparingWidget = () => wrapper.findComponent(Preparing);
const findWidgetContainer = () => wrapper.findComponent(WidgetContainer);
const findExtensionToggleButton = () =>
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]');
......@@ -97,7 +101,7 @@ describe('MrWidgetOptions', () => {
});
const createComponent = (mrData = mockData, options = {}, data = {}, fullMount = true) => {
mockedApprovalsSubscription = createMockApolloSubscription();
const mockedApprovalsSubscription = createMockApolloSubscription();
queryResponse = {
data: {
project: {
......@@ -105,6 +109,9 @@ describe('MrWidgetOptions', () => {
mergeRequest: {
...getStateQueryResponse.data.project.mergeRequest,
mergeError: mrData.mergeError || null,
detailedMergeStatus:
mrData.detailedMergeStatus ||
getStateQueryResponse.data.project.mergeRequest.detailedMergeStatus,
},
},
},
......@@ -128,7 +135,10 @@ describe('MrWidgetOptions', () => {
],
...(options.apolloMock || []),
];
const subscriptionHandlers = [[approvedBySubscription, () => mockedApprovalsSubscription]];
const subscriptionHandlers = [
[approvedBySubscription, () => mockedApprovalsSubscription],
...(options.apolloSubscriptions || []),
];
const apolloProvider = createMockApollo(queryHandlers);
subscriptionHandlers.forEach(([query, stream]) => {
......@@ -1275,4 +1285,86 @@ describe('MrWidgetOptions', () => {
});
});
});
describe('async preparation for a newly opened MR', () => {
beforeEach(() => {
mock
.onGet(mockData.merge_request_widget_path)
.reply(() => [HTTP_STATUS_OK, { ...mockData, state: 'opened' }]);
});
it('does not render the Preparing state component by default', async () => {
await createComponent();
expect(findApprovalsWidget().exists()).toBe(true);
expect(findPreparingWidget().exists()).toBe(false);
});
it('renders the Preparing state component when the MR state is initially "preparing"', async () => {
await createComponent({
...mockData,
state: 'opened',
detailedMergeStatus: 'PREPARING',
});
expect(findApprovalsWidget().exists()).toBe(false);
expect(findPreparingWidget().exists()).toBe(true);
});
describe('when the MR is updated by observing its status', () => {
let stateSubscription;
beforeEach(() => {
window.gon.features.realtimeMrStatusChange = true;
stateSubscription = createMockApolloSubscription();
});
it("shows the Preparing widget when the MR reports it's not ready yet", async () => {
await createComponent(
{
...mockData,
state: 'opened',
detailedMergeStatus: 'PREPARING',
},
{
apolloSubscriptions: [[getStateSubscription, () => stateSubscription]],
},
{},
false,
);
expect(wrapper.html()).toContain('mr-widget-preparing-stub');
});
it('removes the Preparing widget when the MR indicates it has been prepared', async () => {
await createComponent(
{
...mockData,
state: 'opened',
detailedMergeStatus: 'PREPARING',
},
{
apolloSubscriptions: [[getStateSubscription, () => stateSubscription]],
},
{},
false,
);
expect(wrapper.html()).toContain('mr-widget-preparing-stub');
stateSubscription.next({
data: {
mergeRequestMergeStatusUpdated: {
preparedAt: 'non-null value',
},
},
});
// Wait for batched DOM updates
await nextTick();
expect(wrapper.html()).not.toContain('mr-widget-preparing-stub');
});
});
});
});
......@@ -16,10 +16,14 @@ describe('getStateKey', () => {
commitsCount: 2,
hasConflicts: false,
draft: false,
detailedMergeStatus: null,
detailedMergeStatus: 'PREPARING',
};
const bound = getStateKey.bind(context);
expect(bound()).toEqual('preparing');
context.detailedMergeStatus = null;
expect(bound()).toEqual('checking');
context.detailedMergeStatus = 'MERGEABLE';
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment