diff --git a/src/app/components/jsx-helpers/raw-html.tsx b/src/app/components/jsx-helpers/raw-html.tsx index 7fcd4f0db..585c67d14 100644 --- a/src/app/components/jsx-helpers/raw-html.tsx +++ b/src/app/components/jsx-helpers/raw-html.tsx @@ -1,21 +1,27 @@ import React from 'react'; +import usePortalContext from '~/contexts/portal'; // Making scripts work, per https://stackoverflow.com/a/47614491/392102 function activateScripts(el: HTMLElement) { - const scripts: HTMLScriptElement[] = Array.from(el.querySelectorAll('script')); - const processOne = (() => { + const scripts: HTMLScriptElement[] = Array.from( + el.querySelectorAll('script') + ); + const processOne = () => { const s = scripts.shift(); if (!s) { return; } const newScript = document.createElement('script'); - const p = (s.src) ? new Promise((resolve) => { - newScript.onload = resolve; - }) : Promise.resolve(); + const p = s.src + ? new Promise((resolve) => { + newScript.onload = resolve; + }) + : Promise.resolve(); - Array.from(s.attributes) - .forEach((a) => newScript.setAttribute(a.name, a.value)); + Array.from(s.attributes).forEach((a) => + newScript.setAttribute(a.name, a.value) + ); if (s.textContent) { newScript.appendChild(document.createTextNode(s.textContent)); } @@ -23,22 +29,36 @@ function activateScripts(el: HTMLElement) { s.parentNode?.replaceChild(newScript, s); p.then(processOne); - }); + }; processOne(); } -type RawHTMLArgs = {Tag?: string, html?: TrustedHTML, embed?: boolean} & React.HTMLAttributes; +type RawHTMLArgs = { + Tag?: string; + html?: TrustedHTML; + embed?: boolean; +} & React.HTMLAttributes; -export default function RawHTML({Tag='div', html, embed=false, ...otherProps}: RawHTMLArgs) { +export default function RawHTML({ + Tag = 'div', + html, + embed = false, + ...otherProps +}: RawHTMLArgs) { const ref = React.useRef(); + const {rewriteLinks} = usePortalContext(); React.useEffect(() => { if (embed && ref.current) { activateScripts(ref.current); } }); - return ( - React.createElement(Tag, {ref, dangerouslySetInnerHTML: {__html: html}, ...otherProps}) - ); + React.useLayoutEffect(() => rewriteLinks?.(ref.current as HTMLElement), [rewriteLinks]); + + return React.createElement(Tag, { + ref, + dangerouslySetInnerHTML: {__html: html}, + ...otherProps + }); } diff --git a/src/app/components/list-of-links/list-of-links.scss b/src/app/components/list-of-links/list-of-links.scss new file mode 100644 index 000000000..cd5cbeb1b --- /dev/null +++ b/src/app/components/list-of-links/list-of-links.scss @@ -0,0 +1,8 @@ +.list-of-links { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} diff --git a/src/app/components/list-of-links/list-of-links.tsx b/src/app/components/list-of-links/list-of-links.tsx new file mode 100644 index 000000000..f74ce4518 --- /dev/null +++ b/src/app/components/list-of-links/list-of-links.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import './list-of-links.scss'; + +export default function ListOfLinks({children}: React.PropsWithChildren) { + return ( +
    + {React.Children.toArray(children).map((c, i) => (
  • {c}
  • ))} +
