dedicated search results page (redux) (#29902)

* dedicated search results page (redux)

* Update SearchResults.tsx

* adding pagination

* fix pagination

* say something on NoQuery

* better Flash

* tidying link

* small fixes for results

* debug info

* l18n the meta info

* inDebugMode

* basic jest rendering of the skeleton page

* basic jest rendering test

* fix content tests

* better document title

* fix tests

* quote query in page title

* use home page sidebar

* something when nothing is found

* parseInt no longer needs the 10

* fix linting tests

* fix test

* prettier

* Update pages/search.tsx

Co-authored-by: Rachael Sewell <rachmari@github.com>

Co-authored-by: Kevin Heis <heiskr@users.noreply.github.com>
Co-authored-by: Rachael Sewell <rachmari@github.com>
This commit is contained in:
Peter Bengtsson 2022-08-19 15:36:55 +02:00 коммит произвёл GitHub
Родитель 2206e82584
Коммит 8765c628ff
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
25 изменённых файлов: 661 добавлений и 38 удалений

Просмотреть файл

@ -0,0 +1,89 @@
import { useState, useRef } from 'react'
import { useRouter } from 'next/router'
import cx from 'classnames'
import { useTranslation } from 'components/hooks/useTranslation'
import { DEFAULT_VERSION, useVersion } from 'components/hooks/useVersion'
import { useQuery } from 'components/hooks/useQuery'
import styles from './Search.module.scss'
type Props = {
isHeaderSearch?: true
variant?: 'compact' | 'expanded'
iconSize: number
}
export function BasicSearch({ isHeaderSearch = true, variant = 'compact', iconSize = 24 }: Props) {
const router = useRouter()
const { query, debug } = useQuery()
const [localQuery, setLocalQuery] = useState(query)
const inputRef = useRef<HTMLInputElement>(null)
const { t } = useTranslation('search')
const { currentVersion } = useVersion()
function redirectSearch() {
let asPath = `/${router.locale}`
if (currentVersion !== DEFAULT_VERSION) {
asPath += `/${currentVersion}`
}
asPath += '/search'
const params = new URLSearchParams({ query: localQuery })
if (debug) {
params.set('debug', '1')
}
asPath += `?${params}`
router.push(asPath)
}
return (
<div data-testid="search">
<div className="position-relative z-2">
<form
role="search"
className="width-full d-flex"
noValidate
onSubmit={(event) => {
event.preventDefault()
redirectSearch()
}}
>
<label className="text-normal width-full">
<span
className="visually-hidden"
aria-label={t`label`}
aria-describedby={t`description`}
>{t`placeholder`}</span>
<input
data-testid="site-search-input"
ref={inputRef}
className={cx(
styles.searchInput,
iconSize === 24 && styles.searchIconBackground24,
iconSize === 24 && 'form-control px-6 f4',
iconSize === 16 && styles.searchIconBackground16,
iconSize === 16 && 'form-control px-5 f4',
variant === 'compact' && 'py-2',
variant === 'expanded' && 'py-3',
isHeaderSearch && styles.searchInputHeader,
!isHeaderSearch && 'width-full'
)}
type="search"
placeholder={t`placeholder`}
autoComplete={localQuery ? 'on' : 'off'}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
maxLength={512}
onChange={(e) => setLocalQuery(e.target.value)}
value={localQuery}
aria-label={t`label`}
aria-describedby={t`description`}
/>
</label>
<button className="d-none" type="submit" title="Submit the search query." hidden />
</form>
</div>
</div>
)
}

Просмотреть файл

@ -0,0 +1,16 @@
import { useRouter } from 'next/router'
type Info = {
page: number
}
export const usePage = (): Info => {
const router = useRouter()
const page = parseInt(
router.query.page && Array.isArray(router.query.page)
? router.query.page[0]
: router.query.page || ''
)
return {
page: !isNaN(page) && page >= 1 ? page : 1,
}
}

Просмотреть файл

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import cx from 'classnames'
import { useRouter } from 'next/router'
import { MarkGithubIcon, ThreeBarsIcon, XIcon } from '@primer/octicons-react'
import { useVersion } from 'components/hooks/useVersion'
import { DEFAULT_VERSION, useVersion } from 'components/hooks/useVersion'
import { Link } from 'components/Link'
import { useMainContext } from 'components/context/MainContext'
@ -12,6 +12,7 @@ import { HeaderNotifications } from 'components/page-header/HeaderNotifications'
import { ProductPicker } from 'components/page-header/ProductPicker'
import { useTranslation } from 'components/hooks/useTranslation'
import { Search } from 'components/Search'
import { BasicSearch } from 'components/BasicSearch'
import { VersionPicker } from 'components/page-header/VersionPicker'
import { Breadcrumbs } from './Breadcrumbs'
import styles from './Header.module.scss'
@ -30,7 +31,7 @@ export const Header = () => {
const signupCTAVisible =
hasAccount === false && // don't show if `null`
(currentVersion === 'free-pro-team@latest' || currentVersion === 'enterprise-cloud@latest')
(currentVersion === DEFAULT_VERSION || currentVersion === 'enterprise-cloud@latest')
useEffect(() => {
function onScroll() {
@ -52,6 +53,15 @@ export const Header = () => {
return () => window.removeEventListener('keydown', close)
}, [])
// If you're on `/pt/search` the `router.asPath` will be `/search`
// but `/pt/search` is just shorthand for `/pt/free-pro-team@latest/search`
// so we need to make exception to that.
const onSearchResultPage =
currentVersion === DEFAULT_VERSION
? router.asPath.split('?')[0] === '/search'
: router.asPath.split('?')[0] === `/${currentVersion}/search`
const SearchComponent = onSearchResultPage ? BasicSearch : Search
return (
<div
className={cx(
@ -96,7 +106,7 @@ export const Header = () => {
{/* <!-- GitHub.com homepage and 404 page has a stylized search; Enterprise homepages do not --> */}
{error !== '404' && (
<div className="d-inline-block ml-3">
<Search iconSize={16} isHeaderSearch={true} />
<SearchComponent iconSize={16} />
</div>
)}
</div>
@ -161,7 +171,7 @@ export const Header = () => {
{/* <!-- GitHub.com homepage and 404 page has a stylized search; Enterprise homepages do not --> */}
{error !== '404' && (
<div className="my-2 pt-2">
<Search iconSize={16} isMobileSearch={true} />
<SearchComponent iconSize={16} isMobileSearch={true} />
</div>
)}
</div>

Просмотреть файл

@ -0,0 +1,42 @@
import { Spinner } from '@primer/react'
import { useTranslation } from 'components/hooks/useTranslation'
import { useEffect, useState } from 'react'
export function Loading() {
const [showLoading, setShowLoading] = useState(false)
useEffect(() => {
let mounted = true
setTimeout(() => {
if (mounted) {
setShowLoading(true)
}
}, 1000)
return () => {
mounted = false
}
}, [])
return showLoading ? <ShowSpinner /> : <ShowNothing />
}
function ShowSpinner() {
const { t } = useTranslation(['search'])
return (
<div className="my-12">
<Spinner size="large" />
<h2>{t('loading')}</h2>
</div>
)
}
function ShowNothing() {
return (
// The min heigh is based on inspecting what the height became when it
// does render. Making this match makes the footer to not flicker
// up or down when it goes from showing nothing to something.
<div className="my-12" style={{ minHeight: 105 }}>
{/* Deliberately empty */}
</div>
)
}

Просмотреть файл

@ -0,0 +1,19 @@
import { Heading, Flash } from '@primer/react'
import { useMainContext } from 'components/context/MainContext'
import { useTranslation } from 'components/hooks/useTranslation'
export function NoQuery() {
const { t } = useTranslation(['search'])
const { page } = useMainContext()
return (
<>
<Heading as="h1">{page.title}</Heading>
<Flash variant="danger" sx={{ margin: '2rem' }}>
{t('description')}
</Flash>
</>
)
}

Просмотреть файл

@ -0,0 +1,27 @@
import { Box, Flash } from '@primer/react'
import { useRouter } from 'next/router'
import { useTranslation } from 'components/hooks/useTranslation'
interface Props {
error: Error
}
export function SearchError({ error }: Props) {
const { t } = useTranslation('search')
const { locale, asPath } = useRouter()
return (
<div>
{' '}
<Flash variant="danger" sx={{ margin: '3rem' }}>
{t('search_error')}
<br />
{process.env.NODE_ENV === 'development' && <code>{error.toString()}</code>}
</Flash>
<Box>
<a href={`/${locale}${asPath}`}>Try reloading the page</a>
</Box>
</div>
)
}

Просмотреть файл

@ -0,0 +1,133 @@
import { Box, Pagination, Text, Heading } from '@primer/react'
import { useRouter } from 'next/router'
import type { SearchResultsT, SearchResultHitT } from './types'
import { useTranslation } from 'components/hooks/useTranslation'
import { Link } from 'components/Link'
import { useQuery } from 'components/hooks/useQuery'
type Props = {
results: SearchResultsT
}
export function SearchResults({ results }: Props) {
const { t } = useTranslation('search')
const pages = Math.ceil(results.meta.found.value / results.meta.size)
const { page } = results.meta
return (
<div>
<p>
<Text>
{t('results_found')
.replace('{n}', results.meta.found.value.toLocaleString())
.replace('{s}', results.meta.took.total_msec.toFixed(0))}{' '}
</Text>
<br />
{pages > 1 && (
<Text>
{t('results_page').replace('{page}', page).replace('{pages}', pages.toLocaleString())}
</Text>
)}
</p>
<SearchResultHits hits={results.hits} />
{pages > 1 && <ResultsPagination page={page} totalPages={pages} />}
</div>
)
}
function SearchResultHits({ hits }: { hits: SearchResultHitT[] }) {
const { debug } = useQuery()
return (
<div>
{hits.length === 0 && <NoSearchResults />}
{hits.map((hit) => (
<SearchResultHit key={hit.id} hit={hit} debug={debug} />
))}
</div>
)
}
function NoSearchResults() {
const { t } = useTranslation('search')
return (
<div className="my-6">
<Heading as="h2" sx={{ fontSize: 1 }}>
{t('nothing_found')}
</Heading>
</div>
)
}
function SearchResultHit({ hit, debug }: { hit: SearchResultHitT; debug: boolean }) {
const title =
hit.highlights.title && hit.highlights.title.length > 0 ? hit.highlights.title[0] : hit.title
return (
<div className="my-6">
<h2 className="f3">
<Link
href={hit.url}
className="color-fg-accent"
dangerouslySetInnerHTML={{ __html: title }}
></Link>
</h2>
<h3 className="text-normal f4 mb-2">{hit.breadcrumbs}</h3>
<ul className="ml-3">
{(hit.highlights.content || []).map((highlight, i) => {
return <li key={highlight + i} dangerouslySetInnerHTML={{ __html: highlight }}></li>
})}
</ul>
{debug && (
<Text as="p" fontWeight="bold">
score: <code style={{ marginRight: 10 }}>{hit.score}</code> popularity:{' '}
<code>{hit.popularity}</code>
</Text>
)}
</div>
)
}
function ResultsPagination({ page, totalPages }: { page: number; totalPages: number }) {
const router = useRouter()
const [asPathRoot, asPathQuery = ''] = router.asPath.split('?')
function hrefBuilder(page: number) {
const params = new URLSearchParams(asPathQuery)
if (page === 1) {
params.delete('page')
} else {
params.set('page', `${page}`)
}
return `/${router.locale}${asPathRoot}?${params.toString()}`
}
return (
<Box borderRadius={2} p={2}>
<Pagination
pageCount={totalPages}
currentPage={page}
hrefBuilder={hrefBuilder}
onPageChange={(event, page) => {
event.preventDefault()
const [asPathRoot, asPathQuery = ''] = router.asPath.split('#')[0].split('?')
const params = new URLSearchParams(asPathQuery)
if (page !== 1) {
params.set('page', `${page}`)
} else {
params.delete('page')
}
let asPath = `/${router.locale}${asPathRoot}`
if (params.toString()) {
asPath += `?${params.toString()}`
}
router.push(asPath, undefined, { shallow: true })
}}
/>
</Box>
)
}

103
components/search/index.tsx Normal file
Просмотреть файл

@ -0,0 +1,103 @@
import useSWR from 'swr'
import { useRouter } from 'next/router'
import Head from 'next/head'
import { Heading } from '@primer/react'
import { sendEvent, EventType } from 'components/lib/events'
import { useTranslation } from 'components/hooks/useTranslation'
import { DEFAULT_VERSION, useVersion } from 'components/hooks/useVersion'
import type { SearchResultsT } from 'components/search/types'
import { SearchResults } from 'components/search/SearchResults'
import { SearchError } from 'components/search/SearchError'
import { NoQuery } from 'components/search/NoQuery'
import { Loading } from 'components/search/Loading'
import { useQuery } from 'components/hooks/useQuery'
import { usePage } from 'components/hooks/usePage'
import { useMainContext } from 'components/context/MainContext'
export function Search() {
const { locale } = useRouter()
const { t } = useTranslation('search')
const { currentVersion } = useVersion()
const { query, debug } = useQuery()
const { page } = usePage()
// A reference to the `content/search/index.md` Page object.
// Not to be confused with the "page" that is for paginating
// results.
const { allVersions, page: documentPage } = useMainContext()
const searchVersion = allVersions[currentVersion].versionTitle
const sp = new URLSearchParams()
const hasQuery = Boolean(query.trim())
if (hasQuery) {
sp.set('query', query.trim())
sp.set('language', locale || 'en')
if (debug) sp.set('debug', 'true')
sp.set('version', currentVersion)
if (page !== 1) {
sp.set('page', `${page}`)
}
}
const inDebugMode = process.env.NODE_ENV === 'development'
const { data: results, error } = useSWR<SearchResultsT | null, Error | null>(
hasQuery ? `/api/search/v1?${sp.toString()}` : null,
async (url) => {
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,
})
},
// Because the backend never changes between fetches, we can treat
// it as an immutable resource and disable these revalidation
// checks.
revalidateIfStale: inDebugMode,
revalidateOnFocus: inDebugMode,
revalidateOnReconnect: inDebugMode,
}
)
let pageTitle = documentPage.fullTitle
if (hasQuery) {
pageTitle = `${t('search_results_for')} '${query}'`
if (currentVersion !== DEFAULT_VERSION) {
pageTitle += ` (${searchVersion})`
}
if (results) {
pageTitle = `${results.meta.found.value.toLocaleString()} ${pageTitle}`
}
}
return (
<div className="container-xl px-3 px-md-6 my-4" data-testid="search-results">
<Head>
<title>{pageTitle}</title>
</Head>
{hasQuery && (
<Heading as="h1">
{t('search_results_for')} <i>{query}</i>
</Heading>
)}
{error ? (
<SearchError error={error} />
) : results ? (
<SearchResults results={results} />
) : hasQuery ? (
<Loading />
) : (
<NoQuery />
)}
</div>
)
}

Просмотреть файл

@ -0,0 +1,31 @@
export type SearchResultHitT = {
id: string
url: string
title: string
breadcrumbs: string
highlights: {
title?: string[]
content?: string[]
}
score?: number
popularity?: number
es_url?: string
}
type SearchResultsMeta = {
found: {
value: number
relation: string
}
took: {
query_msec: number
total_msec: number
}
page: number
size: number
}
export type SearchResultsT = {
meta: SearchResultsMeta
hits: SearchResultHitT[]
}

Просмотреть файл

@ -40,7 +40,11 @@ export const SidebarNav = () => {
</Link>
</div>
<nav>
{error === '404' || currentProduct === null ? <SidebarHomepage /> : <SidebarProduct />}
{error === '404' || !currentProduct || currentProduct.id === 'search' ? (
<SidebarHomepage />
) : (
<SidebarProduct />
)}
</nav>
</div>
)

Просмотреть файл

@ -19,6 +19,7 @@ redirect_from:
- /troubleshooting-common-issues
versions: '*'
children:
- search
- get-started
- account-and-profile
- authentication
@ -124,4 +125,3 @@ externalProducts:
href: 'https://docs.npmjs.com/'
external: true
---

9
content/search/index.md Normal file
Просмотреть файл

@ -0,0 +1,9 @@
---
title: Search
hidden: true
versions:
fpt: '*'
ghec: '*'
ghes: '*'
ghae: '*'
---

Просмотреть файл

@ -8,6 +8,7 @@ files:
- '/content/early-access'
- '/content/site-policy/site-policy-deprecated'
- '/content/github/index'
- '/content/search'
excluded_target_languages: ['de', 'ko', 'ru']
- source: /data/**/*.yml
translation: /translations/%locale%/%original_path%/%original_file_name%

Просмотреть файл

@ -36,6 +36,9 @@ search:
search_error: An error occurred trying to perform the search.
description: Enter a search term to find it in the GitHub Documentation.
label: Search GitHub Docs
results_found: Found {n} results in {s}ms
results_page: This is page {page} of {pages}.
nothing_found: Nothing found 😿
homepage:
explore_by_product: Explore by product
version_picker: Version

Просмотреть файл

@ -126,10 +126,20 @@ router.get(
})
)
class ValidationError extends Error {}
const validationMiddleware = (req, res, next) => {
const params = [
{ key: 'query' },
{ key: 'version', default_: 'dotcom', validate: (v) => versionAliases[v] || allVersions[v] },
{
key: 'version',
default_: 'dotcom',
validate: (v) => {
if (versionAliases[v] || allVersions[v]) return true
const valid = [...Object.keys(versionAliases), ...Object.keys(allVersions)]
throw new ValidationError(`'${v}' not in ${valid}`)
},
},
{ key: 'language', default_: 'en', validate: (v) => v in languages },
{
key: 'size',
@ -160,10 +170,17 @@ const validationMiddleware = (req, res, next) => {
if (cast) {
value = cast(value)
}
if (validate && !validate(value)) {
return res
.status(400)
.json({ error: `Not a valid value (${JSON.stringify(value)}) for key '${key}'` })
try {
if (validate && !validate(value)) {
return res
.status(400)
.json({ error: `Not a valid value (${JSON.stringify(value)}) for key '${key}'` })
}
} catch (err) {
if (err instanceof ValidationError) {
return res.status(400).json({ error: err.toString(), field: key })
}
throw err
}
search[key] = value
}

Просмотреть файл

@ -7,7 +7,11 @@ export default async function genericToc(req, res, next) {
if (!req.context.page) return next()
if (req.context.currentLayoutName !== 'default') return next()
// This middleware can only run on product, category, and map topics.
if (req.context.page.documentType === 'homepage' || req.context.page.documentType === 'article')
if (
req.context.page.documentType === 'homepage' ||
req.context.page.documentType === 'article' ||
req.context.page.relativePath === 'search/index.md'
)
return next()
// This one product TOC is weird.

Просмотреть файл

@ -24,22 +24,54 @@ export default function handleRedirects(req, res, next) {
return res.redirect(302, `/${language}`)
}
// Don't try to redirect if the URL is `/search` which is the XHR
// endpoint. It should not become `/en/search`.
// It's unfortunate and looks a bit needlessly complicated. But
// it comes from the legacy that the JSON API endpoint was and needs to
// continue to be `/search` when it would have been more neat if it
// was something like `/api/search`.
// If someone types in `/search?query=foo` manually, they'll get JSON.
// Maybe sometime in 2023 we remove `/search` as an endpoint for the
// JSON.
if (req.path === '/search') return next()
// begin redirect handling
let redirect = req.path
let queryParams = req._parsedUrl.query
// update old-style query params (#9467)
if ('q' in req.query) {
const newQueryParams = new URLSearchParams(queryParams)
newQueryParams.set('query', newQueryParams.get('q'))
newQueryParams.delete('q')
return res.redirect(301, `${req.path}?${newQueryParams.toString()}`)
if (
'q' in req.query ||
('query' in req.query && !(req.path.endsWith('/search') || req.path.startsWith('/api/search')))
) {
// If you had the old legacy format of /some/uri?q=stuff
// it needs to redirect to /en/search?query=stuff.
// If you have the new format of /some/uri?query=stuff it too needs
// to redirect to /en/search?query=stuff
// ...or /en/{version}/search?query=stuff
const language = getLanguage(req)
const sp = new URLSearchParams(req.query)
if (sp.has('q') && !sp.has('query')) {
sp.set('query', sp.get('q'))
sp.delete('q')
}
let redirectTo = `/${language}`
const { currentVersion } = req.context
if (currentVersion !== 'free-pro-team@latest') {
redirectTo += `/${currentVersion}`
// The `req.context.currentVersion` is just the portion of the URL
// pathname. It could be that the currentVersion is something
// like `enterprise` which needs to be redirected to its new name.
redirectTo = getRedirect(redirectTo, req.context)
}
redirectTo += `/search?${sp.toString()}`
return res.redirect(301, redirectTo)
}
// have to do this now because searchPath replacement changes the path as well as the query params
if (queryParams) {
queryParams = '?' + queryParams
redirect = (redirect + queryParams).replace(patterns.searchPath, '$1')
}
// remove query params temporarily so we can find the path in the redirects object

Просмотреть файл

@ -0,0 +1,4 @@
import Search from '../search'
export { getServerSideProps } from '../search'
export default Search

46
pages/search.tsx Normal file
Просмотреть файл

@ -0,0 +1,46 @@
import type { GetServerSideProps } from 'next'
import searchVersions from '../lib/search/versions.js'
import { MainContextT, MainContext, getMainContext } from 'components/context/MainContext'
import { DefaultLayout } from 'components/DefaultLayout'
import { Search } from 'components/search/index'
type Props = {
mainContext: MainContextT
}
export default function Page({ mainContext }: Props) {
return (
<MainContext.Provider value={mainContext}>
<DefaultLayout>
<Search />
</DefaultLayout>
</MainContext.Provider>
)
}
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const req = context.req as any
const res = context.res as any
const version = req.context.currentVersion
const searchVersion = searchVersions[Array.isArray(version) ? version[0] : version]
if (!searchVersion) {
// E.g. someone loaded `/en/enterprisy-server@2.99/search`
// That's going to 404 in the XHR later but it simply shouldn't be
// a valid starting page.
return {
notFound: true,
}
}
const mainContext = getMainContext(req, res)
return {
props: {
mainContext,
},
}
}

Просмотреть файл

@ -144,7 +144,8 @@ describeIfElasticsearchURL('search middleware', () => {
sp.set('version', 'xxxxx')
const res = await get('/api/search/v1?' + sp)
expect(res.statusCode).toBe(400)
expect(JSON.parse(res.text).error).toMatch('version')
expect(JSON.parse(res.text).error).toMatch("'xxxxx'")
expect(JSON.parse(res.text).field).toMatch('version')
}
// unrecognized size
{

Просмотреть файл

@ -20,7 +20,13 @@ describe('category pages', () => {
const walkOptions = {
globs: ['*/index.md', 'enterprise/*/index.md'],
ignore: ['{rest,graphql}/**', 'enterprise/index.md', '**/articles/**', 'early-access/**'],
ignore: [
'{rest,graphql}/**',
'enterprise/index.md',
'**/articles/**',
'early-access/**',
'search/index.md',
],
directories: false,
includeBasePath: true,
}

Просмотреть файл

@ -22,8 +22,8 @@ describe('siteTree', () => {
})
test('object order and structure', () => {
expect(siteTree.en[nonEnterpriseDefaultVersion].childPages[0].href).toBe('/en/get-started')
expect(siteTree.en[nonEnterpriseDefaultVersion].childPages[0].childPages[0].href).toBe(
expect(siteTree.en[nonEnterpriseDefaultVersion].childPages[1].href).toBe('/en/get-started')
expect(siteTree.en[nonEnterpriseDefaultVersion].childPages[1].childPages[0].href).toBe(
'/en/get-started/quickstart'
)
})

Просмотреть файл

@ -409,6 +409,7 @@ describe('lint markdown content', () => {
isHidden,
isEarlyAccess,
isSitePolicy,
isSearch,
hasExperimentalAlternative,
frontmatterData
@ -420,8 +421,10 @@ describe('lint markdown content', () => {
frontmatterData = data
ast = fromMarkdown(content)
isHidden = data.hidden === true
isEarlyAccess = markdownRelPath.split('/').includes('early-access')
isSitePolicy = markdownRelPath.split('/').includes('site-policy-deprecated')
const split = markdownRelPath.split('/')
isEarlyAccess = split.includes('early-access')
isSitePolicy = split.includes('site-policy-deprecated')
isSearch = split.includes('search') && !split.includes('reusables')
hasExperimentalAlternative = data.hasExperimentalAlternative === true
links = []
@ -457,10 +460,10 @@ describe('lint markdown content', () => {
.map((schedule) => schedule.cron)
})
// We need to support some non-Early Access hidden docs in Site Policy
test('hidden docs must be Early Access, Site Policy, or Experimental', async () => {
test('hidden docs must be Early Access, Site Policy, Search, or Experimental', async () => {
// We need to support some non-Early Access hidden docs in Site Policy
if (isHidden) {
expect(isEarlyAccess || isSitePolicy || hasExperimentalAlternative).toBe(true)
expect(isEarlyAccess || isSitePolicy || isSearch || hasExperimentalAlternative).toBe(true)
}
})

30
tests/rendering/search.js Normal file
Просмотреть файл

@ -0,0 +1,30 @@
import { expect, jest } from '@jest/globals'
import { getDOM } from '../helpers/e2etest.js'
describe('search results page', () => {
jest.setTimeout(5 * 60 * 1000)
test('says something if no query is provided', async () => {
const $ = await getDOM('/en/search')
const $container = $('[data-testid="search-results"]')
expect($container.text()).toMatch(/Enter a search term/)
// Default is the frontmatter title of the content/search/index.md
expect($('title').text()).toMatch('Search - GitHub Docs')
})
test('says something if query is empty', async () => {
const $ = await getDOM(`/en/search?${new URLSearchParams({ query: ' ' })}`)
const $container = $('[data-testid="search-results"]')
expect($container.text()).toMatch(/Enter a search term/)
})
test('mention search term in h1', async () => {
const $ = await getDOM(`/en/search?${new URLSearchParams({ query: 'peterbe' })}`)
const $container = $('[data-testid="search-results"]')
const h1Text = $container.find('h1').text()
expect(h1Text).toMatch(/Search results for/)
expect(h1Text).toMatch(/peterbe/)
expect($('title').text()).toMatch(/Search results for 'peterbe'/)
})
})

Просмотреть файл

@ -53,15 +53,15 @@ describe('redirects', () => {
describe('query params', () => {
test('are preserved in redirected URLs', async () => {
const res = await get('/enterprise/admin?query=pulls')
expect(res.statusCode).toBe(302)
const expected = `/en/enterprise-server@${enterpriseServerReleases.latest}/admin?query=pulls`
expect(res.statusCode).toBe(301)
const expected = `/en/enterprise-server@${enterpriseServerReleases.latest}/search?query=pulls`
expect(res.headers.location).toBe(expected)
})
test('have q= converted to query=', async () => {
const res = await get('/en/enterprise/admin?q=pulls')
expect(res.statusCode).toBe(301)
const expected = '/en/enterprise/admin?query=pulls'
const expected = `/en/enterprise-server@${enterpriseServerReleases.latest}/search?query=pulls`
expect(res.headers.location).toBe(expected)
})
@ -74,13 +74,6 @@ describe('redirects', () => {
expect(res.headers.location).toBe(expected)
})
test('work with redirected search paths', async () => {
const res = await get('/en/enterprise/admin/search?utf8=%E2%9C%93&query=pulls')
expect(res.statusCode).toBe(301)
const expected = `/en/enterprise-server@${enterpriseServerReleases.latest}/admin?utf8=%E2%9C%93&query=pulls`
expect(res.headers.location).toBe(expected)
})
test('do not work on other paths that include "search"', async () => {
const reqPath = `/en/enterprise-server@${enterpriseServerReleases.latest}/admin/configuration/configuring-github-connect/enabling-unified-search-for-your-enterprise`
const res = await get(reqPath)