import path from 'path' import { beforeAll, describe, expect, test, vi } from 'vitest' import GithubSlugger from 'github-slugger' import { decode } from 'html-entities' import { chain, pick } from 'lodash-es' import { loadPages } from '@/frame/lib/page-data' import libLanguages from '@/languages/lib/languages-server' import { liquid } from '@/content-render/index' import patterns from '@/frame/lib/patterns' import removeFPTFromPath from '@/versions/lib/remove-fpt-from-path' import type { Page } from '@/types' const languageCodes = Object.keys(libLanguages) const slugger = new GithubSlugger() describe('pages module', () => { vi.setConfig({ testTimeout: 60 * 1000 }) let pages: Page[] beforeAll(async () => { pages = await loadPages() }) describe('loadPages', () => { test('yields a non-empty array of Page objects', async () => { expect(Array.isArray(pages)).toBe(true) expect(pages.length).toBeGreaterThan(100) }) test('every page has a `languageCode`', async () => { expect(pages.every((page) => languageCodes.includes(page.languageCode))).toBe(true) }) test('every page has a non-empty `permalinks` array', async () => { const brokenPages = pages.filter( (page) => !Array.isArray(page.permalinks) || page.permalinks.length === 0, ) const expectation = JSON.stringify( brokenPages.map((page) => page.fullPath), null, 2, ) expect(brokenPages.length, expectation).toBe(0) }) test('redirect_from routes are unique across English pages', () => { const englishPages = chain(pages) .filter(['languageCode', 'en']) .filter('redirect_from') .map((page) => pick(page, ['redirect_from', 'applicableVersions', 'fullPath'])) .value() // Map from redirect path to Set of file paths const redirectToFiles = new Map>() const versionedRedirects: Array<{ path: string; file: string }> = [] // Page objects have dynamic properties from chain/lodash that aren't fully typed for (const page of englishPages) { const pageObj = page as Record for (const redirect of pageObj.redirect_from as string[]) { for (const version of pageObj.applicableVersions as string[]) { const versioned = removeFPTFromPath(path.posix.join('/', version, redirect)) versionedRedirects.push({ path: versioned, file: pageObj.fullPath as string }) if (!redirectToFiles.has(versioned)) { redirectToFiles.set(versioned, new Set()) } redirectToFiles.get(versioned)!.add(pageObj.fullPath as string) } } } // Only consider as duplicate if more than one unique file defines the same redirect const duplicates = Array.from(redirectToFiles.entries()) .filter(([, files]) => files.size > 1) .map(([redirectPath]) => redirectPath) // Build a detailed message with sources for each duplicate const message = `Found ${duplicates.length} duplicate redirect_from path${duplicates.length === 1 ? '' : 's'}. Ensure that you don't define the same path more than once in the redirect_from property in a single file and across all English files. You may also receive this error if you have defined the same children property more than once.\n${duplicates .map((dup) => { const files = Array.from(redirectToFiles.get(dup) || []) return `${dup}\n Defined in:\n ${files.join('\n ')}` }) .join('\n\n')}` expect(duplicates.length, message).toBe(0) }) test('every English page has a filename that matches its slugified title or shortTitle', async () => { const nonMatches = pages .filter((page) => { slugger.reset() return ( page.languageCode === 'en' && // only check English !page.relativePath.includes('index.md') && // ignore TOCs // Page class has dynamic frontmatter properties like 'allowTitleToDifferFromFilename' not in type definition !(page as Record).allowTitleToDifferFromFilename && // ignore docs with override slugger.slug(decode(page.title)) !== path.basename(page.relativePath, '.md') && slugger.slug(decode(page.shortTitle || '')) !== path.basename(page.relativePath, '.md') ) }) // make the output easier to read .map((page) => { return JSON.stringify( { file: path.basename(page.relativePath), title: page.title, path: page.fullPath, }, null, 2, ) }) const message = ` Found ${nonMatches.length} ${ nonMatches.length === 1 ? 'file' : 'files' } that do not match their slugified titles.\n ${nonMatches.join('\n')}\n To fix, run: npm run-script update-filepaths --paths [FILEPATHS]\n\n` expect(nonMatches.length, message).toBe(0) }) test('every page has valid frontmatter', async () => { const frontmatterErrors = chain(pages) // .filter(page => page.languageCode === 'en') // Page class has dynamic error properties like 'frontmatterErrors' not in type definition .map((page) => (page as Record).frontmatterErrors) .filter(Boolean) .flatten() .value() const failureMessage = `${JSON.stringify(frontmatterErrors, null, 2)}\n\n${chain( frontmatterErrors, ) .map('filepath') .join('\n') .value()}` expect(frontmatterErrors.length, failureMessage).toBe(0) }) test('every page has valid Liquid templating', async () => { const liquidErrors: Array<{ filename: string; error: string }> = [] for (const page of pages) { // Page class has dynamic properties like 'raw' markdown not in type definition const markdown = (page as Record).raw as string if (!patterns.hasLiquid.test(markdown)) continue try { await liquid.parse(markdown) } catch (error) { liquidErrors.push({ filename: page.fullPath, error: (error as Error).message, }) } } const failureMessage = JSON.stringify(liquidErrors, null, 2) expect(liquidErrors.length, failureMessage).toBe(0) }) }) })