// @vitest-environment jsdom import { beforeEach, describe, expect, it } from 'vitest'; import { findAnchorElementByNormalizedId, normalizeAnchorId } from '../anchor-id'; import { extractSections } from '../markdown-section-extractor'; import { getHashTargetElement } from '../same-page-hash-nav'; describe('normalizeAnchorId', () => { it('drops the hyphen GitHub leaves behind a stripped emoji', () => { expect(normalizeAnchorId('-getting-started')).toBe('getting-started'); expect(normalizeAnchorId('-architecture-diagrams')).toBe('architecture-diagrams'); }); it('collapses the hyphen run GitHub emits around a dropped "&"', () => { expect(normalizeAnchorId('-community--support')).toBe('community-support'); }); it('is a no-op for an already-clean id', () => { expect(normalizeAnchorId('table-of-contents')).toBe('table-of-contents'); }); it('normalizes raw heading text too', () => { expect(normalizeAnchorId('πŸš€ Getting Started')).toBe('getting-started'); }); }); describe('findAnchorElementByNormalizedId', () => { beforeEach(() => { document.body.innerHTML = `

OpenFrame CLI β€” Documentation

πŸ“š Table of Contents

πŸš€ Getting Started

πŸ’¬ Community & Support

A - B

`; }); it('resolves a GitHub-style anchor onto our heading id', () => { expect(findAnchorElementByNormalizedId('-getting-started', document)?.id).toBe('getting-started'); expect(findAnchorElementByNormalizedId('-table-of-contents', document)?.id).toBe('table-of-contents'); }); it('resolves the double-hyphen "&" form', () => { expect(findAnchorElementByNormalizedId('-community--support', document)?.id).toBe('community-support'); }); it('matches an id whose own raw form normalizes differently', () => { // `a---b` is not reachable by normalized lookup β€” only the heading scan finds it. expect(findAnchorElementByNormalizedId('a-b', document)?.id).toBe('a---b'); }); it('returns null for an anchor with no heading behind it', () => { expect(findAnchorElementByNormalizedId('nope', document)).toBeNull(); expect(findAnchorElementByNormalizedId('', document)).toBeNull(); }); }); describe('duplicate headings β€” GitHub numbers from -1, our deduper from -2', () => { beforeEach(() => { document.body.innerHTML = `

Setup

Setup

Setup

Step 2

`; }); it('shifts a GitHub duplicate fragment onto our id', () => { expect(getHashTargetElement('setup-1')?.id).toBe('setup-2'); expect(getHashTargetElement('setup-2')?.id).toBe('setup-2'); }); it('an id that genuinely ends in a number is matched exactly, never shifted', () => { // `step-2` exists, so the exact lookup wins and `step-3` is never consulted. expect(getHashTargetElement('step-2')?.id).toBe('step-2'); }); it('does not invent a match when the shifted id is absent too', () => { expect(getHashTargetElement('missing-1')).toBeNull(); }); }); describe('getHashTargetElement β€” the fuzzy pass is wired into THE resolver', () => { beforeEach(() => { document.body.innerHTML = `

πŸš€ Getting Started

encoded-id row
`; }); it('exact ids still win, unchanged', () => { expect(getHashTargetElement('getting-started')?.id).toBe('getting-started'); }); it('percent-decoding still runs before the fuzzy pass', () => { expect(getHashTargetElement('ticket-a%2Fb')?.id).toBe('ticket-a/b'); }); it('falls back to a normalized match β€” the TOC bug, through the public entry point', () => { expect(getHashTargetElement('-getting-started')?.id).toBe('getting-started'); }); it('still returns null when nothing matches', () => { expect(getHashTargetElement('nope')).toBeNull(); expect(getHashTargetElement('')).toBeNull(); }); }); describe('the openframe-cli doc that reported the bug', () => { // Verbatim shapes from // GET /api/docs/sources/openframe-docs/content?path=openframe-cli const headings = [ '# OpenFrame CLI β€” Documentation', '## πŸ“š Table of Contents', '## πŸš€ Getting Started', '## πŸ› οΈ Development', '## πŸ’¬ Community & Support', ].join('\n\n'); const tocHrefs = ['#-getting-started', '#-development', '#-community--support']; it('every TOC href resolves to the id the section extractor emits', () => { const sections = extractSections(headings); document.body.innerHTML = sections.map(s => `

${s.title}

`).join(''); const resolved = tocHrefs.map(href => getHashTargetElement(href.slice(1))?.id ?? null); expect(resolved).toEqual(['getting-started', 'development', 'community-support']); // …and none of them resolved before this helper existed: expect(tocHrefs.map(href => document.getElementById(href.slice(1)))).toEqual([null, null, null]); }); });