From 46c56c5e6707dc334749687d808dd6f99267b196 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Mon, 11 Apr 2022 15:45:22 -0400 Subject: [PATCH 01/13] chore(tests): Add credentials table test --- src/app/SecurityPanel/SecurityPanel.tsx | 4 +- src/app/SecurityPanel/StoreJmxCredentials.tsx | 12 +- .../StoreJmxCredentials.test.tsx | 222 ++++++++++++++++++ .../StoreJmxCredentials.test.tsx.snap | 171 ++++++++++++++ 4 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 src/test/SecurityPanel/StoreJmxCredentials.test.tsx create mode 100644 src/test/SecurityPanel/__snapshots__/StoreJmxCredentials.test.tsx.snap diff --git a/src/app/SecurityPanel/SecurityPanel.tsx b/src/app/SecurityPanel/SecurityPanel.tsx index 9ba69e9ae..aaa3b88fc 100644 --- a/src/app/SecurityPanel/SecurityPanel.tsx +++ b/src/app/SecurityPanel/SecurityPanel.tsx @@ -38,11 +38,11 @@ import * as React from 'react'; import { Card, CardBody, CardTitle, Text, TextVariants } from '@patternfly/react-core'; import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage'; -import { StoreJmxCredentials } from './StoreJmxCredentials'; +import { StoreJmxCredentialsCard } from './StoreJmxCredentials'; import { ImportCertificate } from './ImportCertificate'; export const SecurityPanel = () => { - const securityCards = [ImportCertificate, StoreJmxCredentials].map((c) => ({ + const securityCards = [ImportCertificate, StoreJmxCredentialsCard].map((c) => ({ title: c.title, description: c.description, element: React.createElement(c.content, null), diff --git a/src/app/SecurityPanel/StoreJmxCredentials.tsx b/src/app/SecurityPanel/StoreJmxCredentials.tsx index 1a2c05dcd..409c9df1f 100644 --- a/src/app/SecurityPanel/StoreJmxCredentials.tsx +++ b/src/app/SecurityPanel/StoreJmxCredentials.tsx @@ -58,7 +58,7 @@ import { SecurityCard } from './SecurityPanel'; import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service'; import { LoadingView } from '@app/LoadingView/LoadingView'; -const Component = () => { +export const StoreJmxCredentials = () => { const context = React.useContext(ServiceContext); const addSubscription = useSubscriptions(); @@ -89,7 +89,7 @@ const Component = () => { }, [context, context.targets, setTargets, refreshStoredTargetsList]); React.useEffect(() => { - const sub = context.notificationChannel.messages(NotificationCategory.TargetCredentialsStored).subscribe((v) => { + const sub = context.notificationChannel.messages('TargetCredentialsStored' as NotificationCategory.TargetCredentialsStored).subscribe((v) => { const updatedTarget = targets.filter((t) => t.connectUrl === v.message.target).pop(); if(!updatedTarget) { return; @@ -100,7 +100,7 @@ const Component = () => { }, [context, context.notificationChannel, targets, setStoredTargets]); React.useEffect(() => { - const sub = context.notificationChannel.messages(NotificationCategory.TargetCredentialsDeleted).subscribe((v) => { + const sub = context.notificationChannel.messages('TargetCredentialsDeleted' as NotificationCategory.TargetCredentialsDeleted).subscribe((v) => { setStoredTargets(old => old.filter(t => t.connectUrl !== v.message.target)); }); return () => sub.unsubscribe(); @@ -197,7 +197,7 @@ const Component = () => { ); }; const targetRows = React.useMemo(() => { - return storedTargets.map((t, idx) => ); + return storedTargets.map((t: Target, idx) => ); }, [storedTargets, checkedIndices]); let content: JSX.Element; @@ -243,10 +243,10 @@ const Component = () => { ); }; -export const StoreJmxCredentials: SecurityCard = { +export const StoreJmxCredentialsCard: SecurityCard = { title: 'Store JMX Credentials', description: `Targets for which Cryostat has stored JMX credentials are listed here. If a Target JVM requires JMX authentication, Cryostat will use stored credentials when attempting to open JMX connections to the target.`, - content: Component, + content: StoreJmxCredentials, }; diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx new file mode 100644 index 000000000..e87b8a0b2 --- /dev/null +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -0,0 +1,222 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import * as React from 'react'; +import userEvent from '@testing-library/user-event'; +import renderer, { act } from 'react-test-renderer'; +import { render, screen } from '@testing-library/react'; +import { of } from 'rxjs'; + +import '@testing-library/jest-dom'; +import { StoreJmxCredentials } from '@app/SecurityPanel/StoreJmxCredentials'; +import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; +import { Target } from '@app/Shared/Services/Target.service'; +import { Modal, ModalVariant } from '@patternfly/react-core'; +import { NotificationMessage } from '@app/Shared/Services/NotificationChannel.service'; + +jest.mock('@app/SecurityPanel/CreateJmxCredentialModal', () => { + return { + CreateJmxCredentialModal: jest.fn((props) => { + return ( + + Jmx Auth Form + + ); + }), + }; +}); + +jest.mock('@app/Shared/Services/Api.service', () => { + const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; +const anotherMockTarget = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherTarget' } as Target; + return { + ApiService: jest.fn(() => { + return { + getTargetsWithStoredJmxCredentials: jest.fn() + .mockReturnValueOnce(of([mockTarget])) + .mockReturnValueOnce(of([])) + .mockReturnValueOnce(of([mockTarget])) + .mockReturnValueOnce(of([mockTarget])) + .mockReturnValueOnce(of([mockTarget, anotherMockTarget])) + .mockReturnValueOnce(of([mockTarget, anotherMockTarget])), + deleteTargetCredentials: jest.fn().mockReturnValue(of(true)), + }; + }), + }; +}); + +jest.mock('@app/Shared/Services/NotificationChannel.service', () => { + const mockNotification = {message: {target: 'service:jmx:rmi://someUrl'}} as NotificationMessage; + const anotherMockNotification = {message: {target: 'service:jmx:rmi://someUrl'}} as NotificationMessage; + + return { + NotificationChannel: jest.fn(() => { + return { + messages: jest.fn() + .mockReturnValueOnce(of()) // '' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) // 'displays empty state text when the table is empty' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) // 'opens the JMX auth modal when Add is clicked' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) // 'renders a table entry when credentials are stored' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) // 'removes a single table entry when the selected credentials are deleted' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of(mockNotification)) + .mockReturnValueOnce(of()) // 'removes all table entries when all credentials are deleted' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of(mockNotification, anotherMockNotification)), + }; + }), + }; +}); + +jest.mock('@app/Shared/Services/Target.service', () => { + return { + TargetService: jest.fn(() => { + return { + deleteCredentials: jest.fn(), + }; + }), + }; +}); + +jest.mock('@app/Shared/Services/Targets.service', () => { + const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; + + return { + TargetsService: jest.fn(() => { + return { + targets: jest.fn().mockReturnValue(of([mockTarget])), + }; + }), + }; +}); + +describe('', () => { + it('renders correctly', async () => { + let tree; + await act(async () => { + tree = renderer.create( + + + + ); + }); + expect(tree.toJSON()).toMatchSnapshot(); + }); + + it('displays empty state text when the table is empty', () => { + render( + + + + ); + + expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); + }); + + it('opens the JMX auth modal when Add is clicked', () => { + render( + + + + ); + userEvent.click(screen.getByText('Add')); + + expect(screen.getByText('CreateJmxCredentialModal')).toBeInTheDocument(); + + }); + + it('renders a table entry when credentials are stored', () => { + render( + + + + ); + + expect(screen.getByText('service:jmx:rmi://someUrl')).toBeInTheDocument(); + expect(screen.getByText('fooTarget')).toBeInTheDocument(); + + }); + + it('removes a single table entry when the selected credentials are deleted', () => { + render( + + + + ); + + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + expect(screen.getByText('fooTarget')).toBeInTheDocument(); + + userEvent.click(screen.getByLabelText('credentials-table-row-0-check')); + userEvent.click(screen.getByText('Delete')); + + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + expect(screen.getByText('fooTarget')).not.toBeInTheDocument(); + + }); + + it('removes all table entries when all credentials are deleted', () => { + render( + + + + ); + + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + expect(screen.getByText('fooTarget')).toBeInTheDocument(); + + userEvent.click(screen.getByLabelText('table-header-check-all')); + userEvent.click(screen.getByText('Delete')); + + expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); + + }); +}); diff --git a/src/test/SecurityPanel/__snapshots__/StoreJmxCredentials.test.tsx.snap b/src/test/SecurityPanel/__snapshots__/StoreJmxCredentials.test.tsx.snap new file mode 100644 index 000000000..dfb4a85b6 --- /dev/null +++ b/src/test/SecurityPanel/__snapshots__/StoreJmxCredentials.test.tsx.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` +Array [ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+ , + + + + + + + + + + + + + + + +
, +] +`; From 07b64d92610bebc7a57c376b03a797078ce12485 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Wed, 13 Apr 2022 18:18:07 -0400 Subject: [PATCH 02/13] Rearrange test order to reuse return values --- .../StoreJmxCredentials.test.tsx | 81 ++++++++----------- 1 file changed, 35 insertions(+), 46 deletions(-) diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx index e87b8a0b2..525ebc5f2 100644 --- a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -44,7 +44,6 @@ import { of } from 'rxjs'; import '@testing-library/jest-dom'; import { StoreJmxCredentials } from '@app/SecurityPanel/StoreJmxCredentials'; import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; -import { Target } from '@app/Shared/Services/Target.service'; import { Modal, ModalVariant } from '@patternfly/react-core'; import { NotificationMessage } from '@app/Shared/Services/NotificationChannel.service'; @@ -68,11 +67,11 @@ const anotherMockTarget = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: ' return { getTargetsWithStoredJmxCredentials: jest.fn() .mockReturnValueOnce(of([mockTarget])) + .mockReturnValueOnce(of([mockTarget, anotherMockTarget])) + .mockReturnValueOnce(of([mockTarget, anotherMockTarget])) .mockReturnValueOnce(of([])) .mockReturnValueOnce(of([mockTarget])) - .mockReturnValueOnce(of([mockTarget])) - .mockReturnValueOnce(of([mockTarget, anotherMockTarget])) - .mockReturnValueOnce(of([mockTarget, anotherMockTarget])), + .mockReturnValueOnce(of([mockTarget])), deleteTargetCredentials: jest.fn().mockReturnValue(of(true)), }; }), @@ -87,30 +86,20 @@ jest.mock('@app/Shared/Services/NotificationChannel.service', () => { NotificationChannel: jest.fn(() => { return { messages: jest.fn() - .mockReturnValueOnce(of()) // '' - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) // 'displays empty state text when the table is empty' - .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) // snapshot .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) // 'opens the JMX auth modal when Add is clicked' .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) // 'renders a table entry when credentials are stored' - .mockReturnValueOnce(of()) + + .mockReturnValueOnce(of(mockNotification)) // 'removes a single table entry when the selected credentials are deleted' .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) // 'removes a single table entry when the selected credentials are deleted' .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of(mockNotification)) + .mockReturnValueOnce(of()) // 'removes all table entries when all credentials are deleted' - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of(mockNotification, anotherMockNotification)), + .mockReturnValueOnce(of(anotherMockNotification)) + .mockReturnValueOnce(of(mockNotification)) //works for one target but not two + .mockReturnValue(of()), }; }), }; @@ -151,72 +140,72 @@ describe('', () => { expect(tree.toJSON()).toMatchSnapshot(); }); - it('displays empty state text when the table is empty', () => { + it('removes the correct table entry when deleting one credential', () => { render( ); - expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); + expect(screen.getByText('fooTarget')).toBeInTheDocument(); + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + + userEvent.click(screen.getByLabelText('credentials-table-row-0-check')); + userEvent.click(screen.getByText('Delete')); + + expect(screen.queryByText('fooTarget')).not.toBeInTheDocument(); + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + }); - it('opens the JMX auth modal when Add is clicked', () => { + it('removes all table entries when all credentials are deleted', () => { render( ); - userEvent.click(screen.getByText('Add')); - expect(screen.getByText('CreateJmxCredentialModal')).toBeInTheDocument(); + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + expect(screen.queryByText('fooTarget')).toBeInTheDocument(); + + userEvent.click(screen.getByLabelText('table-header-check-all')); + userEvent.click(screen.getByText('Delete')); + + expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); }); - it('renders a table entry when credentials are stored', () => { + it('displays empty state text when the table is empty', () => { render( ); - expect(screen.getByText('service:jmx:rmi://someUrl')).toBeInTheDocument(); - expect(screen.getByText('fooTarget')).toBeInTheDocument(); - + expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); }); - it('removes a single table entry when the selected credentials are deleted', () => { + it('opens the JMX auth modal when Add is clicked', () => { render( ); + userEvent.click(screen.getByText('Add')); - expect(screen.getByText('anotherTarget')).toBeInTheDocument(); - expect(screen.getByText('fooTarget')).toBeInTheDocument(); - - userEvent.click(screen.getByLabelText('credentials-table-row-0-check')); - userEvent.click(screen.getByText('Delete')); - - expect(screen.getByText('anotherTarget')).toBeInTheDocument(); - expect(screen.getByText('fooTarget')).not.toBeInTheDocument(); + expect(screen.getByText('CreateJmxCredentialModal')).toBeInTheDocument(); }); - it('removes all table entries when all credentials are deleted', () => { + it('renders a table entry when credentials are stored', () => { render( ); - expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + expect(screen.getByText('service:jmx:rmi://someUrl')).toBeInTheDocument(); expect(screen.getByText('fooTarget')).toBeInTheDocument(); - userEvent.click(screen.getByLabelText('table-header-check-all')); - userEvent.click(screen.getByText('Delete')); - - expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); - }); }); From f196ed414683da8d0c4f0a0ba327c48ddfa186a8 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Wed, 13 Apr 2022 18:24:44 -0400 Subject: [PATCH 03/13] Require targetService imports --- src/test/SecurityPanel/StoreJmxCredentials.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx index 525ebc5f2..b901f383a 100644 --- a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -46,6 +46,7 @@ import { StoreJmxCredentials } from '@app/SecurityPanel/StoreJmxCredentials'; import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; import { Modal, ModalVariant } from '@patternfly/react-core'; import { NotificationMessage } from '@app/Shared/Services/NotificationChannel.service'; +import { Target } from '@app/Shared/Services/Target.service'; jest.mock('@app/SecurityPanel/CreateJmxCredentialModal', () => { return { @@ -107,6 +108,7 @@ jest.mock('@app/Shared/Services/NotificationChannel.service', () => { jest.mock('@app/Shared/Services/Target.service', () => { return { + ...jest.requireActual('@app/Shared/Services/Target.service'), TargetService: jest.fn(() => { return { deleteCredentials: jest.fn(), From a1bf041d198202d622e1342c3363a751274a6bec Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Thu, 14 Apr 2022 11:41:34 -0400 Subject: [PATCH 04/13] Add remaining tests --- .../StoreJmxCredentials.test.tsx | 156 ++++++++++++------ 1 file changed, 106 insertions(+), 50 deletions(-) diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx index b901f383a..e1367d2e4 100644 --- a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -52,7 +52,13 @@ jest.mock('@app/SecurityPanel/CreateJmxCredentialModal', () => { return { CreateJmxCredentialModal: jest.fn((props) => { return ( - + Jmx Auth Form ); @@ -61,46 +67,63 @@ jest.mock('@app/SecurityPanel/CreateJmxCredentialModal', () => { }); jest.mock('@app/Shared/Services/Api.service', () => { - const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; -const anotherMockTarget = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherTarget' } as Target; + const MOCK_TARGET = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; + const ANOTHER_MOCK_TARGET = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherTarget' } as Target; + return { ApiService: jest.fn(() => { return { - getTargetsWithStoredJmxCredentials: jest.fn() - .mockReturnValueOnce(of([mockTarget])) - .mockReturnValueOnce(of([mockTarget, anotherMockTarget])) - .mockReturnValueOnce(of([mockTarget, anotherMockTarget])) - .mockReturnValueOnce(of([])) - .mockReturnValueOnce(of([mockTarget])) - .mockReturnValueOnce(of([mockTarget])), - deleteTargetCredentials: jest.fn().mockReturnValue(of(true)), + getTargetsWithStoredJmxCredentials: jest + .fn() + .mockReturnValueOnce(of([MOCK_TARGET])) + .mockReturnValueOnce(of([])) + .mockReturnValueOnce(of([])) + .mockReturnValueOnce(of([])) + .mockReturnValueOnce(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])) + .mockReturnValueOnce(of([MOCK_TARGET])) + .mockReturnValueOnce(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])) + .mockReturnValueOnce(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])), + deleteTargetCredentials: jest.fn(() => { + return of(true); + }), }; }), }; }); jest.mock('@app/Shared/Services/NotificationChannel.service', () => { - const mockNotification = {message: {target: 'service:jmx:rmi://someUrl'}} as NotificationMessage; - const anotherMockNotification = {message: {target: 'service:jmx:rmi://someUrl'}} as NotificationMessage; + const mockNotification = { message: { target: 'service:jmx:rmi://someUrl' } } as NotificationMessage; return { NotificationChannel: jest.fn(() => { return { - messages: jest.fn() - .mockReturnValueOnce(of()) // snapshot - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - - .mockReturnValueOnce(of(mockNotification)) // 'removes a single table entry when the selected credentials are deleted' - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - - .mockReturnValueOnce(of()) // 'removes all table entries when all credentials are deleted' - .mockReturnValueOnce(of(anotherMockNotification)) - .mockReturnValueOnce(of(mockNotification)) //works for one target but not two - .mockReturnValue(of()), + messages: jest + .fn() + .mockReturnValueOnce(of()) // 'renders correctly' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + + .mockReturnValueOnce(of()) // 'displays empty state text when the table is empty' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + + .mockReturnValueOnce(of()) // 'opens the JMX auth modal when Add is clicked' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of()) + + .mockReturnValueOnce(of()) // 'adds the correct table entry when a stored notification is received' + .mockReturnValueOnce(of()) + .mockReturnValueOnce(of(mockNotification)) + + .mockReturnValueOnce(of()) // 'removes the correct table entry when a deletion notification is received' + .mockReturnValueOnce(of(mockNotification)) + .mockReturnValueOnce(of()) + + .mockReturnValueOnce(of()) // 'renders an empty table after receiving deletion notifications for all credentials' + .mockReturnValueOnce(of(mockNotification)) + .mockReturnValueOnce(of()) + + .mockReturnValue(of()), // all other tests }; }), }; @@ -118,18 +141,22 @@ jest.mock('@app/Shared/Services/Target.service', () => { }); jest.mock('@app/Shared/Services/Targets.service', () => { - const mockTarget = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; + const MOCK_TARGET = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; + const ANOTHER_MOCK_TARGET = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherTarget' } as Target; return { TargetsService: jest.fn(() => { return { - targets: jest.fn().mockReturnValue(of([mockTarget])), + targets: jest.fn().mockReturnValue(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])), }; }), }; }); describe('', () => { + const MOCK_TARGET = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; + const ANOTHER_MOCK_TARGET = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherTarget' } as Target; + it('renders correctly', async () => { let tree; await act(async () => { @@ -142,72 +169,101 @@ describe('', () => { expect(tree.toJSON()).toMatchSnapshot(); }); - it('removes the correct table entry when deleting one credential', () => { + it('displays empty state text when the table is empty', () => { render( ); - expect(screen.getByText('fooTarget')).toBeInTheDocument(); - expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); + }); - userEvent.click(screen.getByLabelText('credentials-table-row-0-check')); - userEvent.click(screen.getByText('Delete')); + it('opens the JMX auth modal when Add is clicked', () => { + render( + + + + ); + userEvent.click(screen.getByText('Add')); + expect(screen.getByText('CreateJmxCredentialModal')).toBeInTheDocument(); - expect(screen.queryByText('fooTarget')).not.toBeInTheDocument(); - expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + screen.debug(); + userEvent.click(screen.getByLabelText('Close')); + expect(screen.queryByText('CreateJmxCredentialModal')).not.toBeInTheDocument(); }); - it('removes all table entries when all credentials are deleted', () => { + it('adds the correct table entry when a stored notification is received', () => { render( ); - expect(screen.getByText('anotherTarget')).toBeInTheDocument(); - expect(screen.queryByText('fooTarget')).toBeInTheDocument(); - - userEvent.click(screen.getByLabelText('table-header-check-all')); - userEvent.click(screen.getByText('Delete')); + expect(screen.getByText('fooTarget')).toBeInTheDocument(); + }); - expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); + it('removes the correct table entry when a deletion notification is received', () => { + render( + + + + ); + expect(screen.queryByText('fooTarget')).not.toBeInTheDocument(); + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); }); - it('displays empty state text when the table is empty', () => { + it('renders an empty table after receiving deletion notifications for all credentials', () => { render( ); + expect(screen.queryByText('fooTarget')).not.toBeInTheDocument(); expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); }); - it('opens the JMX auth modal when Add is clicked', () => { + it('makes a delete request when deleting one credential', () => { render( ); - userEvent.click(screen.getByText('Add')); - expect(screen.getByText('CreateJmxCredentialModal')).toBeInTheDocument(); + expect(screen.getByText('fooTarget')).toBeInTheDocument(); + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + userEvent.click(screen.getByLabelText('credentials-table-row-0-check')); + userEvent.click(screen.getByText('Delete')); + + const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteTargetCredentials'); + + expect(deleteRequestSpy).toHaveBeenCalledTimes(1); + expect(deleteRequestSpy).toHaveBeenCalledWith(MOCK_TARGET); }); - it('renders a table entry when credentials are stored', () => { + it('makes multiple delete requests when all credentials are deleted at once', () => { render( ); - expect(screen.getByText('service:jmx:rmi://someUrl')).toBeInTheDocument(); + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); expect(screen.getByText('fooTarget')).toBeInTheDocument(); + const checkboxes = screen.getAllByRole('checkbox'); + const selectAllCheck = checkboxes[0]; + userEvent.click(selectAllCheck); + userEvent.click(screen.getByText('Delete')); + + const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteTargetCredentials'); + + expect(deleteRequestSpy).toHaveBeenCalledTimes(2); + expect(deleteRequestSpy).nthCalledWith(1, MOCK_TARGET); + expect(deleteRequestSpy).nthCalledWith(2, ANOTHER_MOCK_TARGET); }); }); From eca32bf257f55b44b1b062aca43d0ebf0287aaf7 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Wed, 20 Apr 2022 16:58:09 -0400 Subject: [PATCH 05/13] Swap test order to reduce message() mocks --- .../StoreJmxCredentials.test.tsx | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx index e1367d2e4..92b6a7576 100644 --- a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -77,10 +77,10 @@ jest.mock('@app/Shared/Services/Api.service', () => { .fn() .mockReturnValueOnce(of([MOCK_TARGET])) .mockReturnValueOnce(of([])) - .mockReturnValueOnce(of([])) - .mockReturnValueOnce(of([])) .mockReturnValueOnce(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])) .mockReturnValueOnce(of([MOCK_TARGET])) + .mockReturnValueOnce(of([])) + .mockReturnValueOnce(of([])) .mockReturnValueOnce(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])) .mockReturnValueOnce(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])), deleteTargetCredentials: jest.fn(() => { @@ -103,14 +103,6 @@ jest.mock('@app/Shared/Services/NotificationChannel.service', () => { .mockReturnValueOnce(of()) .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) // 'displays empty state text when the table is empty' - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - - .mockReturnValueOnce(of()) // 'opens the JMX auth modal when Add is clicked' - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) - .mockReturnValueOnce(of()) // 'adds the correct table entry when a stored notification is received' .mockReturnValueOnce(of()) .mockReturnValueOnce(of(mockNotification)) @@ -169,61 +161,59 @@ describe('', () => { expect(tree.toJSON()).toMatchSnapshot(); }); - it('displays empty state text when the table is empty', () => { + it('adds the correct table entry when a stored notification is received', () => { render( ); - expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); + expect(screen.getByText('fooTarget')).toBeInTheDocument(); }); - it('opens the JMX auth modal when Add is clicked', () => { + it('removes the correct table entry when a deletion notification is received', () => { render( ); - userEvent.click(screen.getByText('Add')); - expect(screen.getByText('CreateJmxCredentialModal')).toBeInTheDocument(); - - screen.debug(); - userEvent.click(screen.getByLabelText('Close')); - expect(screen.queryByText('CreateJmxCredentialModal')).not.toBeInTheDocument(); + expect(screen.queryByText('fooTarget')).not.toBeInTheDocument(); + expect(screen.getByText('anotherTarget')).toBeInTheDocument(); }); - it('adds the correct table entry when a stored notification is received', () => { + it('renders an empty table after receiving deletion notifications for all credentials', () => { render( ); - expect(screen.getByText('fooTarget')).toBeInTheDocument(); + expect(screen.queryByText('fooTarget')).not.toBeInTheDocument(); + expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); }); - it('removes the correct table entry when a deletion notification is received', () => { + it('displays empty state text when the table is empty', () => { render( ); - expect(screen.queryByText('fooTarget')).not.toBeInTheDocument(); - expect(screen.getByText('anotherTarget')).toBeInTheDocument(); + expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); }); - it('renders an empty table after receiving deletion notifications for all credentials', () => { + it('opens the JMX auth modal when Add is clicked', () => { render( ); + userEvent.click(screen.getByText('Add')); + expect(screen.getByText('CreateJmxCredentialModal')).toBeInTheDocument(); - expect(screen.queryByText('fooTarget')).not.toBeInTheDocument(); - expect(screen.getByText('No Stored Credentials')).toBeInTheDocument(); + userEvent.click(screen.getByLabelText('Close')); + expect(screen.queryByText('CreateJmxCredentialModal')).not.toBeInTheDocument(); }); it('makes a delete request when deleting one credential', () => { From a7437215f399a01fadf128fdd7af7c25626ac690 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Wed, 20 Apr 2022 17:24:20 -0400 Subject: [PATCH 06/13] fixup! Remove label applied to wrong component --- .../__snapshots__/StoreJmxCredentials.test.tsx.snap | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/SecurityPanel/__snapshots__/StoreJmxCredentials.test.tsx.snap b/src/test/SecurityPanel/__snapshots__/StoreJmxCredentials.test.tsx.snap index dfb4a85b6..171198255 100644 --- a/src/test/SecurityPanel/__snapshots__/StoreJmxCredentials.test.tsx.snap +++ b/src/test/SecurityPanel/__snapshots__/StoreJmxCredentials.test.tsx.snap @@ -90,7 +90,6 @@ Array [ hidden={false} > Date: Thu, 21 Apr 2022 12:29:35 -0400 Subject: [PATCH 07/13] Fix messageKeys import --- src/app/SecurityPanel/StoreJmxCredentials.tsx | 4 ++-- src/test/SecurityPanel/StoreJmxCredentials.test.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/SecurityPanel/StoreJmxCredentials.tsx b/src/app/SecurityPanel/StoreJmxCredentials.tsx index 409c9df1f..e235fb959 100644 --- a/src/app/SecurityPanel/StoreJmxCredentials.tsx +++ b/src/app/SecurityPanel/StoreJmxCredentials.tsx @@ -89,7 +89,7 @@ export const StoreJmxCredentials = () => { }, [context, context.targets, setTargets, refreshStoredTargetsList]); React.useEffect(() => { - const sub = context.notificationChannel.messages('TargetCredentialsStored' as NotificationCategory.TargetCredentialsStored).subscribe((v) => { + const sub = context.notificationChannel.messages(NotificationCategory.TargetCredentialsStored).subscribe((v) => { const updatedTarget = targets.filter((t) => t.connectUrl === v.message.target).pop(); if(!updatedTarget) { return; @@ -100,7 +100,7 @@ export const StoreJmxCredentials = () => { }, [context, context.notificationChannel, targets, setStoredTargets]); React.useEffect(() => { - const sub = context.notificationChannel.messages('TargetCredentialsDeleted' as NotificationCategory.TargetCredentialsDeleted).subscribe((v) => { + const sub = context.notificationChannel.messages(NotificationCategory.TargetCredentialsDeleted).subscribe((v) => { setStoredTargets(old => old.filter(t => t.connectUrl !== v.message.target)); }); return () => sub.unsubscribe(); diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx index 92b6a7576..f2783b95b 100644 --- a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -95,6 +95,7 @@ jest.mock('@app/Shared/Services/NotificationChannel.service', () => { const mockNotification = { message: { target: 'service:jmx:rmi://someUrl' } } as NotificationMessage; return { + ...jest.requireActual('@app/Shared/Services/NotificationChannel.service'), NotificationChannel: jest.fn(() => { return { messages: jest From a4039a358c399b4717ed368d3748a2df38512ad5 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Mon, 25 Apr 2022 13:39:27 -0400 Subject: [PATCH 08/13] Add doMock implementation Fix import filename Fix import ordering Remove uneeded deps --- .../StoreJmxCredentials.test.tsx | 74 +++++++++---------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx index f2783b95b..ff3cdcd08 100644 --- a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -40,13 +40,14 @@ import userEvent from '@testing-library/user-event'; import renderer, { act } from 'react-test-renderer'; import { render, screen } from '@testing-library/react'; import { of } from 'rxjs'; +import { Target } from '@app/Shared/Services/Target.service'; import '@testing-library/jest-dom'; -import { StoreJmxCredentials } from '@app/SecurityPanel/StoreJmxCredentials'; -import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; import { Modal, ModalVariant } from '@patternfly/react-core'; import { NotificationMessage } from '@app/Shared/Services/NotificationChannel.service'; -import { Target } from '@app/Shared/Services/Target.service'; + +const mockTarget: Target = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' }; +const mockAnotherTarget: Target = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherTarget' }; jest.mock('@app/SecurityPanel/CreateJmxCredentialModal', () => { return { @@ -66,34 +67,8 @@ jest.mock('@app/SecurityPanel/CreateJmxCredentialModal', () => { }; }); -jest.mock('@app/Shared/Services/Api.service', () => { - const MOCK_TARGET = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; - const ANOTHER_MOCK_TARGET = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherTarget' } as Target; - - return { - ApiService: jest.fn(() => { - return { - getTargetsWithStoredJmxCredentials: jest - .fn() - .mockReturnValueOnce(of([MOCK_TARGET])) - .mockReturnValueOnce(of([])) - .mockReturnValueOnce(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])) - .mockReturnValueOnce(of([MOCK_TARGET])) - .mockReturnValueOnce(of([])) - .mockReturnValueOnce(of([])) - .mockReturnValueOnce(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])) - .mockReturnValueOnce(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])), - deleteTargetCredentials: jest.fn(() => { - return of(true); - }), - }; - }), - }; -}); - jest.mock('@app/Shared/Services/NotificationChannel.service', () => { const mockNotification = { message: { target: 'service:jmx:rmi://someUrl' } } as NotificationMessage; - return { ...jest.requireActual('@app/Shared/Services/NotificationChannel.service'), NotificationChannel: jest.fn(() => { @@ -133,23 +108,44 @@ jest.mock('@app/Shared/Services/Target.service', () => { }; }); -jest.mock('@app/Shared/Services/Targets.service', () => { - const MOCK_TARGET = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; - const ANOTHER_MOCK_TARGET = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherTarget' } as Target; +jest.doMock('@app/Shared/Services/Api.service', () => { + return { + ...jest.requireActual('@app/Shared/Services/Api.service'), + ApiService: jest.fn(() => { + return { + getTargetsWithStoredJmxCredentials: jest + .fn() + .mockReturnValueOnce(of([mockTarget])) + .mockReturnValueOnce(of([])) + .mockReturnValueOnce(of([mockTarget, mockAnotherTarget])) + .mockReturnValueOnce(of([mockTarget])) + .mockReturnValueOnce(of([])) + .mockReturnValueOnce(of([])) + .mockReturnValueOnce(of([mockTarget, mockAnotherTarget])) + .mockReturnValueOnce(of([mockTarget, mockAnotherTarget])), + deleteTargetCredentials: jest.fn(() => { + return of(true); + }), + }; + }), + }; +}); +jest.doMock('@app/Shared/Services/Targets.service', () => { return { + ...jest.requireActual('@app/Shared/Services/Targets.service'), TargetsService: jest.fn(() => { return { - targets: jest.fn().mockReturnValue(of([MOCK_TARGET, ANOTHER_MOCK_TARGET])), + targets: jest.fn().mockReturnValue(of([mockTarget, mockAnotherTarget])), }; }), }; }); -describe('', () => { - const MOCK_TARGET = { connectUrl: 'service:jmx:rmi://someUrl', alias: 'fooTarget' } as Target; - const ANOTHER_MOCK_TARGET = { connectUrl: 'service:jmx:rmi://anotherUrl', alias: 'anotherTarget' } as Target; +import { StoreJmxCredentials } from '@app/SecurityPanel/StoreJmxCredentials'; +import { ServiceContext, defaultServices } from '@app/Shared/Services/Services'; +describe('', () => { it('renders correctly', async () => { let tree; await act(async () => { @@ -233,7 +229,7 @@ describe('', () => { const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteTargetCredentials'); expect(deleteRequestSpy).toHaveBeenCalledTimes(1); - expect(deleteRequestSpy).toHaveBeenCalledWith(MOCK_TARGET); + expect(deleteRequestSpy).toHaveBeenCalledWith(mockTarget); }); it('makes multiple delete requests when all credentials are deleted at once', () => { @@ -254,7 +250,7 @@ describe('', () => { const deleteRequestSpy = jest.spyOn(defaultServices.api, 'deleteTargetCredentials'); expect(deleteRequestSpy).toHaveBeenCalledTimes(2); - expect(deleteRequestSpy).nthCalledWith(1, MOCK_TARGET); - expect(deleteRequestSpy).nthCalledWith(2, ANOTHER_MOCK_TARGET); + expect(deleteRequestSpy).nthCalledWith(1, mockTarget); + expect(deleteRequestSpy).nthCalledWith(2, mockAnotherTarget); }); }); From c0f64b5442407125846fec95c0d26bc3fae4c021 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Mon, 25 Apr 2022 17:27:27 -0400 Subject: [PATCH 09/13] Add doMock import ordering tip in readme --- TESTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TESTING.md b/TESTING.md index 74e555749..877160e6b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -50,6 +50,8 @@ The decision to mock out a component during testing should adhere to RTL's guidi * Make sure to import the component under test last. In Jest, any `jest.mock` calls are automatically hoisted to the top of the file, above the imports. This ensures that when modules are imported, Jest knows to replace the real implementations with the mocked versions. However, the actual mock implementation code isn’t processed until the component under test is imported, which is why it’s important to do this import last so that any imported modules used inside the implementations will not end up undefined. +* If you want to return mock constants that you defined in the file scope while writing `jest.mock` calls, you will need to replace `jest.mock` with [`jest.doMock`](https://jestjs.io/docs/jest-object#jestdomockmodulename-factory-options) to prevent Jest from hoisting the `jest.mock` call before your constant is defined. However, you will need to import your component(s) under test after all of your `jest.doMock` calls to ensure that Jest uses your mocked function calls. For example, if you are mocking the `Api.Service` with `jest.doMock` for your component under test called `MyComponent`, you will need to move `import { MyComponent } from '@app/Path/To/MyComponent';` as well as `import { ServiceContext, defaultServices } from '@app/Shared/Services/Services';` after all of your `jest.doMock` calls. + * Use [`jest.requireActual`](https://jestjs.io/docs/jest-object#jestrequireactualmodulename) when you need the actual implementation of a mocked module. It can also be used to partially mock modules, allowing you to pick and choose which functions you want to mock or leave untouched. * Unlike `jest.mock`, [`jest.doMock`](https://jestjs.io/docs/jest-object#jestdomockmodulename-factory-options) calls are not hoisted to the top of files. This is useful for when you want to mock a module differently across tests in the same file. From ec21b23f253dbe39c90b2d24ee781cfd986cc1d7 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Mon, 25 Apr 2022 17:36:51 -0400 Subject: [PATCH 10/13] Simplify function mock --- src/test/SecurityPanel/StoreJmxCredentials.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx index ff3cdcd08..ac83a0778 100644 --- a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -136,7 +136,9 @@ jest.doMock('@app/Shared/Services/Targets.service', () => { ...jest.requireActual('@app/Shared/Services/Targets.service'), TargetsService: jest.fn(() => { return { - targets: jest.fn().mockReturnValue(of([mockTarget, mockAnotherTarget])), + targets: jest.fn(() => { + return of([mockTarget, mockAnotherTarget]); + }), }; }), }; From 238b0252d491ef3cb1b1ea172abd582ccceb0ca3 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Mon, 25 Apr 2022 17:37:34 -0400 Subject: [PATCH 11/13] Remove uneeded require --- src/test/SecurityPanel/StoreJmxCredentials.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx index ac83a0778..53bc2d1b1 100644 --- a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -110,7 +110,6 @@ jest.mock('@app/Shared/Services/Target.service', () => { jest.doMock('@app/Shared/Services/Api.service', () => { return { - ...jest.requireActual('@app/Shared/Services/Api.service'), ApiService: jest.fn(() => { return { getTargetsWithStoredJmxCredentials: jest @@ -133,7 +132,6 @@ jest.doMock('@app/Shared/Services/Api.service', () => { jest.doMock('@app/Shared/Services/Targets.service', () => { return { - ...jest.requireActual('@app/Shared/Services/Targets.service'), TargetsService: jest.fn(() => { return { targets: jest.fn(() => { From e95d9bbe43415f7947c8fd0a0bb93dcc79851dd6 Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Mon, 25 Apr 2022 17:43:56 -0400 Subject: [PATCH 12/13] Revert to jest.mock --- TESTING.md | 2 +- src/test/SecurityPanel/StoreJmxCredentials.test.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TESTING.md b/TESTING.md index 877160e6b..92debc44b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -50,7 +50,7 @@ The decision to mock out a component during testing should adhere to RTL's guidi * Make sure to import the component under test last. In Jest, any `jest.mock` calls are automatically hoisted to the top of the file, above the imports. This ensures that when modules are imported, Jest knows to replace the real implementations with the mocked versions. However, the actual mock implementation code isn’t processed until the component under test is imported, which is why it’s important to do this import last so that any imported modules used inside the implementations will not end up undefined. -* If you want to return mock constants that you defined in the file scope while writing `jest.mock` calls, you will need to replace `jest.mock` with [`jest.doMock`](https://jestjs.io/docs/jest-object#jestdomockmodulename-factory-options) to prevent Jest from hoisting the `jest.mock` call before your constant is defined. However, you will need to import your component(s) under test after all of your `jest.doMock` calls to ensure that Jest uses your mocked function calls. For example, if you are mocking the `Api.Service` with `jest.doMock` for your component under test called `MyComponent`, you will need to move `import { MyComponent } from '@app/Path/To/MyComponent';` as well as `import { ServiceContext, defaultServices } from '@app/Shared/Services/Services';` after all of your `jest.doMock` calls. +* If you want to return mock constants that you defined in the file scope while writing `jest.mock` calls, you will need to import your components under test after the `jest.mock` call to prevent Jest from invoking the `jest.mock` calls before your constant is defined. For example, if you are mocking the `Api.Service` with `jest.mock` for your component under test called `MyComponent`, you will need to move `import { MyComponent } from '@app/Path/To/MyComponent';` as well as `import { ServiceContext, defaultServices } from '@app/Shared/Services/Services';` after all of your `jest.mock` calls. * Use [`jest.requireActual`](https://jestjs.io/docs/jest-object#jestrequireactualmodulename) when you need the actual implementation of a mocked module. It can also be used to partially mock modules, allowing you to pick and choose which functions you want to mock or leave untouched. diff --git a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx index 53bc2d1b1..1b81283ff 100644 --- a/src/test/SecurityPanel/StoreJmxCredentials.test.tsx +++ b/src/test/SecurityPanel/StoreJmxCredentials.test.tsx @@ -108,7 +108,7 @@ jest.mock('@app/Shared/Services/Target.service', () => { }; }); -jest.doMock('@app/Shared/Services/Api.service', () => { +jest.mock('@app/Shared/Services/Api.service', () => { return { ApiService: jest.fn(() => { return { @@ -130,7 +130,7 @@ jest.doMock('@app/Shared/Services/Api.service', () => { }; }); -jest.doMock('@app/Shared/Services/Targets.service', () => { +jest.mock('@app/Shared/Services/Targets.service', () => { return { TargetsService: jest.fn(() => { return { From 9afc7ff4274a94a3b670d4c97db6414ed1b604aa Mon Sep 17 00:00:00 2001 From: Janelle Law Date: Mon, 25 Apr 2022 17:59:56 -0400 Subject: [PATCH 13/13] Clarify readme tip --- TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TESTING.md b/TESTING.md index 92debc44b..9170a7b3b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -50,7 +50,7 @@ The decision to mock out a component during testing should adhere to RTL's guidi * Make sure to import the component under test last. In Jest, any `jest.mock` calls are automatically hoisted to the top of the file, above the imports. This ensures that when modules are imported, Jest knows to replace the real implementations with the mocked versions. However, the actual mock implementation code isn’t processed until the component under test is imported, which is why it’s important to do this import last so that any imported modules used inside the implementations will not end up undefined. -* If you want to return mock constants that you defined in the file scope while writing `jest.mock` calls, you will need to import your components under test after the `jest.mock` call to prevent Jest from invoking the `jest.mock` calls before your constant is defined. For example, if you are mocking the `Api.Service` with `jest.mock` for your component under test called `MyComponent`, you will need to move `import { MyComponent } from '@app/Path/To/MyComponent';` as well as `import { ServiceContext, defaultServices } from '@app/Shared/Services/Services';` after all of your `jest.mock` calls. +* If you want to use mocked variables defined outside the scope of the `jest.mock` definition, you will need to import your components under test after the `jest.mock` call to prevent Jest from invoking the `jest.mock` calls before your variable is defined. For example, if you are mocking the `Api.Service` with `jest.mock` for your component under test called `MyComponent`, you will need to move `import { MyComponent } from '@app/Path/To/MyComponent';` as well as `import { ServiceContext, defaultServices } from '@app/Shared/Services/Services';` after all of your `jest.mock` calls. * Use [`jest.requireActual`](https://jestjs.io/docs/jest-object#jestrequireactualmodulename) when you need the actual implementation of a mocked module. It can also be used to partially mock modules, allowing you to pick and choose which functions you want to mock or leave untouched.