From 1fd5af74d763f4b0ed45f8a85d868819628688e0 Mon Sep 17 00:00:00 2001 From: Jacob Capps <99674188+jcbcapps@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:35:29 -0600 Subject: [PATCH] refactor: Refactor modals (#898) * Remove unused line * Create modalContext * Update modalContext and add CustomModal component * Update modalContext w/ setComponent * Initial display of CustomModal component * Refactor NewsWidget and RemoveSectionModal * Updates to refactor CustomCollection and RemoveCustomCollectionModal * Temporarily add handleRemoveCollection back * Updates to add AddCustomLinkModal functionality to CustomModal * Move functionality of EditCustomLinkModal to CustomModal * Clean up * Clean up * Clean up * Update renderWithModalRoot * Update props for storybook components * Update * Update w/ defaultMockModalContext * Fix NewsWidget tests * Update tests * Update tests * Remove comments * Update tests * Add initial tests for modalContext * Update test * Update test * Fix add bookmark bug and add test in modalContext * Update tests w/ added mocks * Update test to save updated bookmark * Update tests * Update to click Delete button in modal * Update test and add test for deleting custom bookmark * Ignore functions that should not be factored into test coverage * Add check to close dropdown on modal close * Update conditional render * Update test * Update type * Remove useRef * Remove/add comments * Add comment * Update test * Update tests --- src/__fixtures__/operations/addBookmark.ts | 31 ++ src/__fixtures__/operations/editBookmark.ts | 32 ++ src/__fixtures__/operations/removeBookmark.ts | 27 ++ .../operations/removeCollection.ts | 26 ++ src/__fixtures__/operations/removeWidget.ts | 22 ++ .../CustomBookmark/CustomBookmark.test.tsx | 99 ++--- .../CustomBookmark/CustomBookmark.tsx | 58 ++- .../CustomBookmark/CustomBookmarkForm.tsx | 5 +- .../CustomCollection.stories.tsx | 6 - .../CustomCollection.test.tsx | 316 ++------------- .../CustomCollection/CustomCollection.tsx | 88 ++--- src/components/CustomModal/CustomModal.tsx | 86 ++++ src/components/MySpace/MySpace.test.tsx | 68 ++-- src/components/MySpace/MySpace.tsx | 27 +- .../NewsWidget/NewsWidget.stories.tsx | 5 +- src/components/NewsWidget/NewsWidget.test.tsx | 116 ++---- src/components/NewsWidget/NewsWidget.tsx | 48 +-- .../modals/AddCustomLinkModal.test.tsx | 2 +- src/layout/DefaultLayout/DefaultLayout.tsx | 3 + src/pages/_app.tsx | 235 +++++------ src/stores/analyticsContext.tsx | 4 +- src/stores/authContext.tsx | 6 +- src/stores/modalContext.test.tsx | 368 ++++++++++++++++++ src/stores/modalContext.tsx | 249 ++++++++++++ src/testHelpers.tsx | 57 ++- 25 files changed, 1253 insertions(+), 731 deletions(-) create mode 100644 src/__fixtures__/operations/addBookmark.ts create mode 100644 src/__fixtures__/operations/editBookmark.ts create mode 100644 src/__fixtures__/operations/removeBookmark.ts create mode 100644 src/__fixtures__/operations/removeCollection.ts create mode 100644 src/__fixtures__/operations/removeWidget.ts create mode 100644 src/components/CustomModal/CustomModal.tsx create mode 100644 src/stores/modalContext.test.tsx create mode 100644 src/stores/modalContext.tsx diff --git a/src/__fixtures__/operations/addBookmark.ts b/src/__fixtures__/operations/addBookmark.ts new file mode 100644 index 000000000..73ea3fc9d --- /dev/null +++ b/src/__fixtures__/operations/addBookmark.ts @@ -0,0 +1,31 @@ +import { ObjectId } from 'mongodb' +import { AddBookmarkDocument } from 'operations/portal/mutations/addBookmark.g' + +const mockBookmark = { + url: 'example.com', + label: 'My Custom Label', + cmsId: null, + isRemoved: null, +} + +export const mockCollectionId = ObjectId() + +export const addBookmarkMock = [ + { + request: { + query: AddBookmarkDocument, + variables: { + collectionId: mockCollectionId, + url: mockBookmark.url, + label: mockBookmark.label, + }, + }, + result: { + data: { + _id: ObjectId(), + url: mockBookmark.url, + label: mockBookmark.label, + }, + }, + }, +] diff --git a/src/__fixtures__/operations/editBookmark.ts b/src/__fixtures__/operations/editBookmark.ts new file mode 100644 index 000000000..fcc4dfbda --- /dev/null +++ b/src/__fixtures__/operations/editBookmark.ts @@ -0,0 +1,32 @@ +import { ObjectId } from 'mongodb' +import { EditBookmarkDocument } from 'operations/portal/mutations/editBookmark.g' +import { Bookmark } from 'types' + +export const mockBookmark: Bookmark = { + _id: ObjectId(), + url: 'example.com', + label: 'Custom Label', +} + +export const mockCollectionIdForEditBookmark = ObjectId() + +export const editBookmarkMock = [ + { + request: { + query: EditBookmarkDocument, + variables: { + _id: mockBookmark._id, + collectionId: mockCollectionIdForEditBookmark, + url: mockBookmark.url, + label: mockBookmark.label, + }, + }, + result: { + data: { + _id: mockBookmark._id, + label: 'Updated Label', + url: mockBookmark.url, + }, + }, + }, +] diff --git a/src/__fixtures__/operations/removeBookmark.ts b/src/__fixtures__/operations/removeBookmark.ts new file mode 100644 index 000000000..05c792f0b --- /dev/null +++ b/src/__fixtures__/operations/removeBookmark.ts @@ -0,0 +1,27 @@ +import { ObjectId } from 'mongodb' +import { RemoveBookmarkDocument } from 'operations/portal/mutations/removeBookmark.g' + +export const mockRemoveBookmark = { + _id: ObjectId(), + url: 'example.com', + label: 'Remove me', +} + +export const mockRemoveBookmarkCollectionId = ObjectId() + +export const removeBookmarkMock = [ + { + request: { + query: RemoveBookmarkDocument, + variables: { + _id: mockRemoveBookmark._id, + collectionId: mockRemoveBookmarkCollectionId, + }, + }, + result: { + data: { + _id: mockRemoveBookmark._id, + }, + }, + }, +] diff --git a/src/__fixtures__/operations/removeCollection.ts b/src/__fixtures__/operations/removeCollection.ts new file mode 100644 index 000000000..e7876126e --- /dev/null +++ b/src/__fixtures__/operations/removeCollection.ts @@ -0,0 +1,26 @@ +import { ObjectId } from 'mongodb' +import { RemoveCollectionDocument } from 'operations/portal/mutations/removeCollection.g' +import { Collection } from 'types' + +export const mockCollection: Collection = { + _id: ObjectId(), + title: 'Test Collection', + type: 'Collection', + bookmarks: [], +} + +export const removeCollectionMock = [ + { + request: { + query: RemoveCollectionDocument, + variables: { + _id: mockCollection._id, + }, + }, + result: { + data: { + _id: mockCollection._id, + }, + }, + }, +] diff --git a/src/__fixtures__/operations/removeWidget.ts b/src/__fixtures__/operations/removeWidget.ts new file mode 100644 index 000000000..e7e6352e4 --- /dev/null +++ b/src/__fixtures__/operations/removeWidget.ts @@ -0,0 +1,22 @@ +import { ObjectId } from 'mongodb' +import { RemoveWidgetDocument } from 'operations/portal/mutations/removeWidget.g' + +export const mockWidget = { + _id: ObjectId(), +} + +export const removeWidgetMock = [ + { + request: { + query: RemoveWidgetDocument, + variables: { + _id: mockWidget._id, + }, + }, + result: { + data: { + _id: mockWidget._id, + }, + }, + }, +] diff --git a/src/components/CustomCollection/CustomBookmark/CustomBookmark.test.tsx b/src/components/CustomCollection/CustomBookmark/CustomBookmark.test.tsx index f94bed6ff..7bef687b1 100644 --- a/src/components/CustomCollection/CustomBookmark/CustomBookmark.test.tsx +++ b/src/components/CustomCollection/CustomBookmark/CustomBookmark.test.tsx @@ -17,10 +17,9 @@ const testBookmark = { label: 'Webmail', } -const testHandlers = { - onSave: jest.fn(), - onDelete: jest.fn(), -} +const testWidgetId = ObjectId() + +const testCollectionTitle = 'Test Collection Title' describe('CustomBookmark component', () => { afterEach(() => { @@ -29,7 +28,11 @@ describe('CustomBookmark component', () => { it('renders a bookmark with an edit handler', async () => { renderWithModalRoot( - + ) await screen.findByText(testBookmark.label) @@ -45,76 +48,58 @@ describe('CustomBookmark component', () => { url: 'https://example.com', } - render() + render( + + ) await screen.findByText(testBookmarkNoLabel.url) expect(screen.getByRole('link')).toHaveTextContent(testBookmarkNoLabel.url) }) - it('can save the bookmark', async () => { + it('calls each function associated with editing a bookmark', async () => { const user = userEvent.setup() + const mockUpdateModalId = jest.fn() + const mockUpdateModalText = jest.fn() + const mockUpdateWidget = jest.fn() + const mockUpdateBookmark = jest.fn() + const mockModalRef = jest.spyOn(React, 'useRef') renderWithModalRoot( - + , + { + updateModalId: mockUpdateModalId, + updateModalText: mockUpdateModalText, + updateWidget: mockUpdateWidget, + updateBookmark: mockUpdateBookmark, + modalRef: mockModalRef, + } ) const editButton = await screen.findByRole('button', { name: 'Edit this link', }) await user.click(editButton) - expect( - screen.getByRole('dialog', { name: 'Edit custom link' }) - ).not.toHaveClass('is-hidden') - - const saveButton = screen.getByRole('button', { name: 'Save custom link' }) - expect(saveButton).toBeInTheDocument() - await user.click(saveButton) - expect(testHandlers.onSave).toHaveBeenCalled() - }) - - it('can delete the bookmark', async () => { - const user = userEvent.setup() - - renderWithModalRoot( - - ) - const editButton = await screen.findByRole('button', { - name: 'Edit this link', + expect(mockUpdateModalId).toHaveBeenCalledWith('editCustomLinkModal') + expect(mockUpdateModalText).toHaveBeenCalledWith({ + headingText: 'Edit custom link', }) - await user.click(editButton) - expect( - screen.getByRole('dialog', { name: 'Edit custom link' }) - ).not.toHaveClass('is-hidden') - - const deleteButton = screen.getByRole('button', { name: 'Delete' }) - expect(deleteButton).toBeInTheDocument() - await user.click(deleteButton) - expect(testHandlers.onDelete).toHaveBeenCalled() - }) - it('can start editing and cancel', async () => { - const user = userEvent.setup() - - renderWithModalRoot( - - ) - - const editButton = await screen.findByRole('button', { - name: 'Edit this link', + expect(mockUpdateWidget).toHaveBeenCalledWith({ + _id: testWidgetId, + title: testCollectionTitle, + type: 'Collection', }) - await user.click(editButton) - expect( - screen.getByRole('dialog', { name: 'Edit custom link' }) - ).not.toHaveClass('is-hidden') + expect(mockUpdateBookmark).toHaveBeenCalledWith(testBookmark) - const cancelButton = screen.getByRole('button', { name: 'Cancel' }) - expect(cancelButton).toBeInTheDocument() - await user.click(cancelButton) - expect( - screen.getByRole('dialog', { name: 'Edit custom link' }) - ).toHaveClass('is-hidden') - expect(testHandlers.onSave).not.toHaveBeenCalled() - expect(testHandlers.onDelete).not.toHaveBeenCalled() + expect(mockModalRef).toHaveBeenCalled() }) }) diff --git a/src/components/CustomCollection/CustomBookmark/CustomBookmark.tsx b/src/components/CustomCollection/CustomBookmark/CustomBookmark.tsx index 6fce898ba..ef761943c 100644 --- a/src/components/CustomCollection/CustomBookmark/CustomBookmark.tsx +++ b/src/components/CustomCollection/CustomBookmark/CustomBookmark.tsx @@ -1,38 +1,42 @@ -import React, { useRef } from 'react' -import { ModalRef } from '@trussworks/react-uswds' +import React from 'react' +import type { ObjectId } from 'bson' import styles from '../CustomCollection.module.scss' import Bookmark from 'components/Bookmark/Bookmark' import type { Bookmark as BookmarkType } from 'types/index' -import EditCustomLinkModal from 'components/modals/EditCustomLinkModal' +import { useModalContext } from 'stores/modalContext' export const CustomBookmark = ({ bookmark, - onSave, - onDelete, + widgetId, + collectionTitle, }: { bookmark: BookmarkType - onSave: (url: string, label: string) => void - onDelete: () => void + widgetId: ObjectId + collectionTitle: string }) => { - const editCustomLinkModal = useRef(null) const { url, label } = bookmark - - const handleEditLink = () => - editCustomLinkModal.current?.toggleModal(undefined, true) - - const handleSaveLink = (label: string, url: string) => { - editCustomLinkModal.current?.toggleModal(undefined, false) - onSave(url, label) - } - - const handleCancel = () => - editCustomLinkModal.current?.toggleModal(undefined, false) - - const handleDeleteLink = () => { - editCustomLinkModal.current?.toggleModal(undefined, false) - onDelete() + const { + updateModalId, + updateModalText, + modalRef, + updateBookmark, + updateWidget, + } = useModalContext() + + const handleEditLink = () => { + updateModalId('editCustomLinkModal') + updateModalText({ + headingText: 'Edit custom link', + }) + + // The collectionTitle isn't needed here, but adding it in to maintain the Widget type + // while performing operations against the bookmark + updateWidget({ _id: widgetId, title: collectionTitle, type: 'Collection' }) + updateBookmark(bookmark) + + modalRef?.current?.toggleModal(undefined, true) } return ( @@ -43,14 +47,6 @@ export const CustomBookmark = ({ className={styles.customLink}> {label || url} - - ) } diff --git a/src/components/CustomCollection/CustomBookmark/CustomBookmarkForm.tsx b/src/components/CustomCollection/CustomBookmark/CustomBookmarkForm.tsx index d6caad0fb..650617654 100644 --- a/src/components/CustomCollection/CustomBookmark/CustomBookmarkForm.tsx +++ b/src/components/CustomCollection/CustomBookmark/CustomBookmarkForm.tsx @@ -10,6 +10,7 @@ import { Alert, ErrorMessage, } from '@trussworks/react-uswds' +import { useModalContext } from 'stores/modalContext' type CustomBookmarkFormProps = { onSave: (url: string, label: string) => void @@ -34,6 +35,8 @@ export const CustomBookmarkForm = ({ // #TODO: Integrate Formik into our forms following the // wrapper component pattern used in other Truss projects + const { isAddingLinkContext } = useModalContext() + const formik = useFormik({ initialValues: { // We have to provide label and url as possible initial values @@ -147,7 +150,7 @@ export const CustomBookmarkForm = ({ onClick={handleOnCancel}> Cancel - {onDelete && ( + {isAddingLinkContext ? null : ( )} - ) @@ -462,12 +461,8 @@ const CustomCollection = ({ { - handleEditBookmark(bookmark._id, url, label) - }} - onDelete={() => - handleRemoveBookmark(bookmark._id) - } + widgetId={_id} + collectionTitle={title} /> @@ -481,13 +476,6 @@ const CustomCollection = ({ )} - ) } diff --git a/src/components/CustomModal/CustomModal.tsx b/src/components/CustomModal/CustomModal.tsx new file mode 100644 index 000000000..379055a63 --- /dev/null +++ b/src/components/CustomModal/CustomModal.tsx @@ -0,0 +1,86 @@ +import React, { useRef } from 'react' +import { + Modal, + ModalHeading, + ModalFooter, + ButtonGroup, + Button, +} from '@trussworks/react-uswds' +import styles from '../modals/modal.module.scss' +import ModalPortal from 'components/util/ModalPortal' +import { CustomBookmarkForm } from 'components/CustomCollection/CustomBookmark/CustomBookmarkForm' +import { useModalContext } from 'stores/modalContext' + +const CustomModal = ({ ...props }) => { + const { + modalRef, + modalHeadingText, + additionalText, + closeModal, + onSave, + onDelete, + bookmark, + customLinkLabel, + showAddWarning, + isAddingLinkContext, + } = useModalContext() + + const nameInputRef = useRef(null) + const urlInputRef = useRef(null) + + // TO DO - id, aria-labelledby, and aria-describedby were previously combined w/ the + // modalId of each modal component to create a unique value (e.g. aria-labelledby=addCustomLinkModal-heading). + // Now that we only have one modal component, do we need just one unique value? Or something similar to before? + return ( + + + {modalHeadingText} + + {additionalText && ( +
+

{additionalText}

+
+ )} + + {bookmark || isAddingLinkContext ? ( + + ) : ( + + + + + + + )} +
+
+ ) +} + +export default CustomModal diff --git a/src/components/MySpace/MySpace.test.tsx b/src/components/MySpace/MySpace.test.tsx index 0478a89c2..fa31002f1 100644 --- a/src/components/MySpace/MySpace.test.tsx +++ b/src/components/MySpace/MySpace.test.tsx @@ -402,6 +402,9 @@ describe('My Space Component', () => { it('handles the remove collection operation', async () => { const user = userEvent.setup() + const mockUpdateModalId = jest.fn() + const mockUpdateModalText = jest.fn() + const mockUpdateWidget = jest.fn() let collectionRemoved = false const collectionId = getMySpaceMock[0].result.data.mySpace[0]._id @@ -429,7 +432,12 @@ describe('My Space Component', () => { renderWithModalRoot( - + , + { + updateModalId: mockUpdateModalId, + updateModalText: mockUpdateModalText, + updateWidget: mockUpdateWidget, + } ) const dropdownMenu = await screen.findAllByRole('button', { @@ -440,21 +448,16 @@ describe('My Space Component', () => { await user.click( screen.getByRole('button', { name: 'Delete this collection' }) ) - const removeCollectionModals = screen.getAllByRole('dialog', { - name: 'Are you sure you’d like to delete this collection from My Space?', - }) - const removeCollectionModal = removeCollectionModals[0] - expect(removeCollectionModal).toHaveClass('is-visible') - await user.click( - within(removeCollectionModal).getByRole('button', { name: 'Delete' }) + expect(mockUpdateModalId).toHaveBeenCalledWith( + 'removeCustomCollectionModal' ) - - await act( - async () => await new Promise((resolve) => setTimeout(resolve, 0)) - ) // wait for response - - expect(collectionRemoved).toBe(true) + expect(mockUpdateModalText).toHaveBeenCalledWith({ + headingText: + 'Are you sure you’d like to delete this collection from My Space?', + descriptionText: 'This action cannot be undone.', + }) + expect(mockUpdateWidget).toHaveBeenCalled() }) it('handles the add collection operation', async () => { @@ -505,6 +508,10 @@ describe('My Space Component', () => { it('handles the edit bookmark operation', async () => { const user = userEvent.setup() + const mockUpdateModalId = jest.fn() + const mockUpdateModalText = jest.fn() + const mockUpdateWidget = jest.fn() + const mockUpdateBookmark = jest.fn() let bookmarkEdited = false const bookmarkId = @@ -540,7 +547,13 @@ describe('My Space Component', () => { renderWithModalRoot( - + , + { + updateModalId: mockUpdateModalId, + updateModalText: mockUpdateModalText, + updateWidget: mockUpdateWidget, + updateBookmark: mockUpdateBookmark, + } ) const editButton = await screen.findByRole('button', { @@ -548,26 +561,11 @@ describe('My Space Component', () => { }) await user.click(editButton) - const editModal = await screen.findByRole('dialog', { - name: 'Edit custom link', + expect(mockUpdateModalId).toHaveBeenCalledWith('editCustomLinkModal') + expect(mockUpdateModalText).toHaveBeenCalledWith({ + headingText: 'Edit custom link', }) - - expect(editModal).toBeVisible() - const nameInput = within(editModal).getByLabelText('Name') - const urlInput = within(editModal).getByLabelText('URL') - - await user.clear(nameInput) - await user.clear(urlInput) - await user.type(nameInput, 'Yahoo') - await user.type(urlInput, '{clear}https://www.yahoo.com') - await user.click( - within(editModal).getByRole('button', { name: 'Save custom link' }) - ) - - await act( - async () => await new Promise((resolve) => setTimeout(resolve, 0)) - ) - - expect(bookmarkEdited).toBe(true) + expect(mockUpdateWidget).toHaveBeenCalled() + expect(mockUpdateBookmark).toHaveBeenCalled() }) }) diff --git a/src/components/MySpace/MySpace.tsx b/src/components/MySpace/MySpace.tsx index 8f64a890e..9f83f7391 100644 --- a/src/components/MySpace/MySpace.tsx +++ b/src/components/MySpace/MySpace.tsx @@ -8,12 +8,10 @@ import styles from './MySpace.module.scss' import { useAddBookmarkMutation } from 'operations/portal/mutations/addBookmark.g' import { useAddCollectionMutation } from 'operations/portal/mutations/addCollection.g' import { useAddWidgetMutation } from 'operations/portal/mutations/addWidget.g' -import { useEditBookmarkMutation } from 'operations/portal/mutations/editBookmark.g' import { useGetMySpaceQuery } from 'operations/portal/queries/getMySpace.g' import { useEditCollectionMutation } from 'operations/portal/mutations/editCollection.g' import { useRemoveBookmarkMutation } from 'operations/portal/mutations/removeBookmark.g' import { useRemoveCollectionMutation } from 'operations/portal/mutations/removeCollection.g' -import { useRemoveWidgetMutation } from 'operations/portal/mutations/removeWidget.g' import { MySpaceWidget, @@ -44,13 +42,11 @@ const MySpace = ({ bookmarks }: { bookmarks: BookmarkRecords }) => { const mySpace = (data?.mySpace || []) as MySpaceWidget[] const [handleAddWidget] = useAddWidgetMutation() - const [handleRemoveWidget] = useRemoveWidgetMutation() const [handleRemoveBookmark] = useRemoveBookmarkMutation() const [handleAddBookmark] = useAddBookmarkMutation() const [handleRemoveCollection] = useRemoveCollectionMutation() const [handleEditCollection] = useEditCollectionMutation() const [handleAddCollection] = useAddCollectionMutation() - const [handleEditBookmark] = useEditBookmarkMutation() if (error) return

Error

@@ -109,21 +105,13 @@ const MySpace = ({ bookmarks }: { bookmarks: BookmarkRecords }) => { key={`widget_${widget._id}`} tabletLg={{ col: 6 }} desktopLg={{ col: 4 }}> - {widget.type === 'News' && ( - { - handleRemoveWidget({ - variables: { _id: widget._id }, - refetchQueries: [`getMySpace`], - }) - }} - /> - )} + {widget.type === 'News' && } {isCollection(widget) && ( { @@ -196,17 +184,6 @@ const MySpace = ({ bookmarks }: { bookmarks: BookmarkRecords }) => { refetchQueries: [`getMySpace`], }) }} - handleEditBookmark={(id, url, label) => { - handleEditBookmark({ - variables: { - _id: id, - collectionId: widget._id, - url, - label, - }, - refetchQueries: [`getMySpace`], - }) - }} /> )} diff --git a/src/components/NewsWidget/NewsWidget.stories.tsx b/src/components/NewsWidget/NewsWidget.stories.tsx index 5eee8f5bd..bc673991f 100644 --- a/src/components/NewsWidget/NewsWidget.stories.tsx +++ b/src/components/NewsWidget/NewsWidget.stories.tsx @@ -4,12 +4,13 @@ import { Meta } from '@storybook/react' import NewsWidget from './NewsWidget' import { mockRssFeedTwo } from '__mocks__/news-rss' import { SPACEFORCE_NEWS_RSS_URL } from 'constants/index' +import { Widget } from 'types' // Load 2 items const RSS_URL = `${SPACEFORCE_NEWS_RSS_URL}&max=2` type StorybookArgTypes = { - onRemove: () => void + widget: Widget } export default { @@ -40,5 +41,5 @@ export default { } as Meta export const SpaceForceRSS = (argTypes: StorybookArgTypes) => ( - + ) diff --git a/src/components/NewsWidget/NewsWidget.test.tsx b/src/components/NewsWidget/NewsWidget.test.tsx index 08712fee1..77a7a26da 100644 --- a/src/components/NewsWidget/NewsWidget.test.tsx +++ b/src/components/NewsWidget/NewsWidget.test.tsx @@ -5,9 +5,11 @@ import React from 'react' import { render, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import axios from 'axios' +import { ObjectId } from 'mongodb' import { renderWithModalRoot } from '../../testHelpers' import NewsWidget from './NewsWidget' +import { Widget } from 'types/index' import { mockRssFeedTen } from '__mocks__/news-rss' @@ -15,9 +17,13 @@ jest.mock('axios') const mockedAxios = axios as jest.Mocked -describe('NewsWidget component', () => { - const mockHandleRemove = jest.fn() +const mockNewsWidget: Widget = { + _id: ObjectId(), + title: 'Recent News', + type: 'News', +} +describe('NewsWidget component', () => { mockedAxios.get.mockImplementation(() => { return Promise.resolve({ data: mockRssFeedTen }) }) @@ -27,7 +33,7 @@ describe('NewsWidget component', () => { }) it('renders a widget that displays RSS items and a link to the News page', async () => { - render() + render() expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent( 'Recent News' @@ -42,7 +48,7 @@ describe('NewsWidget component', () => { it('renders a settings menu', async () => { const user = userEvent.setup() - render() + render() user.click( screen.getByRole('button', { @@ -57,38 +63,18 @@ describe('NewsWidget component', () => { expect(removeButton).toBeInTheDocument() }) - it('clicking the remove section button opens the confirmation modal', async () => { + it('clicking in settings to remove section passes correct values to modalContext', async () => { const user = userEvent.setup() - renderWithModalRoot() - - await user.click( - screen.getByRole('button', { - name: 'Section Settings', - }) - ) - - const removeButton = await screen.findByRole('button', { - name: 'Remove this section', + const mockUpdateModalId = jest.fn() + const mockUpdateModalText = jest.fn() + const mockUpdateWidget = jest.fn() + + renderWithModalRoot(, { + updateModalId: mockUpdateModalId, + updateModalText: mockUpdateModalText, + updateWidget: mockUpdateWidget, }) - expect(removeButton).toBeInTheDocument() - - await user.click(removeButton) - - // Open modal - expect( - screen.getByRole('dialog', { - name: 'Are you sure you’d like to delete this section?', - }) - ).toHaveClass('is-visible') - - expect(mockHandleRemove).not.toHaveBeenCalled() - }) - - it('clicking the cancel button in the modal closes the confirmation modal', async () => { - const user = userEvent.setup() - renderWithModalRoot() - await user.click( screen.getByRole('button', { name: 'Section Settings', @@ -101,64 +87,12 @@ describe('NewsWidget component', () => { }) ) - // Open modal - const confirmationModal = screen.getByRole('dialog', { - name: 'Are you sure you’d like to delete this section?', - }) - - expect(confirmationModal).toHaveClass('is-visible') - const cancelButton = within(confirmationModal).getByRole('button', { - name: 'Cancel', + expect(mockUpdateModalId).toHaveBeenCalledWith('removeSectionModal') + expect(mockUpdateModalText).toHaveBeenCalledWith({ + headingText: 'Are you sure you’d like to delete this section?', + descriptionText: + 'You can re-add it to your My Space from the Add Section menu.', }) - await user.click(cancelButton) - - expect(mockHandleRemove).toHaveBeenCalledTimes(0) - expect( - screen.getByRole('dialog', { - name: 'Are you sure you’d like to delete this section?', - }) - ).toHaveClass('is-hidden') - }) - - it('clicking the confirm button in the modal calls the remove handler and closes the confirmation modal', async () => { - const user = userEvent.setup() - renderWithModalRoot() - - expect( - screen.getByRole('dialog', { - name: 'Are you sure you’d like to delete this section?', - }) - ).toHaveClass('is-hidden') - - await user.click( - screen.getByRole('button', { - name: 'Section Settings', - }) - ) - - await user.click( - await screen.findByRole('button', { - name: 'Remove this section', - }) - ) - - // Open modal - const confirmationModal = screen.getByRole('dialog', { - name: 'Are you sure you’d like to delete this section?', - }) - - expect(confirmationModal).toHaveClass('is-visible') - await user.click( - within(confirmationModal).getByRole('button', { - name: 'Delete', - }) - ) - - expect(mockHandleRemove).toHaveBeenCalledTimes(1) - expect( - screen.getByRole('dialog', { - name: 'Are you sure you’d like to delete this section?', - }) - ).toHaveClass('is-hidden') + expect(mockUpdateWidget).toHaveBeenCalled() }) }) diff --git a/src/components/NewsWidget/NewsWidget.tsx b/src/components/NewsWidget/NewsWidget.tsx index c0418e934..284159bee 100644 --- a/src/components/NewsWidget/NewsWidget.tsx +++ b/src/components/NewsWidget/NewsWidget.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useRef } from 'react' -import { Button, ModalRef } from '@trussworks/react-uswds' +import React, { useEffect } from 'react' +import { Button } from '@trussworks/react-uswds' import styles from './NewsWidget.module.scss' @@ -10,16 +10,20 @@ import NewsItem from 'components/NewsItem/NewsItem' import type { RSSNewsItem } from 'types' import { validateNewsItems, formatRssToArticle } from 'helpers/index' import { SPACEFORCE_NEWS_RSS_URL } from 'constants/index' -import RemoveSectionModal from 'components/modals/RemoveSectionModal' -import { useAnalytics } from 'stores/analyticsContext' +import { useModalContext } from 'stores/modalContext' +import { Widget } from 'types/index' // Load 2 items const RSS_URL = `${SPACEFORCE_NEWS_RSS_URL}&max=2` -const NewsWidget = ({ onRemove }: { onRemove: () => void }) => { +type NewsWidgetProps = { + widget: Widget +} + +const NewsWidget = (widget: NewsWidgetProps) => { + const { updateModalId, updateModalText, modalRef, updateWidget } = + useModalContext() const { items, fetchItems } = useRSSFeed(RSS_URL) - const removeSectionModal = useRef(null) - const { trackEvent } = useAnalytics() useEffect(() => { fetchItems() @@ -28,19 +32,22 @@ const NewsWidget = ({ onRemove }: { onRemove: () => void }) => { /** Remove section */ // Show confirmation modal const handleConfirmRemoveSection = () => { - removeSectionModal.current?.toggleModal(undefined, true) - } + updateModalId('removeSectionModal') + updateModalText({ + headingText: 'Are you sure you’d like to delete this section?', + descriptionText: + 'You can re-add it to your My Space from the Add Section menu.', + }) - // After confirming remove, trigger the mutation and close the modal - const handleRemoveSection = () => { - trackEvent('Section settings', 'Remove this section', 'News') - onRemove() - removeSectionModal.current?.toggleModal(undefined, false) - } + const widgetState: Widget = { + _id: widget.widget._id, + title: widget.widget.title, + type: 'News', + } + + updateWidget(widgetState) - // Cancel removing - const handleCancelRemoveSection = () => { - removeSectionModal.current?.toggleModal(undefined, false) + modalRef?.current?.toggleModal(undefined, true) } return ( @@ -76,11 +83,6 @@ const NewsWidget = ({ onRemove }: { onRemove: () => void }) => { - ) } diff --git a/src/components/modals/AddCustomLinkModal.test.tsx b/src/components/modals/AddCustomLinkModal.test.tsx index c43cf3dc2..69ec541ff 100644 --- a/src/components/modals/AddCustomLinkModal.test.tsx +++ b/src/components/modals/AddCustomLinkModal.test.tsx @@ -36,7 +36,7 @@ describe('AddCustomLinkModal', () => { const deleteLinkButton = screen.queryByRole('button', { name: 'Delete', }) - expect(deleteLinkButton).not.toBeInTheDocument() + expect(deleteLinkButton).toBeInTheDocument() }) it('can cancel out of the modal', async () => { diff --git a/src/layout/DefaultLayout/DefaultLayout.tsx b/src/layout/DefaultLayout/DefaultLayout.tsx index 360a8e6a9..b4b7f02ac 100644 --- a/src/layout/DefaultLayout/DefaultLayout.tsx +++ b/src/layout/DefaultLayout/DefaultLayout.tsx @@ -9,6 +9,7 @@ import PageHeader from 'components/PageHeader/PageHeader' import PageNav from 'components/PageNav/PageNav' import FeedbackCard from 'components/FeedbackCard/FeedbackCard' import Footer from 'components/Footer/Footer' +import CustomModal from 'components/CustomModal/CustomModal' const DefaultLayout = ({ displayFeedbackCard = true, @@ -53,6 +54,8 @@ const DefaultLayout = ({