diff --git a/cli/__tests__/symLinkConfig.test.ts b/cli/__tests__/symLinkConfig.test.ts new file mode 100644 index 0000000..e9f5152 --- /dev/null +++ b/cli/__tests__/symLinkConfig.test.ts @@ -0,0 +1,44 @@ +import { symlink } from 'fs/promises' +import { symLinkConfig } from '../symLinkConfig' + +jest.mock('fs/promises') + +// suppress console.log so that it doesn't clutter the test output +jest.spyOn(console, 'log').mockImplementation(() => {}) + +it('should create a symlink successfully', async () => { + ;(symlink as jest.Mock).mockResolvedValue(undefined) + + await symLinkConfig('/astro', '/consumer') + + expect(symlink).toHaveBeenCalledWith( + '/consumer/pf-docs.config.mjs', + '/astro/pf-docs.config.mjs', + ) +}) + +it('should log an error if symlink creation fails', async () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}) + + const error = new Error('Symlink creation failed') + ;(symlink as jest.Mock).mockRejectedValue(error) + + await symLinkConfig('/astro', '/consumer') + + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error creating symlink to /consumer/pf-docs.config.mjs in /astro`, + error, + ) +}) + +it('should log a success message after creating the symlink', async () => { + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) + + await symLinkConfig('/astro', '/consumer') + + expect(consoleLogSpy).toHaveBeenCalledWith( + `Symlink to /consumer/pf-docs.config.mjs in /astro created`, + ) +}) diff --git a/cli/cli.ts b/cli/cli.ts index 0fb3950..caa0f83 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -8,6 +8,7 @@ import { setFsRootDir } from './setFsRootDir.js' import { createConfigFile } from './createConfigFile.js' import { updatePackageFile } from './updatePackageFile.js' import { getConfig } from './getConfig.js' +import { symLinkConfig } from './symLinkConfig.js' import { buildPropsData } from './buildPropsData.js' import { hasFile } from './hasFile.js' @@ -80,6 +81,7 @@ program.command('setup').action(async () => { program.command('init').action(async () => { await setFsRootDir(astroRoot, currentDir) + await symLinkConfig(astroRoot, currentDir) console.log( '\nInitialization complete, next update your pf-docs.config.mjs file and then run the `start` script to start the dev server', ) diff --git a/cli/symLinkConfig.ts b/cli/symLinkConfig.ts new file mode 100644 index 0000000..9f267c0 --- /dev/null +++ b/cli/symLinkConfig.ts @@ -0,0 +1,21 @@ +/* eslint-disable no-console */ +import { symlink } from 'fs/promises' + +export async function symLinkConfig( + astroRootDir: string, + consumerRootDir: string, +) { + const configFileName = '/pf-docs.config.mjs' + const docsConfigFile = consumerRootDir + configFileName + + try { + await symlink(docsConfigFile, astroRootDir + configFileName) + } catch (e: any) { + console.error( + `Error creating symlink to ${docsConfigFile} in ${astroRootDir}`, + e, + ) + } finally { + console.log(`Symlink to ${docsConfigFile} in ${astroRootDir} created`) + } +} diff --git a/cli/templates/pf-docs.config.mjs b/cli/templates/pf-docs.config.mjs index 2a44d1e..c769289 100644 --- a/cli/templates/pf-docs.config.mjs +++ b/cli/templates/pf-docs.config.mjs @@ -16,6 +16,7 @@ export const config = { // name: "react-component-docs", // }, ], + navSectionOrder: ["get-started", "design-foundations"], outputDir: './dist/docs', propsGlobs: [ // { diff --git a/src/components/Navigation.astro b/src/components/Navigation.astro index c07c7a0..02feb92 100644 --- a/src/components/Navigation.astro +++ b/src/components/Navigation.astro @@ -3,11 +3,22 @@ import { getCollection } from 'astro:content' import { Navigation as ReactNav } from './Navigation.tsx' -import { content } from "../content" +import { content } from '../content' -const collections = await Promise.all(content.map(async (entry) => await getCollection(entry.name as 'textContent'))) +import { config } from '../pf-docs.config.mjs' -const navEntries = collections.flat(); +const collections = await Promise.all( + content.map( + async (entry) => await getCollection(entry.name as 'textContent'), + ), +) + +const navEntries = collections.flat() --- - + diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index f055015..b7fb42c 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -5,10 +5,12 @@ import { type TextContentEntry } from './NavEntry' interface NavigationProps { navEntries: TextContentEntry[] + navSectionOrder?: string[] } export const Navigation: React.FunctionComponent = ({ navEntries, + navSectionOrder, }: NavigationProps) => { const [activeItem, setActiveItem] = useState('') @@ -24,9 +26,31 @@ export const Navigation: React.FunctionComponent = ({ setActiveItem(selectedItem.itemId.toString()) } - const sections = new Set(navEntries.map((entry) => entry.data.section)) + const uniqueSections = Array.from( + new Set(navEntries.map((entry) => entry.data.section)), + ) + + // We want to list any ordered sections first, followed by any unordered sections sorted alphabetically + const [orderedSections, unorderedSections] = uniqueSections.reduce( + (acc, section) => { + if (!navSectionOrder) { + acc[1].push(section) + return acc + } + + const index = navSectionOrder.indexOf(section) + if (index > -1) { + acc[0][index] = section + } else { + acc[1].push(section) + } + return acc + }, + [[], []] as [string[], string[]], + ) + const sortedSections = [...orderedSections, ...unorderedSections.sort()] - const navSections = Array.from(sections).map((section) => { + const navSections = sortedSections.map((section) => { const entries = navEntries.filter((entry) => entry.data.section === section) return ( diff --git a/src/components/__tests__/Navigation.test.tsx b/src/components/__tests__/Navigation.test.tsx index 553f1fe..13893c5 100644 --- a/src/components/__tests__/Navigation.test.tsx +++ b/src/components/__tests__/Navigation.test.tsx @@ -6,36 +6,46 @@ import { TextContentEntry } from '../NavEntry' const mockEntries: TextContentEntry[] = [ { id: 'entry1', - data: { id: 'Entry1', section: 'section1' }, + data: { id: 'Entry1', section: 'section-one' }, collection: 'textContent', }, { id: 'entry2', - data: { id: 'Entry2', section: 'section1' }, + data: { id: 'Entry2', section: 'section-two' }, collection: 'textContent', }, { id: 'entry3', - data: { id: 'Entry3', section: 'section2' }, + data: { id: 'Entry3', section: 'section-two' }, + collection: 'textContent', + }, + { + id: 'entry4', + data: { id: 'Entry4', section: 'section-three' }, + collection: 'textContent', + }, + { + id: 'entry5', + data: { id: 'Entry5', section: 'section-four' }, collection: 'textContent', }, ] it('renders without crashing', () => { render() - expect(screen.getByText('Section1')).toBeInTheDocument() - expect(screen.getByText('Section2')).toBeInTheDocument() + expect(screen.getByText('Section one')).toBeInTheDocument() + expect(screen.getByText('Section two')).toBeInTheDocument() }) it('renders the correct number of sections', () => { render() - expect(screen.getAllByRole('listitem')).toHaveLength(2) + expect(screen.getAllByRole('listitem')).toHaveLength(4) }) it('sets the active item based on the current pathname', () => { Object.defineProperty(window, 'location', { value: { - pathname: '/section1/entry1', + pathname: '/section-one/entry1', }, writable: true, }) @@ -48,10 +58,17 @@ it('sets the active item based on the current pathname', () => { }) it('updates the active item on selection', async () => { + // prevent errors when trying to navigate from logging in the console and cluttering the test output + jest.spyOn(console, 'error').mockImplementation(() => {}) + const user = userEvent.setup() render() + const sectionTwo = screen.getByRole('button', { name: 'Section two' }) + + await user.click(sectionTwo) + const entryLink = screen.getByRole('link', { name: 'Entry2' }) await user.click(entryLink) @@ -59,6 +76,42 @@ it('updates the active item on selection', async () => { expect(entryLink).toHaveClass('pf-m-current') }) +it('sorts all sections alphabetically by default', () => { + render() + + const sections = screen.getAllByRole('button') + + expect(sections[0]).toHaveTextContent('Section four') + expect(sections[1]).toHaveTextContent('Section one') + expect(sections[2]).toHaveTextContent('Section three') + expect(sections[3]).toHaveTextContent('Section two') +}) + +it('sorts sections based on the order provided', () => { + render( + , + ) + + const sections = screen.getAllByRole('button') + + expect(sections[0]).toHaveTextContent('Section two') + expect(sections[1]).toHaveTextContent('Section one') +}) + +it('sorts unordered sections alphabetically after ordered sections', () => { + render( + , + ) + + const sections = screen.getAllByRole('button') + + expect(sections[2]).toHaveTextContent('Section one') + expect(sections[3]).toHaveTextContent('Section three') +}) + it('matches snapshot', () => { const { asFragment } = render() expect(asFragment()).toMatchSnapshot() diff --git a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap index fbf910f..47283dd 100644 --- a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap @@ -9,7 +9,7 @@ exports[`matches snapshot 1`] = `