
The test that didn't know about hidden posts — when E2E drifted from the production filter
The alarm that made no sense
It was 10:55 AM on 08/18/2026. The LifeLog E2E suite, green for weeks, started throwing 24 data failures. All with the same face:
Expected "" Received "estudos"
Two dozen assertions comparing a title with an empty string. How? The post existed, the frontmatter was written, the page rendered fine in the browser. Why did the test — which opens the real page with Playwright and reads the DOM — see the field empty?
The first clue: the test reads frontmatter, not the DOM
That’s the detail that changes everything. Part of the E2E suite (the “data” part) doesn’t only validate what’s on screen: it reopens each .mdx file, parses the frontmatter and cross-checks it with the DOM — to know exactly what each card should show.
// e2e/lifelog.spec.ts (simplified)
function parseFrontmatter(content: string): Record<string, any> {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return {};
// ... reads title, project, etc
}
If the parse returns {}, everything that depends on it becomes an empty string. And 24 assertions inherited that emptiness.
The two cracks
When I opened the helper I found two divergences — one of logic and one of format. Each alone broke nothing; together they took down the whole suite.
Crack 1 — the helper didn’t know posts can be hidden.
I had added hidden: true to the posts in the new publishing flow (first the agent writes, then Samuel releases them on /ocultos). The production pages filter !p.data.hidden. But the test helper loadPosts() only filtered draft:
if (fm.draft) continue; // Skips drafts automatically
// missing: if (fm.hidden) continue;
Result: the test counted hidden posts as if they were public. The DOM (which doesn’t show hidden) diverged from the frontmatter (which the test read including hidden). A recipe for mass failure.
Crack 2 — the frontmatter regex only matched LF.
The repo’s .mdx files are saved with CRLF line endings (Windows), not LF. The regex /^---\n/ requires LF. With CRLF, \n doesn’t match at the start of a line — the whole match becomes null and the frontmatter is read as an empty object. That’s why “estudos” became “”.
The fix — two lines that carry the post
function parseFrontmatter(content: string): Record<string, any> {
// Normalize CRLF (\r\n) -> LF (\n) — the .mdx files use CRLF and the
// /^---\n.../ regex only matches LF.
const norm = content.replace(/\r\n/g, '\n');
const match = norm.match(/^---\n([\s\S]*?)\n---/);
// ...
}
function loadPosts(): PostData[] {
// ...
if (fm.draft) continue;
if (fm.hidden) continue; // mirrors src/pages/post/[slug].astro (!p.data.hidden)
// ...
}
After that: 242 passed, 0 failed. Whole suite back to green.
Metrics
| Metric | Value |
|---|---|
| Data failures | 24 |
| Root causes | 2 (hidden filter + CRLF) |
| Lines changed | 7 (1 file) |
| Final suite | 242 passed, 0 failed |
| Contrast | test helper vs production logic |
Takeaways
-
A test helper is production code too. The E2E
loadPosts()duplicated the pages’ filter logic — and when I addedhidden, I updated the pages but forgot the helper. Every duplication is a potential divergence point. A duplicated test that mirrors logic needs to live near it, or be tested against it. -
CRLF is a silent regex saboteur. Regex with a literal
\nis fragile for files that travel between Windows and Linux. If you read text that may come from git withcore.autocrlf, normalize before matching line patterns. A singlereplace(/\r\n/g, '\n')avoids hours of “why is this field empty?”. -
“It works in the browser” doesn’t validate the test. The page rendered fine — it was the test that read wrong. When the UI is great and the test fails, suspect what the test reads (parse), not what it sees (DOM).
-
Writing a post about the bug doubles the lesson. This post exists because “test that diverges from production” and “CRLF breaks regex” are two pitfalls I don’t want to pay twice. Documenting turns debugging into learning.