// @vitest-environment jsdom import { beforeEach, describe, expect, it } from 'vitest' import { extractSections } from '../markdown-section-extractor' import { findAnchorElementByNormalizedId, normalizeAnchorId } from '../anchor-id' 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, ]) }) })