import assert from 'node:assert/strict'; import { after, before, test } from 'node:test'; import { cp, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; import { execFileSync } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import fg from 'fast-glob'; import matter from 'gray-matter'; import { load } from 'cheerio'; import { createLibraryCatalog, extractMarkdownHeadings } from '../src/utils/library-catalog.ts'; import { createLibraryPaginationPublications, libraryPageRoute, paginateLibraryItems } from '../src/utils/library-pagination.ts'; import { boundedSearchOffset, normalizeLibrarySearchText, parseLegacyLibrarySearchState, searchLibrary } from '../src/utils/library-search.ts'; import { createPublicationManifest, renderSitemap, robotsFor } from '../src/utils/publication-policy.ts'; import { createLibrarySearchPublication } from '../src/utils/search-publication.ts'; import { createAuthorResolver } from '../src/utils/authors.ts'; import { authorPageRoute, createAuthorArchivePublications } from '../src/utils/author-archives.ts'; import { redirectArtifacts, redirects as approvedRedirects, validateRedirects } from './generate-redirects.mjs'; import { resolveNavigationDestination, visitorLanguageChoices } from '../src/utils/visitor-navigation.ts'; const project = fileURLToPath(new URL('../', import.meta.url)); const site = JSON.parse(await readFile(path.join(project, 'src/data/site.json'), 'utf8')).url; async function contentEntries(root = project) { return Promise.all((await fg('src/content/{pages,blog}/**/*.{md,mdx}', { cwd: root })).sort().map(async (file) => { const source = await readFile(path.join(root, file), 'utf8'); const parsed = matter(source); const [, , collection, ...parts] = file.split('/'); return { id: parts.join('/').replace(/\.mdx?$/, ''), collection, body: parsed.content, data: parsed.data }; })); } async function authorEntries(root = project) { return Promise.all((await fg('src/content/authors/**/*.{md,mdx}', { cwd: root })).sort().map(async (file) => { const parsed = matter(await readFile(path.join(root, file), 'utf8')); return { id: file.replace(/^src\/content\/authors\//, '').replace(/\.mdx?$/, ''), data: parsed.data, body: parsed.content }; })); } const entries = await contentEntries(); const manifest = createPublicationManifest(entries, site); const authors = await authorEntries(); const catalog = createLibraryCatalog(manifest, authors); const paginationPublications = createLibraryPaginationPublications(catalog, manifest, site); const authorPublications = createAuthorArchivePublications(catalog, authors, [...manifest, ...paginationPublications], site); const routeManifest = [...manifest, ...paginationPublications, ...authorPublications].sort((left, right) => left.route.localeCompare(right.route, 'en')); function record(overrides = {}) { return { url: '/library/example', locale: 'en', title: 'Autism Support Guide', description: 'Practical family resources', headings: ['Planning Daily Routines'], category: 'Library', tags: [], author: { name: 'Rula Diab', slug: 'rula-diab' }, publishedAt: '2026-01-01', ...overrides }; } const phase6CArticles = [ ['autism-after-school-meltdowns', '2026-09-01', '2026-09-02'], ['autism-back-to-school-transition', '2026-09-10', '2026-09-10'], ['autism-daily-living-skills-challenges', '2026-07-01', '2026-07-09'], ['autism-social-skills-by-age', '2026-09-24', '2026-09-24'], ['does-aba-therapy-replace-school', '2026-07-08', '2026-07-22'], ['halloween-and-autism', '2026-10-01', '2026-10-03'], ['how-many-hours-aba-child-need', '2026-09-08', '2026-09-08'], ['picky-eating-autism-parent-guide', '2026-08-01', '2026-10-03'], ['sleep-issues-autistic-children', '2026-09-28', '2026-09-28'] ]; test('actual catalog groups every eligible post by locale and preserves syndicated noindex records', () => { assert.deepEqual(Object.fromEntries(Object.entries(catalog.byLocale).map(([locale, items]) => [locale, items.length])), { en: 55, es: 12, ar: 7 }); assert.equal(catalog.all.length, 74); for (const slug of ['community-highlight-meet-rula-diab', 'new-aia-scottsdale-office']) { assert.ok(catalog.byLocale.en.some(({ search }) => search.url === `/library/${slug}`)); } assert.equal(new Set(catalog.all.map(({ route }) => route)).size, 74); assert.ok(catalog.all.every(({ search }) => search.author.name === 'Rula Diab')); }); test('all nine Phase 6C records are source-backed, public English articles with complete collection metadata', async () => { for (const [slug, date, updatedDate] of phase6CArticles) { const entry = entries.find((item) => item.collection === 'blog' && item.id === `en/${slug}`); assert.ok(entry, slug); assert.equal(entry.data.slug, slug); assert.equal(entry.data.lang, 'en'); assert.equal(entry.data.author, 'rula-diab'); assert.equal(entry.data.category, 'Library'); assert.equal(entry.data.draft, false); assert.equal(entry.data.date, date); assert.equal(entry.data.updatedDate, updatedDate); assert.equal(entry.data.canonical, `${site}/library/${slug}`); assert.equal(entry.data.translationKey, undefined); assert.ok(entry.data.title && entry.data.description && entry.data.featuredImage && entry.data.alt); assert.match(entry.body, / item.route === `/library/${slug}`).search.headings.length > 0); await readFile(path.join(project, 'public', entry.data.featuredImage)); } }); test('Phase 6C.2 material reconciliations retain the complete source-backed structure', async () => { const expected = [ { id: 'en/aba-school-readiness-arizona', title: 'ABA School Readiness in Arizona: A Parent Guide', displayH1: 'ABA School Readiness for Autistic Children in Arizona', updatedDate: '2026-09-17', faqCount: 9, tableCount: 1, images: ['school-readiness-supports-infographic.webp', 'one-to-one-to-group-progression.webp'], headings: ['What Does School Readiness Mean in Arizona?', 'How ABA Therapy May Support School Readiness', 'How AIA Can Help With School Readiness'] }, { id: 'es/aba-school-readiness-arizona', title: 'Preparación escolar y autismo en Arizona: guía para familias', displayH1: 'Preparación escolar para niños autistas en Arizona: cómo puede ayudar la terapia ABA', updatedDate: '2026-09-25', faqCount: 9, tableCount: 1, images: ['school-readiness-supports-infographic_es.webp', 'one-to-one-to-group-progression_es.webp'], headings: ['¿Qué significa estar preparado para la escuela en Arizona?', 'Cómo puede ayudar la terapia ABA con la preparación escolar', 'Cómo puede ayudar AIA con la preparación escolar'] }, { id: 'en/aba-school-readiness-guide', title: 'Antecedent Interventions & Consequence Strategies in ABA', displayH1: 'Antecedent and Consequence Interventions in ABA: Strategies and Examples', updatedDate: '2026-08-13', faqCount: 6, tableCount: 3, images: ['antecedent-behavior-consequence.webp', 'reduce-environmental-distractions.webp', 'reinforcement-replacement-behavior.webp'], headings: ['What are antecedents and consequences in ABA?', 'How antecedent and consequence strategies fit with proactive and reactive ABA', 'Individualized ABA support at AIA'] }, { id: 'ar/aba-school-readiness-guide', title: 'التدخلات التمهيدية واستراتيجيات العواقب في تحليل السلوك التطبيقي', displayH1: 'التدخلات التمهيدية وتدخلات العواقب في تحليل السلوك التطبيقي: استراتيجيات وأمثلة', updatedDate: undefined, faqCount: 6, tableCount: 3, images: ['antecedent-behavior-consequence_ar.svg', 'reduce-environmental-distractions_ar.svg', 'reinforcement-replacement-behavior_ar.svg'], headings: ['ما المقدمات والعواقب في تحليل السلوك التطبيقي؟', 'ما استراتيجيات العواقب في تحليل السلوك التطبيقي؟', 'كيف ترتبط استراتيجيات المقدمات والعواقب بتحليل السلوك التطبيقي الاستباقي والتفاعلي؟'] }, { id: 'en/autism-fall-break-routine-tips-halloween-thanksgiving', title: 'Autism and Fall Break: Routines and Transition Tips', displayH1: 'Autism and Fall Break: How to Keep Routines Calm and Predictable', updatedDate: '2026-10-03', faqCount: 6, tableCount: 2, images: ['trick-or-treat-porch-800x.jpg', 'quiet-nature-walk-800x.jpg'], headings: ['Why breaks feel hard for autistic learners', 'Halloween strategies that reduce overload', 'Conclusion & Next Steps'] } ]; for (const item of expected) { const entry = entries.find(({ collection, id }) => collection === 'blog' && id === item.id); assert.ok(entry, item.id); assert.equal(entry.data.title, item.title); assert.equal(entry.data.displayH1, item.displayH1); assert.equal(entry.data.updatedDate, item.updatedDate); assert.equal((entry.body.match(/["']question["']\s*:/gu) ?? []).length, item.faqCount, item.id); assert.equal((entry.body.match(//gu) ?? []).length, item.tableCount, item.id); for (const heading of item.headings) assert.ok(entry.body.includes(`>${heading} collection === 'blog' && id === 'en/aba-school-readiness-guide').length, 1); assert.equal(entries.filter(({ collection, id }) => collection === 'blog' && id === 'ar/aba-school-readiness-guide').length, 1); assert.match(entries.find(({ id }) => id === 'en/autism-fall-break-routine-tips-halloween-thanksgiving').body, /\[Halloween and autism\]\(\/library\/halloween-and-autism\)/u); }); test('Phase 6C.3 reconciles audited article metadata and Arabic visual translations', async () => { const blog = entries.filter(({ collection }) => collection === 'blog'); assert.equal(blog.length, 74); assert.ok(blog.every(({ data }) => typeof data.alt === 'string' && data.alt.trim()), 'Every currently audited featured image is informative and has a deliberate localized description'); const missingModified = blog.filter(({ data }) => data.updatedDate === undefined).map(({ id }) => id); assert.deepEqual(missingModified, ['ar/aba-school-readiness-guide'], 'The newly translated Arabic article must not inherit the superseded public article timestamp'); const headings = new Map([ ['ar/first-then-cards-autism-transitions', 'بطاقات "أولًا/ثم": تسهيل الانتقال بين الأنشطة للأطفال المصابين بالتوحّد'], ['es/executive-functioning-skills-autism', 'Habilidades de funcionamiento ejecutivo y autismo'], ['es/parents-guide-to-autism-and-aba', 'Guía de Autismo para Padres y Cuidadores'], ['en/communication-social-skills-autistic-children-guide', 'Building Communication & Social Skills in Autistic Children: A Practical Guide for Families'], ['en/executive-functioning-skills-autism', 'Executive Functioning Skills and Autism'], ['en/individualized-aba-therapy', 'Individualized Care Plans in ABA Therapy'], ['en/parents-guide-to-autism-and-aba', 'Autism Guide for Parents and Caregivers'] ]); for (const [id, displayH1] of headings) assert.equal(blog.find((entry) => entry.id === id).data.displayH1, displayH1, id); const descriptions = new Map([ ['en/aba-therapy-benefits', "Discover how ABA therapy, tailored to each child's unique needs, can enhance social skills, communication, and independence in children with autism."], ['en/community-highlight-meet-rula-diab', "Read about Rula Diab's mission to make ABA therapy accessible to children with Autism Spectrum Disorder through her Arizona Institute for Autism."], ['en/new-aia-scottsdale-office', "Discover how Arizona Institute for Autism's new Scottsdale office expands autism services, empowering lives with integrity and excellence. Learn more!"], ['en/november-2022-aia-update', "Nov 2022: Arizona Institute for Autism expresses gratitude to families for their support and commitment, highlighting the community's strength and unity."] ]); for (const [id, description] of descriptions) assert.equal(blog.find((entry) => entry.id === id).data.description, description, id); const arabic = blog.find(({ id }) => id === 'ar/aba-school-readiness-guide'); assert.equal((arabic.body.match(/class="article-diagram-scroll"/gu) ?? []).length, 3); assert.equal((arabic.body.match(/role="region"/gu) ?? []).length, 3); assert.equal((arabic.body.match(/tabindex="0"/gu) ?? []).length, 3); for (const name of ['antecedent-behavior-consequence_ar.svg', 'reduce-environmental-distractions_ar.svg', 'reinforcement-replacement-behavior_ar.svg']) { assert.ok(arabic.body.includes(`/assets/images/${name}`), name); const svg = await readFile(path.join(project, 'public/assets/images', name), 'utf8'); assert.match(svg, /^ { for (let index = 1; index < catalog.all.length; index++) { const previous = catalog.all[index - 1].search; const current = catalog.all[index].search; const dateOrder = Date.parse(previous.publishedAt) - Date.parse(current.publishedAt); assert.ok(dateOrder > 0 || (dateOrder === 0 && previous.url.localeCompare(current.url, 'en') < 0)); } }); test('author resolution is exact by locale and slug, keeps identity separate, and rejects invalid or ambiguous data', () => { const resolve = createAuthorResolver(authors); for (const locale of ['en', 'es', 'ar']) { const author = resolve(locale, 'rula-diab'); assert.equal(author.name, 'Rula Diab'); assert.equal(author.displayName, authors.find(({ data }) => data.lang === locale).data.displayName); assert.equal(author.archiveRoute, `${locale === 'en' ? '' : '/' + locale}/library/author/rula-diab`); assert.equal(author.avatar, '/assets/images/rula-diab-avatar.jpg'); } assert.throws(() => resolve('fr', 'rula-diab'), /Missing or ambiguous/); assert.throws(() => resolve('en', 'Rula-Diab'), /Missing or ambiguous/); assert.throws(() => createAuthorResolver([...authors, authors[0]]), /Missing or ambiguous/); const source = authors[0]; for (const override of [{ name: ' ' }, { name: 4 }, { displayName: '' }, { displayName: ' \t' }, { slug: '../other' }, { slug: 'name/page/2' }, { slug: '%72ula' }, { lang: 'fr' }, { avatar: 'https://example.org/image.jpg' }, { description: 1 }]) { assert.throws(() => createAuthorResolver([{ ...source, data: { ...source.data, ...override } }]), /Invalid author/); } const plain = { id: 'en/another', data: { lang: 'en', slug: 'another', name: 'Another Author' } }; assert.equal(createAuthorResolver([plain])('en', 'another').displayName, 'Another Author'); }); test('visitor language navigation resolves explicit content and author relationships without changing SEO translations', () => { const englishAuthor = routeManifest.find(({ route }) => route === '/library/author/rula-diab'); const englishAuthorPage = routeManifest.find(({ route }) => route === '/library/author/rula-diab/page/5'); assert.deepEqual(englishAuthor.translations, []); assert.deepEqual(visitorLanguageChoices(englishAuthor, routeManifest), [ { lang: 'en', route: '/library/author/rula-diab', current: true, kind: 'equivalent' }, { lang: 'es', route: '/es/library/author/rula-diab', current: false, kind: 'equivalent' }, { lang: 'ar', route: '/ar/library/author/rula-diab', current: false, kind: 'equivalent' } ]); assert.deepEqual(visitorLanguageChoices(englishAuthorPage, routeManifest), [ { lang: 'en', route: '/library/author/rula-diab', current: true, kind: 'section-root' }, { lang: 'es', route: '/es/library/author/rula-diab', current: false, kind: 'section-root' }, { lang: 'ar', route: '/ar/library/author/rula-diab', current: false, kind: 'section-root' } ]); const libraryPage = routeManifest.find(({ route }) => route === '/library/page/2'); assert.deepEqual(visitorLanguageChoices(libraryPage, routeManifest).map(({ route, kind }) => ({ route, kind })), [ { route: '/library', kind: 'section-root' }, { route: '/es/library', kind: 'section-root' }, { route: '/ar/library', kind: 'section-root' } ]); const untranslated = routeManifest.find(({ route }) => route === '/library/new-aia-scottsdale-office'); assert.deepEqual(visitorLanguageChoices(untranslated, routeManifest), []); }); test('menu destinations prefer eligible locale publications and expose deliberate fallbacks', () => { assert.deepEqual(resolveNavigationDestination('/library', 'ar', routeManifest), { href: '/ar/library', lang: 'ar', fallback: false, source: 'translation' }); assert.deepEqual(resolveNavigationDestination('/ar/library', 'ar', routeManifest), { href: '/ar/library', lang: 'ar', fallback: false, source: 'exact' }); assert.deepEqual(resolveNavigationDestination('/ar', 'ar', routeManifest), { href: '/', lang: 'en', fallback: true, source: 'explicit-fallback' }); assert.deepEqual(resolveNavigationDestination('/team', 'ar', routeManifest), { href: '/team', lang: 'en', fallback: true, source: 'explicit-fallback' }); assert.equal(resolveNavigationDestination('/missing', 'es', routeManifest), null); assert.deepEqual(resolveNavigationDestination('/library', 'es', [{ ...routeManifest.find(({ route }) => route === '/library'), translations: [] }]), { href: '/library', lang: 'en', fallback: true, source: 'explicit-fallback' }, 'Missing explicit equivalence falls back to the known published source instead of guessing /es/library'); }); test('author archives automatically follow multiple authors, exact locale references, draft removal and growth', () => { const author = (locale, slug) => ({ id: `${locale}/${slug}`, data: { lang: locale, slug, name: `${locale} ${slug}` } }); const sources = [author('en', 'one'), author('es', 'one'), author('en', 'two'), author('en', 'empty'), author('en', 'draft-only')]; const post = (slug, authorSlug, lang = 'en', draft = false) => ({ id: `${lang}/${slug}`, collection: 'blog', data: { lang, slug, title: slug, date: '2026-01-01', author: authorSlug, draft } }); const posts = [...Array.from({ length: 11 }, (_, i) => post(`post-${String(i).padStart(2, '0')}`, 'one')), post('spanish', 'one', 'es'), post('another', 'two'), post('unpublished', 'draft-only', 'en', true)]; const generate = (records) => { const m = createPublicationManifest(records, site); return createAuthorArchivePublications(createLibraryCatalog(m, sources), sources, m, site); }; const generated = generate(posts); assert.deepEqual(generated.map(({ route }) => route), [ '/es/library/author/one', '/library/author/one', '/library/author/one/page/2', '/library/author/two' ]); assert.equal(generated.find(({ route }) => route === '/library/author/one').authorPage.totalItems, 11); assert.ok(!generate(posts.slice(1)).some(({ route }) => route.endsWith('/page/2'))); assert.ok(!generate(posts.map((p, i) => i === 0 ? { ...p, data: { ...p.data, draft: true } } : p)) .some(({ route }) => route.endsWith('/page/2'))); assert.equal(generate([]).length, 0); assert.ok(generate([...posts, post('new-author-post', 'empty')]).some(({ route }) => route === '/library/author/empty')); }); test('author descriptors reject reserved routes including drafts, duplicate canonical targets, and invalid page paths', () => { const root = manifest.find(({ route }) => route === '/library'); for (const route of ['/library/author', '/library/author/rula-diab', '/library/author/rula-diab/page/02', '/es/library/author/unknown']) { assert.throws(() => createAuthorArchivePublications(catalog, authors, [...manifest, { ...root, route, eligible: false }], site), /Reserved author archive route collision/); } assert.throws(() => createAuthorArchivePublications(catalog, authors, [ ...manifest, { ...root, route: '/other', canonical: `${site}/library/author/rula-diab` } ], site), /Duplicate canonical/); for (const page of [0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, '02', '2']) { assert.throws(() => authorPageRoute('/library/author/rula-diab', page), /Invalid author page/); } assert.throws(() => authorPageRoute('/library/author/../bad', 1), /Invalid author archive root/); }); test('pagination handles empty, boundary and exact-multiple catalogs without empty numbered pages', () => { for (const [count, totalPages, pageSizes] of [ [0, 0, [0]], [1, 1, [1]], [9, 1, [9]], [10, 1, [10]], [11, 2, [10, 1]], [20, 2, [10, 10]] ]) { const items = Array.from({ length: count }, (_, index) => index); const root = paginateLibraryItems(items, 1); assert.equal(root.totalItems, count); assert.equal(root.totalPages, totalPages); assert.equal(root.items.length, pageSizes[0]); for (let page = 2; page <= totalPages; page++) { assert.equal(paginateLibraryItems(items, page).items.length, pageSizes[page - 1]); } assert.equal(paginateLibraryItems(items, Math.max(2, totalPages + 1)), null); } for (const page of [0, -1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 1]) { assert.equal(paginateLibraryItems([1], page), null); } assert.equal(paginateLibraryItems([1], 1, 0), null); }); test('current catalogs generate six English and two Spanish Library pages with expected slices', () => { assert.deepEqual(paginationPublications.map(({ route }) => route), [ '/es/library/page/2', '/library/page/2', '/library/page/3', '/library/page/4', '/library/page/5', '/library/page/6' ]); assert.deepEqual(Object.fromEntries(['en', 'es', 'ar'].map((locale) => { const root = paginateLibraryItems(catalog.byLocale[locale], 1); return [locale, { total: root.totalItems, pages: root.totalPages }]; })), { en: { total: 55, pages: 6 }, es: { total: 12, pages: 2 }, ar: { total: 7, pages: 1 } }); assert.deepEqual([1, 2, 3, 4, 5, 6].map((page) => paginateLibraryItems(catalog.byLocale.en, page).items.length), [10, 10, 10, 10, 10, 5]); assert.deepEqual([1, 2].map((page) => paginateLibraryItems(catalog.byLocale.es, page).items.length), [10, 2]); assert.equal(paginateLibraryItems(catalog.byLocale.ar, 1).items.length, 7); for (const publication of paginationPublications) { assert.equal(publication.canonical, site + publication.route); assert.equal(publication.sitemapEligible, true); assert.deepEqual(publication.translations, []); } }); test('generated page inventory follows catalog growth and removal without manual routes', () => { const english = catalog.byLocale.en; const withSixMore = [...english, ...english.slice(0, 6)]; const grown = { ...catalog, byLocale: { ...catalog.byLocale, en: withSixMore } }; assert.ok(createLibraryPaginationPublications(grown, manifest, site).some(({ route }) => route === '/library/page/7')); const reduced = { ...catalog, byLocale: { ...catalog.byLocale, en: english.slice(0, 40) } }; assert.deepEqual(createLibraryPaginationPublications(reduced, manifest, site) .filter(({ libraryPage }) => libraryPage.locale === 'en').map(({ route }) => route), [ '/library/page/2', '/library/page/3', '/library/page/4' ]); }); test('page routes reject invalid numbers and generated descriptors reject reserved-family collisions', () => { assert.equal(libraryPageRoute('en', 1), '/library'); assert.equal(libraryPageRoute('es', 2), '/es/library/page/2'); for (const page of [0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1]) { assert.throws(() => libraryPageRoute('en', page), /Invalid Library page number/); } const root = manifest.find((page) => page.route === '/library'); assert.throws(() => createLibraryPaginationPublications(catalog, [ ...manifest, { ...root, route: '/library/page/2', url: `${site}/library/page/2`, canonical: `${site}/library/page/2` } ], site), /Reserved Library pagination route collision/); }); test('catalog fixtures include new eligible posts automatically and exclude drafts', () => { const author = [{ id: 'en/author', data: { lang: 'en', slug: 'author', name: 'Example Author' } }]; const make = (slug, draft = false) => ({ id: `en/${slug}`, collection: 'blog', body: '## Searchable heading', data: { lang: 'en', slug, title: slug, description: '', date: '2026-01-01', author: 'author', category: 'Library', tags: [], draft } }); const fixtureManifest = createPublicationManifest([make('new-post'), make('draft-post', true)], site); const fixture = createLibraryCatalog(fixtureManifest, author); assert.deepEqual(fixture.all.map(({ route }) => route), ['/library/new-post']); assert.deepEqual(fixture.all[0].search.headings, ['Searchable heading']); assert.equal(paginateLibraryItems(fixture.byLocale.en, 1).totalItems, 1); }); test('catalog rejects duplicate routes and missing or ambiguous locale authors', () => { const source = { id: 'en/example', collection: 'blog', body: '', data: { lang: 'en', slug: 'example', title: 'Example', description: '', date: '2026-01-01', author: 'author' } }; const publication = { route: '/library/example', eligible: true, entry: source }; assert.throws(() => createLibraryCatalog([publication, publication], [{ id: 'a', data: { lang: 'en', slug: 'author', name: 'Author' } }]), /Duplicate Library route/); assert.throws(() => createLibraryCatalog([publication], []), /Missing or ambiguous article author/); assert.throws(() => createLibraryCatalog([publication], [ { id: 'a', data: { lang: 'en', slug: 'author', name: 'Author' } }, { id: 'b', data: { lang: 'en', slug: 'author', name: 'Other' } } ]), /Missing or ambiguous article author/); }); test('heading extraction includes visible Markdown and literal HTML headings while ignoring code and MDX implementation', () => { assert.deepEqual(extractMarkdownHeadings(`## Visible **heading**\n\n\`\`\`md\n## Hidden code\n\`\`\`\n## [Linked heading](/path)\n

Source heading

\n## \n## ![Image heading](/image.jpg)`), [ 'Visible heading', 'Linked heading', 'Source heading', 'Image heading' ]); }); test('normalization handles case, whitespace, punctuation, Spanish accents, and Arabic marks', () => { assert.equal(normalizeLibrarySearchText(' EVALUACIÓN—ABA ', 'es'), 'evaluacion aba'); assert.equal(normalizeLibrarySearchText('التَّـوحُّد', 'ar'), 'التوحد'); assert.doesNotThrow(() => normalizeLibrarySearchText('\ud800 🙂 ???', 'en')); }); test('search handles empty, exact-title, headings, descriptions, authors, categories and multiple terms', () => { const records = [ record(), record({ url: '/library/second', title: 'Second article', description: 'Autism support', publishedAt: '2026-02-01' }) ]; assert.deepEqual(searchLibrary(records, ''), []); assert.deepEqual(searchLibrary(records, ' '), []); assert.equal(searchLibrary(records, 'Autism Support Guide')[0].record.url, '/library/example'); assert.equal(searchLibrary([records[0]], 'daily routines')[0].record.url, '/library/example'); assert.equal(searchLibrary([records[0]], 'family resources')[0].record.url, '/library/example'); assert.equal(searchLibrary([records[0]], 'Rula Diab')[0].record.url, '/library/example'); assert.equal(searchLibrary([records[0]], 'Library')[0].record.url, '/library/example'); assert.equal(searchLibrary(records, 'autism resources').length, 1); assert.equal(searchLibrary(records, 'nonexistent').length, 0); }); test('search matches Spanish accents and Arabic diacritics without changing display text', () => { const spanish = record({ locale: 'es', title: 'Evaluación del autismo', url: '/es/library/evaluacion' }); const arabic = record({ locale: 'ar', title: 'دليل التَّـوحُّد', url: '/ar/library/guide' }); assert.equal(searchLibrary([spanish], 'evaluacion')[0].record.title, 'Evaluación del autismo'); assert.equal(searchLibrary([arabic], 'التوحد')[0].record.title, 'دليل التَّـوحُّد'); }); test('equal search scores use publication date then normalized route', () => { const records = [ record({ url: '/library/z', title: 'Shared title', publishedAt: '2025-01-01' }), record({ url: '/library/b', title: 'Shared title', publishedAt: '2026-01-01' }), record({ url: '/library/a', title: 'Shared title', publishedAt: '2026-01-01' }) ]; assert.deepEqual(searchLibrary(records, 'shared').map(({ record: item }) => item.url), ['/library/a', '/library/b', '/library/z']); }); test('legacy query parsing gives term precedence, preserves repeated types, and normalizes offset/language', () => { assert.deepEqual(parseLegacyLibrarySearchState('?term=autism&q=ignored&type=BLOG_POST&type=SITE_PAGE&lang=es&offset=20'), { query: 'autism', language: 'es', offset: 20, types: ['BLOG_POST', 'SITE_PAGE'] }); assert.deepEqual(parseLegacyLibrarySearchState('?q=%3Cscript%3E&lang=unknown&offset=-10'), { query: '