Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
www
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
PLN
www
Commits
bc875d7f
Commit
bc875d7f
authored
Oct 07, 2026
by
PLN (Algolia)
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
test: article anatomy — heading ids, TOC, margin clones, neighbors
parent
12387c68
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
234 additions
and
1 deletion
+234
-1
blog-anatomy.test.js
tests/unit/blog-anatomy.test.js
+232
-0
blog-drafts.test.js
tests/unit/blog-drafts.test.js
+2
-1
No files found.
tests/unit/blog-anatomy.test.js
0 → 100644
View file @
bc875d7f
import
fs
from
"fs"
;
import
path
from
"path"
;
import
{
rehypeHeadingIds
,
rehypeMarginNotes
,
buildTocFromHtml
}
from
"@/lib/markdown"
;
import
{
getBlogNeighbors
}
from
"@/lib/blog"
;
// The unified chain is ESM-only and does not load under jest (see the note in
// blog-drafts.test.js). These tests exercise the rehype plugins directly on
// hand-built hast trees, so the ESM packages are stubbed at the module gate.
// The module builds its processor at import time — give it a chainable stub.
jest
.
mock
(
"unified"
,
()
=>
({
unified
:
()
=>
{
const
chain
=
{
use
:
()
=>
chain
};
return
chain
;
},
}));
jest
.
mock
(
"remark-parse"
,
()
=>
({}));
jest
.
mock
(
"remark-gfm"
,
()
=>
({}));
jest
.
mock
(
"remark-directive"
,
()
=>
({}));
jest
.
mock
(
"remark-rehype"
,
()
=>
({}));
jest
.
mock
(
"rehype-pretty-code"
,
()
=>
({}));
jest
.
mock
(
"rehype-stringify"
,
()
=>
({}));
// A faithful-enough stand-in for the real traversal: elements first, then
// their children, with in-place replacement semantics matching unist visit.
jest
.
mock
(
"unist-util-visit"
,
()
=>
({
visit
:
(
tree
,
_type
,
fn
)
=>
{
const
walk
=
(
node
,
index
,
parent
)
=>
{
if
(
node
.
type
===
"element"
)
fn
(
node
,
index
,
parent
);
node
.
children
?.
forEach
((
child
,
i
)
=>
walk
(
child
,
i
,
node
));
};
walk
(
tree
,
null
,
null
);
},
}));
const
el
=
(
tagName
,
properties
=
{},
children
=
[])
=>
({
type
:
"element"
,
tagName
,
properties
,
children
,
});
const
text
=
(
value
)
=>
({
type
:
"text"
,
value
});
/**
* Article anatomy — the build-time pieces of the article page: heading
* anchors, TOC extraction, footnote margin clones, and date-ordered
* neighbours.
*/
describe
(
"rehypeHeadingIds"
,
()
=>
{
it
(
"mints GitHub-style ids on h1–h4, folding diacritics and inline markup"
,
()
=>
{
const
tree
=
{
type
:
"root"
,
children
:
[
el
(
"h2"
,
{},
[
text
(
"Résumé & méthode: "
),
el
(
"em"
,
{},
[
text
(
"le plan"
)])]),
el
(
"h3"
,
{},
[
text
(
"Deep dive"
)]),
el
(
"h4"
,
{},
[
text
(
"Too deep"
)]),
],
};
rehypeHeadingIds
()(
tree
);
expect
(
tree
.
children
.
map
((
n
)
=>
n
.
properties
.
id
)).
toEqual
([
"resume-methode-le-plan"
,
"deep-dive"
,
"too-deep"
,
]);
});
it
(
"dedupes repeated heading slugs"
,
()
=>
{
const
tree
=
{
type
:
"root"
,
children
:
[
el
(
"h2"
,
{},
[
text
(
"Notes"
)]),
el
(
"h2"
,
{},
[
text
(
"Notes"
)])],
};
rehypeHeadingIds
()(
tree
);
expect
(
tree
.
children
.
map
((
n
)
=>
n
.
properties
.
id
)).
toEqual
([
"notes"
,
"notes-1"
]);
});
});
describe
(
"buildTocFromHtml"
,
()
=>
{
it
(
"reads h2/h3 back from rendered HTML, stripping inline tags and entities"
,
()
=>
{
const
html
=
'<h1 id="top">Top</h1>'
+
'<h2 id="resume-methode-le-plan">Résumé & méthode: <em>le plan</em></h2>'
+
'<h3 id="deep-dive">Deep <code>dive</code></h3>'
+
'<h4 id="too-deep">Too deep</h4>'
+
'<h2 id="deuxieme-section">Deuxième & dernière</h2>'
;
expect
(
buildTocFromHtml
(
html
)).
toEqual
([
{
id
:
"resume-methode-le-plan"
,
text
:
"Résumé & méthode: le plan"
,
depth
:
2
},
{
id
:
"deep-dive"
,
text
:
"Deep dive"
,
depth
:
3
},
{
id
:
"deuxieme-section"
,
text
:
"Deuxième & dernière"
,
depth
:
2
},
]);
});
it
(
"returns an empty TOC for posts without h2/h3"
,
()
=>
{
expect
(
buildTocFromHtml
(
"<p>Only paragraphs.</p>"
)).
toEqual
([]);
});
});
describe
(
"rehypeMarginNotes"
,
()
=>
{
const
refSup
=
(
noteId
,
refId
)
=>
el
(
"sup"
,
{},
[
el
(
"a"
,
{
href
:
`#user-content-fn-
${
noteId
}
`
,
id
:
`user-content-fnref-
${
refId
}
`
,
dataFootnoteRef
:
true
,
},
[
text
(
String
(
noteId
))]),
]);
const
defLi
=
(
noteId
,
backrefId
)
=>
el
(
"li"
,
{
id
:
`user-content-fn-
${
noteId
}
`
},
[
el
(
"p"
,
{},
[
text
(
"First note with a "
),
el
(
"a"
,
{
href
:
"https://example.com"
},
[
text
(
"link"
)]),
text
(
". "
),
el
(
"a"
,
{
href
:
`#user-content-fnref-
${
backrefId
}
`
,
dataFootnoteBackref
:
""
},
[
text
(
"↩"
)]),
]),
el
(
"ul"
,
{},
[
el
(
"li"
,
{},
[
text
(
"alpha"
)])]),
]);
const
treeWith
=
(
sups
,
defs
)
=>
({
type
:
"root"
,
children
:
[
el
(
"p"
,
{},
[
text
(
"Para with "
),
...
sups
,
text
(
"."
)]),
el
(
"section"
,
{
dataFootnotes
:
true
},
[
el
(
"ol"
,
{},
defs
)]),
],
});
it
(
"wraps each reference with an aria-hidden inline clone of its definition"
,
()
=>
{
const
sup
=
refSup
(
1
,
1
);
const
tree
=
treeWith
([
sup
],
[
defLi
(
1
,
1
)]);
rehypeMarginNotes
()(
tree
);
const
wrapper
=
tree
.
children
[
0
].
children
[
1
];
expect
(
wrapper
.
tagName
).
toBe
(
"span"
);
expect
(
wrapper
.
properties
.
className
).
toEqual
([
"prose-fn"
]);
expect
(
wrapper
.
children
[
0
]).
toBe
(
sup
);
// the original ref stays clickable
const
body
=
wrapper
.
children
[
1
];
expect
(
body
.
tagName
).
toBe
(
"span"
);
expect
(
body
.
properties
.
className
).
toEqual
([
"prose-fn-body"
]);
expect
(
body
.
properties
.
ariaHidden
).
toBe
(
"true"
);
// Clone content: p → span block, link survives, backref dropped,
// ul → block with item spans.
const
[
block
,
list
]
=
body
.
children
;
expect
(
block
.
tagName
).
toBe
(
"span"
);
expect
(
block
.
properties
.
className
).
toEqual
([
"prose-fn-block"
]);
const
serialized
=
JSON
.
stringify
(
block
);
expect
(
serialized
).
toContain
(
"https://example.com"
);
expect
(
serialized
).
not
.
toContain
(
"dataFootnoteBackref"
);
expect
(
list
.
children
[
0
].
properties
.
className
).
toEqual
([
"prose-fn-item"
]);
expect
(
JSON
.
stringify
(
list
)).
toContain
(
"alpha"
);
});
it
(
"clones once per reference, even to the same note"
,
()
=>
{
const
tree
=
treeWith
([
refSup
(
1
,
1
),
refSup
(
1
,
"1-2"
)],
[
defLi
(
1
,
1
)]);
rehypeMarginNotes
()(
tree
);
const
wrappers
=
tree
.
children
[
0
].
children
.
filter
(
(
n
)
=>
n
.
type
===
"element"
&&
n
.
properties
?.
className
?.
includes
(
"prose-fn"
),
);
expect
(
wrappers
).
toHaveLength
(
2
);
for
(
const
wrapper
of
wrappers
)
expect
(
wrapper
.
children
[
0
].
tagName
).
toBe
(
"sup"
);
});
it
(
"leaves the endnote section untouched as the accessible source"
,
()
=>
{
const
tree
=
treeWith
([
refSup
(
1
,
1
)],
[
defLi
(
1
,
1
)]);
rehypeMarginNotes
()(
tree
);
expect
(
tree
.
children
[
1
].
children
[
0
].
children
[
0
]).
toMatchObject
({
tagName
:
"li"
,
properties
:
{
id
:
"user-content-fn-1"
},
});
});
});
describe
(
"getBlogNeighbors — prev/next by date"
,
()
=>
{
/**
* A deterministic trio with far-out dates, so assertions hold regardless
* of the other suites' fixtures. Removed with the suite.
*/
const
DIR
=
path
.
join
(
process
.
cwd
(),
"content"
,
"blog"
);
const
TRIO
=
[
[
"_fixture-neighbors-alpha"
,
"2046-01-01"
,
true
],
[
"_fixture-neighbors-beta"
,
"2045-01-01"
,
true
],
[
"_fixture-neighbors-gamma"
,
"2044-01-01"
,
false
],
// fr-only: the fallback case
];
beforeAll
(()
=>
{
for
(
const
[
slug
,
date
,
bilingual
]
of
TRIO
)
{
const
dir
=
path
.
join
(
DIR
,
slug
);
fs
.
mkdirSync
(
dir
,
{
recursive
:
true
});
fs
.
writeFileSync
(
path
.
join
(
dir
,
"meta.md"
),
`---\ntitle:
${
slug
.
replace
(
"_fixture-neighbors-"
,
""
)}
\ndate: '
${
date
}
'\ndraft: false\n---\n`
,
);
fs
.
writeFileSync
(
path
.
join
(
dir
,
"fr.md"
),
"---
\
ntitle: x
\
n---
\
n
\
nbody
\
n"
);
if
(
bilingual
)
{
fs
.
writeFileSync
(
path
.
join
(
dir
,
"en.md"
),
"---
\
ntitle: x
\
n---
\
n
\
nbody
\
n"
);
}
}
});
afterAll
(()
=>
{
for
(
const
[
slug
]
of
TRIO
)
fs
.
rmSync
(
path
.
join
(
DIR
,
slug
),
{
recursive
:
true
,
force
:
true
});
});
it
(
"orders older/newer around the current post (newest-first listing)"
,
()
=>
{
const
{
older
,
newer
}
=
getBlogNeighbors
(
"_fixture-neighbors-alpha"
);
expect
(
newer
).
toBeNull
();
// alpha is the newest post on disk
expect
(
older
).
toMatchObject
({
slug
:
"_fixture-neighbors-beta"
});
});
it
(
"links both sides for a middle post"
,
()
=>
{
const
{
older
,
newer
}
=
getBlogNeighbors
(
"_fixture-neighbors-beta"
);
expect
(
newer
).
toMatchObject
({
slug
:
"_fixture-neighbors-alpha"
});
expect
(
older
).
toMatchObject
({
slug
:
"_fixture-neighbors-gamma"
});
});
it
(
"points at the requested language when the target post has it"
,
()
=>
{
const
{
newer
}
=
getBlogNeighbors
(
"_fixture-neighbors-beta"
,
"en"
);
expect
(
newer
).
toMatchObject
({
slug
:
"_fixture-neighbors-alpha"
,
href
:
"/blog/_fixture-neighbors-alpha/en"
,
});
});
it
(
"falls back to the canonical URL when the target lacks the language"
,
()
=>
{
// gamma is fr-only; an en reader still lands on its canonical post.
const
{
older
}
=
getBlogNeighbors
(
"_fixture-neighbors-beta"
,
"en"
);
expect
(
older
).
toMatchObject
({
slug
:
"_fixture-neighbors-gamma"
,
href
:
"/blog/_fixture-neighbors-gamma"
});
});
it
(
"returns nulls for slugs outside the visible listing"
,
()
=>
{
expect
(
getBlogNeighbors
(
"no-such-post"
)).
toEqual
({
older
:
null
,
newer
:
null
});
});
});
tests/unit/blog-drafts.test.js
View file @
bc875d7f
...
...
@@ -17,9 +17,10 @@ import { parseShareId, shareTokenOf, getDraftPost } from "@/lib/content";
// The unified/shiki markdown chain is ESM-only and does not load under jest
// (see jest.config.js note); these tests exercise the folder accessors, not
// rendering, so stub the renderer.
// rendering, so stub the renderer
and its TOC read-back
.
jest
.
mock
(
"@/lib/markdown"
,
()
=>
({
renderMarkdown
:
jest
.
fn
(
async
(
source
)
=>
source
),
buildTocFromHtml
:
jest
.
fn
(()
=>
[]),
}));
/**
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment