Commit 74b7763f authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch 'pipeline-editor/refactor-commit-sha' into 'master'

Refactor handling of commit sha in pipeline editor

See merge request gitlab-org/gitlab!67542
parents c410054e 60984ff4
......@@ -10,7 +10,6 @@ import {
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
import updateCurrentBranchMutation from '../../graphql/mutations/update_current_branch.mutation.graphql';
import updateLastCommitBranchMutation from '../../graphql/mutations/update_last_commit_branch.mutation.graphql';
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from '../../graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from '../../graphql/queries/client/is_new_ci_config_file.graphql';
import getPipelineEtag from '../../graphql/queries/client/pipeline_etag.graphql';
......@@ -37,6 +36,11 @@ export default {
type: String,
required: true,
},
commitSha: {
type: String,
required: false,
default: '',
},
},
data() {
return {
......@@ -49,9 +53,6 @@ export default {
isNewCiConfigFile: {
query: getIsNewCiConfigFile,
},
commitSha: {
query: getCommitSha,
},
currentBranch: {
query: getCurrentBranch,
},
......@@ -96,13 +97,7 @@ export default {
lastCommitId: this.commitSha,
},
update(store, { data }) {
const commitSha = data?.commitCreate?.commit?.sha;
const pipelineEtag = data?.commitCreate?.commit?.commitPipelinePath;
if (commitSha) {
store.writeQuery({ query: getCommitSha, data: { commitSha } });
}
if (pipelineEtag) {
store.writeQuery({ query: getPipelineEtag, data: { pipelineEtag } });
}
......
......@@ -3,7 +3,6 @@ import { EDITOR_READY_EVENT } from '~/editor/constants';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
export default {
components: {
......@@ -12,14 +11,11 @@ export default {
mixins: [glFeatureFlagMixin()],
inject: ['ciConfigPath', 'projectPath', 'projectNamespace', 'defaultBranch'],
inheritAttrs: false,
data() {
return {
commitSha: '',
};
},
apollo: {
props: {
commitSha: {
query: getCommitSha,
type: String,
required: false,
default: '',
},
},
methods: {
......
......@@ -158,11 +158,9 @@ export default {
const updatedPath = setUrlParams({ branch_name: newBranch });
historyPushState(updatedPath);
this.$emit('updateCommitSha', { newBranch });
// refetching the content will cause a lot of components to re-render,
// including the text editor which uses the commit sha to register the CI schema
// so we need to make sure the commit sha is updated first
// so we need to make sure the currentBranch (and consequently, the commitSha) are updated first
await this.$nextTick();
this.$emit('refetchContent');
},
......
......@@ -33,6 +33,11 @@ export default {
type: Object,
required: true,
},
commitSha: {
type: String,
required: false,
default: '',
},
isNewCiConfigFile: {
type: Boolean,
required: true,
......@@ -54,7 +59,11 @@ export default {
</script>
<template>
<div class="gl-mb-5">
<pipeline-status v-if="showPipelineStatus" :class="$options.pipelineStatusClasses" />
<pipeline-status
v-if="showPipelineStatus"
:commit-sha="commitSha"
:class="$options.pipelineStatusClasses"
/>
<validation-segment :class="validationStyling" :ci-config="ciConfigData" />
</div>
</template>
......@@ -3,7 +3,6 @@ import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import getCommitSha from '~/pipeline_editor/graphql/queries/client/commit_sha.graphql';
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
import getPipelineEtag from '~/pipeline_editor/graphql/queries/client/pipeline_etag.graphql';
import {
......@@ -33,10 +32,14 @@ export default {
GlSprintf,
},
inject: ['projectFullPath'],
apollo: {
props: {
commitSha: {
query: getCommitSha,
type: String,
required: false,
default: '',
},
},
apollo: {
pipelineEtag: {
query: getPipelineEtag,
},
......@@ -51,7 +54,7 @@ export default {
sha: this.commitSha,
};
},
update: (data) => {
update(data) {
const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {};
return {
......@@ -60,6 +63,11 @@ export default {
detailedStatus,
};
},
result(res) {
if (res.data?.project?.pipeline) {
this.hasError = false;
}
},
error() {
this.hasError = true;
},
......@@ -68,7 +76,6 @@ export default {
},
data() {
return {
commitSha: '',
hasError: false,
};
},
......@@ -84,7 +91,11 @@ export default {
// (e.g. pipeline is null during fetch when the pipeline hasn't been
// triggered yet), we can just show the loading state until the pipeline
// details are ready to be fetched
return this.$apollo.queries.pipeline.loading || (!this.hasPipelineData && !this.hasError);
return (
this.$apollo.queries.pipeline.loading ||
this.commitSha.length === 0 ||
(!this.hasPipelineData && !this.hasError)
);
},
shortSha() {
return truncateSha(this.commitSha);
......
......@@ -69,6 +69,11 @@ export default {
type: String,
required: true,
},
commitSha: {
type: String,
required: false,
default: '',
},
},
apollo: {
appStatus: {
......@@ -110,7 +115,7 @@ export default {
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
>
<ci-editor-header />
<text-editor :value="ciFileContent" v-on="$listeners" />
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
</editor-tab>
<editor-tab
class="gl-mb-3"
......
mutation updateCommitSha($commitSha: String) {
updateCommitSha(commitSha: $commitSha) @client
}
import produce from 'immer';
import axios from '~/lib/utils/axios_utils';
import getCommitShaQuery from './queries/client/commit_sha.graphql';
import getCurrentBranchQuery from './queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
......@@ -32,14 +31,6 @@ export const resolvers = {
__typename: 'CiLintContent',
}));
},
updateCommitSha: (_, { commitSha }, { cache }) => {
cache.writeQuery({
query: getCommitShaQuery,
data: produce(cache.readQuery({ query: getCommitShaQuery }), (draftData) => {
draftData.commitSha = commitSha;
}),
});
},
updateCurrentBranch: (_, { currentBranch }, { cache }) => {
cache.writeQuery({
query: getCurrentBranchQuery,
......
......@@ -4,7 +4,6 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
import getCommitSha from './graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './graphql/queries/client/last_commit_branch.query.graphql';
import getPipelineEtag from './graphql/queries/client/pipeline_etag.graphql';
......@@ -26,7 +25,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const {
// Add to apollo cache as it can be updated by future queries
commitSha,
initialBranchName,
pipelineEtag,
// Add to provide/inject API for static values
......@@ -69,13 +67,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
},
});
cache.writeQuery({
query: getCommitSha,
data: {
commitSha,
},
});
cache.writeQuery({
query: getPipelineEtag,
data: {
......
......@@ -16,11 +16,9 @@ import {
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
import updateCommitShaMutation from './graphql/mutations/update_commit_sha.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
import getCommitSha from './graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
......@@ -156,7 +154,32 @@ export default {
query: getAppStatus,
},
commitSha: {
query: getCommitSha,
query: getLatestCommitShaQuery,
variables() {
return {
projectPath: this.projectFullPath,
ref: this.currentBranch,
};
},
update(data) {
const pipelineNodes = data.project?.pipelines?.nodes ?? [];
// it's possible to query for the commit sha too early after an update
// (e.g. after committing a new branch, we might query for the commit sha
// but the pipeline nodes are still empty).
// in this case, we start polling until we get a commit sha.
if (pipelineNodes.length === 0) {
if (![EDITOR_APP_STATUS_LOADING, EDITOR_APP_STATUS_EMPTY].includes(this.appStatus)) {
this.$apollo.queries.commitSha.startPolling(1000);
return this.commitSha;
}
return '';
}
this.$apollo.queries.commitSha.stopPolling();
return pipelineNodes[0].sha;
},
},
currentBranch: {
query: getCurrentBranch,
......@@ -257,38 +280,6 @@ export default {
updateCiConfig(ciFileContent) {
this.currentCiFileContent = ciFileContent;
},
async updateCommitSha({ newBranch }) {
let fetchResults;
try {
fetchResults = await this.$apollo.query({
query: getLatestCommitShaQuery,
variables: {
projectPath: this.projectFullPath,
ref: newBranch,
},
});
} catch {
this.showFetchError();
return;
}
if (fetchResults.errors?.length > 0) {
this.showFetchError();
return;
}
const pipelineNodes = fetchResults?.data?.project?.pipelines?.nodes ?? [];
if (pipelineNodes.length === 0) {
return;
}
const commitSha = pipelineNodes[0].sha;
this.$apollo.mutate({
mutation: updateCommitShaMutation,
variables: { commitSha },
});
},
updateOnCommit({ type }) {
this.reportSuccess(type);
......@@ -336,12 +327,12 @@ export default {
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
:is-new-ci-config-file="isNewCiConfigFile"
:commit-sha="commitSha"
@commit="updateOnCommit"
@resetContent="resetContent"
@showError="showErrorAlert"
@refetchContent="refetchContent"
@updateCiConfig="updateCiConfig"
@updateCommitSha="updateCommitSha"
/>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
......
......@@ -25,6 +25,11 @@ export default {
type: String,
required: true,
},
commitSha: {
type: String,
required: false,
default: '',
},
isNewCiConfigFile: {
type: Boolean,
required: true,
......@@ -56,15 +61,22 @@ export default {
<pipeline-editor-file-nav v-on="$listeners" />
<pipeline-editor-header
:ci-config-data="ciConfigData"
:commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
/>
<pipeline-editor-tabs
:ci-config-data="ciConfigData"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
v-on="$listeners"
@set-current-tab="setCurrentTab"
/>
<commit-section v-if="showCommitForm" :ci-file-content="ciFileContent" v-on="$listeners" />
<commit-section
v-if="showCommitForm"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
v-on="$listeners"
/>
<pipeline-editor-drawer v-if="showPipelineDrawer" />
</div>
</template>
import { GlFormTextarea, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
......@@ -48,7 +49,10 @@ describe('Pipeline Editor | Commit section', () => {
let wrapper;
let mockMutate;
const defaultProps = { ciFileContent: mockCiYml };
const defaultProps = {
ciFileContent: mockCiYml,
commitSha: mockCommitSha,
};
const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => {
mockMutate = jest.fn().mockResolvedValue({
......@@ -67,7 +71,6 @@ describe('Pipeline Editor | Commit section', () => {
provide: { ...mockProvide, ...provide },
data() {
return {
commitSha: mockCommitSha,
currentBranch: mockDefaultBranch,
isNewCiConfigFile: Boolean(options?.isNewCiConfigfile),
};
......@@ -97,8 +100,7 @@ describe('Pipeline Editor | Commit section', () => {
await findCommitForm().find('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest);
}
await findCommitForm().find('[type="submit"]').trigger('click');
// Simulate the write to local cache that occurs after a commit
await wrapper.setData({ commitSha: mockCommitNextSha });
await waitForPromises();
};
const cancelCommitForm = async () => {
......@@ -188,7 +190,6 @@ describe('Pipeline Editor | Commit section', () => {
update: expect.any(Function),
variables: {
...mockVariables,
lastCommitId: mockCommitNextSha,
branch: mockDefaultBranch,
},
});
......
......@@ -42,15 +42,12 @@ describe('Pipeline Editor | Text editor component', () => {
defaultBranch: mockDefaultBranch,
glFeatures,
},
propsData: {
commitSha: mockCommitSha,
},
attrs: {
value: mockCiYml,
},
// Simulate graphQL client query result
data() {
return {
commitSha: mockCommitSha,
};
},
listeners: {
[EDITOR_READY_EVENT]: editorReadyListener,
},
......
......@@ -247,15 +247,6 @@ describe('Pipeline editor branch switcher', () => {
expect(wrapper.emitted('refetchContent')).toBeUndefined();
});
it('emits the updateCommitSha event when selecting a different branch', async () => {
expect(wrapper.emitted('updateCommitSha')).toBeUndefined();
const branch = findDropdownItems().at(1);
branch.vm.$emit('click');
expect(wrapper.emitted('updateCommitSha')).toHaveLength(1);
});
});
describe('when searching', () => {
......
......@@ -27,13 +27,11 @@ describe('Pipeline Status', () => {
wrapper = shallowMount(PipelineStatus, {
localVue,
apolloProvider: mockApollo,
propsData: {
commitSha: mockCommitSha,
},
provide: mockProvide,
stubs: { GlLink, GlSprintf },
data() {
return {
commitSha: mockCommitSha,
};
},
});
};
......
......@@ -156,35 +156,33 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
};
};
export const mockNewCommitShaResults = {
export const mockCommitShaResults = {
data: {
project: {
pipelines: {
nodes: [
{
id: 'gid://gitlab/Ci::Pipeline/1',
sha: 'd0d56d363d8a3f67a8ab9fc00207d468f30032ca',
sha: mockCommitSha,
path: `/${mockProjectFullPath}/-/pipelines/488`,
commitPath: `/${mockProjectFullPath}/-/commit/d0d56d363d8a3f67a8ab9fc00207d468f30032ca`,
},
{
id: 'gid://gitlab/Ci::Pipeline/2',
sha: 'fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa',
path: `/${mockProjectFullPath}/-/pipelines/487`,
commitPath: `/${mockProjectFullPath}/-/commit/fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa`,
},
{
id: 'gid://gitlab/Ci::Pipeline/3',
sha: '6c16b17c7f94a438ae19a96c285bb49e3c632cf4',
path: `/${mockProjectFullPath}/-/pipelines/433`,
commitPath: `/${mockProjectFullPath}/-/commit/6c16b17c7f94a438ae19a96c285bb49e3c632cf4`,
},
],
},
},
},
};
export const mockEmptyCommitShaResults = {
data: {
project: {
pipelines: {
nodes: [],
},
},
},
};
export const mockProjectBranches = {
data: {
project: {
......
......@@ -26,9 +26,10 @@ import {
mockBlobContentQueryResponseNoCiFile,
mockCiYml,
mockCommitSha,
mockCommitShaResults,
mockDefaultBranch,
mockEmptyCommitShaResults,
mockProjectFullPath,
mockNewCommitShaResults,
} from './mock_data';
const localVue = createLocalVue();
......@@ -54,7 +55,6 @@ describe('Pipeline editor app component', () => {
let mockBlobContentData;
let mockCiConfigData;
let mockGetTemplate;
let mockUpdateCommitSha;
let mockLatestCommitShaQuery;
let mockPipelineQuery;
......@@ -71,6 +71,11 @@ describe('Pipeline editor app component', () => {
SourceEditor: MockSourceEditor,
PipelineEditorEmptyState,
},
data() {
return {
commitSha: '',
};
},
mocks: {
$apollo: {
queries: {
......@@ -96,18 +101,7 @@ describe('Pipeline editor app component', () => {
[getPipelineQuery, mockPipelineQuery],
];
const resolvers = {
Query: {
commitSha() {
return mockCommitSha;
},
},
Mutation: {
updateCommitSha: mockUpdateCommitSha,
},
};
mockApollo = createMockApollo(handlers, resolvers);
mockApollo = createMockApollo(handlers);
const options = {
localVue,
......@@ -137,7 +131,6 @@ describe('Pipeline editor app component', () => {
mockBlobContentData = jest.fn();
mockCiConfigData = jest.fn();
mockGetTemplate = jest.fn();
mockUpdateCommitSha = jest.fn();
mockLatestCommitShaQuery = jest.fn();
mockPipelineQuery = jest.fn();
});
......@@ -159,11 +152,16 @@ describe('Pipeline editor app component', () => {
beforeEach(() => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
});
describe('when file exists', () => {
beforeEach(async () => {
await createComponentWithApollo();
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
.mockImplementation(jest.fn());
});
it('shows pipeline editor home component', () => {
......@@ -181,18 +179,32 @@ describe('Pipeline editor app component', () => {
sha: mockCommitSha,
});
});
it('does not poll for the commit sha', () => {
expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
});
});
describe('when no CI config file exists', () => {
it('shows an empty state and does not show editor home component', async () => {
beforeEach(async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
await createComponentWithApollo();
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
.mockImplementation(jest.fn());
});
it('shows an empty state and does not show editor home component', async () => {
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
});
it('does not poll for the commit sha', () => {
expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
});
describe('because of a fetching error', () => {
it('shows a unkown error message', async () => {
const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
......@@ -230,6 +242,7 @@ describe('Pipeline editor app component', () => {
describe('when landing on the empty state with feature flag on', () => {
it('user can click on CTA button and see an empty editor', async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
await createComponentWithApollo({
provide: {
......@@ -254,9 +267,9 @@ describe('Pipeline editor app component', () => {
const updateSuccessMessage = 'Your changes have been successfully committed.';
describe('and the commit mutation succeeds', () => {
beforeEach(() => {
beforeEach(async () => {
window.scrollTo = jest.fn();
createComponent();
await createComponentWithApollo();
findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
});
......@@ -268,7 +281,32 @@ describe('Pipeline editor app component', () => {
it('scrolls to the top of the page to bring attention to the confirmation message', () => {
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
});
it('polls for commit sha while pipeline data is not yet available', async () => {
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
.mockImplementation(jest.fn());
// simulate updating current branch (which triggers commitSha refetch)
// while pipeline data is not yet available
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
await wrapper.vm.$apollo.queries.commitSha.refetch();
expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(1);
});
it('stops polling for commit sha when pipeline data is available', async () => {
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
.mockImplementation(jest.fn());
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
await wrapper.vm.$apollo.queries.commitSha.refetch();
expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
});
});
describe('and the commit mutation fails', () => {
const commitFailedReasons = ['Commit failed'];
......@@ -320,6 +358,10 @@ describe('Pipeline editor app component', () => {
});
describe('when refetching content', () => {
beforeEach(() => {
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
});
it('refetches blob content', async () => {
await createComponentWithApollo();
jest
......@@ -352,6 +394,7 @@ describe('Pipeline editor app component', () => {
const originalLocation = window.location.href;
beforeEach(() => {
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
setWindowLocation('?template=Android');
});
......@@ -371,45 +414,4 @@ describe('Pipeline editor app component', () => {
expect(findTextEditor().exists()).toBe(true);
});
});
describe('when updating commit sha', () => {
const newCommitSha = mockNewCommitShaResults.data.project.pipelines.nodes[0].sha;
beforeEach(async () => {
mockUpdateCommitSha.mockResolvedValue(newCommitSha);
mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
await createComponentWithApollo();
});
it('fetches updated commit sha for the new branch', async () => {
expect(mockLatestCommitShaQuery).not.toHaveBeenCalled();
wrapper
.findComponent(PipelineEditorHome)
.vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
await waitForPromises();
expect(mockLatestCommitShaQuery).toHaveBeenCalledWith({
projectPath: mockProjectFullPath,
ref: 'new-branch',
});
});
it('updates commit sha with the newly fetched commit sha', async () => {
expect(mockUpdateCommitSha).not.toHaveBeenCalled();
wrapper
.findComponent(PipelineEditorHome)
.vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
await waitForPromises();
expect(mockUpdateCommitSha).toHaveBeenCalled();
expect(mockUpdateCommitSha).toHaveBeenCalledWith(
expect.any(Object),
{ commitSha: mockNewCommitShaResults.data.project.pipelines.nodes[0].sha },
expect.any(Object),
expect.any(Object),
);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment