Skip to content

Commit

Permalink
chore: Refactor logic for docs versions dropdown and add tests (#2576)
Browse files Browse the repository at this point in the history
  • Loading branch information
heatlikeheatwave authored Sep 24, 2024
1 parent c185ddf commit 0e4a9d0
Show file tree
Hide file tree
Showing 5 changed files with 426 additions and 10 deletions.
61 changes: 61 additions & 0 deletions src/lib/docs/__tests__/is-release-notes-page.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import { isReleaseNotesPage } from 'lib/docs/is-release-notes-page'

describe('isReleaseNotesPage', () => {
it('returns true for valid release notes page paths', () => {
const validPaths = [
'v202409-2/releases/2024/v202407-1',
'releases/2022/v220601-1',
'releases/2021/v210601-2',
'release-notes/1.2.3',
'release-notes/2.0.x',
'/release-notes/v2.0.x',
'/boundary/docs/release-notes/v0_15_0',
'/vault/docs/release-notes/1.13.0',
]

validPaths.forEach((path) => {
expect(isReleaseNotesPage(path)).toBe(true)
})
})

it('returns false for invalid release notes page paths', () => {
const invalidPaths = [
'releases/2022/v220601',
'releases/2021/v210601',
'release-notes/1.2',
'release-notes/2.0',
'release-notes/2.x',
'releases/2022/v220601-',
'releases/2021/v210601-',
'/release-notes/1.2.',
'/release-notes/2.0.',
'/release-notes/2.x.',
'/releases/2022/v220601-1234-5678',
'/releases/2021/v210601-5678-1234',
'/release-notes/1.2.3.4',
'/release-notes/2.0.x.y',
]
invalidPaths.forEach((path) => {
expect(isReleaseNotesPage(path)).toBe(false)
})
})

it('returns false for non-release notes page paths', () => {
const nonReleaseNotesPaths = [
'/releases',
'/getting-started',
'/enterprise/v202401-1/migrate',
'/enterprise/v202401-1/releases',
'/waypoint/reference/config',
'/vault/install',
]
nonReleaseNotesPaths.forEach((path) => {
expect(isReleaseNotesPage(path)).toBe(false)
})
})
})
25 changes: 25 additions & 0 deletions src/lib/docs/is-release-notes-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

/**
* Determines if a given path corresponds to a release notes page.
*
* This function uses a regular expression to check if the provided path matches
* the expected patterns for release notes pages. The patterns include:
* - `vYYYYMM-NN/releases/YYYY/vYYYYMM-NN`
* - `releases/YYYY/vYYYYMM-NN`
* - `/releases/YYYY/vYYYYMM-NN`
* - `release-notes/vX.X.X` or `/release-notes/X.X.X`
*
* @param path - The path to be checked.
* @returns `true` if the path matches the release notes pattern, otherwise `false`.
*/
export const isReleaseNotesPage = (path: string): boolean => {
const regexPatterns = [
/(\/?releases\/\d{4}\/(v\d{6}-\d{1}))$/i,
/\/?release-notes\/(v\d+[.|_]|(\d+[.|_]))\d+[.|_]([0-9]|x)$/i,
]
return regexPatterns.some((pattern) => pattern.test(path))
}
82 changes: 72 additions & 10 deletions src/views/docs-view/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

// Third-party imports
import { GetStaticPaths, GetStaticProps, GetStaticPropsResult } from 'next'
import path from 'path'
import path from 'node:path'
import { Pluggable } from 'unified'
import slugify from 'slugify'

Expand Down Expand Up @@ -36,13 +36,79 @@ import {
import tutorialMap from 'data/_tutorial-map.generated.json'

// Local imports
import { getValidVersions } from './utils/get-valid-versions'
import { getProductUrlAdjuster } from './utils/product-url-adjusters'
import { getBackToLink } from './utils/get-back-to-link'
import { getDeployPreviewLoader } from './utils/get-deploy-preview-loader'
import { getCustomLayout } from './utils/get-custom-layout'
import type { DocsViewPropOptions } from './utils/get-root-docs-path-generation-functions'
import { DocsViewProps } from './types'
import { isReleaseNotesPage } from 'lib/docs/is-release-notes-page'
import { getValidVersions } from './utils/get-valid-versions'
import { VersionSelectItem } from './loaders/remote-content'

/**
* Fetches valid versions of a document based on the provided path parts and version information.
*
* @param pathParts - An array of strings representing parts of the document path.
* @param versionPathPart - A string representing the version part of the path.
* @param basePathForLoader - The base path used for loading the document.
* @param versions - An array of `VersionSelectItem` objects representing available versions.
* @param productSlugForLoader - A string representing the product slug used for loading the document.
* @returns A promise that resolves to an array of `VersionSelectItem` objects representing valid versions.
*
* This function filters the provided versions to include only those where the document exists.
* It handles special cases for release notes pages, ensuring the correct version is used in the path.
* For other pages, it constructs a document path that the content API will recognize and fetches valid versions.
*/
export async function fetchValidVersions(
pathParts: string[],
versionPathPart: string,
basePathForLoader: string,
versions: VersionSelectItem[],
productSlugForLoader: string
): Promise<VersionSelectItem[]> {
/**
* Filter versions to include only those where this document exists
*/
let pathToFetchValidVersions = pathParts.join('/')

if (isReleaseNotesPage(pathToFetchValidVersions)) {
/**
* Check specific to PTFE releases notes page, which may have a version in the path twice
* e.g. v202409-2/releases/2024/v202407-1
* Remove the first version instance, which is the docs version
* e.g. releases/2024/v202407-1
* the mdx file this page pulls from has a version in the title (e.g. 202407-1mdx)
* the second version is the path should not be removed for this reason.
* This block is here because the default (else statement below)
* removes all versions from the path, which is not desired for release notes.
*/
if (/(v\d{6}-\d{1})\/releases/i.test(pathToFetchValidVersions)) {
pathToFetchValidVersions = pathToFetchValidVersions.replace(
versionPathPart,
''
)
}
} else {
// Construct a document path that the content API will recognize
pathToFetchValidVersions = pathParts
.filter((part) => part !== versionPathPart)
.join('/')
}
const fullPath = `doc#${path.join(
basePathForLoader,
pathToFetchValidVersions
)}`

// Filter for valid versions, fetching from the content API under the hood
const validVersions = await getValidVersions(
versions,
fullPath,
productSlugForLoader
)

return validVersions
}

/**
* Returns static generation functions which can be exported from a page to fetch docs data
Expand Down Expand Up @@ -387,15 +453,11 @@ export function getStaticGenerationFunctions<
/**
* Filter versions to include only those where this document exists
*/
// Construct a document path that the content API will recognize
const pathWithoutVersion = pathParts
.filter((part) => part !== versionPathPart)
.join('/')
const fullPath = `doc#${path.join(basePathForLoader, pathWithoutVersion)}`
// Filter for valid versions, fetching from the content API under the hood
const validVersions = await getValidVersions(
const validVersions = await fetchValidVersions(
pathParts,
versionPathPart,
basePathForLoader,
versions,
fullPath,
productSlugForLoader
)

Expand Down
170 changes: 170 additions & 0 deletions src/views/docs-view/utils/__tests__/fetch-valid-versions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import { describe, it, expect, vi } from 'vitest'
import { isReleaseNotesPage } from 'lib/docs/is-release-notes-page'
import { getValidVersions } from '../get-valid-versions'
import { VersionSelectItem } from '../../loaders/remote-content'
import { fetchValidVersions } from 'views/docs-view/server'

vi.mock('lib/docs/is-release-notes-page')
vi.mock('../get-valid-versions')

describe('fetchValidVersions', () => {
const versions: VersionSelectItem[] = [
{
version: 'v1.0.0',
name: 'v1.0.0',
label: 'v1.0.0',
isLatest: false,
releaseStage: 'stable',
},
{
version: 'v2.0.0',
name: '',
label: '',
isLatest: false,
releaseStage: 'stable',
},
]

const mockIsReleaseNotesPage = (returnValue: boolean) => {
vi.mocked(isReleaseNotesPage).mockReturnValue(returnValue)
}

const mockGetValidVersions = (returnValue: VersionSelectItem[]) => {
vi.mocked(getValidVersions).mockResolvedValue(returnValue)
}

const runTest = async (
pathParts: string[],
versionPathPart: string,
basePathForLoader: string,
productSlugForLoader: string,
expectedPath: string,
expectedVersions: VersionSelectItem[]
) => {
const result = await fetchValidVersions(
pathParts,
versionPathPart,
basePathForLoader,
versions,
productSlugForLoader
)

expect(isReleaseNotesPage).toHaveBeenCalledWith(pathParts.join('/'))
expect(getValidVersions).toHaveBeenCalledWith(
versions,
expectedPath,
productSlugForLoader
)
expect(result).toEqual(expectedVersions)
}

it('should filter versions correctly for non-release notes pages', async () => {
const pathParts = ['docs', 'v1.0.0', 'guide']
const versionPathPart = 'v1.0.0'
const basePathForLoader = '/base/path'
const productSlugForLoader = 'product-slug'

mockIsReleaseNotesPage(false)
mockGetValidVersions([
{
version: 'v1.0.0',
name: 'v1.0.0',
label: 'v1.0.0',
isLatest: false,
releaseStage: 'stable',
},
])

await runTest(
pathParts,
versionPathPart,
basePathForLoader,
productSlugForLoader,
'doc#/base/path/docs/guide',
[
{
version: 'v1.0.0',
name: 'v1.0.0',
label: 'v1.0.0',
isLatest: false,
releaseStage: 'stable',
},
]
)
})

it('should filter versions correctly for release notes pages', async () => {
const pathParts = ['v202409-2', 'releases', '2024', 'v202409-1']
const versionPathPart = 'v202409-2'
const basePathForLoader = 'enterprise'
const productSlugForLoader = 'ptfe-releases'
const releaseNotesVersions: VersionSelectItem[] = [
{
name: 'latest',
label: 'v202409-2 (latest)',
isLatest: true,
releaseStage: 'stable',
version: 'v202409-2',
},
{
name: 'v202409-1',
label: 'v202409-1',
isLatest: false,
releaseStage: 'stable',
version: 'v202409-1',
},
]

mockIsReleaseNotesPage(true)
mockGetValidVersions(releaseNotesVersions)

await runTest(
pathParts,
versionPathPart,
basePathForLoader,
productSlugForLoader,
'doc#enterprise/releases/2024/v202409-1',
releaseNotesVersions
)
})

it('should handle paths without versions correctly', async () => {
const pathParts = ['docs', 'guide']
const versionPathPart = 'v1.0.0'
const basePathForLoader = '/base/path'
const productSlugForLoader = 'product-slug'

mockIsReleaseNotesPage(false)
mockGetValidVersions([
{
version: 'v1.0.0',
name: 'v1.0.0',
label: 'v1.0.0',
isLatest: false,
releaseStage: 'stable',
},
])

await runTest(
pathParts,
versionPathPart,
basePathForLoader,
productSlugForLoader,
'doc#/base/path/docs/guide',
[
{
version: 'v1.0.0',
name: 'v1.0.0',
label: 'v1.0.0',
isLatest: false,
releaseStage: 'stable',
},
]
)
})
})
Loading

0 comments on commit 0e4a9d0

Please sign in to comment.