verify-doc-site-fragments.spec.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. /** Tests for built-site fragment validation. */
  2. import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import { inspectSiteFragments, missingSiteFiles } from './verify-doc-site-fragments.ts'
  7. const roots: string[] = []
  8. afterEach(() => {
  9. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  10. })
  11. function fixture(): string {
  12. const root = mkdtempSync(join(tmpdir(), 'dsh-doc-fragments-'))
  13. roots.push(root)
  14. mkdirSync(join(root, 'guide'), { recursive: true })
  15. writeFileSync(join(root, 'index.html'), '<a id="home"></a><a href="/guide/start#ready">start</a>')
  16. writeFileSync(join(root, 'guide/start.html'), [
  17. '<h1 id="ready">Ready</h1>',
  18. '<a name="legacy"></a>',
  19. '<a href="#ready">same page</a>',
  20. '<a href="./start.html#legacy">html alias</a>',
  21. '<a href="../#home">root</a>',
  22. '<a href="https://example.com/page#missing">external</a>',
  23. ].join(''))
  24. return root
  25. }
  26. describe('inspectSiteFragments', () => {
  27. it('rejects a directory with no built pages', () => {
  28. const root = mkdtempSync(join(tmpdir(), 'dsh-doc-fragments-empty-'))
  29. roots.push(root)
  30. expect(() => inspectSiteFragments(root)).toThrow('no HTML files found')
  31. })
  32. it('resolves clean, encoded, and same-page routes', () => {
  33. const root = fixture()
  34. writeFileSync(
  35. join(root, 'guide/encoded.html'),
  36. '<h1 id="a b">Encoded</h1><h2 id="%">Literal</h2><a href="./encoded#a%20b">encoded</a><a href="#%">literal</a>',
  37. )
  38. expect(inspectSiteFragments(root)).toEqual({ checked: 6, broken: [] })
  39. })
  40. it('rejects ambiguous built routes', () => {
  41. const root = fixture()
  42. writeFileSync(join(root, 'guide.html'), '<h1 id="flat">Flat</h1>')
  43. writeFileSync(join(root, 'guide/index.html'), '<h1 id="index">Index</h1>')
  44. expect(() => inspectSiteFragments(root)).toThrow('share route "/guide"')
  45. })
  46. it('rejects malformed fragment hrefs', () => {
  47. const root = fixture()
  48. writeFileSync(join(root, 'guide/invalid.html'), '<a href="http://[invalid]#fragment">invalid</a>')
  49. expect(() => inspectSiteFragments(root)).toThrow(
  50. 'guide/invalid.html has invalid fragment href "http://[invalid]#fragment"',
  51. )
  52. })
  53. it('reports missing ids and missing built routes', () => {
  54. const root = fixture()
  55. writeFileSync(join(root, 'guide/broken.html'), [
  56. '<a href="./start#missing">id</a>',
  57. '<a href="./absent#missing">route</a>',
  58. ].join(''))
  59. expect(inspectSiteFragments(root).broken).toEqual([
  60. {
  61. source: 'guide/broken.html',
  62. href: './start#missing',
  63. target: 'guide/start.html',
  64. fragment: 'missing',
  65. },
  66. {
  67. source: 'guide/broken.html',
  68. href: './absent#missing',
  69. fragment: 'missing',
  70. },
  71. ])
  72. })
  73. })
  74. describe('missingSiteFiles', () => {
  75. it('reports the expected files a build did not emit', () => {
  76. const root = fixture()
  77. writeFileSync(join(root, 'guide/start.md'), '# Ready\n')
  78. expect(missingSiteFiles(root, ['guide/start.md', 'guide/absent.md', 'llms.txt']))
  79. .toEqual(['guide/absent.md', 'llms.txt'])
  80. })
  81. })