From 26d40c39072e9e979e0a80625713bbc50e502e60 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Tue, 8 Apr 2025 14:30:29 -0500 Subject: [PATCH 1/9] Paginator & tests (plus some imports that needlessly specify .js) --- src/app/components/body-units/body-units.tsx | 2 +- ...inator-context.js => paginator-context.ts} | 11 ++-- .../{paginator.js => paginator.tsx} | 10 ++-- ...mple-paginator.js => simple-paginator.tsx} | 50 +++++++++++++------ .../microsurvey-popup/microsurvey-popup.tsx | 2 +- .../blog/search-results/search-results.tsx | 2 +- .../errata-form/form/ErrorSourceSelector.js | 2 +- .../errata-form/form/ErrorTypeSelector.js | 2 +- src/app/pages/errata-form/form/form.js | 2 +- test/src/components/paginator.test.js | 28 ----------- test/src/components/paginator.test.tsx | 45 +++++++++++++++++ test/src/pages/webinars/explore-page.test.tsx | 19 ++++++- 12 files changed, 117 insertions(+), 58 deletions(-) rename src/app/components/paginator/{paginator-context.js => paginator-context.ts} (71%) rename src/app/components/paginator/search-results/{paginator.js => paginator.tsx} (90%) rename src/app/components/paginator/{simple-paginator.js => simple-paginator.tsx} (68%) delete mode 100644 test/src/components/paginator.test.js create mode 100644 test/src/components/paginator.test.tsx diff --git a/src/app/components/body-units/body-units.tsx b/src/app/components/body-units/body-units.tsx index 18c44ac9a..2fd0c11d8 100644 --- a/src/app/components/body-units/body-units.tsx +++ b/src/app/components/body-units/body-units.tsx @@ -1,6 +1,6 @@ import React from 'react'; import RawHTML from '~/components/jsx-helpers/raw-html'; -import Quote from '~/components/quote/quote.js'; +import Quote from '~/components/quote/quote'; import JITLoad from '~/helpers/jit-load'; function Unknown({data, type}: {data: unknown; type: string}) { diff --git a/src/app/components/paginator/paginator-context.js b/src/app/components/paginator/paginator-context.ts similarity index 71% rename from src/app/components/paginator/paginator-context.js rename to src/app/components/paginator/paginator-context.ts index 71371f93e..8bf5f39d5 100644 --- a/src/app/components/paginator/paginator-context.js +++ b/src/app/components/paginator/paginator-context.ts @@ -1,17 +1,22 @@ import {useState, useCallback} from 'react'; import buildContext from '~/components/jsx-helpers/build-context'; -function useContextValue(params={}) { +type ContextParams = { + resultsPerPage?: number; + initialPage?: number; +} + +function useContextValue(params: ContextParams={}) { const {resultsPerPage=1, initialPage=1} = params; const [currentPage, setCurrentPage] = useState(initialPage); const firstOnPage = (currentPage - 1) * resultsPerPage; const lastOnPage = firstOnPage + resultsPerPage - 1; const isVisible = useCallback( - (childIndex) => childIndex >= firstOnPage && childIndex <= lastOnPage, + (childIndex: number) => childIndex >= firstOnPage && childIndex <= lastOnPage, [firstOnPage, lastOnPage] ); const visibleChildren = useCallback( - (children) => children.slice(firstOnPage, lastOnPage + 1), + (children: React.ReactNode[]) => children.slice(firstOnPage, lastOnPage + 1), [firstOnPage, lastOnPage] ); diff --git a/src/app/components/paginator/search-results/paginator.js b/src/app/components/paginator/search-results/paginator.tsx similarity index 90% rename from src/app/components/paginator/search-results/paginator.js rename to src/app/components/paginator/search-results/paginator.tsx index 5f7810a65..a0de69d43 100644 --- a/src/app/components/paginator/search-results/paginator.js +++ b/src/app/components/paginator/search-results/paginator.tsx @@ -2,15 +2,15 @@ import React from 'react'; import usePaginatorContext from '../paginator-context'; import './paginator.scss'; -function getPageIndicators(pages, currentPage) { +function getPageIndicators(pages: number, currentPage: number) { const indicatorCount = Math.min(pages, 5); - const propsFor = (label) => ({ + const propsFor = (label: number) => ({ label, page: `page ${label}`, disabled: label === currentPage, selected: label === currentPage }); - const result = Array(indicatorCount).fill(); + const result = Array(indicatorCount).fill(0); if (pages - currentPage < 3) { result[0] = pages - indicatorCount + 1; @@ -23,7 +23,7 @@ function getPageIndicators(pages, currentPage) { return result.map(propsFor); } -function PageButtonBar({pages}) { +function PageButtonBar({pages}: {pages: number}) { const {currentPage, setCurrentPage} = usePaginatorContext(); const disablePrevious = currentPage === 1; const disableNext = currentPage === pages; @@ -56,7 +56,7 @@ function PageButtonBar({pages}) { ); } -export function PaginatorControls({items}) { +export function PaginatorControls({items}: {items: number}) { const {currentPage, resultsPerPage} = usePaginatorContext(); const pages = Math.ceil(items / resultsPerPage); const firstIndex = (currentPage - 1) * resultsPerPage; diff --git a/src/app/components/paginator/simple-paginator.js b/src/app/components/paginator/simple-paginator.tsx similarity index 68% rename from src/app/components/paginator/simple-paginator.js rename to src/app/components/paginator/simple-paginator.tsx index 742418a29..3c072d6f5 100644 --- a/src/app/components/paginator/simple-paginator.js +++ b/src/app/components/paginator/simple-paginator.tsx @@ -5,17 +5,28 @@ import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'; import range from 'lodash/range'; import './simple-paginator.scss'; -function ControlButton({active, icon, onClick}) { +type Direction = 'next' | 'previous'; + +function ControlButton({active, direction, onClick}: { + active?: boolean; + direction: Direction; + onClick: React.MouseEventHandler +}) { + const icon = direction === 'next' ? faChevronRight : faChevronLeft; + return ( - ); } -function PageLink({page, setPage}) { +function PageLink({page, setPage}: { + page: number; + setPage: (p: number) => void; +}) { const onClick = React.useCallback( - (e) => { + (e: React.MouseEvent) => { e.preventDefault(); setPage(page); }, @@ -23,11 +34,17 @@ function PageLink({page, setPage}) { ); return ( - {page} + {page} ); } -function PagesBeforeCurrent({currentPage, totalPages, setPage}) { +type PaginatorArgs = { + currentPage: number; + setPage: (p: number) => void; + totalPages: number; +} + +function PagesBeforeCurrent({currentPage, totalPages, setPage}: PaginatorArgs) { if (currentPage === 1) { return null; } @@ -53,7 +70,7 @@ function PagesBeforeCurrent({currentPage, totalPages, setPage}) { ); } -function PagesAfterCurrent({currentPage, totalPages, setPage}) { +function PagesAfterCurrent({currentPage, totalPages, setPage}: PaginatorArgs) { if (currentPage === totalPages) { return null; } @@ -82,29 +99,34 @@ function PagesAfterCurrent({currentPage, totalPages, setPage}) { ); } -export default function SimplePaginator({currentPage, setPage, totalPages}) { +export default function SimplePaginator({currentPage, setPage, totalPages}: PaginatorArgs) { const nextPage = () => setPage(currentPage + 1); const prevPage = () => setPage(currentPage - 1); return ( -
- 1} icon={faChevronLeft} onClick={prevPage} /> +
+ + ); } -export function itemRangeOnPage(page, perPage, totalCount) { +export function itemRangeOnPage(page: number, perPage: number, totalCount: number) { const end = Math.min(totalCount, page * perPage); const start = (page - 1) * perPage + 1; return [start, end]; } -export function Showing({page, perPage=9, totalCount, ofWhat}) { +export function Showing({page, perPage, totalCount, ofWhat}: { + page: number; + perPage: number; + totalCount: number; + ofWhat: string; +}) { const [start, end] = itemRangeOnPage(page, perPage, totalCount); const countMessage = totalCount <= perPage ? 'all' : `${start}-${end} of`; diff --git a/src/app/layouts/default/microsurvey-popup/microsurvey-popup.tsx b/src/app/layouts/default/microsurvey-popup/microsurvey-popup.tsx index 658e7888f..742ce5635 100644 --- a/src/app/layouts/default/microsurvey-popup/microsurvey-popup.tsx +++ b/src/app/layouts/default/microsurvey-popup/microsurvey-popup.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {PutAway} from '../shared'; -import useMSQueue from './queue.js'; +import useMSQueue from './queue'; import useSharedDataContext from '~/contexts/shared-data'; import './microsurvey-popup.scss'; diff --git a/src/app/pages/blog/search-results/search-results.tsx b/src/app/pages/blog/search-results/search-results.tsx index 31fb560f8..6f66ef730 100644 --- a/src/app/pages/blog/search-results/search-results.tsx +++ b/src/app/pages/blog/search-results/search-results.tsx @@ -7,7 +7,7 @@ import ArticleSummary, { import usePaginatorContext, { PaginatorContextProvider } from '~/components/paginator/paginator-context'; -import {PaginatorControls} from '~/components/paginator/search-results/paginator.js'; +import {PaginatorControls} from '~/components/paginator/search-results/paginator'; import NoResults from './no-results'; import useAllArticles from './use-all-articles'; import './search-results.scss'; diff --git a/src/app/pages/errata-form/form/ErrorSourceSelector.js b/src/app/pages/errata-form/form/ErrorSourceSelector.js index 17aa279a5..68fd6b231 100644 --- a/src/app/pages/errata-form/form/ErrorSourceSelector.js +++ b/src/app/pages/errata-form/form/ErrorSourceSelector.js @@ -1,6 +1,6 @@ import React, {useState, useRef, useMemo} from 'react'; import useErrataFormContext from '../errata-form-context'; -import managedInvalidMessage from './InvalidMessage.js'; +import managedInvalidMessage from './InvalidMessage'; import getFields from '~/models/errata-fields'; const sourceNames = { diff --git a/src/app/pages/errata-form/form/ErrorTypeSelector.js b/src/app/pages/errata-form/form/ErrorTypeSelector.js index 67b2cc8f7..6efcc195b 100644 --- a/src/app/pages/errata-form/form/ErrorTypeSelector.js +++ b/src/app/pages/errata-form/form/ErrorTypeSelector.js @@ -1,5 +1,5 @@ import React, {useState, useRef} from 'react'; -import managedInvalidMessage from './InvalidMessage.js'; +import managedInvalidMessage from './InvalidMessage'; function OtherErrorInput() { const inputRef = useRef(); diff --git a/src/app/pages/errata-form/form/form.js b/src/app/pages/errata-form/form/form.js index bf62aad8b..049b645d0 100644 --- a/src/app/pages/errata-form/form/form.js +++ b/src/app/pages/errata-form/form/form.js @@ -5,7 +5,7 @@ import ErrorTypeSelector from './ErrorTypeSelector'; import ErrorSourceSelector from './ErrorSourceSelector'; import ErrorLocationSelector from './ErrorLocationSelector/ErrorLocationSelector'; import FileUploader from './FileUploader'; -import managedInvalidMessage from './InvalidMessage.js'; +import managedInvalidMessage from './InvalidMessage'; import $ from '~/helpers/$'; import Dialog from '~/components/dialog/dialog'; import {useNavigate} from 'react-router-dom'; diff --git a/test/src/components/paginator.test.js b/test/src/components/paginator.test.js deleted file mode 100644 index 880312d11..000000000 --- a/test/src/components/paginator.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import {render, screen} from '@testing-library/preact'; -import userEvent from '@testing-library/user-event'; -import {PaginatorContextProvider} from '~/components/paginator/paginator-context'; -import {PaginatorControls} from '~/components/paginator/search-results/paginator.js'; -import {test, expect} from '@jest/globals'; - -function activePage() { - const activeButton = screen.getByRole('button', {current: 'page'}); - - return activeButton.textContent; -} - -test('operates by button clicks', async () => { - const user = userEvent.setup(); - - render( - - - - ); - await user.click(screen.getByText('Next')); - expect(activePage()).toBe('3'); - await user.click(screen.getByText('4')); - expect(activePage()).toBe('4'); - await user.click(screen.getByText('Previous')); - expect(activePage()).toBe('3'); -}); diff --git a/test/src/components/paginator.test.tsx b/test/src/components/paginator.test.tsx new file mode 100644 index 000000000..8bf336ab7 --- /dev/null +++ b/test/src/components/paginator.test.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import {render, screen} from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; +import {PaginatorContextProvider} from '~/components/paginator/paginator-context'; +import {PaginatorControls} from '~/components/paginator/search-results/paginator'; +import SimplePaginator from '~/components/paginator/simple-paginator'; + +function activePage() { + const activeButton = screen.getByRole('button', {current: 'page'}); + + return activeButton.textContent; +} + +describe('paginator', () => { + const user = userEvent.setup(); + + it('operates by button clicks', async () => { + render( + + + + ); + await user.click(screen.getByText('Next')); + expect(activePage()).toBe('3'); + await user.click(screen.getByText('4')); + expect(activePage()).toBe('4'); + await user.click(screen.getByText('Previous')); + expect(activePage()).toBe('3'); + }); + it('does ellipses when beyond page 4', async () => { + const setPage = jest.fn(); + + render( + + ); + + screen.getByText('…', {exact: false}); + await user.click(screen.getByRole('link', {name: '10'})); + expect(setPage).toHaveBeenCalledWith(10); + }); +}); diff --git a/test/src/pages/webinars/explore-page.test.tsx b/test/src/pages/webinars/explore-page.test.tsx index fa4ed962c..623a938ed 100644 --- a/test/src/pages/webinars/explore-page.test.tsx +++ b/test/src/pages/webinars/explore-page.test.tsx @@ -1,15 +1,19 @@ import React from 'react'; import {describe, it} from '@jest/globals'; import {render, screen} from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; import {MemoryRouter, Routes, Route} from 'react-router-dom'; import ExplorePage from '~/pages/webinars/explore-page/explore-page'; import useWebinarContext from '~/pages/webinars/webinar-context'; import {pastWebinar} from '../../data/webinars'; import type {Webinar} from '~/pages/webinars/types'; +// @ts-expect-error does not exist on +const {routerFuture} = global; + function Component({path}: {path: string}) { return ( - + jest.fn()); +jest.mock('~/helpers/use-document-head', () => jest.fn()); const webinars: Webinar[] = [0, 1, 2, 3, 4] .map(() => JSON.parse(JSON.stringify(pastWebinar))) @@ -48,6 +53,8 @@ webinars[4].subjects.push({ }); describe('webinars explore page', () => { + const user = userEvent.setup(); + (useWebinarContext as jest.Mock).mockImplementation(() => { return { latestWebinars: webinars, @@ -93,7 +100,7 @@ describe('webinars explore page', () => { 2 ); }); - it('paginates if there are more than 9', () => { + it('paginates if there are more than 9', async () => { (useWebinarContext as jest.Mock).mockImplementation(() => { return { latestWebinars: webinars.concat(Array(15).fill(webinars[3])), @@ -105,6 +112,14 @@ describe('webinars explore page', () => { expect(screen.getAllByText('Showing 1-9 of 16 webinars')).toHaveLength( 1 ); + await user.click(screen.getByRole('button', {name: 'next'})); + expect(screen.getAllByText('Showing 10-16 of 16 webinars')).toHaveLength( + 1 + ); + await user.click(screen.getByRole('button', {name: 'previous'})); + expect(screen.getAllByText('Showing 1-9 of 16 webinars')).toHaveLength( + 1 + ); }); it('shows no results with invalid explore type', () => { render(); From ae42ca8e9798f841437a9b18f716b6702d9712c1 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Tue, 8 Apr 2025 16:42:57 -0500 Subject: [PATCH 2/9] progress-ring --- .../{progress-ring.js => progress-ring.tsx} | 25 ++++++++----------- src/app/pages/blog/article/article.tsx | 8 +++--- 2 files changed, 14 insertions(+), 19 deletions(-) rename src/app/components/progress-ring/{progress-ring.js => progress-ring.tsx} (73%) diff --git a/src/app/components/progress-ring/progress-ring.js b/src/app/components/progress-ring/progress-ring.tsx similarity index 73% rename from src/app/components/progress-ring/progress-ring.js rename to src/app/components/progress-ring/progress-ring.tsx index aca753828..89bb0f9f9 100644 --- a/src/app/components/progress-ring/progress-ring.js +++ b/src/app/components/progress-ring/progress-ring.tsx @@ -1,17 +1,12 @@ import React from 'react'; import './progress-ring.scss'; -function Circle({basicProps, className, strokeDashoffset=0}) { - return ( - - ); -} - -export default function ProgressRing({message, radius, progress, stroke}) { +export default function ProgressRing({message, radius, progress, stroke}: { + message?: number; + radius: number; + progress: number; + stroke: number; +}) { const normalizedRadius = radius - stroke * 2; const circumference = normalizedRadius * 2 * Math.PI; const basicCircleProps = React.useMemo( @@ -35,11 +30,11 @@ export default function ProgressRing({message, radius, progress, stroke}) { {message} min read - - diff --git a/src/app/pages/blog/article/article.tsx b/src/app/pages/blog/article/article.tsx index 3789a2154..a6149db3c 100644 --- a/src/app/pages/blog/article/article.tsx +++ b/src/app/pages/blog/article/article.tsx @@ -229,16 +229,16 @@ function FloatingSideBar({ readTime, progress }: { - readTime: number | undefined; - progress: number | React.MutableRefObject; + readTime?: number; + progress: number; }) { return (
From d09364fea6da72952e92d7af1d258120b12f0457 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Wed, 9 Apr 2025 10:30:16 -0500 Subject: [PATCH 3/9] quote component simplified and moved to body-units --- src/app/components/body-units/body-units.tsx | 3 +- src/app/components/body-units/quote.tsx | 21 ++++++++++ src/app/components/quote/quote.js | 42 -------------------- src/styles/components/bucket.scss | 3 -- src/styles/main.scss | 1 - test/src/pages/blog/article.test.tsx | 4 +- 6 files changed, 24 insertions(+), 50 deletions(-) create mode 100644 src/app/components/body-units/quote.tsx delete mode 100644 src/app/components/quote/quote.js delete mode 100644 src/styles/components/bucket.scss diff --git a/src/app/components/body-units/body-units.tsx b/src/app/components/body-units/body-units.tsx index 2fd0c11d8..5102380c3 100644 --- a/src/app/components/body-units/body-units.tsx +++ b/src/app/components/body-units/body-units.tsx @@ -1,6 +1,6 @@ import React from 'react'; import RawHTML from '~/components/jsx-helpers/raw-html'; -import Quote from '~/components/quote/quote'; +import Quote from './quote'; import JITLoad from '~/helpers/jit-load'; function Unknown({data, type}: {data: unknown; type: string}) { @@ -75,7 +75,6 @@ type PQData = { function PullQuote({data}: {data: PQData}) { const model = { - image: {}, content: data.quote, attribution: data.attribution }; diff --git a/src/app/components/body-units/quote.tsx b/src/app/components/body-units/quote.tsx new file mode 100644 index 000000000..52b8161a0 --- /dev/null +++ b/src/app/components/body-units/quote.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import RawHTML from '~/components/jsx-helpers/raw-html'; + +export default function Quote({model}: { + model: { + content: string; + attribution: string; + } +}) { + return ( +
+
+ + { + Boolean(model.attribution) && +
— {model.attribution}
+ } +
+
+ ); +} diff --git a/src/app/components/quote/quote.js b/src/app/components/quote/quote.js deleted file mode 100644 index 439fb872f..000000000 --- a/src/app/components/quote/quote.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import RawHTML from '~/components/jsx-helpers/raw-html'; - -// eslint-disable-next-line complexity -export default function Quote({model}) { - const classList = ['quote-bucket', model.image.image && model.image.alignment || 'full'] - .join(' '); - const styleSpecs = [`background-image: url(${model.image.image})`]; - - if (model.height) { - styleSpecs.push(`height: ${model.height}px`); - } - - return ( -
- { - model.image.image && -
- } -
-
- - { - Boolean(model.attribution) && -
— {model.attribution}
- } -
- { - Boolean(model.overlay) && - {model.alt_text} - } - { - Boolean(model.cta) && - {model.cta} - } -
-
- ); -} diff --git a/src/styles/components/bucket.scss b/src/styles/components/bucket.scss deleted file mode 100644 index 885aa9f46..000000000 --- a/src/styles/components/bucket.scss +++ /dev/null @@ -1,3 +0,0 @@ -.quote-bucket .image { - background-repeat: no-repeat; -} diff --git a/src/styles/main.scss b/src/styles/main.scss index ab3ec5ae1..c7bee738f 100755 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -358,7 +358,6 @@ a:not([href]) { } @import 'components/animations'; -@import 'components/bucket'; @import 'components/buttons'; @import 'components/filter-buttons'; @import 'components/forms'; diff --git a/test/src/pages/blog/article.test.tsx b/test/src/pages/blog/article.test.tsx index 2f2efb5fa..ac48694f9 100644 --- a/test/src/pages/blog/article.test.tsx +++ b/test/src/pages/blog/article.test.tsx @@ -33,9 +33,9 @@ describe('blog/article', () => { expect(onload).toHaveBeenCalled(); screen.getByText('2 min read'); - expect(document.body.querySelector('.quote-bucket .quote .attribution')).toBeTruthy(); + expect(document.body.querySelector('.quote .attribution')).toBeTruthy(); // Bottom-aligned stuff comes at the end - expect(document.body.querySelector('.quote-bucket ~ figure.bottom')).toBeTruthy(); + expect(document.body.querySelector('.quote ~ figure.bottom')).toBeTruthy(); // CTA should be there screen.getByRole('link', {name: 'click me'}); }); From b2b2d500cba9f26ea6d7c298a9e56c9d200526d3 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Wed, 9 Apr 2025 13:13:50 -0500 Subject: [PATCH 4/9] Radio panel --- .../{radio-panel.js => radio-panel.tsx} | 31 ++++++++-- test/src/pages/errata/errata-summary.test.js | 44 -------------- test/src/pages/errata/errata-summary.test.tsx | 57 +++++++++++++++++++ 3 files changed, 82 insertions(+), 50 deletions(-) rename src/app/components/radio-panel/{radio-panel.js => radio-panel.tsx} (72%) delete mode 100644 test/src/pages/errata/errata-summary.test.js create mode 100644 test/src/pages/errata/errata-summary.test.tsx diff --git a/src/app/components/radio-panel/radio-panel.js b/src/app/components/radio-panel/radio-panel.tsx similarity index 72% rename from src/app/components/radio-panel/radio-panel.js rename to src/app/components/radio-panel/radio-panel.tsx index 32d9f8607..64ce9a9a2 100644 --- a/src/app/components/radio-panel/radio-panel.js +++ b/src/app/components/radio-panel/radio-panel.tsx @@ -4,11 +4,22 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'; import cn from 'classnames'; -function RadioButton({item, isSelected, onChange}) { +type Item = { + value: string; + html: string; +} + +type OnChange = (v: string) => void; + +function RadioButton({item, isSelected, onChange}: { + item: Item; + isSelected: (v: string) => boolean; + onChange: OnChange; +}) { const onClick = () => { onChange(item.value); }; - const onKeyDown = (event) => { + const onKeyDown = (event: React.KeyboardEvent) => { if ([' ', 'Enter'].includes(event.key)) { onClick(); } @@ -16,7 +27,7 @@ function RadioButton({item, isSelected, onChange}) { return (
{ +function RadioPanelJsx({items, selectedValue, onChange}: { + items: Item[]; + selectedValue: string; + onChange: OnChange; +}) { + const isSelected = (val: string) => { return val === selectedValue; }; @@ -54,7 +69,11 @@ function RadioPanelJsx({items, selectedValue, onChange}) { ); } -export function RadioPanel({selectedItem, items, onChange}) { +export function RadioPanel({selectedItem, items, onChange}: { + selectedItem: string; + items: Item[]; + onChange: OnChange; +}) { const [active, setActive] = useState(false); function toggleActive() { diff --git a/test/src/pages/errata/errata-summary.test.js b/test/src/pages/errata/errata-summary.test.js deleted file mode 100644 index 133e2780e..000000000 --- a/test/src/pages/errata/errata-summary.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import {render, screen} from '@testing-library/preact'; -import {within} from '@testing-library/dom'; -import userEvent from '@testing-library/user-event'; -import ErrataSummaryLoader from '~/pages/errata-summary/errata-summary'; -import {MemoryRouter} from 'react-router-dom'; -import {test, expect} from '@jest/globals'; - -const searchStr = '/errata/?book=Anatomy%20and%20Physiology'; - -window.history.pushState('', '', searchStr); - -// This is complicated by the fact that there are two versions that -// display at once, but one is hidden depending on screen resolution -// which testing knows nothing about. -// The desktop version is the last table; there are multiple tables -// (one for each row) in the mobile version -async function getTableRows() { - const tables = await screen.findAllByRole('table'); - - return within(tables.pop()).getAllByRole('row'); -} - -test('shows all items in table', async () => { - render( - - - - ); - expect(await getTableRows()).toHaveLength(54); -}); -test('filters', async () => { - render( - - - - ); - const filters = await screen.findByRole('radiogroup'); - const user = userEvent.setup({delay: null}); - const reviewButton = within(filters).queryByText('In Review'); - - await user.click(reviewButton); - expect(await getTableRows()).toHaveLength(19); -}); diff --git a/test/src/pages/errata/errata-summary.test.tsx b/test/src/pages/errata/errata-summary.test.tsx new file mode 100644 index 000000000..cd5e78216 --- /dev/null +++ b/test/src/pages/errata/errata-summary.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import {render, screen, fireEvent} from '@testing-library/preact'; +import {within} from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; +import ErrataSummaryLoader from '~/pages/errata-summary/errata-summary'; +import {MemoryRouter} from 'react-router-dom'; + +// @ts-expect-error does not exist on +const {routerFuture} = global; + +const searchStr = '/errata/?book=Anatomy%20and%20Physiology'; + +window.history.pushState('', '', searchStr); + +// This is complicated by the fact that there are two versions that +// display at once, but one is hidden depending on screen resolution +// which testing knows nothing about. +// The desktop version is the last table; there are multiple tables +// (one for each row) in the mobile version +async function getTableRows() { + const tables = await screen.findAllByRole('table'); + + return within(tables.pop() as HTMLElement).getAllByRole('row'); +} + +describe('errata-summary', () => { + test('shows all items in table', async () => { + render( + + + + ); + expect(await getTableRows()).toHaveLength(54); + }); + test('filters', async () => { + render( + + + + ); + const user = userEvent.setup({delay: null}); + const reviewButton = await screen.findByRole('radio', {name: 'In Review'}); + + await user.click(reviewButton); + expect(await getTableRows()).toHaveLength(19); + const allButton = screen.getByRole('radio', {name: 'View All'}); + + expect(screen.getAllByRole('table')).toHaveLength(19); + // Ignores anything but space or enter + fireEvent.keyDown(allButton, {key: 'a'}); + expect(screen.getAllByRole('table')).toHaveLength(19); + + // Acts on Enter + fireEvent.keyDown(allButton, {key: 'Enter'}); + expect(screen.getAllByRole('table')).toHaveLength(54); + }); +}); From 0eac4c8a8d3130ecf067224fc6fc9ea1d045efad Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Wed, 9 Apr 2025 14:38:05 -0500 Subject: [PATCH 5/9] role-selector --- .../{role-selector.js => role-selector.tsx} | 28 +++++++++++++++---- .../gated-content-dialog.tsx | 4 +-- test/src/components/role-selector.test.tsx | 23 +++++++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) rename src/app/components/role-selector/{role-selector.js => role-selector.tsx} (77%) create mode 100644 test/src/components/role-selector.test.tsx diff --git a/src/app/components/role-selector/role-selector.js b/src/app/components/role-selector/role-selector.tsx similarity index 77% rename from src/app/components/role-selector/role-selector.js rename to src/app/components/role-selector/role-selector.tsx index 5ee75f0b0..5e0fcb795 100644 --- a/src/app/components/role-selector/role-selector.js +++ b/src/app/components/role-selector/role-selector.tsx @@ -5,8 +5,17 @@ import {FormattedMessage, useIntl} from 'react-intl'; import useLanguageContext from '~/contexts/language'; import './role-selector.scss'; -export function RoleDropdown({ options, setValue, name = 'subject' }) { - const optionsAsOptions = options.map((opt) => ({ +export type Option = { + displayName: string; + salesforceName: string; +} + +export function RoleDropdown({ options, setValue, name = 'subject' }: { + options: Option[]; + setValue: (v: string) => void; + name?: string; +}) { + const optionsAsOptions = options.map((opt: Option) => ({ label: opt.displayName, value: opt.salesforceName })); @@ -23,14 +32,23 @@ export function RoleDropdown({ options, setValue, name = 'subject' }) { ); } +type Props = { + value: string; + setValue: (v: string) => void; + hidden?: boolean; + children: React.ReactNode[]; +} + /* eslint-disable */ function RoleSelector({ data: options, value, setValue, children, - hidden = false -}) { + hidden +}: { + data: Option[]; +} & Props) { const [studentContent, facultyContent] = children; return ( @@ -47,7 +65,7 @@ function RoleSelector({ ); } -export default function RoleSelectorLoader(props) { +export default function RoleSelectorLoader(props: Props) { const {language} = useLanguageContext(); return ( diff --git a/src/app/pages/blog/gated-content-dialog/gated-content-dialog.tsx b/src/app/pages/blog/gated-content-dialog/gated-content-dialog.tsx index 12e00bc96..e0202593b 100644 --- a/src/app/pages/blog/gated-content-dialog/gated-content-dialog.tsx +++ b/src/app/pages/blog/gated-content-dialog/gated-content-dialog.tsx @@ -4,7 +4,7 @@ import linkHelper from '~/helpers/link'; import useUserContext from '~/contexts/user'; import {useDataFromSlug, camelCaseKeys} from '~/helpers/page-data-utils'; import ContactInfo from '~/components/contact-info/contact-info'; -import {RoleDropdown} from '~/components/role-selector/role-selector'; +import {RoleDropdown, Option} from '~/components/role-selector/role-selector'; import FormInput from '~/components/form-input/form-input'; import DropdownSelect from '~/components/select/drop-down/drop-down'; import useFormTarget from '~/components/form-target/form-target'; @@ -170,7 +170,7 @@ function RoleSelector({
diff --git a/test/src/components/role-selector.test.tsx b/test/src/components/role-selector.test.tsx new file mode 100644 index 000000000..251b4efbc --- /dev/null +++ b/test/src/components/role-selector.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import {render, screen} from '@testing-library/preact'; +import RoleSelector from '~/components/role-selector/role-selector'; +import {MemoryRouter} from 'react-router-dom'; +import {LanguageContextProvider} from '~/contexts/language'; + +// @ts-expect-error does not exist on +const {routerFuture} = global; + +describe('role-selector', () => { + it('renders student content', async () => { + render( + + + +

Student stuff

+

Instructor stuff

+
+
+
); + expect((await screen.findByRole('heading', {level: 1})).textContent).toBe('Student stuff'); + }); +}); From 438bdbc1b03e9573dcfa9da214c56ba47d32367d Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Wed, 9 Apr 2025 16:20:12 -0500 Subject: [PATCH 6/9] salesforce-form, plus finally fixed the tilde issue --- ...salesforce-form.js => salesforce-form.tsx} | 39 +++++++------------ src/app/models/blog-entries.ts | 2 +- src/app/pages/renewal-form/renewal-form.tsx | 2 +- test/src/components/salesforce-form.test.tsx | 39 +++++++++++++++++++ tsconfig.json | 2 +- 5 files changed, 55 insertions(+), 29 deletions(-) rename src/app/components/salesforce-form/{salesforce-form.js => salesforce-form.tsx} (56%) create mode 100644 test/src/components/salesforce-form.test.tsx diff --git a/src/app/components/salesforce-form/salesforce-form.js b/src/app/components/salesforce-form/salesforce-form.tsx similarity index 56% rename from src/app/components/salesforce-form/salesforce-form.js rename to src/app/components/salesforce-form/salesforce-form.tsx index 228ec12b6..88e62457f 100644 --- a/src/app/components/salesforce-form/salesforce-form.js +++ b/src/app/components/salesforce-form/salesforce-form.tsx @@ -1,35 +1,21 @@ import React from 'react'; import useSalesforceContext from '~/contexts/salesforce'; -export function HiddenFields({leadSource}) { - const {oid, debug} = useSalesforceContext(); +type FormParams = React.PropsWithChildren<{ + postTo: string; + afterSubmit: () => void; +}> - if (!oid) { - return (
Loading...
); - } - return ( - - - - - { - debug && - } - - - ); -} - -function SfForm({children, postTo, afterSubmit}) { +function SfForm({children, postTo, afterSubmit}: FormParams) { const [listening, setListening] = React.useState(false); - const {webtocaseUrl, debug, oid} = useSalesforceContext(); + const {debug, oid} = useSalesforceContext(); const onSubmit = React.useCallback( () => setListening(true), [] ); const onLoad = React.useCallback( () => { - if (listening && afterSubmit) { + if (listening) { setListening(false); afterSubmit(); } @@ -41,14 +27,15 @@ function SfForm({children, postTo, afterSubmit}) {