diff --git a/components/Search.tsx b/components/Search.tsx index 03e5548174..76f7019ffc 100644 --- a/components/Search.tsx +++ b/components/Search.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef, ReactNode, RefObject } from 'react' import { useRouter } from 'next/router' -import debounce from 'lodash/debounce' +import useSWR from 'swr' import cx from 'classnames' import { useTranslation } from 'components/hooks/useTranslation' @@ -23,120 +23,147 @@ type SearchResult = { type Props = { isHeaderSearch?: boolean + isMobileSearch?: boolean variant?: 'compact' | 'expanded' - updateSearchParams?: boolean iconSize: number children?: (props: { SearchInput: ReactNode; SearchResults: ReactNode }) => ReactNode } export function Search({ isHeaderSearch = false, - updateSearchParams = true, + isMobileSearch = false, variant = 'compact', iconSize = 24, children, }: Props) { const router = useRouter() - const [query, setQuery] = useState(router.query.query || '') - const [results, setResults] = useState | null>(null) - const [isLoading, setIsLoading] = useState(false) + const query = + router.query.query && Array.isArray(router.query.query) + ? router.query.query[0] + : router.query.query || '' + const [localQuery, setLocalQuery] = useState(query) + const [debouncedQuery, setDebouncedQuery] = useDebounce(localQuery, 300) const inputRef = useRef(null) const { t } = useTranslation('search') const { currentVersion } = useVersion() const { languages } = useLanguages() - const initialRender = useRef(true) // Figure out language and version for index const { searchVersions, nonEnterpriseDefaultVersion } = useMainContext() // fall back to the non-enterprise default version (FPT currently) on the homepage, 404 page, etc. const version = searchVersions[currentVersion] || searchVersions[nonEnterpriseDefaultVersion] const language = (Object.keys(languages).includes(router.locale || '') && router.locale) || 'en' - const isHomePage = !('productId' in router.query) - // If the user shows up with a query in the URL, go ahead and search for it - useEffect(() => { - if ( - (!isHeaderSearch && isHomePage && router.query.query) || - (isHeaderSearch && updateSearchParams && router.query.query) - ) { - /* await */ fetchSearchResults((router.query.query as string).trim()) - } - }, []) + const fetchURL = query + ? `/search?${new URLSearchParams({ + language, + version, + query, + })}` + : null - // If the version changed from the dropdown version or language picker - // close the search pane and clear the query - // If the version changed from the search result window, keep the query - // and results but reset the versionFromSearchPane value - useEffect(() => { - if (initialRender.current) { - initialRender.current = false - } else { - closeSearch() + const { data: results, error: searchError } = useSWR( + fetchURL, + async (url: string) => { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`${response.status} on ${url}`) + } + return await response.json() + }, + { + onSuccess: () => { + sendEvent({ + type: EventType.search, + search_query: query, + // search_context + }) + }, + // Because the backend never changes between fetches, we can treat + // it as an immutable resource and disable these revalidation + // checks. + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, } - }, [currentVersion, language]) + ) + + const [previousResults, setPreviousResults] = useState() + useEffect(() => { + if (results) { + setPreviousResults(results) + } else if (!query) { + setPreviousResults(undefined) + } + }, [results, query]) + + // The `isLoading` boolean will become false every time the useSWR hook + // fires off a new XHR. So it toggles from false/true often. + // But we don't want to display "Loading..." every time a new XHR query + // begins, immediately, because the XHR requests are usually very fast + // so that you just see it flicker by. That's why we introduce a + // debounced version of that same boolean value. + // The problem is that the debounce is *trailing*. Meaning, it will + // always yield the last thing you sent to it, but with a delay. + // The problem is that, by the time the debounce finally fires, + // it might say 'true' when in fact the XHR has finished! That would + // mean saying "Loading..." is a lie! + // That's why we combine them into a final one. We're basically doing + // this to favor *NOT* saying "Loading...". + const isLoadingRaw = Boolean(query && !results && !searchError) + const [isLoadingDebounced] = useDebounce(isLoadingRaw, 500) + const isLoading = isLoadingRaw && isLoadingDebounced + + useEffect(() => { + if ((router.query.query || '') !== debouncedQuery) { + const [asPathRoot, asPathQuery = ''] = router.asPath.split('?') + const params = new URLSearchParams(asPathQuery) + if (debouncedQuery) { + params.set('query', debouncedQuery) + } else { + params.delete('query') + } + let asPath = `/${router.locale}${asPathRoot}` + if (params.toString()) { + asPath += `?${params.toString()}` + } + // Workaround a next.js routing behavior that + // will cause the default locale path of the index page + // "/en" to change to just "/". + if (router.pathname === '/') { + // Don't include router.locale so next doesn't attempt a + // request to `/_next/static/chunks/pages/en.js` + router.replace(`/?${params.toString()}`, asPath) + } else { + router.replace(asPath) + } + } + }, [debouncedQuery]) // When the user finishes typing, update the results - async function onSearch(e: React.ChangeEvent) { - const xquery = e.target?.value?.trim() - setQuery(xquery) - - // Update the URL with the search parameters in the query string - if (updateSearchParams) { - const pushUrl = new URL(location.toString()) - pushUrl.searchParams.set('query', xquery) - history.pushState({}, '', pushUrl.toString()) - } - return await fetchSearchResults(xquery) + function onSearch(e: React.ChangeEvent) { + setLocalQuery(e.target.value) } - - // If there's a query, call the endpoint - // Otherwise, there's no results by default - async function fetchSearchResults(xquery: string) { - setIsLoading(true) - try { - if (xquery) { - const endpointUrl = new URL(location.origin) - endpointUrl.pathname = '/search' - const endpointParams: Record = { - language, - version, - query: xquery, - } - endpointUrl.search = new URLSearchParams(endpointParams).toString() - - const response = await fetch(endpointUrl.toString(), { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }) - setResults(response.ok ? await response.json() : []) - } else { - setResults(null) + useEffect(() => { + if (localQuery.trim()) { + if (localQuery.endsWith(' ')) { + setDebouncedQuery(localQuery.trim()) } - } finally { - setIsLoading(false) + } else { + setDebouncedQuery('') } - - // Analytics tracking - if (xquery) { - sendEvent({ - type: EventType.search, - search_query: xquery, - // search_context - }) - } - } + }, [localQuery]) // Close panel if overlay is clicked function closeSearch() { - setQuery('') - setResults(null) - if (inputRef.current) { - inputRef.current.value = '' - } + setLocalQuery('') } // Prevent the page from refreshing when you "submit" the form - function preventRefresh(evt: React.FormEvent) { + function onFormSubmit(evt: React.FormEvent) { evt.preventDefault() + if (localQuery.trim()) { + setDebouncedQuery(localQuery.trim()) + } } const SearchResults = ( @@ -151,12 +178,12 @@ export function Search({ query && styles.resultsContainerOpen )} > -
+ ) // When there are search results, it doesn't matter if this is overlay or not. return (
- { - closeSearch()} - onClickOutside={() => closeSearch()} - aria-labelledby="title" - sx={ - isHeaderSearch && { - background: 'none', - boxShadow: 'none', - position: 'static', - overflowY: 'auto', - maxHeight: '80vh', - maxWidth: '96%', - margin: '1.5em 2em 0 0.5em', - scrollbarWidth: 'none', + {!isHeaderSearch && !isMobileSearch ? ( + <> + {/* Only if you're going to use an do you need + to specify a portal div tag. */} +
+ closeSearch()} + onClickOutside={() => closeSearch()} + aria-labelledby="title" + sx={ + isHeaderSearch && { + background: 'none', + boxShadow: 'none', + position: 'static', + overflowY: 'auto', + maxHeight: '80vh', + maxWidth: '96%', + margin: '1.5em 2em 0 0.5em', + scrollbarWidth: 'none', + } } - } - > -
-
-

- You're searching the {searchVersion}{' '} - version. Didn't find what you're looking for? Click a different version to try - again. -

- {versions.map(({ title, version }) => { - return ( - - ) - })} -
- { - return { - key: url, - text: title, - renderItem: () => ( - - -
  • -
    - {/* Breadcrumbs in search records don't include the page title. These fields may contain elements that we need to render */} - - {debug && ( - - score: {score.toFixed(4)} popularity: {popularity.toFixed(4)} - - )} -
    -
    -
    ]+(>|$)|(\/)/g, '') } - : { - __html: breadcrumbs - .split(' / ') - .slice(0, breadcrumbs.length - 1) - .join(' / ') - .replace(/<\/?[^>]+(>|$)/g, ''), - } - } - /> -
    -
  • - -
    - ), - } - })} - /> -
    -
    - } + > + {ActionListResults} + + + ) : ( + ActionListResults + )}
    ) } diff --git a/components/page-header/Header.tsx b/components/page-header/Header.tsx index 9dcdd36c28..1960722c49 100644 --- a/components/page-header/Header.tsx +++ b/components/page-header/Header.tsx @@ -18,12 +18,11 @@ export const Header = () => { const router = useRouter() const { relativePath, error } = useMainContext() const { t } = useTranslation(['header', 'homepage']) - const [isMenuOpen, setIsMenuOpen] = useState(false) + const [isMenuOpen, setIsMenuOpen] = useState( + router.pathname !== '/' && router.query.query && true + ) const [scroll, setScroll] = useState(false) - // the graphiql explorer utilizes `?query=` in the url and we don't want our search bar to mess that up - const updateSearchParams = router.asPath !== 'graphql/overview/explorer' - useEffect(() => { function onScroll() { setScroll(window.scrollY > 10) @@ -61,7 +60,7 @@ export const Header = () => {
    - +
    @@ -69,7 +68,7 @@ export const Header = () => { {/* */} {relativePath !== 'index.md' && error !== '404' && (
    - +
    )} @@ -107,26 +106,22 @@ export const Header = () => {
    -
    -
    - -
    -

    {t('explore_by_product')}

    - - +
    +
    -
    + + +
    - {/* */} -
    +
    {/* */} {relativePath !== 'index.md' && error !== '404' && ( -
    - +
    +
    )}
    diff --git a/components/page-header/LanguagePicker.tsx b/components/page-header/LanguagePicker.tsx index db15075ef3..235a6fdfac 100644 --- a/components/page-header/LanguagePicker.tsx +++ b/components/page-header/LanguagePicker.tsx @@ -1,85 +1,41 @@ import { useRouter } from 'next/router' -import { Box, Dropdown, Details, Text, useDetails } from '@primer/components' -import { ChevronDownIcon } from '@primer/octicons-react' - import { Link } from 'components/Link' import { useLanguages } from 'components/context/LanguagesContext' +import { Picker } from 'components/ui/Picker' type Props = { variant?: 'inline' } + export const LanguagePicker = ({ variant }: Props) => { const router = useRouter() const { languages } = useLanguages() - const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true }) const locale = router.locale || 'en' const langs = Object.values(languages) const selectedLang = languages[locale] - if (variant === 'inline') { - return ( -
    - -
    - {selectedLang.nativeName || selectedLang.name} - -
    -
    - - {langs.map((lang) => { - if (lang.wip) { - return null - } - - return ( - setOpen(false)} key={lang.code}> - - {lang.nativeName ? ( - <> - {lang.nativeName} ({lang.name}) - - ) : ( - lang.name - )} - - - ) - })} - -
    - ) - } - return ( -
    - - {selectedLang.nativeName || selectedLang.name} - - - - {langs.map((lang) => { - if (lang.wip) { - return null - } - - return ( - setOpen(false)}> - - {lang.nativeName ? ( - <> - {lang.nativeName} ({lang.name}) - - ) : ( - lang.name - )} - - - ) - })} - -
    + !lang.wip) + .map((lang) => ({ + text: lang.nativeName || lang.name, + selected: lang === selectedLang, + item: ( + + {lang.nativeName ? ( + <> + {lang.nativeName} ({lang.name}) + + ) : ( + lang.name + )} + + ), + }))} + /> ) } diff --git a/components/page-header/ProductPicker.tsx b/components/page-header/ProductPicker.tsx index 5a671a157c..c6653087c4 100644 --- a/components/page-header/ProductPicker.tsx +++ b/components/page-header/ProductPicker.tsx @@ -2,47 +2,33 @@ import { useRouter } from 'next/router' import { Link } from 'components/Link' import { useMainContext } from 'components/context/MainContext' -import { ChevronDownIcon, LinkExternalIcon } from '@primer/octicons-react' -import { Box, Dropdown, Details, useDetails } from '@primer/components' +import { LinkExternalIcon } from '@primer/octicons-react' +import { Picker } from 'components/ui/Picker' -// Product Picker - GitHub.com, Enterprise Server, etc export const ProductPicker = () => { const router = useRouter() const { activeProducts, currentProduct } = useMainContext() - const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true }) return ( -
    - -
    - {currentProduct?.name || 'All Products'} - -
    -
    - - {activeProducts.map((product) => { - return ( - setOpen(false)}> - - {product.name} - {product.external && ( - - - - )} - - - ) - })} - -
    + ({ + text: product.name, + selected: product === currentProduct, + item: ( + + {product.name} + {product.external && ( + + + + )} + + ), + }))} + /> ) } diff --git a/components/page-header/VersionPicker.tsx b/components/page-header/VersionPicker.tsx index 8568c212cf..be7466b058 100644 --- a/components/page-header/VersionPicker.tsx +++ b/components/page-header/VersionPicker.tsx @@ -1,21 +1,20 @@ import { useRouter } from 'next/router' -import cx from 'classnames' -import { Dropdown, Details, Box, Text, useDetails } from '@primer/components' -import { ArrowRightIcon, ChevronDownIcon } from '@primer/octicons-react' +import { ArrowRightIcon } from '@primer/octicons-react' import { Link } from 'components/Link' import { useMainContext } from 'components/context/MainContext' import { useVersion } from 'components/hooks/useVersion' import { useTranslation } from 'components/hooks/useTranslation' +import { Picker } from 'components/ui/Picker' type Props = { - variant?: 'inline' | 'compact' + variant?: 'inline' } + export const VersionPicker = ({ variant }: Props) => { const router = useRouter() const { currentVersion } = useVersion() const { allVersions, page, enterpriseServerVersions } = useMainContext() - const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true }) const { t } = useTranslation('pages') if (page.permalinks && page.permalinks.length <= 1) { @@ -23,87 +22,31 @@ export const VersionPicker = ({ variant }: Props) => { } return ( - <> -
    -
    - - {variant === 'inline' ? ( -
    - {allVersions[currentVersion].versionTitle} - -
    - ) : ( - <> - {allVersions[currentVersion].versionTitle} - - - )} -
    - {variant === 'inline' ? ( - - {(page.permalinks || []).map((permalink) => { - return ( - setOpen(false)}> - {permalink.pageVersionTitle} - - ) - })} - - { - setOpen(false) - }} - href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`} - className="f6 no-underline color-fg-muted pl-3 pr-2 no-wrap" - > - {t('all_enterprise_releases')}{' '} - - - - - ) : ( - - {(page.permalinks || []).map((permalink) => { - return ( - setOpen(false)}> - {permalink.pageVersionTitle} - - ) - })} - ({ + text: permalink.pageVersionTitle, + selected: allVersions[currentVersion].versionTitle === permalink.pageVersionTitle, + item: {permalink.pageVersionTitle}, + })) + .concat([ + { + text: t('all_enterprise_releases'), + selected: false, + item: ( + - { - setOpen(false) - }} - href={`/${router.locale}/${enterpriseServerVersions[0]}/admin/all-releases`} - className="f6 no-underline color-fg-muted pl-3 pr-2 no-wrap" - > - {t('all_enterprise_releases')}{' '} - - - - - )} -
    -
    - + {t('all_enterprise_releases')}{' '} + + + ), + }, + ])} + /> ) } diff --git a/components/ui/Picker/Picker.tsx b/components/ui/Picker/Picker.tsx new file mode 100644 index 0000000000..3f70d01c8b --- /dev/null +++ b/components/ui/Picker/Picker.tsx @@ -0,0 +1,83 @@ +import { ReactNode } from 'react' +import cx from 'classnames' + +import { Details, useDetails, Text, Dropdown, Box } from '@primer/components' +import { ChevronDownIcon } from '@primer/octicons-react' + +export type PickerOptionsTypeT = { + text: string + item: ReactNode + selected?: boolean +} + +export type PickerPropsT = { + variant?: 'inline' + defaultText: string + options: Array +} + +type PickerWrapperPropsT = { + variant?: 'inline' + children: ReactNode +} + +function PickerSummaryWrapper({ variant, children }: PickerWrapperPropsT) { + if (variant === 'inline') { + return ( +
    + {children} + +
    + ) + } + return ( + <> + {children} + + + ) +} + +function PickerOptionsWrapper({ variant, children }: PickerWrapperPropsT) { + if (variant === 'inline') { + return {children} + } + return ( + + {children} + + ) +} + +export function Picker({ variant, defaultText, options, ...restProps }: PickerPropsT) { + const { getDetailsProps, setOpen } = useDetails({ closeOnOutsideClick: true }) + const selectedOption = options.find((option) => option.selected) + + return ( +
    + + + {selectedOption?.text || defaultText} + + + + {options.map((option) => ( + setOpen(false)} key={option.text}> + {option.item} + + ))} + +
    + ) +} diff --git a/components/ui/Picker/index.ts b/components/ui/Picker/index.ts new file mode 100644 index 0000000000..913dac72d0 --- /dev/null +++ b/components/ui/Picker/index.ts @@ -0,0 +1 @@ +export { Picker } from './Picker' diff --git a/package-lock.json b/package-lock.json index 20cc356c24..834f8b6a2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,6 +89,7 @@ "slash": "^4.0.0", "strip-html-comments": "^1.0.0", "styled-components": "^5.3.3", + "swr": "1.0.1", "throng": "^5.0.0", "ts-dedent": "^2.2.0", "unified": "^10.1.0", @@ -9559,9 +9560,9 @@ } }, "node_modules/find-process": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.4.tgz", - "integrity": "sha512-rRSuT1LE4b+BFK588D2V8/VG9liW0Ark1XJgroxZXI0LtwmQJOb490DvDYvbm+Hek9ETFzTutGfJ90gumITPhQ==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.5.tgz", + "integrity": "sha512-v11rJYYISUWn+s8qZzgGnBvlzRKf3bOtlGFM8H0kw56lGQtOmLuLCzuclA5kehA2j7S5sioOWdI4woT3jDavAw==", "optional": true, "dependencies": { "chalk": "^4.0.0", @@ -16818,9 +16819,9 @@ } }, "node_modules/parse-headers": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz", - "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.4.tgz", + "integrity": "sha512-psZ9iZoCNFLrgRjZ1d8mn0h9WRqJwFxM9q3x7iUjN/YT2OksthDJ5TiPCu2F38kS4zutqfW+YdVVkBZZx3/1aw==", "optional": true }, "node_modules/parse-json": { @@ -20709,6 +20710,17 @@ "node": ">=8" } }, + "node_modules/swr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-1.0.1.tgz", + "integrity": "sha512-EPQAxSjoD4IaM49rpRHK0q+/NzcwoT8c0/Ylu/u3/6mFj/CWnQVjNJ0MV2Iuw/U+EJSd2TX5czdAwKPYZIG0YA==", + "dependencies": { + "dequal": "2.0.2" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -30341,9 +30353,9 @@ } }, "find-process": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.4.tgz", - "integrity": "sha512-rRSuT1LE4b+BFK588D2V8/VG9liW0Ark1XJgroxZXI0LtwmQJOb490DvDYvbm+Hek9ETFzTutGfJ90gumITPhQ==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.5.tgz", + "integrity": "sha512-v11rJYYISUWn+s8qZzgGnBvlzRKf3bOtlGFM8H0kw56lGQtOmLuLCzuclA5kehA2j7S5sioOWdI4woT3jDavAw==", "optional": true, "requires": { "chalk": "^4.0.0", @@ -35842,9 +35854,9 @@ } }, "parse-headers": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz", - "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.4.tgz", + "integrity": "sha512-psZ9iZoCNFLrgRjZ1d8mn0h9WRqJwFxM9q3x7iUjN/YT2OksthDJ5TiPCu2F38kS4zutqfW+YdVVkBZZx3/1aw==", "optional": true }, "parse-json": { @@ -38865,6 +38877,14 @@ "supports-color": "^7.0.0" } }, + "swr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-1.0.1.tgz", + "integrity": "sha512-EPQAxSjoD4IaM49rpRHK0q+/NzcwoT8c0/Ylu/u3/6mFj/CWnQVjNJ0MV2Iuw/U+EJSd2TX5czdAwKPYZIG0YA==", + "requires": { + "dequal": "2.0.2" + } + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index fa7fe2ac09..0940a94371 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "slash": "^4.0.0", "strip-html-comments": "^1.0.0", "styled-components": "^5.3.3", + "swr": "1.0.1", "throng": "^5.0.0", "ts-dedent": "^2.2.0", "unified": "^10.1.0", diff --git a/pages/storybook.tsx b/pages/storybook.tsx index 93c2f7965b..21f849375f 100644 --- a/pages/storybook.tsx +++ b/pages/storybook.tsx @@ -9,6 +9,7 @@ import { } from 'components/ui/MarkdownContent/MarkdownContent' import { ScrollButton, ScrollButtonPropsT } from 'components/ui/ScrollButton/ScrollButton' import { TruncateLines, TruncateLinesPropsT } from 'components/ui/TruncateLines/TruncateLines' +import { Picker, PickerPropsT } from 'components/ui/Picker/Picker' const markdownExample = ( <> @@ -131,6 +132,29 @@ const stories = [ component: MarkdownContent, variants: [{ children: markdownExample } as MarkdownContentPropsT], }, + { + name: 'Picker', + component: Picker, + variants: [ + { + defaultText: 'Choose color', + options: [ + { text: 'Red', item: Red }, + { text: 'Green', item: Green }, + { text: 'Blue', item: Blue }, + ], + } as PickerPropsT, + { + defaultText: 'Choose color', + variant: 'inline', + options: [ + { text: 'Red', item: Red }, + { text: 'Green', item: Green }, + { text: 'Blue', item: Blue }, + ], + } as PickerPropsT, + ], + }, { name: 'ScrollButton', component: ScrollButton, diff --git a/tests/rendering/header.js b/tests/rendering/header.js index 5e7e2d92f9..a64a9b143e 100644 --- a/tests/rendering/header.js +++ b/tests/rendering/header.js @@ -142,13 +142,11 @@ describe('header', () => { const $ = await getDOM( '/en/github/importing-your-projects-to-github/importing-source-code-to-github/about-github-importer' ) - const github = $('[data-testid=current-product][data-current-product-path="/github"]') + const github = $('[data-testid=product-picker][data-current-product-path="/github"] summary') expect(github.length).toBe(1) expect(github.text().trim()).toBe('GitHub') - const ghec = $( - `[data-testid=product-picker-list] a[href="/en/enterprise-cloud@latest/admin"]` - ) + const ghec = $(`[data-testid=product-picker] a[href="/en/enterprise-cloud@latest/admin"]`) expect(ghec.length).toBe(1) expect(ghec.text().trim()).toBe('Enterprise administrators') }) @@ -159,11 +157,9 @@ describe('header', () => { '/ja/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests' ) expect( - $('[data-testid=current-product][data-current-product-path="/repositories"]').length - ).toBe(1) - expect( - $(`[data-testid=product-picker-list] a[href="/ja/enterprise-cloud/admin"]`).length + $('[data-testid=product-picker][data-current-product-path="/repositories"]').length ).toBe(1) + expect($(`[data-testid=product-picker] a[href="/ja/enterprise-cloud/admin"]`).length).toBe(1) }) test('emphasizes the product that corresponds to the current page', async () => { @@ -171,7 +167,7 @@ describe('header', () => { `/en/enterprise-server@${oldestSupported}/github/importing-your-projects-to-github/importing-source-code-to-github/importing-a-git-repository-using-the-command-line` ) - expect($('[data-testid=current-product]').text()).toBe('GitHub') + expect($('[data-testid=product-picker] summary').text()).toBe('GitHub') }) }) }) diff --git a/tests/rendering/server.js b/tests/rendering/server.js index 6e11c72970..95a3f80e0f 100644 --- a/tests/rendering/server.js +++ b/tests/rendering/server.js @@ -543,13 +543,12 @@ describe('server', () => { ) expect( $( - `[data-testid="mobile-header"] [data-testid=article-version-picker] a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/${articlePath}"]` + `[data-testid="mobile-header"] [data-testid=version-picker] a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/${articlePath}"]` ).length ).toBe(1) // 2.13 predates this feature, so it should be excluded: expect( - $(`[data-testid=article-version-picker] a[href="/en/enterprise/2.13/user/${articlePath}"]`) - .length + $(`[data-testid=version-picker] a[href="/en/enterprise/2.13/user/${articlePath}"]`).length ).toBe(0) }) diff --git a/tests/unit/products.js b/tests/unit/products.js index 31ef6e4a63..36054ebbee 100644 --- a/tests/unit/products.js +++ b/tests/unit/products.js @@ -23,36 +23,25 @@ describe('products module', () => { }) describe('mobile-only products nav', () => { - test('renders current product on various product pages for each product', async () => { + const cases = [ // Note the unversioned homepage at `/` does not have a product selected in the mobile dropdown - expect((await getDOM('/github'))('[data-testid=current-product]').text().trim()).toBe('GitHub') - + ['/github', 'GitHub'], // Enterprise server - expect( - (await getDOM('/en/enterprise/admin'))('[data-testid=current-product]').text().trim() - ).toBe('Enterprise administrators') - expect( - ( - await getDOM( - '/en/enterprise/user/github/importing-your-projects-to-github/importing-source-code-to-github/importing-a-git-repository-using-the-command-line' - ) - )('[data-testid=current-product]') - .text() - .trim() - ).toBe('GitHub') + ['/en/enterprise/admin', 'Enterprise administrators'], + [ + '/en/enterprise/user/github/importing-your-projects-to-github/importing-source-code-to-github/importing-a-git-repository-using-the-command-line', + 'GitHub', + ], - expect((await getDOM('/desktop'))('[data-testid=current-product]').text().trim()).toBe( - 'GitHub Desktop' - ) - - expect((await getDOM('/actions'))('[data-testid=current-product]').text().trim()).toBe( - 'GitHub Actions' - ) + ['/desktop', 'GitHub Desktop'], + ['/actions', 'GitHub Actions'], // localized - expect((await getDOM('/ja/desktop'))('[data-testid=current-product]').text().trim()).toBe( - 'GitHub Desktop' - ) + ['/ja/desktop', 'GitHub Desktop'], + ] + + test.each(cases)('on %p, renders current product %p', async (url, name) => { + expect((await getDOM(url))('[data-testid=product-picker] summary').text().trim()).toBe(name) }) })