+ ); +} diff --git a/src/app/components/shell/portal-router.tsx b/src/app/components/shell/portal-router.tsx new file mode 100644 index 000000000..53376c9a8 --- /dev/null +++ b/src/app/components/shell/portal-router.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import {RoutesAlsoInPortal} from './router'; +import usePortalContext from '~/contexts/portal'; +import {useParams} from 'react-router-dom'; + +export default function PortalRouter() { + // If we want to validate the portal name, that can be done here + const {portal, '*': rest} = useParams(); + const {setPortal} = usePortalContext(); + + React.useEffect(() => { + // It seems that the path "/press" in particular winds up matching + // the portal-router Route, so we need to ensure it's really a portal + // route by verifying that there's something after the :portal param + if (rest) { + setPortal(portal as string); + } + }, [rest, portal, setPortal]); + + return ; +} diff --git a/src/app/components/shell/router-helpers/fallback-to.js b/src/app/components/shell/router-helpers/fallback-to.js index 0ea2c2d8c..5b690ae20 100644 --- a/src/app/components/shell/router-helpers/fallback-to.js +++ b/src/app/components/shell/router-helpers/fallback-to.js @@ -5,6 +5,7 @@ import usePageData from '~/helpers/use-page-data'; import loadable from 'react-loadable'; import LoadingPlaceholder from '~/components/loading-placeholder/loading-placeholder'; import useLayoutContext from '~/contexts/layout'; +import usePortalContext from '~/contexts/portal'; const FallbackToGeneralPage = loadable({ loader: () => import('./fallback-to-general.js'), @@ -22,7 +23,7 @@ export default function FallbackTo({name}) { } export const isFlexPage = (data) => ( - typeof data.meta?.type === 'string' && + typeof data?.meta?.type === 'string' && ['pages.FlexPage', 'pages.RootPage'].includes(data.meta.type) ); @@ -31,6 +32,14 @@ function LoadedPage({data, name}) { const {setLayoutParameters, layoutParameters} = useLayoutContext(); const hasError = 'error' in data; const isFlex = !hasError && isFlexPage(data); + const isPortal = isFlex && layoutParameters.name === 'landing'; + const {setPortal} = usePortalContext(); + + React.useEffect(() => { + if (isPortal) { + setPortal(name); + } + }, [isPortal, name, setPortal]); React.useEffect(() => { if (isFlex) { diff --git a/src/app/components/shell/router.js b/src/app/components/shell/router.js index 6a3cb3473..d5244d688 100644 --- a/src/app/components/shell/router.js +++ b/src/app/components/shell/router.js @@ -13,6 +13,7 @@ import useRouterContext, {RouterContextProvider} from './router-context'; import loadable from 'react-loadable'; import LoadingPlaceholder from '~/components/loading-placeholder/loading-placeholder'; import useLayoutContext, { LayoutContextProvider } from '~/contexts/layout'; +import PortalRouter from './portal-router'; import './skip-to-content.scss'; function useAnalyticsPageView() { @@ -28,6 +29,7 @@ const Fallback = loadable({ loader: () => import('./router-helpers/fallback-to.js'), loading: () =>

...Loading

}); + const Error404 = loadable({ loader: () => import('~/pages/404/404'), loading: () =>

404

@@ -56,14 +58,14 @@ function useLoading(name) { } function DefaultLayout({children}) { + const {portal} = useParams(); const {setLayoutParameters, layoutParameters} = useLayoutContext(); - React.useEffect( - () => setLayoutParameters(), - [setLayoutParameters] - ); + if (portal) { + setLayoutParameters({name: 'landing', data: layoutParameters.data}); + } - return layoutParameters.name === 'default' ? children : null; + return layoutParameters.name ? children : null; } function usePage(name) { @@ -133,39 +135,50 @@ function MainRoutes() { } /> - { - FOOTER_PAGES.map( - (path) => } /> - ) - } - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> + + + + } /> Fell through} /> ); } + +export function RoutesAlsoInPortal() { + return ( + + { + FOOTER_PAGES.map( + (path) => } /> + ) + } + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + + ); +} + function doSkipToContent(event) { event.preventDefault(); const mainEl = document.getElementById('main'); diff --git a/src/app/components/shell/shell.tsx b/src/app/components/shell/shell.tsx index 74c5e63a9..7542d9e0c 100644 --- a/src/app/components/shell/shell.tsx +++ b/src/app/components/shell/shell.tsx @@ -6,6 +6,7 @@ import {BrowserRouter, Routes, Route} from 'react-router-dom'; import {SharedDataContextProvider} from '../../contexts/shared-data'; import JITLoad from '~/helpers/jit-load'; import {SalesforceContextProvider} from '~/contexts/salesforce'; +import {PortalContextProvider} from '~/contexts/portal'; import Error404 from '~/pages/404/404'; @@ -14,9 +15,11 @@ function AppContext({children}: React.PropsWithChildren) { - - {children} - + + + {children} + + diff --git a/src/app/contexts/portal.tsx b/src/app/contexts/portal.tsx new file mode 100644 index 000000000..756bc9b66 --- /dev/null +++ b/src/app/contexts/portal.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import buildContext from '~/components/jsx-helpers/build-context'; + +function useContextValue() { + const [portal, setPortal] = React.useState(''); + const portalPrefix = portal ? `/${portal}` : ''; + const rewriteLinks = React.useCallback((container: HTMLElement) => { + if (!portalPrefix) {return;} + const linkNodes = container.querySelectorAll('a[href^="/"]'); + + for (const node of linkNodes) { + const href = node.getAttribute('href'); + + node.setAttribute('href', `${portalPrefix}${href}`); + } + }, + [portalPrefix]); + + return {portalPrefix, setPortal, rewriteLinks}; +} + +const {useContext, ContextProvider} = buildContext({useContextValue}); + +export {useContext as default, ContextProvider as PortalContextProvider}; diff --git a/src/app/layouts/default/footer/footer.scss b/src/app/layouts/default/footer/footer.scss index a762bb402..f7e891808 100644 --- a/src/app/layouts/default/footer/footer.scss +++ b/src/app/layouts/default/footer/footer.scss @@ -74,15 +74,6 @@ $lower-footer: #3b3b3b; margin: 0; } - .list-of-links { - list-style-type: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: 0.5rem; - } - .mission { grid-area: mission; diff --git a/src/app/layouts/default/footer/footer.js b/src/app/layouts/default/footer/footer.tsx similarity index 80% rename from src/app/layouts/default/footer/footer.js rename to src/app/layouts/default/footer/footer.tsx index 8bb3be277..9f54f0cec 100644 --- a/src/app/layouts/default/footer/footer.js +++ b/src/app/layouts/default/footer/footer.tsx @@ -3,28 +3,45 @@ import RawHTML from '~/components/jsx-helpers/raw-html'; import LoaderPage from '~/components/jsx-helpers/loader-page'; import Copyright from './copyright'; import CookieYesToggle from './cookie-yes-toggle'; +import ListOfLinks from '~/components/list-of-links/list-of-links'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faFacebookF} from '@fortawesome/free-brands-svg-icons/faFacebookF'; import {faXTwitter} from '@fortawesome/free-brands-svg-icons/faXTwitter'; import {faLinkedinIn} from '@fortawesome/free-brands-svg-icons/faLinkedinIn'; import {faInstagram} from '@fortawesome/free-brands-svg-icons/faInstagram'; import {faYoutube} from '@fortawesome/free-brands-svg-icons/faYoutube'; +import usePortalContext from '~/contexts/portal'; import './footer.scss'; -function ListOfLinks({children}) { - return ( -
    - {React.Children.toArray(children).map((c) => (
  • {c}
  • ))} -
- ); -} - function Footer({ data: { - supporters, copyright, apStatement, - facebookLink, twitterLink, linkedinLink + supporters, + copyright, + apStatement, + facebookLink, + twitterLink, + linkedinLink } +}: { + data: { + supporters: string; + copyright: string; + apStatement: string; + facebookLink: string; + twitterLink: string; + linkedinLink: string; + }; }) { + const {rewriteLinks} = usePortalContext(); + + React.useLayoutEffect( + () => + rewriteLinks?.( + document.querySelector('.page-footer') as HTMLElement + ), + [rewriteLinks] + ); + return (
@@ -34,7 +51,9 @@ function Footer({

Help

Contact Us - Support Center + + Support Center + FAQ
@@ -42,14 +61,18 @@ function Footer({

OpenStax

Press - Newsletter + + Newsletter + Careers

Policies

- Accessibility Statement + + Accessibility Statement + Terms of Use Licensing Privacy Notice @@ -61,7 +84,10 @@ function Footer({
- +
  • @@ -128,8 +154,8 @@ function Footer({ export default function FooterLoader() { return ( -
    +
    -
    +
); } diff --git a/src/app/layouts/default/header/menus/main-menu/dropdown/dropdown.js b/src/app/layouts/default/header/menus/main-menu/dropdown/dropdown.js index eab34ea03..413ffbcea 100644 --- a/src/app/layouts/default/header/menus/main-menu/dropdown/dropdown.js +++ b/src/app/layouts/default/header/menus/main-menu/dropdown/dropdown.js @@ -6,6 +6,7 @@ import useNavigateByKey from './use-navigate-by-key'; import useMenuControls from './use-menu-controls'; import {treatSpaceOrEnterAsClick} from '~/helpers/events'; import {useLocation} from 'react-router-dom'; +import usePortalContext from '~/contexts/portal'; import cn from 'classnames'; import './dropdown.scss'; @@ -18,12 +19,13 @@ export function MenuItem({label, url, local=undefined}) { const {innerWidth: _} = useWindowContext(); const urlPath = url.replace('/view-all', ''); const {pathname} = useLocation(); + const {portalPrefix} = usePortalContext(); return ( ) { - return ( -
    - {React.Children.toArray(children).map((c, i) => (
  • {c}
  • ))} -
- ); -} - export function useContactDialog() { const [Dialog, open, close] = useDialog(); @@ -73,6 +67,10 @@ export function useContactDialog() { function FlexFooter({data}: Props) { const {ContactDialog, open: openContactDialog} = useContactDialog(); const contactFormParams = [{key: 'source_url', value: window.location.href}]; + const {rewriteLinks} = usePortalContext(); + + React.useLayoutEffect(() => rewriteLinks?.(document.querySelector('.page-footer') as HTMLElement), + [rewriteLinks]); return (
@@ -107,8 +105,8 @@ function FlexFooter({data}: Props) { export default function FooterLoader() { return ( -
+
-
+
); } diff --git a/src/app/layouts/landing/landing.tsx b/src/app/layouts/landing/landing.tsx index 68b301a48..56bf7faf4 100644 --- a/src/app/layouts/landing/landing.tsx +++ b/src/app/layouts/landing/landing.tsx @@ -13,7 +13,7 @@ import { isFlexPage } from '~/components/shell/router-helpers/fallback-to'; import './landing.scss'; type Props = { - data: { + data?: { title: string; meta?: { type: string; @@ -39,14 +39,15 @@ export default function LandingLayout({ children, data }: React.PropsWithChildren) { - const showGive = data.layout[0]?.value.showGive; + const layoutValue = data?.layout[0]?.value; + const showGive = layoutValue?.showGive; // BrowserRouter has to include everything that uses useLocation return (
@@ -75,7 +76,7 @@ function Main({children, data}: React.PropsWithChildren) {
diff --git a/src/app/pages/details/common/get-this-title-files/options.tsx b/src/app/pages/details/common/get-this-title-files/options.tsx index e31381b56..59951d009 100644 --- a/src/app/pages/details/common/get-this-title-files/options.tsx +++ b/src/app/pages/details/common/get-this-title-files/options.tsx @@ -18,6 +18,7 @@ import RecommendedCallout from './recommended-callout/recommended-callout'; import {useOpenGiveDialog} from './give-before-pdf/use-give-dialog'; import useCalloutCounter from './recommended-callout/use-callout-counter'; import trackLink from '../track-link'; +import usePortalContext from '~/contexts/portal'; import type {Model} from '../get-this-title'; import type {TrackedMouseEvent} from '~/components/shell/router-helpers/use-link-handler'; @@ -104,6 +105,15 @@ export function TocOption({model}: {model: Model}) { ); } +function useRexPortalLinkOrNot(link: string) { + const {portalPrefix} = usePortalContext(); + + if (!portalPrefix) { + return link; + } + return link.replace('books/', `portal${portalPrefix}/books/`); +} + export function WebviewOption({model}: {model: Model}) { const [showCallout, hideForever] = useCalloutCounter(model.slug); @@ -113,7 +123,7 @@ export function WebviewOption({model}: {model: Model}) { link: intl.formatMessage({id: 'getit.webview.link'}) }; const isRex = Boolean(model.webviewRexLink); - const webviewLink = model.webviewRexLink || model.webviewLink; + const webviewLink = useRexPortalLinkOrNot(model.webviewRexLink) || model.webviewLink; const iconAndTextArgs = { icon: faLaptop, text: $.isPolish(model.title) ? 'Zobacz w przeglÄ…darce' : texts.link diff --git a/src/app/pages/details/common/let-us-know/let-us-know.tsx b/src/app/pages/details/common/let-us-know/let-us-know.tsx index 9fb913803..4e72e2b63 100644 --- a/src/app/pages/details/common/let-us-know/let-us-know.tsx +++ b/src/app/pages/details/common/let-us-know/let-us-know.tsx @@ -5,11 +5,13 @@ import {useIntl} from 'react-intl'; import {FontAwesomeIcon, FontAwesomeIconProps} from '@fortawesome/react-fontawesome'; import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus'; import {faBook} from '@fortawesome/free-solid-svg-icons/faBook'; +import usePortalContext from '~/contexts/portal'; import cn from 'classnames'; import './let-us-know.scss'; function useDataStuffFor(title:string) { const intl = useIntl(); + const {portalPrefix} = usePortalContext(); const [text1, text2] = [ intl.formatMessage({id: 'letusknow.text1'}), intl.formatMessage({id: 'letusknow.text2'}) @@ -25,8 +27,8 @@ function useDataStuffFor(title:string) { } return { - url1: `/interest?${encodeURIComponent(title)}`, - url2: `/adoption?${encodeURIComponent(title)}`, + url1: `${portalPrefix}/interest?${encodeURIComponent(title)}`, + url2: `${portalPrefix}/adoption?${encodeURIComponent(title)}`, text1, text2 }; } diff --git a/src/app/pages/footer-page/footer-page.tsx b/src/app/pages/footer-page/footer-page.tsx index f1a9fd738..dd7c36f4f 100644 --- a/src/app/pages/footer-page/footer-page.tsx +++ b/src/app/pages/footer-page/footer-page.tsx @@ -1,7 +1,7 @@ import React from 'react'; import RawHTML from '~/components/jsx-helpers/raw-html'; import usePageData from '~/helpers/use-page-data'; -import {useLocation} from 'react-router-dom'; +import {useParams} from 'react-router-dom'; import useDocumentHead from '~/helpers/use-document-head'; import './footer-page.scss'; @@ -41,7 +41,7 @@ function FooterPage({data}: {data: PageData}) { } export default function LoadFooterPage() { - const {pathname} = useLocation(); + const pathname = `/${useParams()['*']}`; const slugEnd = specialSlugFromPath[pathname] ?? pathname; const slug = `pages${slugEnd}`; const data = usePageData(slug); diff --git a/test/src/components/shell.test.tsx b/test/src/components/shell.test.tsx index cd45dc596..a2d7e828a 100644 --- a/test/src/components/shell.test.tsx +++ b/test/src/components/shell.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; import {render, screen} from '@testing-library/preact'; import AppElement from '~/components/shell/shell'; -import {BrowserRouter, MemoryRouter as MR} from 'react-router-dom'; +import * as RRD from 'react-router-dom'; +import * as CF from '~/helpers/cms-fetch'; +import MR from '~/../../test/helpers/future-memory-router'; import ReactModal from 'react-modal'; -// @ts-expect-error does not exist on -const {routerFuture} = global; +const {BrowserRouter} = RRD; jest.mock('react-router-dom', () => { const actualRouterDom = jest.requireActual('react-router-dom'); @@ -37,7 +38,7 @@ describe('shell', () => { console.warn = jest.fn(); console.debug = jest.fn(); (BrowserRouter as jest.Mock).mockImplementationOnce(({children}) => ( - {children} + {children} )); render(AppElement); @@ -46,7 +47,7 @@ describe('shell', () => { }); it('Delivers normal contact page', async () => { (BrowserRouter as jest.Mock).mockImplementationOnce(({children}) => ( - {children} + {children} )); render(AppElement); @@ -63,4 +64,24 @@ describe('shell', () => { (ReactModal.setAppElement as jest.Mock).mockImplementation(() => externalResolution('ok')); await modalCalled; }); + it('delivers press page in a portal', async () => { + jest.spyOn(CF, 'default').mockImplementation((path) => { + if (path.includes('books?')) { + return Promise.resolve({books: []}); + } + if (path.includes('pages/?type')) { + return Promise.resolve({items: []}); + } + return Promise.resolve([]); + }); + (BrowserRouter as jest.Mock).mockImplementationOnce(({children}) => ( + {children} + )); + jest.spyOn(RRD, 'useParams').mockReturnValue({portal: 'some-portal', '*': '/contact'}); + + render(AppElement); + const pressLink = await screen.findByRole('link', {name: 'Press'}); + + expect(pressLink.getAttribute('href')).toBe('/some-portal/press'); + }); }); diff --git a/test/src/layouts/landing/footer.test.tsx b/test/src/layouts/landing/footer.test.tsx index f4875ca11..c84754db6 100644 --- a/test/src/layouts/landing/footer.test.tsx +++ b/test/src/layouts/landing/footer.test.tsx @@ -23,9 +23,9 @@ describe('flex landing footer', () => { beforeEach(() => { jest.useFakeTimers(); }); + const getIframe = () => document.querySelector('iframe'); it('opens and closes', async () => { - const getIframe = () => document.querySelector('iframe'); const contactFormParams = [ { key: 'userId', value: 'test' } ]; @@ -49,8 +49,6 @@ describe('flex landing footer', () => { await waitFor(() => expect(getIframe()).toBeNull()); }); it('handles undefined contactFormParams', async () => { - const getIframe = () => document.querySelector('iframe'); - render( diff --git a/test/src/layouts/layouts.test.tsx b/test/src/layouts/layouts.test.tsx index ab4a8092e..8a1e84b8f 100644 --- a/test/src/layouts/layouts.test.tsx +++ b/test/src/layouts/layouts.test.tsx @@ -7,7 +7,8 @@ import LandingLayout from '~/layouts/landing/landing'; // @ts-expect-error does not exist on const {routerFuture} = global; -type Layout = Parameters[0]['data']['layout']; +type Data = Parameters[0]['data']; +type Layout = Exclude['layout']; describe('layouts/landing', () => { function Component({layout}: {layout: Layout}) { @@ -25,6 +26,15 @@ describe('layouts/landing', () => { ); } + it('renders without data object', () => { + render( + +
child contents
+
+
); + expect(screen.getAllByRole('img')).toHaveLength(2); + expect(screen.getAllByRole('link')).toHaveLength(1); + }); it('renders without layout values', () => { render(); expect(screen.getAllByRole('img')).toHaveLength(2); @@ -68,7 +78,7 @@ describe('layouts/landing', () => { const data = {title, layout, meta} as const; render( - +
child contents
@@ -88,7 +98,7 @@ describe('layouts/landing', () => { const data = {title, layout, meta} as const; render( - +
child contents
diff --git a/test/src/pages/adoption/adoption.test.js b/test/src/pages/adoption/adoption.test.js deleted file mode 100644 index e3787d7b0..000000000 --- a/test/src/pages/adoption/adoption.test.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import {render, screen} from '@testing-library/preact'; -import userEvent from '@testing-library/user-event'; -import AdoptionForm from '~/pages/adoption/adoption'; -import {MemoryRouter} from 'react-router-dom'; -import {MainClassContextProvider} from '~/contexts/main-class'; -import {SharedDataContextProvider} from '~/contexts/shared-data'; -import {LanguageContextProvider} from '~/contexts/language'; -import {test, expect, beforeEach} from '@jest/globals'; - -beforeEach(async () => { - render( - - - - - - - - - - ); - await screen.findByText(/Let us know you're using/); -}); - -test('creates with role selector', () => - expect(screen.queryAllByRole('option', {hidden: true})).toHaveLength(8)); - -test('form appears when role is selected', async () => { - const listBoxes = screen.queryAllByRole('listbox'); - const user = userEvent.setup(); - - await user.click(listBoxes[1]); - const options = await screen.findAllByRole('option', {hidden: true}); - const instructorOption = options.find( - (o) => o.textContent === 'Instructor' - ); - - await user.click(instructorOption); - await screen.findByRole('form'); -}); diff --git a/test/src/pages/adoption/adoption.test.tsx b/test/src/pages/adoption/adoption.test.tsx new file mode 100644 index 000000000..286a4e9af --- /dev/null +++ b/test/src/pages/adoption/adoption.test.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import {render, screen} from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; +import AdoptionForm from '~/pages/adoption/adoption'; +import FormHeader from '~/components/form-header/form-header'; +import MemoryRouter from '~/../../test/helpers/future-memory-router'; +import {MainClassContextProvider} from '~/contexts/main-class'; +import {SharedDataContextProvider} from '~/contexts/shared-data'; +import {LanguageContextProvider} from '~/contexts/language'; +import usePortalContext, {PortalContextProvider} from '~/contexts/portal'; + +describe('adoption-form', () => { + const saveWarn = console.warn; + + beforeAll(() => { + console.warn = jest.fn(); + }); + afterAll(() => { + console.warn = saveWarn; + }); + + beforeEach(async () => { + render( + + + + + + + + + + ); + await screen.findByText(/Let us know you're using/); + }); + + it('creates with role selector', () => + expect(screen.queryAllByRole('option', {hidden: true})).toHaveLength(8)); + + it('form appears when role is selected', async () => { + const listBoxes = screen.queryAllByRole('listbox'); + const user = userEvent.setup(); + + await user.click(listBoxes[1]); + const options = await screen.findAllByRole('option', {hidden: true}); + const instructorOption = options.find( + (o) => o.textContent === 'Instructor' + ); + + await user.click(instructorOption as HTMLElement); + await screen.findByRole('form'); + }); +}); + +describe('form-header in portal', () => { + function Component() { + const {setPortal} = usePortalContext(); + + setPortal('some-portal'); + + return ; + } + + it('rewrites links', async () => { + render( + + + + + + + + ); + const link = await screen.findByRole('link'); + + expect(link.getAttribute('href')).toBe('/some-portal/interest'); + }); +});