From 0f84e5a3bd10daa432f3ac87543bac210176b094 Mon Sep 17 00:00:00 2001 From: Brandon Scott Date: Thu, 18 May 2023 19:33:36 -0400 Subject: [PATCH] Consolidate anchor id slugify logic, add tests + docs test workflow --- .github/workflows/docs-tests.yml | 30 ++++++ .../CustomHTMLELements.utils.test.ts | 96 +++++++++++++++++++ .../CustomHTMLElements.utils.ts | 53 +++++----- apps/docs/jest.config.ts | 9 ++ apps/docs/package.json | 1 + package.json | 1 + 6 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/docs-tests.yml create mode 100644 apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts create mode 100644 apps/docs/jest.config.ts diff --git a/.github/workflows/docs-tests.yml b/.github/workflows/docs-tests.yml new file mode 100644 index 00000000000..bd402e9f365 --- /dev/null +++ b/.github/workflows/docs-tests.yml @@ -0,0 +1,30 @@ +name: Docs Tests + +on: + pull_request: + branches: ['master'] + paths: + - 'apps/docs/**/*.{ts,tsx}' + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install deps + run: npm ci + + - name: Run tests + run: npm run test:docs diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts new file mode 100644 index 00000000000..2d2d1285229 --- /dev/null +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts @@ -0,0 +1,96 @@ +import { getAnchor, removeAnchor } from './CustomHTMLElements.utils' + +describe('CustomHTMLElementsUtils', () => { + describe('getAnchor', () => { + describe('when value is an object', () => { + it('returns slugified version of props.children', () => { + const value = { + props: { + children: 'Full Text Search', + }, + } + + const result = getAnchor(value) + + expect(result).toStrictEqual('full-text-search') + }) + }) + + describe('when value is an array', () => { + describe('when custom anchor exists', () => { + it('returns inner slug', () => { + const value = ['Proximity: <->', '[#proximity]'] + + const result = getAnchor(value) + + expect(result).toStrictEqual('proximity') + }) + + it('trims whitespace', () => { + const value = ['Proximity: <->', ' [#proximity] '] + + const result = getAnchor(value) + + expect(result).toStrictEqual('proximity') + }) + }) + + it('returns concatenated slug of elements', () => { + const value = ['Full', 'Text', 'Search'] + + const result = getAnchor(value) + + expect(result).toStrictEqual('full-text-search') + }) + + it('trims whitespace', () => { + const value = [' Full ', ' Text ', ' Search '] + + const result = getAnchor(value) + + expect(result).toStrictEqual('full-text-search') + }) + + it('removes special characters', () => { + const value = ['function()'] + + const result = getAnchor(value) + + expect(result).toStrictEqual('function') + }) + }) + + describe('when value is a string', () => { + it('returns slugified version of string', () => { + const value = 'My (Very) Awesome Heading' + + const result = getAnchor(value) + + expect(result).toStrictEqual('my-very-awesome-heading') + }) + }) + }) + + describe('removeAnchor', () => { + describe('when value is an array', () => { + it('filters out custom anchor elements', () => { + const value = ['My (Very) Awesome Heading', '[#my-custom-heading]'] + + const result = removeAnchor(value) + + expect(result).toStrictEqual(['My (Very) Awesome Heading']) + }) + }) + + describe('when value is a string', () => { + it('strips out custom anchor string', () => { + const value = 'My (Very) Awesome Heading [#my-custom-heading]' + + const result = removeAnchor(value) + + // Original implementation didn't trim the resulting string - not sure if it really matters + expect(result).toStrictEqual('My (Very) Awesome Heading ') + }) + }) + }) +}) diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts index 0103cef6aa7..844724a5466 100644 --- a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts @@ -2,53 +2,60 @@ export const getAnchor = (text: any): string | undefined => { if (typeof text === 'object') { if (Array.isArray(text)) { - const customAnchor = text.find( - (x) => typeof x === 'string' && x.includes('[#') && x.endsWith(']') - ) - if (customAnchor !== undefined) return customAnchor.slice(2, customAnchor.indexOf(']')) + const customAnchor = text.find((x) => typeof x === 'string' && hasCustomAnchor(x)) + if (customAnchor !== undefined) { + return parseCustomAnchor(customAnchor) + } const formattedText = text .map((x) => { - if (typeof x !== 'string') return x.props.children - else return x.trim() + if (typeof x !== 'string') { + return x.props.children + } + + return x.trim() }) .map((x) => { - if (typeof x !== 'string') return x - else + if (typeof x !== 'string') { return x - .toLowerCase() - .replace(/[^a-z0-9- ]/g, '') - .replace(/[ ]/g, '-') + } + + return slugify(x) }) return formattedText.join('-').toLowerCase() } else { const anchor = text.props.children if (typeof anchor === 'string') { - return anchor - .toLowerCase() - .replace(/[^a-z0-9- ]/g, '') - .replace(/[ ]/g, '-') + return slugify(anchor) } return anchor } } else if (typeof text === 'string') { - if (text.includes('[#') && text.endsWith(']')) { - return text.slice(text.indexOf('[#') + 2, text.indexOf(']')) - } else { - return text - .toLowerCase() - .replace(/[^a-z0-9- ]/g, '') - .replace(/[ ]/g, '-') + if (hasCustomAnchor(text)) { + return parseCustomAnchor(text) } + return slugify(text) } else { return undefined } } +const hasCustomAnchor = (value: string): boolean => value.includes('[#') && value.includes(']') + +const parseCustomAnchor = (value: string): string => + value.slice(value.indexOf('[#') + 2, value.indexOf(']')) + +const slugify = (value: string): string => + value + .toLowerCase() + .trim() + .replace(/[^a-z0-9- ]/g, '') + .replace(/[ ]/g, '-') + export const removeAnchor = (text: any) => { if (typeof text === 'object' && Array.isArray(text)) { - return text.filter((x) => !(typeof x === 'string' && x.includes('[#') && x.endsWith(']'))) + return text.filter((x) => !(typeof x === 'string' && hasCustomAnchor(x))) } else if (typeof text === 'string') { if (text.indexOf('[#') > 0) return text.slice(0, text.indexOf('[#')) else return text diff --git a/apps/docs/jest.config.ts b/apps/docs/jest.config.ts new file mode 100644 index 00000000000..348b75c0bb7 --- /dev/null +++ b/apps/docs/jest.config.ts @@ -0,0 +1,9 @@ +import type { Config } from '@jest/types' + +const config: Config.InitialOptions = { + preset: 'ts-jest', + setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], + testEnvironment: 'jsdom', +} + +export default config diff --git a/apps/docs/package.json b/apps/docs/package.json index 28ce0d084d8..c59b1dcf3c4 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -9,6 +9,7 @@ "build:analyze": "ANALYZE=true next build", "start": "next start", "lint": "next lint", + "test": "jest", "build:sitemap": "node ./internals/generate-sitemap.mjs", "embeddings": "tsx scripts/search/generate-embeddings.ts", "embeddings:refresh": "npm run embeddings -- --refresh", diff --git a/package.json b/package.json index ff003cd5799..0a856ee8320 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "docker:up": "cd docker && docker compose up", "docker:down": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down --remove-orphans", "docker:remove": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml rm -vfs", + "test:docs": "turbo run test --filter=docs", "test:ui": "turbo run test --filter=ui", "test:studio": "turbo run test --filter=studio", "perf:kong": "ab -t 5 -c 20 -T application/json http://localhost:8000/",