diff --git a/app/assets/javascripts/repository/components/header_area.vue b/app/assets/javascripts/repository/components/header_area.vue index 64428119b44419ff39ade2b64774686c83b75170..93c8481a3b75489458aad31731cd7358af13533e 100644 --- a/app/assets/javascripts/repository/components/header_area.vue +++ b/app/assets/javascripts/repository/components/header_area.vue @@ -1,6 +1,5 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; -import { computed } from 'vue'; import { __ } from '~/locale'; import Shortcuts from '~/behaviors/shortcuts/shortcuts'; import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle'; @@ -91,7 +90,7 @@ export default { ], provide() { return { - currentRef: computed(() => this.currentRef ?? this.blobInfo.ref), + currentRef: this.currentRef, }; }, props: { diff --git a/app/assets/javascripts/repository/components/header_area/blob_button_group.vue b/app/assets/javascripts/repository/components/header_area/blob_button_group.vue index 7e793209decfb7c9b1fe9ff7dfb3c855a034166e..acbfd61b6a50daeaa2d2d3343656fa54b3aea149 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_button_group.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_button_group.vue @@ -1,10 +1,11 @@ <script> import { GlDisclosureDropdownItem, GlDisclosureDropdownGroup } from '@gitlab/ui'; import { sprintf, __ } from '~/locale'; -import { isLoggedIn } from '~/lib/utils/common_utils'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { showForkSuggestion } from '~/repository/utils/fork_suggestion_utils'; import { DEFAULT_BLOB_INFO } from '~/repository/constants'; import getRefMixin from '~/repository/mixins/get_ref'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; const REPLACE_BLOB_MODAL_ID = 'modal-replace-blob'; @@ -17,6 +18,7 @@ export default { components: { GlDisclosureDropdownItem, GlDisclosureDropdownGroup, + ForkSuggestionModal, UploadBlobModal, LockFileDropdownItem: () => import('ee_component/repository/components/header_area/lock_file_dropdown_item.vue'), @@ -64,7 +66,7 @@ export default { }, data() { return { - isLoggedIn: isLoggedIn(), + isForkSuggestionModalVisible: false, }; }, computed: { @@ -73,33 +75,20 @@ export default { text: this.$options.i18n.replace, extraAttrs: { 'data-testid': 'replace', - // a temporary solution before resolving https://gitlab.com/gitlab-org/gitlab/-/issues/450774#note_2319974833 - disabled: this.showForkSuggestion, }, }; }, replaceCommitMessage() { return sprintf(__('Replace %{name}'), { name: this.blobInfo.name }); }, - canFork() { - const { createMergeRequestIn, forkProject } = this.userPermissions; - - return this.isLoggedIn && !this.isUsingLfs && createMergeRequestIn && forkProject; - }, - showSingleFileEditorForkSuggestion() { - return this.canFork && !this.blobInfo.canModifyBlob; - }, - showWebIdeForkSuggestion() { - return this.canFork && !this.blobInfo.canModifyBlobWithWebIde; - }, - showForkSuggestion() { - return this.showSingleFileEditorForkSuggestion || this.showWebIdeForkSuggestion; + shouldShowForkSuggestion() { + return showForkSuggestion(this.userPermissions, this.isUsingLfs, this.blobInfo); }, }, methods: { showModal() { - if (this.showForkSuggestion) { - this.$emit('fork', 'view'); + if (this.shouldShowForkSuggestion) { + this.isForkSuggestionModalVisible = true; return; } @@ -121,6 +110,11 @@ export default { :is-loading="isLoading" /> <gl-disclosure-dropdown-item :item="replaceFileItem" @action="showModal" /> + <fork-suggestion-modal + :visible="isForkSuggestionModalVisible" + :fork-path="blobInfo.forkAndViewPath" + @hide="isForkSuggestionModalVisible = false" + /> <upload-blob-modal :ref="$options.replaceBlobModalId" :modal-id="$options.replaceBlobModalId" diff --git a/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue b/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue index 3489b1d0882b8d2d21492a3b429a94b1a5ec06a7..f1f2b024771ac0cf7136e2e8c3aaeddfcdaceb3b 100644 --- a/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue +++ b/app/assets/javascripts/repository/components/header_area/blob_delete_file_group.vue @@ -2,7 +2,8 @@ import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui'; import { uniqueId } from 'lodash'; import { sprintf, __ } from '~/locale'; -import { isLoggedIn } from '~/lib/utils/common_utils'; +import { showForkSuggestion } from '~/repository/utils/fork_suggestion_utils'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue'; import { DEFAULT_BLOB_INFO } from '~/repository/constants'; @@ -10,6 +11,7 @@ export default { components: { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, + ForkSuggestionModal, DeleteBlobModal, }, inject: { @@ -45,7 +47,7 @@ export default { }, data() { return { - isLoggedIn: isLoggedIn(), + isModalVisible: false, }; }, computed: { @@ -54,8 +56,6 @@ export default { text: __('Delete'), extraAttrs: { 'data-testid': 'delete', - // a temporary solution before resolving https://gitlab.com/gitlab-org/gitlab/-/issues/450774#note_2319974833 - disabled: this.showForkSuggestion, }, }; }, @@ -65,25 +65,14 @@ export default { deleteModalCommitMessage() { return sprintf(__('Delete %{name}'), { name: this.blobInfo.name }); }, - canFork() { - const { createMergeRequestIn, forkProject } = this.userPermissions; - - return this.isLoggedIn && !this.isUsingLfs && createMergeRequestIn && forkProject; - }, - showSingleFileEditorForkSuggestion() { - return this.canFork && !this.blobInfo.canModifyBlob; - }, - showWebIdeForkSuggestion() { - return this.canFork && !this.blobInfo.canModifyBlobWithWebIde; - }, - showForkSuggestion() { - return this.showSingleFileEditorForkSuggestion || this.showWebIdeForkSuggestion; + shouldShowForkSuggestion() { + return showForkSuggestion(this.userPermissions, this.isUsingLfs, this.blobInfo); }, }, methods: { showModal() { - if (this.showForkSuggestion) { - this.$emit('fork', 'view'); + if (this.shouldShowForkSuggestion) { + this.isModalVisible = true; return; } @@ -96,6 +85,11 @@ export default { <template> <gl-disclosure-dropdown-group bordered> <gl-disclosure-dropdown-item :item="deleteFileItem" variant="danger" @action="showModal" /> + <fork-suggestion-modal + :visible="isModalVisible" + :fork-path="blobInfo.forkAndViewPath" + @hide="isModalVisible = false" + /> <delete-blob-modal :ref="deleteModalId" :delete-path="blobInfo.webPath" diff --git a/app/assets/javascripts/repository/components/header_area/fork_suggestion_modal.vue b/app/assets/javascripts/repository/components/header_area/fork_suggestion_modal.vue new file mode 100644 index 0000000000000000000000000000000000000000..0ebae6cef7c2c3d2eaf0d64e76475aa2ad935ee0 --- /dev/null +++ b/app/assets/javascripts/repository/components/header_area/fork_suggestion_modal.vue @@ -0,0 +1,64 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlModal, + }, + props: { + visible: { + type: Boolean, + required: true, + }, + forkPath: { + type: String, + required: true, + }, + }, + computed: { + primaryAction() { + return { + text: __('Fork'), + attributes: { + variant: 'confirm', + href: this.forkPath, + class: 'gl-w-full sm:gl-w-auto', + 'data-method': 'post', + 'data-testid': 'fork', + }, + }; + }, + secondaryAction() { + return { + text: __('Cancel'), + attributes: { + class: 'gl-w-full sm:gl-w-auto', + 'data-testid': 'cancel', + }, + }; + }, + }, +}; +</script> + +<template> + <gl-modal + ref="forkSuggestionModal" + modal-id="fork-suggestion-modal" + no-focus-on-show + :visible="visible" + :title="__('Fork to make changes')" + :action-primary="primaryAction" + :action-secondary="secondaryAction" + v-on="$listeners" + > + <p data-testid="message"> + {{ + __( + "You're not allowed to make changes to this project directly. Create a fork to make changes and submit a merge request.", + ) + }} + </p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/repository/queries/blob_controls.query.graphql b/app/assets/javascripts/repository/queries/blob_controls.query.graphql index 9c50ba6236b9df9f62b71e6c69b93214d2da9ec1..2be31da1e0eeaa244bcf5e354bf8969a1fe8b85f 100644 --- a/app/assets/javascripts/repository/queries/blob_controls.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_controls.query.graphql @@ -26,6 +26,7 @@ query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!, $ref canCurrentUserPushToBranch canModifyBlob canModifyBlobWithWebIde + forkAndViewPath simpleViewer { __typename fileType diff --git a/app/assets/javascripts/repository/utils/fork_suggestion_utils.js b/app/assets/javascripts/repository/utils/fork_suggestion_utils.js index 2e5b22a1d9d39bdad7f115ca0c26b8ae0ce73fd8..24b477e5991accd39c8d07d5f2d757b9b3265a2d 100644 --- a/app/assets/javascripts/repository/utils/fork_suggestion_utils.js +++ b/app/assets/javascripts/repository/utils/fork_suggestion_utils.js @@ -1,31 +1,4 @@ import { isLoggedIn } from '~/lib/utils/common_utils'; -import { createAlert, VARIANT_INFO } from '~/alert'; -import { __ } from '~/locale'; - -export function showForkSuggestionAlert(forkAndViewPath) { - const i18n = { - forkSuggestion: __( - "You can't edit files directly in this project. Fork this project and submit a merge request with your changes.", - ), - fork: __('Fork'), - cancel: __('Cancel'), - }; - - const alert = createAlert({ - message: i18n.forkSuggestion, - variant: VARIANT_INFO, - primaryButton: { - text: i18n.fork, - link: forkAndViewPath, - }, - secondaryButton: { - text: i18n.cancel, - clickHandler: () => alert.dismiss(), - }, - }); - - return alert; -} /** * Checks if the user can fork the project diff --git a/ee/spec/frontend/repository/mock_data.js b/ee/spec/frontend/repository/mock_data.js index 52efd94c53237a8ae18ab1fc19dfef3e4261624b..8a5b1a9ac8a183a868dbcb7577d4772a2e75eccc 100644 --- a/ee/spec/frontend/repository/mock_data.js +++ b/ee/spec/frontend/repository/mock_data.js @@ -226,6 +226,7 @@ export const blobControlsDataMock = { canCurrentUserPushToBranch: true, canModifyBlob: true, canModifyBlobWithWebIde: true, + forkAndViewPath: 'fork/view/path', simpleViewer: { __typename: 'BlobViewer', collapsed: false, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 96b3892af3ed065a26b4c62e5ebf741cd538178f..741f13d6f9b3639083133966c72e39290b3eeca7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -25560,6 +25560,9 @@ msgstr "" msgid "Fork project?" msgstr "" +msgid "Fork to make changes" +msgstr "" + msgid "ForkProject|A fork is a copy of a project." msgstr "" @@ -67402,9 +67405,6 @@ msgstr "" msgid "You can't approve because you added one or more commits to this merge request." msgstr "" -msgid "You can't edit files directly in this project. Fork this project and submit a merge request with your changes." -msgstr "" - msgid "You can't follow more than %{limit} users. To follow more users, unfollow some others." msgstr "" @@ -67852,6 +67852,9 @@ msgstr "" msgid "You're not allowed to make changes to this project directly. A fork of this project is being created that you can make changes in, so you can submit a merge request." msgstr "" +msgid "You're not allowed to make changes to this project directly. Create a fork to make changes and submit a merge request." +msgstr "" + msgid "You're receiving this email because of your account on %{host}." msgstr "" diff --git a/spec/frontend/repository/components/header_area/blob_button_group_spec.js b/spec/frontend/repository/components/header_area/blob_button_group_spec.js index d4485ca0816bf51d10d2d64f5b7a616aa6039173..1711d49bab61e3f86a684d3cdc2329c379fdb90c 100644 --- a/spec/frontend/repository/components/header_area/blob_button_group_spec.js +++ b/spec/frontend/repository/components/header_area/blob_button_group_spec.js @@ -1,8 +1,10 @@ +import { nextTick } from 'vue'; import { GlDisclosureDropdownItem } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import BlobButtonGroup from '~/repository/components/header_area/blob_button_group.vue'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; import { blobControlsDataMock, refMock } from 'ee_else_ce_jest/repository/mock_data'; @@ -55,6 +57,7 @@ describe('BlobButtonGroup component', () => { const findReplaceItem = () => wrapper.findComponent(GlDisclosureDropdownItem); const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); + const findForkSuggestionModal = () => wrapper.findComponent(ForkSuggestionModal); beforeEach(async () => { await createComponent(); @@ -100,13 +103,21 @@ describe('BlobButtonGroup component', () => { it('does not trigger the UploadBlobModal from the replace item', () => { findReplaceItem().vm.$emit('action'); - expect(findReplaceItem().props('item')).toMatchObject({ - extraAttrs: { disabled: true }, - }); - expect(showUploadBlobModalMock).not.toHaveBeenCalled(); - expect(wrapper.emitted().fork).toHaveLength(1); }); + + it('triggers ForkSuggestionModal from the replace item', async () => { + findReplaceItem().vm.$emit('action'); + await nextTick(); + + expect(findForkSuggestionModal().props('visible')).toBe(true); + }); + }); + }); + + it('renders ForkSuggestionModal', () => { + expect(findForkSuggestionModal().props()).toMatchObject({ + forkPath: 'fork/view/path', }); }); diff --git a/spec/frontend/repository/components/header_area/blob_controls_spec.js b/spec/frontend/repository/components/header_area/blob_controls_spec.js index f9df72abe2be306edd4b70ab09b8c3116c6666eb..dbf76ef42e55c17b154eb4a805c2f7812b79f0a5 100644 --- a/spec/frontend/repository/components/header_area/blob_controls_spec.js +++ b/spec/frontend/repository/components/header_area/blob_controls_spec.js @@ -202,9 +202,11 @@ describe('Blob controls component', () => { }); describe('BlobOverflow dropdown', () => { - it('renders BlobOverflow component with correct props', async () => { + beforeEach(async () => { await createComponent({ glFeatures: { blobOverflowMenu: true } }); + }); + it('renders BlobOverflow component with correct props', () => { expect(findOverflowMenu().exists()).toBe(true); expect(findOverflowMenu().props()).toEqual({ projectPath: 'some/project', @@ -234,9 +236,7 @@ describe('Blob controls component', () => { expect(findOverflowMenu().props('isBinary')).toBe(true); }); - it('copies to clipboard raw blob text, when receives copy event', async () => { - await createComponent({ glFeatures: { blobOverflowMenu: true } }); - + it('copies to clipboard raw blob text, when receives copy event', () => { jest.spyOn(navigator.clipboard, 'writeText'); findOverflowMenu().vm.$emit('copy'); diff --git a/spec/frontend/repository/components/header_area/blob_delete_file_group_spec.js b/spec/frontend/repository/components/header_area/blob_delete_file_group_spec.js index c718e0c1ce73a2e96b4bd8759816a41045f89941..f4e89f8d4971a3b988ceeb0ec70239e77efb2ee8 100644 --- a/spec/frontend/repository/components/header_area/blob_delete_file_group_spec.js +++ b/spec/frontend/repository/components/header_area/blob_delete_file_group_spec.js @@ -1,9 +1,11 @@ +import { nextTick } from 'vue'; import { GlDisclosureDropdownItem } from '@gitlab/ui'; import { blobControlsDataMock } from 'ee_else_ce_jest/repository/mock_data'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import BlobDeleteFileGroup from '~/repository/components/header_area/blob_delete_file_group.vue'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; import CommitChangesModal from '~/repository/components/commit_changes_modal.vue'; jest.mock('~/lib/utils/common_utils', () => ({ @@ -54,6 +56,7 @@ describe('BlobDeleteFileGroup component', () => { const findDeleteItem = () => wrapper.findComponent(GlDisclosureDropdownItem); const findDeleteBlobModal = () => wrapper.findComponent(CommitChangesModal); + const findForkSuggestionModal = () => wrapper.findComponent(ForkSuggestionModal); beforeEach(async () => { await createComponent(); @@ -99,13 +102,21 @@ describe('BlobDeleteFileGroup component', () => { it('does not trigger the DeleteBlobModal from the delete item', () => { findDeleteItem().vm.$emit('action'); - expect(findDeleteItem().props('item')).toMatchObject({ - extraAttrs: { disabled: true }, - }); - expect(showDeleteBlobModalMock).not.toHaveBeenCalled(); - expect(wrapper.emitted('fork')).toEqual([['view']]); }); + + it('changes ForkSuggestionModal visibility', async () => { + findDeleteItem().vm.$emit('action'); + await nextTick(); + + expect(findForkSuggestionModal().props('visible')).toBe(true); + }); + }); + }); + + it('renders ForkSuggestionModal', () => { + expect(findForkSuggestionModal().props()).toMatchObject({ + forkPath: 'fork/view/path', }); }); diff --git a/spec/frontend/repository/components/header_area/fork_suggestion_modal_spec.js b/spec/frontend/repository/components/header_area/fork_suggestion_modal_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..03bb3e3b34d27c80ddf8d9bcf1a4baedd3788a41 --- /dev/null +++ b/spec/frontend/repository/components/header_area/fork_suggestion_modal_spec.js @@ -0,0 +1,49 @@ +import { GlModal } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue'; + +const DEFAULT_PROPS = { visible: true, forkPath: '/fork/project/path' }; + +describe('ForkSuggestionModal component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(ForkSuggestionModal, { + propsData: { ...DEFAULT_PROPS, ...props }, + stubs: { + GlModal, + }, + }); + }; + + const findModal = () => wrapper.findComponent(GlModal); + const findMessage = () => wrapper.findByTestId('message'); + + beforeEach(() => { + createComponent(); + }); + + it('sets correct modal visibility', () => { + createComponent({ visible: false }); + expect(findModal().props('visible')).toBe(false); + }); + + it('renders the modal with correct variant', () => { + expect(findModal().exists()).toBe(true); + expect(findMessage().text()).toBe( + "You're not allowed to make changes to this project directly. Create a fork to make changes and submit a merge request.", + ); + }); + + describe('reactivity to prop changes', () => { + it('updates the fork path when forkPath prop changes', () => { + createComponent({ forkPath: '/new/fork/path' }); + + expect(findModal().props('actionPrimary')).toMatchObject({ + attributes: { + href: '/new/fork/path', + }, + }); + }); + }); +}); diff --git a/spec/frontend/repository/components/header_area_spec.js b/spec/frontend/repository/components/header_area_spec.js index 28a504ac1527a1b00f6501ec2f06bdda3eb32456..6f68c509cc34f3e78d264236fd5a489686afedce 100644 --- a/spec/frontend/repository/components/header_area_spec.js +++ b/spec/frontend/repository/components/header_area_spec.js @@ -42,6 +42,7 @@ describe('HeaderArea', () => { const findPageHeading = () => wrapper.findByTestId('repository-heading'); const findFileIcon = () => wrapper.findComponent(FileIcon); const findRepositoryOverflowMenu = () => wrapper.findComponent(RepositoryOverflowMenu); + const findBlobControls = () => wrapper.findComponent(BlobControls); const { bindInternalEventDocument } = useMockInternalEventsTracking(); @@ -210,10 +211,9 @@ describe('HeaderArea', () => { describe('when rendered for blob view', () => { it('renders BlobControls component with correct props', () => { wrapper = createComponent({ refType: 'branch' }); - const blobControls = wrapper.findComponent(BlobControls); - expect(blobControls.exists()).toBe(true); - expect(blobControls.props('projectPath')).toBe('test/project'); - expect(blobControls.props('refType')).toBe(''); + expect(findBlobControls().exists()).toBe(true); + expect(findBlobControls().props('projectPath')).toBe('test/project'); + expect(findBlobControls().props('refType')).toBe(''); }); it('does not render CodeDropdown and SourceCodeDownloadDropdown', () => { diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js index e4f023a6a22778fa020a943d03761f262dad842e..bbc18d904b4cb34e55a98440711b2a7f70020ad4 100644 --- a/spec/frontend/repository/mock_data.js +++ b/spec/frontend/repository/mock_data.js @@ -116,6 +116,7 @@ export const blobControlsDataMock = { canCurrentUserPushToBranch: true, canModifyBlob: true, canModifyBlobWithWebIde: true, + forkAndViewPath: 'fork/view/path', simpleViewer: { __typename: 'BlobViewer', collapsed: false, diff --git a/spec/frontend/repository/utils/fork_suggestion_utils_spec.js b/spec/frontend/repository/utils/fork_suggestion_utils_spec.js index d111f65e078e683f7cf24a67ba314ddd913f839c..c3d5b74db9e42ea4f5b34dd3d4aed41e694a4490 100644 --- a/spec/frontend/repository/utils/fork_suggestion_utils_spec.js +++ b/spec/frontend/repository/utils/fork_suggestion_utils_spec.js @@ -1,14 +1,11 @@ -import { createAlert, VARIANT_INFO } from '~/alert'; import * as commonUtils from '~/lib/utils/common_utils'; import { - showForkSuggestionAlert, canFork, showSingleFileEditorForkSuggestion, showWebIdeForkSuggestion, showForkSuggestion, } from '~/repository/utils/fork_suggestion_utils'; -jest.mock('~/alert'); jest.mock('~/lib/utils/common_utils'); describe('forkSuggestionUtils', () => { @@ -106,43 +103,4 @@ describe('forkSuggestionUtils', () => { ).toBe(false); }); }); - - describe('showForkSuggestionAlert', () => { - const forkAndViewPath = '/path/to/fork'; - - beforeEach(() => { - createAlert.mockClear(); - }); - - it('calls createAlert with correct parameters', () => { - showForkSuggestionAlert(forkAndViewPath); - - expect(createAlert).toHaveBeenCalledTimes(1); - expect(createAlert).toHaveBeenCalledWith({ - message: - "You can't edit files directly in this project. Fork this project and submit a merge request with your changes.", - variant: VARIANT_INFO, - primaryButton: { - text: 'Fork', - link: forkAndViewPath, - }, - secondaryButton: { - text: 'Cancel', - clickHandler: expect.any(Function), - }, - }); - }); - - it('secondary button click handler dismisses the alert', () => { - const mockAlert = { dismiss: jest.fn() }; - createAlert.mockReturnValue(mockAlert); - - showForkSuggestionAlert(forkAndViewPath); - - const secondaryButtonClickHandler = createAlert.mock.calls[0][0].secondaryButton.clickHandler; - secondaryButtonClickHandler(mockAlert); - - expect(mockAlert.dismiss).toHaveBeenCalledTimes(1); - }); - }); });