From 10b1d625092ab01787b7bd5539210a22fa2c86bb Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 22 May 2024 18:51:50 +0000 Subject: [PATCH 01/13] pin @testing-library/dom [URO-209] multilple out-of-date transitiive dependency upon testing-library/dom leads to errors with user-event. see https://github.com/testing-library/user-event/issues/1104#issuecomment-1461658418 --- package-lock.json | 98 ++++++----------------------------------------- package.json | 3 ++ 2 files changed, 14 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a2df255..36885c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10501,22 +10501,22 @@ } }, "node_modules/@testing-library/dom": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.0.tgz", - "integrity": "sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^5.0.0", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", + "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "engines": { - "node": ">=12" + "node": ">=14" } }, "node_modules/@testing-library/dom/node_modules/ansi-styles": { @@ -10656,83 +10656,6 @@ "react-dom": "^18.0.0" } }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.0.tgz", - "integrity": "sha512-Dffe68pGwI6WlLRYR2I0piIkyole9cSBH5jGQKCGMRpHW5RHCqAUaqc2Kv0tUyd4dU4DLPKhJIjyKOnjv4tuUw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "^5.0.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/react/node_modules/@types/aria-query": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", - "dev": true - }, - "node_modules/@testing-library/react/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/react/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/react/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/user-event": { "version": "14.4.3", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz", @@ -10818,9 +10741,10 @@ } }, "node_modules/@types/aria-query": { - "version": "4.2.2", - "dev": true, - "license": "MIT" + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true }, "node_modules/@types/babel__core": { "version": "7.1.16", diff --git a/package.json b/package.json index 8cd54391..72999de4 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,9 @@ "stylelint-config-standard": "^22.0.0", "webpack": "^5.75.0" }, + "overrides": { + "@testing-library/dom": "^9.0.1" + }, "engines": { "node": ">=20.11.1" }, From 167fb5fa93d1523f556d1888a6727757f11c697f Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 22 May 2024 19:06:02 +0000 Subject: [PATCH 02/13] add "link" endpoint [URO-209] wip - it doesn't actually link yet --- src/app/Routes.tsx | 4 + .../orcidlink/CreateLink/index.test.tsx | 135 ++++++++++++++ src/features/orcidlink/CreateLink/index.tsx | 174 ++++++++++++++++++ 3 files changed, 313 insertions(+) create mode 100644 src/features/orcidlink/CreateLink/index.test.tsx create mode 100644 src/features/orcidlink/CreateLink/index.tsx diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index 8482b84f..a475074d 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -26,6 +26,7 @@ import { usePageTracking, } from '../common/hooks'; import ORCIDLinkFeature from '../features/orcidlink'; +import ORCIDLinkCreateLink from '../features/orcidlink/CreateLink'; export const LOGIN_ROUTE = '/legacy/login'; export const ROOT_REDIRECT_ROUTE = '/narratives'; @@ -83,6 +84,9 @@ const Routes: FC = () => { } />} /> + + } />} /> + {/* IFrame Fallback Routes */} diff --git a/src/features/orcidlink/CreateLink/index.test.tsx b/src/features/orcidlink/CreateLink/index.test.tsx new file mode 100644 index 00000000..572fb967 --- /dev/null +++ b/src/features/orcidlink/CreateLink/index.test.tsx @@ -0,0 +1,135 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Provider } from 'react-redux'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { createTestStore } from '../../../app/store'; +import { INITIAL_STORE_STATE } from '../test/data'; +import CreateLinkIndex from './index'; + +describe('The CreateLink component', () => { + const user = userEvent.setup(); + let debugLogSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + }); + beforeEach(() => { + debugLogSpy = jest.spyOn(console, 'debug'); + }); + + async function expectAccordion( + container: HTMLElement, + titleText: string, + contentText: string + ) { + expect(container).toHaveTextContent(titleText); + + const faq1Content = await screen.findByText(new RegExp(contentText), { + exact: false, + }); + + expect(faq1Content).not.toBeVisible(); + + const faq1Title = await screen.findByText(new RegExp(titleText), { + exact: false, + }); + await user.click(faq1Title); + + await waitFor(() => { + expect(faq1Content).toBeVisible(); + }); + } + + it('renders placeholder content', () => { + const { container } = render( + + + + + + ); + + expect(container).toHaveTextContent('Create Your KBase ORCID® Link'); + expect(container).toHaveTextContent('FAQs'); + expect(document.title).toBe('KBase: ORCID Link - Create Link'); + }); + + it('cancel button returns to the ORCID Link home page', async () => { + const user = userEvent.setup(); + let fakeHomeCalled = false; + function FakeHome() { + fakeHomeCalled = true; + return
FAKE HOME
; + } + const { container } = render( + + + + } /> + } /> + + + + ); + + expect(container).toHaveTextContent('Create Your KBase ORCID® Link'); + expect(container).toHaveTextContent('FAQs'); + expect(document.title).toBe('KBase: ORCID Link - Create Link'); + + const cancelButton = await screen.findByText('Cancel'); + await user.click(cancelButton); + + await waitFor(() => { + expect(fakeHomeCalled).toBe(true); + }); + }); + + it('cancel button returns to the ORCID Link home page', async () => { + const user = userEvent.setup(); + const { container } = render( + + + + } /> + + + + ); + + expect(container).toHaveTextContent('Create Your KBase ORCID® Link'); + expect(container).toHaveTextContent('FAQs'); + expect(document.title).toBe('KBase: ORCID Link - Create Link'); + + const continueButton = await screen.findByText('Continue to ORCID®'); + await user.click(continueButton); + + await waitFor(() => { + expect(debugLogSpy).toHaveBeenCalledWith( + 'WILL START THE LINKING PROCESS' + ); + }); + }); + + it('faq accordions are present and work', async () => { + const { container } = render( + + + + } /> + + + + ); + + expectAccordion( + container, + "What if I don't have an ORCID® Account", + "But what if you don't have an ORCID® account?" + ); + + expectAccordion( + container, + 'But I already log in with ORCID®', + 'Your ORCID® sign-in link is only used to obtain your ORCID® iD during sign-in' + ); + }); +}); diff --git a/src/features/orcidlink/CreateLink/index.tsx b/src/features/orcidlink/CreateLink/index.tsx new file mode 100644 index 00000000..e4c22bab --- /dev/null +++ b/src/features/orcidlink/CreateLink/index.tsx @@ -0,0 +1,174 @@ +import { faArrowRight, faMailReply } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Button, + Card, + CardActions, + CardContent, + CardHeader, + Unstable_Grid2 as Grid, +} from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { usePageTitle } from '../../layout/layoutSlice'; +import { + ORCID_LABEL, + ORCID_LINK_LABEL, + ORCID_SIGN_IN_SCREENSHOT, +} from '../constants'; +import styles from '../orcidlink.module.scss'; + +export default function ORCIDLinkCreateLink() { + usePageTitle('ORCID Link - Create Link'); + const navigate = useNavigate(); + return ( + + + + + + +

+ You do not currently have a link from your KBase account to an{' '} + {ORCID_LABEL} account. +

+ +

+ + After clicking the "Continue" button below, you will + be redirected to {ORCID_LABEL} + + , where you may sign in to your {ORCID_LABEL} account and grant + permission to KBase to access certain aspects of your{' '} + {ORCID_LABEL} account. +

+ +

+ What if you don't have an {ORCID_LABEL} Account?{' '} + Check out the FAQs to the right for an answer. +

+ +

+ After finishing at {ORCID_LABEL}, you will be returned to KBase + and asked to confirm the link. Once confirmed, the{' '} + {ORCID_LINK_LABEL} + will be added to your account. +

+ +

+ For security purposes, once you start a linking session, you + will have 10 minutes to complete the process. +

+ +

+ For more information,{' '} + + consult the {ORCID_LINK_LABEL} documentation + + . +

+
+ {/* Note that the card actions padding is overridden so that it matches + that of the card content and header. There are a number of formatting + issues with Cards. Some will apparently be fixed in v6. */} + + + + +
+
+ + + + + + + What if I don't have an {ORCID_LABEL} Account? + + +

+ In order to link your {ORCID_LABEL} account to your KBase + account, you will need to sign in at {ORCID_LABEL}. +

+

+ But what if you don't have an {ORCID_LABEL} account? +

+

+ When you reach the {ORCID_LABEL} Sign In page, you may elect + to register for a new account. +

+ ORCID® Sign In +

+ After registering, the linking process will be resumed, just + as if you had simply signed in with an existing{' '} + {ORCID_LABEL} account. +

+
+
+ + + But I already log in with {ORCID_LABEL} + + +

+ If you already log in with {ORCID_LABEL}, it may seem odd to + need to create a separate {ORCID_LINK_LABEL}. +

+

+ Your {ORCID_LABEL} sign-in link is only used to obtain your{' '} + {ORCID_LABEL} iD during sign-in. This is, in turn, used to + look up the associated KBase account and log you in. +

+

+ In contrast, {ORCID_LINK_LABEL} provides expanded and + long-term access, which allows KBase to provide tools for + you that that can access limited aspects of your{' '} + {ORCID_LABEL} account. The {ORCID_LINK_LABEL} can be added + or removed at any time without affecting your ability to + sign in to KBase through {ORCID_LABEL}. +

+
+
+
+
+
+
+
+ ); +} From 5182bf5f5c51a66e367b03001258a5f0f6103263 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 22 May 2024 19:51:25 +0000 Subject: [PATCH 03/13] replace images module with image url constants [URO-209] and add an image for content --- public/assets/images/ORCID-sign-in.png | Bin 0 -> 33316 bytes src/features/orcidlink/common/ORCIDIdLink.tsx | 4 ++-- src/features/orcidlink/constants.ts | 8 +++++++ src/features/orcidlink/images.ts | 20 ------------------ 4 files changed, 10 insertions(+), 22 deletions(-) create mode 100644 public/assets/images/ORCID-sign-in.png delete mode 100644 src/features/orcidlink/images.ts diff --git a/public/assets/images/ORCID-sign-in.png b/public/assets/images/ORCID-sign-in.png new file mode 100644 index 0000000000000000000000000000000000000000..f4274693aa9dcdacc8cf6f726c77e70e9a9a367a GIT binary patch literal 33316 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 literal 0 HcmV?d00001 diff --git a/src/features/orcidlink/common/ORCIDIdLink.tsx b/src/features/orcidlink/common/ORCIDIdLink.tsx index 59e9cfb0..9e2ec914 100644 --- a/src/features/orcidlink/common/ORCIDIdLink.tsx +++ b/src/features/orcidlink/common/ORCIDIdLink.tsx @@ -1,4 +1,4 @@ -import { image } from '../images'; +import { ORCID_ICON_URL } from '../constants'; import styles from './ORCIDIdLink.module.scss'; export interface ORCIDIdLinkProps { @@ -18,7 +18,7 @@ export function ORCIDIdLink({ url, orcidId }: ORCIDIdLinkProps) { rel="noreferrer" className={styles.main} > - ORCID Icon + ORCID Icon {url}/{orcidId} ); diff --git a/src/features/orcidlink/constants.ts b/src/features/orcidlink/constants.ts index 94e77506..7558aaa4 100644 --- a/src/features/orcidlink/constants.ts +++ b/src/features/orcidlink/constants.ts @@ -66,3 +66,11 @@ export const SCOPE_HELP: { [K in ORCIDScope]: ScopeHelp } = { ], }, }; + +export const ORCID_ICON_URL = `${process.env.PUBLIC_URL}/assets/images/ORCID-iD_icon-vector.svg`; + +export const ORCID_SIGN_IN_SCREENSHOT = `${process.env.PUBLIC_URL}/assets/images/ORCID-sign-in.png`; + +export const ORCID_LABEL = 'ORCID®'; + +export const ORCID_LINK_LABEL = `KBase ${ORCID_LABEL} Link`; diff --git a/src/features/orcidlink/images.ts b/src/features/orcidlink/images.ts deleted file mode 100644 index c6aa70c3..00000000 --- a/src/features/orcidlink/images.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Constructs urls for images used in the orcidlink ui - * - * Note that the usual technique, importing the image, which creates a url back - * into the web app, does not work with the current state of Europa dependencies. - * Therefore, we use hardcoded image paths, combined with the PUBLIC_URL. - * When Europa deps are updated, or it is switched to vite, the image imports - * should be re-enabled. - */ - -const ORCID_ICON = '/assets/images/ORCID-iD_icon-vector.svg'; - -export type ImageName = 'orcidIcon'; - -export function image(name: ImageName): string { - switch (name) { - case 'orcidIcon': - return `${process.env.PUBLIC_URL}${ORCID_ICON}`; - } -} From 8cbe426fd5c88bd45947fa22dac2fd90a6dd0ad3 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 22 May 2024 19:54:49 +0000 Subject: [PATCH 04/13] add orcidlink manage tab [URO-209] and some test mock additions and changes --- .../orcidlink/HomeLinked/ManageTab.test.tsx | 208 ++++++++++++++++++ .../orcidlink/HomeLinked/ManageTab.tsx | 148 +++++++++++++ .../orcidlink/HomeLinked/view.test.tsx | 89 +++++++- src/features/orcidlink/HomeLinked/view.tsx | 13 +- src/features/orcidlink/test/mocks.ts | 136 +++++++++++- 5 files changed, 575 insertions(+), 19 deletions(-) create mode 100644 src/features/orcidlink/HomeLinked/ManageTab.test.tsx create mode 100644 src/features/orcidlink/HomeLinked/ManageTab.tsx diff --git a/src/features/orcidlink/HomeLinked/ManageTab.test.tsx b/src/features/orcidlink/HomeLinked/ManageTab.test.tsx new file mode 100644 index 00000000..8dad52f8 --- /dev/null +++ b/src/features/orcidlink/HomeLinked/ManageTab.test.tsx @@ -0,0 +1,208 @@ +import { screen } from '@testing-library/dom'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter } from 'react-router-dom'; +import { LINK_RECORD_1, PROFILE_1 } from '../test/data'; +import { noop } from '../test/mocks'; +import ManageTab from './ManageTab'; + +describe('The ManageTab component', () => { + it('renders normally', () => { + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + + const { container } = render( + + + + ); + + expect(container).toHaveTextContent('Remove your KBase ORCID'); + expect(container).toHaveTextContent( + 'Removing the link will not alter any of your data' + ); + expect(container).toHaveTextContent( + 'Please note that after you remove the link at KBase' + ); + }); + + it('cancel confirmation for removal of link', async () => { + const user = userEvent.setup(); + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + + render( + + + + ); + + const removeButton = await screen.findByText('Remove KBase ORCID® Link …'); + + expect(removeButton).toBeVisible(); + + await user.click(removeButton); + + await waitFor(() => { + expect( + screen.queryByText('Confirm Removal of ORCID® Link') + ).toBeVisible(); + }); + + // Cancel the dialog + + const cancelButton = await screen.findByText('Cancel'); + + expect(cancelButton).toBeVisible(); + + await user.click(cancelButton); + + await waitFor(() => { + expect( + screen.queryByText('Confirm Removal of ORCID® Link') + ).not.toBeVisible(); + }); + }); + + it('confirm removal of link', async () => { + const user = userEvent.setup(); + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + let removeLinkCalled = false; + const removeLink = () => { + removeLinkCalled = true; + }; + + render( + + + + ); + + expect(removeLinkCalled).toBe(false); + + const removeButton = await screen.findByText('Remove KBase ORCID® Link …'); + + expect(removeButton).toBeVisible(); + + await user.click(removeButton); + + const dialog = await screen.findByText('Confirm Removal of ORCID® Link'); + + expect(dialog).toBeVisible(); + + // Confirm the dialog + + const yesButton = await screen.findByText( + 'Yes, go ahead and remove this link' + ); + + expect(yesButton).toBeVisible(); + + await user.click(yesButton); + + expect(removeLinkCalled).toBe(true); + }); + + it('confirmation dialog can be canceled', async () => { + const user = userEvent.setup(); + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + + const { container } = render( + + + + ); + + // First, show that the main content is displayed. + expect(container).toHaveTextContent('Remove your KBase ORCID® Link'); + + // Then open the dialog. + const removeButton = await screen.findByText('Remove KBase ORCID® Link …'); + expect(removeButton).toBeVisible(); + await user.click(removeButton); + const dialog = await screen.findByText('Confirm Removal of ORCID® Link'); + expect(dialog).toBeVisible(); + + // Cancel the dialog + const cancelButton = await screen.findByText('Cancel'); + expect(cancelButton).toBeVisible(); + await user.click(cancelButton); + + await waitFor(() => { + expect( + screen.queryByText('Confirm Removal of ORCID® Link') + ).not.toBeVisible(); + }); + + // And the main view should still be there. + expect(container).toHaveTextContent('Remove your KBase ORCID® Link'); + }); + + it('the "show in user profile" toggle calls the correct prop when clicked', async () => { + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + let toggleValue = false; + const toggleShowInProfile = () => { + toggleValue = !toggleValue; + }; + + render( + + + + ); + + const toggleControl = await screen.findByText('Yes'); + + const user = userEvent.setup(); + + await user.click(toggleControl); + + await waitFor(() => { + expect(toggleValue).toBe(true); + }); + + await user.click(toggleControl); + + await waitFor(() => { + expect(toggleValue).toBe(false); + }); + }); +}); diff --git a/src/features/orcidlink/HomeLinked/ManageTab.tsx b/src/features/orcidlink/HomeLinked/ManageTab.tsx new file mode 100644 index 00000000..eb5312ba --- /dev/null +++ b/src/features/orcidlink/HomeLinked/ManageTab.tsx @@ -0,0 +1,148 @@ +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + Button, + Card, + CardActions, + CardContent, + CardHeader, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Switch, + Typography, + Unstable_Grid2 as Grid, +} from '@mui/material'; +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { + LinkRecordPublic, + ORCIDProfile, +} from '../../../common/api/orcidLinkCommon'; +import RealName from '../common/RealName'; + +export interface ManageTabProps { + linkRecord: LinkRecordPublic; + profile: ORCIDProfile; + orcidSiteURL: string; + removeLink: () => void; + toggleShowInProfile: () => void; +} + +export default function ManageTab({ + linkRecord, + profile, + orcidSiteURL, + removeLink, + toggleShowInProfile, +}: ManageTabProps) { + const [confirm, setConfirm] = useState(false); + + return ( + + + + + + + You may remove your KBase ORCID® Link at any time. + + +

+ Removing the link will not alter any of your data stored at KBase + or ORCID®. It will simply delete the link to your ORCID® account, + preventing KBase from accessing your ORCID® profile thereafter. +

+ +

+ Please note that after you remove the link at KBase, you may also + want to{' '} + + revoke the permissions granted to KBase at ORCID® + {' '} + as well. +

+
+ + + + Confirm Removal of ORCID® Link + +

Are you sure you want to remove this KBase ORCID® Link?

+

+ ORCID® iD is {linkRecord.orcid_auth.orcid} for{' '} + + + +

+
+ + + + +
+
+
+
+ + + + + + Show in User Profile? + + } + label="Yes" + onChange={() => { + toggleShowInProfile(); + }} + /> + + + When enabled your ORCID® iD will be displayed in{' '} + + your User Profile + + + + + + +
+ ); +} diff --git a/src/features/orcidlink/HomeLinked/view.test.tsx b/src/features/orcidlink/HomeLinked/view.test.tsx index 4f1b0801..305a264f 100644 --- a/src/features/orcidlink/HomeLinked/view.test.tsx +++ b/src/features/orcidlink/HomeLinked/view.test.tsx @@ -1,34 +1,56 @@ -import { act, render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; import { LINK_RECORD_1, PROFILE_1, SERVICE_INFO_1 } from '../test/data'; +import { noop } from '../test/mocks'; import HomeLinked from './view'; describe('The HomeLinked Component', () => { + let debugLogSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + }); + beforeEach(() => { + debugLogSpy = jest.spyOn(console, 'debug'); + }); + it('renders with minimal props', async () => { const info = SERVICE_INFO_1; const linkRecord = LINK_RECORD_1; const profile = PROFILE_1; render( - + ); const creditName = 'Foo B. Bar'; const realName = 'Foo Bar'; - // Part of the profile should be available + // The profile fields should be displayed. expect(await screen.findByText(creditName)).toBeVisible(); expect(await screen.findByText(realName)).toBeVisible(); }); it('tab content switches when tabs are selected', async () => { + const user = userEvent.setup(); const info = SERVICE_INFO_1; const linkRecord = LINK_RECORD_1; const profile = PROFILE_1; - render( - + ); @@ -48,20 +70,65 @@ describe('The HomeLinked Component', () => { expect(await screen.findByText(creditName)).toBeVisible(); expect(await screen.findByText(realName)).toBeVisible(); - act(() => { - manageTab.click(); - }); + await user.click(manageTab); waitFor(async () => { expect(await screen.findByText(removeAreaTitle)).toBeVisible(); }); - act(() => { - overviewTab.click(); - }); + await user.click(overviewTab); waitFor(async () => { expect(await screen.findByText(overviewAreaTitle)).toBeVisible(); }); }); + + it('"show in user profile" toggle calls the function passed down to it', async () => { + const user = userEvent.setup(); + const info = SERVICE_INFO_1; + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + + const toggleShowInProfile = () => { + // eslint-disable-next-line no-console + console.debug('TOGGLE SHOW IN PROFILE'); + }; + + render( + + + + ); + + const creditName = 'Foo B. Bar'; + const removeAreaTitle = 'Remove your KBase ORCID® Link'; + const manageTabLabel = 'Manage Your Link'; + + const manageTab = await screen.findByText(manageTabLabel); + + // Part of the profile should be available on initial tab. + expect(await screen.findByText(creditName)).toBeVisible(); + + // Now select the manage tab + await user.click(manageTab); + + waitFor(async () => { + expect(await screen.findByText(removeAreaTitle)).toBeVisible(); + }); + + // And finally, click the toggle + const toggleControl = await screen.findByText('Yes'); + + await user.click(toggleControl); + + await waitFor(() => { + expect(debugLogSpy).toHaveBeenCalledWith('TOGGLE SHOW IN PROFILE'); + }); + }); }); diff --git a/src/features/orcidlink/HomeLinked/view.tsx b/src/features/orcidlink/HomeLinked/view.tsx index afe9b16e..fa7683ee 100644 --- a/src/features/orcidlink/HomeLinked/view.tsx +++ b/src/features/orcidlink/HomeLinked/view.tsx @@ -6,18 +6,23 @@ import { ORCIDProfile, } from '../../../common/api/orcidLinkCommon'; import TabPanel from '../common/TabPanel'; +import ManageTab from './ManageTab'; import OverviewTab from './OverviewTab'; export interface HomeLinkedProps { info: InfoResult; linkRecord: LinkRecordPublic; profile: ORCIDProfile; + removeLink: () => void; + toggleShowInProfile: () => void; } export default function HomeLinked({ info, linkRecord, profile, + removeLink, + toggleShowInProfile, }: HomeLinkedProps) { const [tab, setTab] = useState(0); @@ -37,7 +42,13 @@ export default function HomeLinked({ -
MANAGE TAB
+
); diff --git a/src/features/orcidlink/test/mocks.ts b/src/features/orcidlink/test/mocks.ts index 1e515560..4239cabb 100644 --- a/src/features/orcidlink/test/mocks.ts +++ b/src/features/orcidlink/test/mocks.ts @@ -1,6 +1,13 @@ +import { MockResponseInit } from 'jest-fetch-mock/types'; import { JSONRPC20Error } from '../../../common/api/utils/kbaseBaseQuery'; +import { + LINK_RECORD_1, + ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED, + PROFILE_1, + SERVICE_INFO_1, +} from './data'; -export function jsonRPC20_ResultResponse(id: string, result: unknown) { +export function jsonrpc20_resultResponse(id: string, result: unknown) { return { body: JSON.stringify({ jsonrpc: '2.0', @@ -14,7 +21,7 @@ export function jsonRPC20_ResultResponse(id: string, result: unknown) { }; } -export function jsonRPC20_ErrorResponse(id: string, error: JSONRPC20Error) { +export function jsonrpc20_errorResponse(id: string, error: JSONRPC20Error) { return { body: JSON.stringify({ jsonrpc: '2.0', @@ -29,7 +36,7 @@ export function jsonRPC20_ErrorResponse(id: string, error: JSONRPC20Error) { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function rest_response(result: any, status = 200) { +export function restResponse(result: any, status = 200) { return { body: JSON.stringify(result), status, @@ -40,7 +47,7 @@ export function rest_response(result: any, status = 200) { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function mockIsLinked(body: any) { +export function mockIsLinkedResponse(body: any) { const username = body['params']['username']; const result = (() => { @@ -53,10 +60,125 @@ export function mockIsLinked(body: any) { throw new Error('Invalid test value for username'); } })(); - return jsonRPC20_ResultResponse(body['id'], result); + return jsonrpc20_resultResponse(body['id'], result); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function mockIsLinked_not(body: any) { - return jsonRPC20_ResultResponse(body['id'], false); +export function mockIsLinkedNotResponse(body: any) { + return jsonrpc20_resultResponse(body['id'], false); +} + +export function setupMockRegularUser() { + fetchMock.mockResponse( + async (request): Promise => { + const { pathname } = new URL(request.url); + // put a little delay in here so that we have a better + // chance of catching temporary conditions, like loading. + await new Promise((resolve) => { + setTimeout(() => { + resolve(null); + }, 300); + }); + switch (pathname) { + // Mocks for the orcidlink api + case '/services/orcidlink/api/v1': { + if (request.method !== 'POST') { + return ''; + } + const body = await request.json(); + const id = body['id']; + switch (body['method']) { + case 'is-linked': + // In this mock, user "foo" is linked, user "bar" is not. + return jsonrpc20_resultResponse(id, mockIsLinkedResponse(body)); + case 'get-orcid-profile': + // simulate fetching an orcid profile + return jsonrpc20_resultResponse(id, PROFILE_1); + case 'owner-link': + // simulate fetching the link record for a user + return jsonrpc20_resultResponse(id, LINK_RECORD_1); + case 'info': + // simulate getting service info. + return jsonrpc20_resultResponse(id, SERVICE_INFO_1); + default: + return ''; + } + } + default: + return ''; + } + } + ); +} + +export function setupMockRegularUserWithError() { + fetchMock.mockResponse( + async (request): Promise => { + const { pathname } = new URL(request.url); + // put a little delay in here so that we have a better + // chance of catching temporary conditions, like loading. + await new Promise((resolve) => { + setTimeout(() => { + resolve(null); + }, 300); + }); + switch (pathname) { + // Mocks for the orcidlink api + case '/services/orcidlink/api/v1': { + if (request.method !== 'POST') { + return ''; + } + const body = await request.json(); + const id = body['id'] as string; + switch (body['method']) { + case 'is-linked': + return jsonrpc20_errorResponse( + id, + ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED + ); + case 'get-orcid-profile': { + return jsonrpc20_errorResponse( + id, + ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED + ); + } + case 'owner-link': + // simulate fetching the link record for a user + return jsonrpc20_resultResponse(id, LINK_RECORD_1); + + case 'info': + // simulate getting service info + return jsonrpc20_resultResponse(id, SERVICE_INFO_1); + + default: + return ''; + } + } + default: + return ''; + } + } + ); +} + +/** + * Make a copy of a given "json-compatible" value. + * + * This can be replaced with "structuredClone". In order to that that, we need + * to use core-js, but we don't have a direct dependency upon core-js; it is a + * transitive dependency of several packages already. + * + * This technique is not ideal, but in the limited usage of it for test data, + * works just fine. + */ +export function jsonCopy(value: T): T { + return JSON.parse(JSON.stringify(value)); +} + +/** + * This is an empty function that serves as a placeholder for props that expect + * one, but in tests in which these props are not tested, so they need not do anything. + */ +export function noop() { + // do nothing } From ae222f2bf5e0400f4dd7ff7e9dafa288ee0d006f Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 22 May 2024 19:57:59 +0000 Subject: [PATCH 05/13] add content to orcidlink views [URO-209] --- .../orcidlink/HomeLinked/OverviewTab.tsx | 19 +++++++++-- src/features/orcidlink/HomeUnlinked.tsx | 25 ++++++++++++-- .../orcidlink/common/MoreInformation.tsx | 34 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 src/features/orcidlink/common/MoreInformation.tsx diff --git a/src/features/orcidlink/HomeLinked/OverviewTab.tsx b/src/features/orcidlink/HomeLinked/OverviewTab.tsx index 7a892a03..6571413b 100644 --- a/src/features/orcidlink/HomeLinked/OverviewTab.tsx +++ b/src/features/orcidlink/HomeLinked/OverviewTab.tsx @@ -10,6 +10,7 @@ import { LinkRecordPublic, ORCIDProfile, } from '../../../common/api/orcidLinkCommon'; +import MoreInformation from '../common/MoreInformation'; import LinkInfo from './LinkInfo'; export interface OverviewTabProps { @@ -39,16 +40,28 @@ export default function OverviewTab({ - + - NOTES HERE + + Your KBase ORCID® Link gives KBase tools access to your ORCID® + account while you are logged into KBase. + +

+ Your KBase ORCID® Link will be stored at KBase until you remove + it. +

+

+ The link will only be used when you are signed in to KBase. In + addition, any tool that uses the link will alert you before using + it, and will explain how it will use it. +

- LINKS TO MORE INFO HERE +
diff --git a/src/features/orcidlink/HomeUnlinked.tsx b/src/features/orcidlink/HomeUnlinked.tsx index 9d0580a3..56e67c3b 100644 --- a/src/features/orcidlink/HomeUnlinked.tsx +++ b/src/features/orcidlink/HomeUnlinked.tsx @@ -12,6 +12,7 @@ import { Typography, Unstable_Grid2 as Grid, } from '@mui/material'; +import MoreInformation from './common/MoreInformation'; export default function Unlinked() { return ( @@ -19,6 +20,7 @@ export default function Unlinked() { + You do not currently have a link from your KBase account to an @@ -41,15 +43,32 @@ export default function Unlinked() { - + - NOTES HERE + + A KBase ORCID® Link gives KBase limited access to your ORCID® + account while you are logged into KBase. + +

+ If you don't have an ORCID® account, you may create one before + creating the link, or even "on the fly" while creating a link. +

+

+ You can only create a KBase ORCID® Link from this page. It will be + stored at KBase until you remove it. +

+

+ The link will only be used when you are signed in to KBase. In + addition, any tool that uses the link will alert you before using + it, and will explain how it will use it. +

+ - LINKS TO MORE INFORMATION HERE +
diff --git a/src/features/orcidlink/common/MoreInformation.tsx b/src/features/orcidlink/common/MoreInformation.tsx new file mode 100644 index 00000000..e2e526ec --- /dev/null +++ b/src/features/orcidlink/common/MoreInformation.tsx @@ -0,0 +1,34 @@ +import { List, ListItem, ListItemText } from '@mui/material'; + +/** + * Implements a list of information resources about ORCID and KBase ORCID linking, + * each with a link and short description. + */ +export default function MoreInformation() { + return ( + + + + About KBase ORCID® Links + + } + /> + + + + About ORCID® + + } + /> + + + ); +} From dc6d7b49a769fa12d83936a3586925e7142d7ef7 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 22 May 2024 20:00:06 +0000 Subject: [PATCH 06/13] add support for user link managment in the form of stubs [URO-209] --- .../orcidlink/HomeLinked/index.test.tsx | 195 +++++++++--------- src/features/orcidlink/HomeLinked/index.tsx | 27 ++- 2 files changed, 120 insertions(+), 102 deletions(-) diff --git a/src/features/orcidlink/HomeLinked/index.test.tsx b/src/features/orcidlink/HomeLinked/index.test.tsx index 7e4edbc9..9a821ce7 100644 --- a/src/features/orcidlink/HomeLinked/index.test.tsx +++ b/src/features/orcidlink/HomeLinked/index.test.tsx @@ -1,119 +1,31 @@ import { render, screen, waitFor } from '@testing-library/react'; -import fetchMock, { MockResponseInit } from 'jest-fetch-mock'; +import userEvent from '@testing-library/user-event'; +import fetchMock from 'jest-fetch-mock'; +import { ErrorBoundary } from 'react-error-boundary'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { createTestStore } from '../../../app/store'; import { INITIAL_STORE_STATE, - LINK_RECORD_1, - ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED, + INITIAL_UNAUTHENTICATED_STORE_STATE, PROFILE_1, SERVICE_INFO_1, } from '../test/data'; import { - jsonRPC20_ErrorResponse, - jsonRPC20_ResultResponse, - mockIsLinked, + setupMockRegularUser, + setupMockRegularUserWithError, } from '../test/mocks'; import HomeLinkedController from './index'; -function setupMockRegularUser() { - fetchMock.mockResponse( - async (request): Promise => { - const { pathname } = new URL(request.url); - // put a little delay in here so that we have a better - // chance of catching temporary conditions, like loading. - await new Promise((resolve) => { - setTimeout(() => { - resolve(null); - }, 300); - }); - switch (pathname) { - // Mocks for the orcidlink api - case '/services/orcidlink/api/v1': { - if (request.method !== 'POST') { - return ''; - } - const body = await request.json(); - const id = body['id']; - switch (body['method']) { - case 'is-linked': - // In this mock, user "foo" is linked, user "bar" is not. - return jsonRPC20_ResultResponse(id, mockIsLinked(body)); - case 'get-orcid-profile': - // simulate fetching an orcid profile - return jsonRPC20_ResultResponse(id, PROFILE_1); - case 'owner-link': - // simulate fetching the link record for a user - return jsonRPC20_ResultResponse(id, LINK_RECORD_1); - case 'info': - // simulate getting service info. - return jsonRPC20_ResultResponse(id, SERVICE_INFO_1); - default: - return ''; - } - } - default: - return ''; - } - } - ); -} - -function setupMockRegularUserWithError() { - fetchMock.mockResponse( - async (request): Promise => { - const { pathname } = new URL(request.url); - // put a little delay in here so that we have a better - // chance of catching temporary conditions, like loading. - await new Promise((resolve) => { - setTimeout(() => { - resolve(null); - }, 300); - }); - switch (pathname) { - // MOcks for the orcidlink api - case '/services/orcidlink/api/v1': { - if (request.method !== 'POST') { - return ''; - } - const body = await request.json(); - const id = body['id'] as string; - switch (body['method']) { - case 'is-linked': - return jsonRPC20_ErrorResponse( - id, - ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED - ); - case 'get-orcid-profile': { - return jsonRPC20_ErrorResponse( - id, - ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED - ); - } - case 'owner-link': - // simulate fetching the link record for a user - return jsonRPC20_ResultResponse(id, LINK_RECORD_1); - - case 'info': - // simulate getting service info - return jsonRPC20_ResultResponse(id, SERVICE_INFO_1); - - default: - return ''; - } - } - default: - return ''; - } - } - ); -} - describe('The HomeLinkedController component', () => { + let debugLogSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + }); beforeEach(() => { fetchMock.resetMocks(); fetchMock.enableMocks(); + debugLogSpy = jest.spyOn(console, 'debug'); }); it('renders normally for a normal user', async () => { @@ -165,4 +77,89 @@ describe('The HomeLinkedController component', () => { await expect(container).toHaveTextContent('Authorization Required'); }); }); + + it('throws an impossible error if called without authentication', async () => { + const { container } = render( + { + return
{error.message}
; + }} + onError={() => { + // noop + }} + > + + + + + +
+ ); + + await waitFor(() => { + expect(container).toHaveTextContent( + 'Impossible - username is not defined' + ); + }); + }); + + it('responds as expected to the remove link button being pressed', async () => { + const user = userEvent.setup(); + setupMockRegularUser(); + + render( + + + + + + ); + + // Now poke around and make sure things are there. + await waitFor(async () => { + expect(screen.queryByText('Loading ORCID Link')).toBeVisible(); + }); + + await waitFor(async () => { + // Ensure some expected fields are rendered. + expect( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + screen.queryByText(PROFILE_1.nameGroup.fields!.creditName!) + ).toBeVisible(); + expect(screen.queryByText('5/1/24')).toBeVisible(); + }); + + // First need to open the manage tab + + const tab = await screen.findByText('Manage Your Link'); + expect(tab).not.toBeNull(); + await user.click(tab); + + await waitFor(() => { + expect( + screen.queryByText('Remove your KBase ORCID® Link') + ).not.toBeNull(); + }); + + // Now find and click the Remove button + const button = await screen.findByText('Remove KBase ORCID® Link …'); + expect(button).toBeVisible(); + await user.click(button); + + // Now the dialog should be displayed. + await waitFor(() => { + const title = screen.queryByText('Confirm Removal of ORCID® Link'); + expect(title).toBeVisible(); + }); + + const confirmButton = await screen.findByText( + 'Yes, go ahead and remove this link' + ); + expect(confirmButton).toBeVisible(); + await user.click(confirmButton); + + await waitFor(() => { + expect(debugLogSpy).toHaveBeenCalledWith('WILL REMOVE LINK'); + }); + }); }); diff --git a/src/features/orcidlink/HomeLinked/index.tsx b/src/features/orcidlink/HomeLinked/index.tsx index 95d34cac..209ed186 100644 --- a/src/features/orcidlink/HomeLinked/index.tsx +++ b/src/features/orcidlink/HomeLinked/index.tsx @@ -12,8 +12,11 @@ export interface HomeLinkedControllerProps { export default function HomeLinkedController({ info, }: HomeLinkedControllerProps) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const username = useAppSelector(authUsername)!; + const username = useAppSelector(authUsername); + + if (typeof username === 'undefined') { + throw new Error('Impossible - username is not defined'); + } const { data, error, isError, isFetching, isSuccess } = orcidlinkAPI.useOrcidlinkLinkedUserInfoQuery( @@ -21,6 +24,18 @@ export default function HomeLinkedController({ { refetchOnMountOrArgChange: true } ); + const removeLink = () => { + // This console output is only for this intermediate state of the code, to facilitate testing. + // eslint-disable-next-line no-console + console.debug('WILL REMOVE LINK'); + }; + + const toggleShowInProfile = () => { + // This console output is only for this intermediate state of the code, to facilitate testing. + // eslint-disable-next-line no-console + console.debug('TOGGLE SHOW IN PROFILE'); + }; + // Renderers function renderState() { if (isError) { @@ -28,7 +43,13 @@ export default function HomeLinkedController({ } else if (isSuccess) { const { linkRecord, profile } = data; return ( - + ); } } From 2efc7ce5f5e9b864964893bcf63e96e4d220ab80 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 22 May 2024 20:01:04 +0000 Subject: [PATCH 07/13] simplify orcidlink loading overlay [URO-209] --- .../common/LoadingOverlay.module.scss | 2 +- .../orcidlink/common/LoadingOverlay.tsx | 31 +++++-------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/features/orcidlink/common/LoadingOverlay.module.scss b/src/features/orcidlink/common/LoadingOverlay.module.scss index 6958b70b..8c476927 100644 --- a/src/features/orcidlink/common/LoadingOverlay.module.scss +++ b/src/features/orcidlink/common/LoadingOverlay.module.scss @@ -8,6 +8,6 @@ margin-top: 8rem; } -.loading-title { +.title { margin-left: 0.5rem; } diff --git a/src/features/orcidlink/common/LoadingOverlay.tsx b/src/features/orcidlink/common/LoadingOverlay.tsx index 1b436c12..b47184af 100644 --- a/src/features/orcidlink/common/LoadingOverlay.tsx +++ b/src/features/orcidlink/common/LoadingOverlay.tsx @@ -1,28 +1,6 @@ import { Alert, AlertTitle, CircularProgress, Modal } from '@mui/material'; import styles from './LoadingOverlay.module.scss'; -export interface LoadingAlertProps { - title: string; - description: string; -} - -/** - * A wrapper around MUI Alert to show a loading indicator (spinner) and message, - * with a description. - */ -export function LoadingAlert({ title, description }: LoadingAlertProps) { - return ( -
- }> - - {title} - -

{description}

-
-
- ); -} - export interface LoadingOverlayProps { open: boolean; } @@ -34,7 +12,14 @@ export interface LoadingOverlayProps { export default function LoadingOverlay({ open }: LoadingOverlayProps) { return ( - +
+ }> + + Loading... + +

Loading ORCID Link

+
+
); } From f3cd43baca2802bc7d3164821cf06e2e9ae6661a Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 22 May 2024 20:03:06 +0000 Subject: [PATCH 08/13] simplify orcidlink realname component and tests [URO-209] replace structuredClone with jsonCopy as core-js is not directly specified in package.json --- src/features/orcidlink/common/RealName.test.tsx | 10 ++++++---- src/features/orcidlink/common/RealName.tsx | 7 +++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/features/orcidlink/common/RealName.test.tsx b/src/features/orcidlink/common/RealName.test.tsx index c5d95fcc..dd1d88ed 100644 --- a/src/features/orcidlink/common/RealName.test.tsx +++ b/src/features/orcidlink/common/RealName.test.tsx @@ -1,12 +1,12 @@ import { render } from '@testing-library/react'; -import 'core-js/stable/structured-clone'; import { ORCIDProfile } from '../../../common/api/orcidLinkCommon'; import { PROFILE_1 } from '../test/data'; +import { jsonCopy } from '../test/mocks'; import RealName from './RealName'; describe('the renderRealName render function ', () => { it('renders correctly if not private', () => { - const profile = structuredClone(PROFILE_1); + const profile = jsonCopy(PROFILE_1); const { container } = render(); @@ -14,7 +14,9 @@ describe('the renderRealName render function ', () => { }); it('renders just the first name if no last name', () => { - const profile = structuredClone(PROFILE_1); + const profile = jsonCopy(PROFILE_1); + + // We know how the test profile is populated. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion profile.nameGroup.fields!.lastName = null; @@ -24,7 +26,7 @@ describe('the renderRealName render function ', () => { }); it('renders a special string if it is private', () => { - const profile = structuredClone(PROFILE_1); + const profile = jsonCopy(PROFILE_1); profile.nameGroup.private = true; const { container } = render(); diff --git a/src/features/orcidlink/common/RealName.tsx b/src/features/orcidlink/common/RealName.tsx index cc376ffb..3043c66f 100644 --- a/src/features/orcidlink/common/RealName.tsx +++ b/src/features/orcidlink/common/RealName.tsx @@ -1,4 +1,3 @@ -import { Typography } from '@mui/material'; import { ORCIDProfile } from '../../../common/api/orcidLinkCommon'; import PrivateField from './PrivateField'; @@ -17,10 +16,10 @@ export default function RealName({ profile }: RealNameProps) { const { firstName, lastName } = profile.nameGroup.fields; if (lastName) { return ( - + {firstName} {lastName} - + ); } - return {firstName}; + return {firstName}; } From b48f5afc757f9dbefd157d0706f5974d9dc13f53 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 22 May 2024 20:04:00 +0000 Subject: [PATCH 09/13] add and improve orcidlink tests [URO-209] --- src/features/orcidlink/Home/index.test.tsx | 8 +- .../orcidlink/common/CreditName.test.tsx | 8 +- .../orcidlink/common/ORCIDIdLink.test.tsx | 1 - src/features/orcidlink/common/Scopes.test.tsx | 11 +- src/features/orcidlink/index.test.tsx | 110 ++++++++++++++++++ 5 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 src/features/orcidlink/index.test.tsx diff --git a/src/features/orcidlink/Home/index.test.tsx b/src/features/orcidlink/Home/index.test.tsx index 11f7c91f..8d0e2cd5 100644 --- a/src/features/orcidlink/Home/index.test.tsx +++ b/src/features/orcidlink/Home/index.test.tsx @@ -9,7 +9,7 @@ import { INITIAL_STORE_STATE, INITIAL_UNAUTHENTICATED_STORE_STATE, } from '../test/data'; -import { mockIsLinked, mockIsLinked_not } from '../test/mocks'; +import { mockIsLinkedNotResponse, mockIsLinkedResponse } from '../test/mocks'; import HomeController from './index'; jest.mock('../HomeLinked', () => { @@ -39,7 +39,7 @@ describe('The HomeController Component', () => { const body = await request.json(); switch (body['method']) { case 'is-linked': { - return mockIsLinked(body); + return mockIsLinkedResponse(body); } default: return ''; @@ -76,7 +76,7 @@ describe('The HomeController Component', () => { const body = await request.json(); switch (body['method']) { case 'is-linked': { - return mockIsLinked_not(body); + return mockIsLinkedNotResponse(body); } default: return ''; @@ -117,7 +117,7 @@ describe('The HomeController Component', () => { switch (body['method']) { case 'is-linked': { // In this mock, user "foo" is linked, user "bar" is not. - return mockIsLinked(body); + return mockIsLinkedResponse(body); } default: return ''; diff --git a/src/features/orcidlink/common/CreditName.test.tsx b/src/features/orcidlink/common/CreditName.test.tsx index 5f36f895..fb54958d 100644 --- a/src/features/orcidlink/common/CreditName.test.tsx +++ b/src/features/orcidlink/common/CreditName.test.tsx @@ -1,12 +1,12 @@ import { render } from '@testing-library/react'; -import 'core-js/stable/structured-clone'; import { ORCIDProfile } from '../../../common/api/orcidLinkCommon'; import { PROFILE_1 } from '../test/data'; +import { jsonCopy } from '../test/mocks'; import CreditName from './CreditName'; describe('The renderCreditName render function', () => { it('renders correctly if not private', () => { - const profile = structuredClone(PROFILE_1); + const profile = jsonCopy(PROFILE_1); const { container } = render(); @@ -14,7 +14,7 @@ describe('The renderCreditName render function', () => { }); it('renders a special string if it is private', () => { - const profile = structuredClone(PROFILE_1); + const profile = jsonCopy(PROFILE_1); profile.nameGroup.private = true; @@ -24,7 +24,7 @@ describe('The renderCreditName render function', () => { }); it('renders a "not available" string if it is absent', () => { - const profile = structuredClone(PROFILE_1); + const profile = jsonCopy(PROFILE_1); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion profile.nameGroup.fields!.creditName = null; diff --git a/src/features/orcidlink/common/ORCIDIdLink.test.tsx b/src/features/orcidlink/common/ORCIDIdLink.test.tsx index 00eec477..c3fb831d 100644 --- a/src/features/orcidlink/common/ORCIDIdLink.test.tsx +++ b/src/features/orcidlink/common/ORCIDIdLink.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/react'; -import 'core-js/stable/structured-clone'; import { ORCIDIdLink } from './ORCIDIdLink'; describe('The renderORCIDId render function', () => { diff --git a/src/features/orcidlink/common/Scopes.test.tsx b/src/features/orcidlink/common/Scopes.test.tsx index 9e3a3bb9..a3f928ed 100644 --- a/src/features/orcidlink/common/Scopes.test.tsx +++ b/src/features/orcidlink/common/Scopes.test.tsx @@ -1,4 +1,5 @@ -import { act, render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import Scopes from './Scopes'; describe('The Scopes component', () => { @@ -11,15 +12,17 @@ describe('The Scopes component', () => { }); it('renders scope description when scope is selected', async () => { + const user = userEvent.setup(); const scopes = '/read-limited'; + render(); + const buttonText = 'Allows KBase to read your information with visibility set to Trusted Organizations.'; const button = await screen.findByText(buttonText); expect(button).toBeVisible(); - act(() => { - button.click(); - }); + await user.click(button); + const revealedContentSample = 'Allows KBase to read any information from your record'; waitFor(async () => { diff --git a/src/features/orcidlink/index.test.tsx b/src/features/orcidlink/index.test.tsx new file mode 100644 index 00000000..72b496a7 --- /dev/null +++ b/src/features/orcidlink/index.test.tsx @@ -0,0 +1,110 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import fetchMock from 'jest-fetch-mock'; +import { Provider } from 'react-redux'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { createTestStore } from '../../app/store'; +import MainView from './index'; +import { INITIAL_STORE_STATE } from './test/data'; +import { setupMockRegularUser } from './test/mocks'; + +describe('The Main Component', () => { + let debugLogSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + }); + beforeEach(() => { + fetchMock.resetMocks(); + fetchMock.enableMocks(); + debugLogSpy = jest.spyOn(console, 'debug'); + }); + + it('renders with minimal props', async () => { + setupMockRegularUser(); + + render( + + + + } /> + + {/* */} + + + ); + + const creditName = 'Foo B. Bar'; + const realName = 'Foo Bar'; + + // Part of the profile should be available + expect(await screen.findByText(creditName)).toBeVisible(); + expect(await screen.findByText(realName)).toBeVisible(); + }); + + it('can switch to the "manage your link" tab', async () => { + const user = userEvent.setup(); + setupMockRegularUser(); + + render( + + + + } /> + + + + ); + + // Matches test data (see the setup function above) + const creditName = 'Foo B. Bar'; + // Matches what would be synthesized from the test data + const realName = 'Foo Bar'; + + // Part of the profile should be available + expect(await screen.findByText(creditName)).toBeVisible(); + expect(await screen.findByText(realName)).toBeVisible(); + + const tab = await screen.findByText('Manage Your Link'); + expect(tab).not.toBeNull(); + + await user.click(tab); + + await waitFor(() => { + expect( + screen.queryByText('Remove your KBase ORCID® Link') + ).not.toBeNull(); + expect(screen.queryByText('Settings')).not.toBeNull(); + }); + }); + + it('the "Show in User Profile?" switch calls the prop function we pass', async () => { + const user = userEvent.setup(); + setupMockRegularUser(); + + render( + + + + } /> + + + + ); + + const tab = await screen.findByText('Manage Your Link'); + expect(tab).not.toBeNull(); + await user.click(tab); + + await waitFor(() => { + expect(screen.queryByText('Settings')).not.toBeNull(); + }); + + const toggleControl = await screen.findByText('Yes'); + + await user.click(toggleControl); + + await waitFor(() => { + expect(debugLogSpy).toHaveBeenCalledWith('TOGGLE SHOW IN PROFILE'); + }); + }); +}); From 5e08d575b271bd89116c82c7cde25eb5906a862a Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Thu, 23 May 2024 16:52:16 +0000 Subject: [PATCH 10/13] refactor image assets [URO-209] store in regularly named directory (per-feature), clean up url constants --- .../orcidlink}/images/ORCID-iD_icon-vector.svg | 0 .../orcidlink}/images/ORCID-sign-in.png | Bin src/features/orcidlink/CreateLink/index.tsx | 4 ++-- src/features/orcidlink/constants.ts | 7 +++++-- 4 files changed, 7 insertions(+), 4 deletions(-) rename public/assets/{ => features/orcidlink}/images/ORCID-iD_icon-vector.svg (100%) rename public/assets/{ => features/orcidlink}/images/ORCID-sign-in.png (100%) diff --git a/public/assets/images/ORCID-iD_icon-vector.svg b/public/assets/features/orcidlink/images/ORCID-iD_icon-vector.svg similarity index 100% rename from public/assets/images/ORCID-iD_icon-vector.svg rename to public/assets/features/orcidlink/images/ORCID-iD_icon-vector.svg diff --git a/public/assets/images/ORCID-sign-in.png b/public/assets/features/orcidlink/images/ORCID-sign-in.png similarity index 100% rename from public/assets/images/ORCID-sign-in.png rename to public/assets/features/orcidlink/images/ORCID-sign-in.png diff --git a/src/features/orcidlink/CreateLink/index.tsx b/src/features/orcidlink/CreateLink/index.tsx index e4c22bab..499fea32 100644 --- a/src/features/orcidlink/CreateLink/index.tsx +++ b/src/features/orcidlink/CreateLink/index.tsx @@ -17,7 +17,7 @@ import { usePageTitle } from '../../layout/layoutSlice'; import { ORCID_LABEL, ORCID_LINK_LABEL, - ORCID_SIGN_IN_SCREENSHOT, + ORCID_SIGN_IN_SCREENSHOT_URL, } from '../constants'; import styles from '../orcidlink.module.scss'; @@ -126,7 +126,7 @@ export default function ORCIDLinkCreateLink() { to register for a new account.

ORCID® Sign In Date: Thu, 23 May 2024 16:53:34 +0000 Subject: [PATCH 11/13] add navigation to linking view [URO-209] --- src/features/orcidlink/HomeUnlinked.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/features/orcidlink/HomeUnlinked.tsx b/src/features/orcidlink/HomeUnlinked.tsx index 56e67c3b..0963db30 100644 --- a/src/features/orcidlink/HomeUnlinked.tsx +++ b/src/features/orcidlink/HomeUnlinked.tsx @@ -12,6 +12,7 @@ import { Typography, Unstable_Grid2 as Grid, } from '@mui/material'; +import { Link } from 'react-router-dom'; import MoreInformation from './common/MoreInformation'; export default function Unlinked() { @@ -31,13 +32,15 @@ export default function Unlinked() { - + + + From 0c6515663b72f2fd1ea72349926b3aa1135f4880 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Thu, 20 Jun 2024 17:46:04 +0000 Subject: [PATCH 12/13] replace copyJSON with structuredClone, adding 'core-js' as s direct dev dependency [URO-209] --- package-lock.json | 7 ++++--- package.json | 1 + src/features/orcidlink/common/CreditName.test.tsx | 8 ++++---- src/features/orcidlink/common/RealName.test.tsx | 8 ++++---- src/features/orcidlink/test/mocks.ts | 14 -------------- 5 files changed, 13 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 36885c57..ea061690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.46.1", "babel-plugin-named-exports-order": "^0.0.2", + "core-js": "3.37.1", "eslint-plugin-prettier": "^3.4.0", "husky": "^7.0.1", "prettier": "^2.3.2", @@ -14878,9 +14879,9 @@ } }, "node_modules/core-js": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz", - "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==", + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", "hasInstallScript": true, "funding": { "type": "opencollective", diff --git a/package.json b/package.json index 72999de4..2e00d1d1 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.46.1", "babel-plugin-named-exports-order": "^0.0.2", + "core-js": "3.37.1", "eslint-plugin-prettier": "^3.4.0", "husky": "^7.0.1", "prettier": "^2.3.2", diff --git a/src/features/orcidlink/common/CreditName.test.tsx b/src/features/orcidlink/common/CreditName.test.tsx index fb54958d..71c91536 100644 --- a/src/features/orcidlink/common/CreditName.test.tsx +++ b/src/features/orcidlink/common/CreditName.test.tsx @@ -1,12 +1,12 @@ import { render } from '@testing-library/react'; +import 'core-js/actual/structured-clone'; import { ORCIDProfile } from '../../../common/api/orcidLinkCommon'; import { PROFILE_1 } from '../test/data'; -import { jsonCopy } from '../test/mocks'; import CreditName from './CreditName'; describe('The renderCreditName render function', () => { it('renders correctly if not private', () => { - const profile = jsonCopy(PROFILE_1); + const profile = structuredClone(PROFILE_1); const { container } = render(); @@ -14,7 +14,7 @@ describe('The renderCreditName render function', () => { }); it('renders a special string if it is private', () => { - const profile = jsonCopy(PROFILE_1); + const profile = structuredClone(PROFILE_1); profile.nameGroup.private = true; @@ -24,7 +24,7 @@ describe('The renderCreditName render function', () => { }); it('renders a "not available" string if it is absent', () => { - const profile = jsonCopy(PROFILE_1); + const profile = structuredClone(PROFILE_1); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion profile.nameGroup.fields!.creditName = null; diff --git a/src/features/orcidlink/common/RealName.test.tsx b/src/features/orcidlink/common/RealName.test.tsx index dd1d88ed..f7af59ed 100644 --- a/src/features/orcidlink/common/RealName.test.tsx +++ b/src/features/orcidlink/common/RealName.test.tsx @@ -1,12 +1,12 @@ import { render } from '@testing-library/react'; +import 'core-js/actual/structured-clone'; import { ORCIDProfile } from '../../../common/api/orcidLinkCommon'; import { PROFILE_1 } from '../test/data'; -import { jsonCopy } from '../test/mocks'; import RealName from './RealName'; describe('the renderRealName render function ', () => { it('renders correctly if not private', () => { - const profile = jsonCopy(PROFILE_1); + const profile = structuredClone(PROFILE_1); const { container } = render(); @@ -14,7 +14,7 @@ describe('the renderRealName render function ', () => { }); it('renders just the first name if no last name', () => { - const profile = jsonCopy(PROFILE_1); + const profile = structuredClone(PROFILE_1); // We know how the test profile is populated. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -26,7 +26,7 @@ describe('the renderRealName render function ', () => { }); it('renders a special string if it is private', () => { - const profile = jsonCopy(PROFILE_1); + const profile = structuredClone(PROFILE_1); profile.nameGroup.private = true; const { container } = render(); diff --git a/src/features/orcidlink/test/mocks.ts b/src/features/orcidlink/test/mocks.ts index 4239cabb..b93bf530 100644 --- a/src/features/orcidlink/test/mocks.ts +++ b/src/features/orcidlink/test/mocks.ts @@ -161,20 +161,6 @@ export function setupMockRegularUserWithError() { ); } -/** - * Make a copy of a given "json-compatible" value. - * - * This can be replaced with "structuredClone". In order to that that, we need - * to use core-js, but we don't have a direct dependency upon core-js; it is a - * transitive dependency of several packages already. - * - * This technique is not ideal, but in the limited usage of it for test data, - * works just fine. - */ -export function jsonCopy(value: T): T { - return JSON.parse(JSON.stringify(value)); -} - /** * This is an empty function that serves as a placeholder for props that expect * one, but in tests in which these props are not tested, so they need not do anything. From 1aba3e38052481c996463b081623137467ab50d1 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Thu, 20 Jun 2024 17:48:16 +0000 Subject: [PATCH 13/13] replace "noop" in orcidlink testing with "noOp" in common [URO-209] --- .../orcidlink/HomeLinked/ManageTab.test.tsx | 18 +++++++++--------- .../orcidlink/HomeLinked/view.test.tsx | 12 ++++++------ src/features/orcidlink/test/mocks.ts | 8 -------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/features/orcidlink/HomeLinked/ManageTab.test.tsx b/src/features/orcidlink/HomeLinked/ManageTab.test.tsx index 8dad52f8..b8b1ffc3 100644 --- a/src/features/orcidlink/HomeLinked/ManageTab.test.tsx +++ b/src/features/orcidlink/HomeLinked/ManageTab.test.tsx @@ -2,8 +2,8 @@ import { screen } from '@testing-library/dom'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; +import { noOp } from '../../common'; import { LINK_RECORD_1, PROFILE_1 } from '../test/data'; -import { noop } from '../test/mocks'; import ManageTab from './ManageTab'; describe('The ManageTab component', () => { @@ -18,8 +18,8 @@ describe('The ManageTab component', () => { linkRecord={linkRecord} profile={profile} orcidSiteURL={orcidSiteURL} - removeLink={noop} - toggleShowInProfile={noop} + removeLink={noOp} + toggleShowInProfile={noOp} /> ); @@ -45,8 +45,8 @@ describe('The ManageTab component', () => { linkRecord={linkRecord} profile={profile} orcidSiteURL={orcidSiteURL} - removeLink={noop} - toggleShowInProfile={noop} + removeLink={noOp} + toggleShowInProfile={noOp} /> ); @@ -95,7 +95,7 @@ describe('The ManageTab component', () => { profile={profile} orcidSiteURL={orcidSiteURL} removeLink={removeLink} - toggleShowInProfile={noop} + toggleShowInProfile={noOp} /> ); @@ -137,8 +137,8 @@ describe('The ManageTab component', () => { linkRecord={linkRecord} profile={profile} orcidSiteURL={orcidSiteURL} - removeLink={noop} - toggleShowInProfile={noop} + removeLink={noOp} + toggleShowInProfile={noOp} /> ); @@ -183,7 +183,7 @@ describe('The ManageTab component', () => { linkRecord={linkRecord} profile={profile} orcidSiteURL={orcidSiteURL} - removeLink={noop} + removeLink={noOp} toggleShowInProfile={toggleShowInProfile} /> diff --git a/src/features/orcidlink/HomeLinked/view.test.tsx b/src/features/orcidlink/HomeLinked/view.test.tsx index 305a264f..4188cc44 100644 --- a/src/features/orcidlink/HomeLinked/view.test.tsx +++ b/src/features/orcidlink/HomeLinked/view.test.tsx @@ -1,8 +1,8 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; +import { noOp } from '../../common'; import { LINK_RECORD_1, PROFILE_1, SERVICE_INFO_1 } from '../test/data'; -import { noop } from '../test/mocks'; import HomeLinked from './view'; describe('The HomeLinked Component', () => { @@ -24,8 +24,8 @@ describe('The HomeLinked Component', () => { info={info} linkRecord={linkRecord} profile={profile} - removeLink={noop} - toggleShowInProfile={noop} + removeLink={noOp} + toggleShowInProfile={noOp} /> ); @@ -48,8 +48,8 @@ describe('The HomeLinked Component', () => { info={info} linkRecord={linkRecord} profile={profile} - removeLink={noop} - toggleShowInProfile={noop} + removeLink={noOp} + toggleShowInProfile={noOp} /> ); @@ -100,7 +100,7 @@ describe('The HomeLinked Component', () => { info={info} linkRecord={linkRecord} profile={profile} - removeLink={noop} + removeLink={noOp} toggleShowInProfile={toggleShowInProfile} /> diff --git a/src/features/orcidlink/test/mocks.ts b/src/features/orcidlink/test/mocks.ts index b93bf530..d8ab8956 100644 --- a/src/features/orcidlink/test/mocks.ts +++ b/src/features/orcidlink/test/mocks.ts @@ -160,11 +160,3 @@ export function setupMockRegularUserWithError() { } ); } - -/** - * This is an empty function that serves as a placeholder for props that expect - * one, but in tests in which these props are not tested, so they need not do anything. - */ -export function noop() { - // do nothing -}