July 16, 2026
How to Test AI-Generated Markdown, Code Blocks, and Citation Links in Web UIs
A practical tutorial for testing AI-generated markdown, code blocks, and citation links in web apps, with coverage for parsing, escaping, broken links, and layout regressions.
AI-generated content looks simple until it reaches a browser. A model may emit markdown that is syntactically valid but visually awkward, code blocks with backticks inside strings, citation links that point to placeholders, or nested formatting that breaks a renderer’s assumptions. The hard part is not generating text, it is proving that the web UI renders that text safely, consistently, and in a way that users can trust.
For teams shipping chat surfaces, knowledge bases, support assistants, and content editors, how to test AI-generated markdown is really a bundle of smaller questions: does the parser preserve structure, do code blocks keep their spacing, are links valid, can the page resist injection attempts, and does the layout still work when the model produces long, unusual output? Those questions cut across frontend rendering, automated tests, security checks, and content quality review.
This tutorial focuses on practical validation patterns for AI content rendering. It assumes you already have a web UI that displays markdown, code blocks, and citation links, and you want a testing strategy that catches the regressions that matter without turning your test suite into a brittle snapshot farm.
What makes AI-generated markdown harder to test than hand-authored content
Hand-authored markdown usually comes from humans who follow a style guide, use a small set of templates, and avoid pathological syntax. AI-generated markdown behaves differently:
- It may mix paragraphs, headings, lists, tables, and blockquotes in a single response.
- It often includes code fences with language labels, inline code, or snippets that contain nested punctuation.
- It may emit malformed links, duplicated references, or citations whose target is only known after a retrieval step.
- It can be overly long, repetitive, or structurally valid but visually hostile, such as a single list item with a 4,000 character line.
That means your validation strategy needs to separate at least four layers:
- Parsing correctness, the markdown AST or HTML produced by the renderer.
- Security and escaping, whether dangerous content is neutralized.
- Visual and layout behavior, whether the browser can display the content without breaking the page.
- Data integrity, whether citation links, anchors, and code samples match the expected source material.
A common mistake is to treat markdown rendering as a text problem. In practice, it is a browser rendering problem with a security boundary attached.
Define the behavior you actually need to guarantee
Before writing tests, decide what the UI must promise. Different products need different guarantees.
Common rendering requirements
- Headings map to the expected semantic tags, usually
h1throughh6. - Paragraphs preserve line breaks according to your markdown rules.
- Lists render with correct nesting and indentation.
- Code fences preserve whitespace, monospace font, and horizontal scrolling when needed.
- Inline code is visually distinct and does not wrap unexpectedly.
- Links open the right destination and use safe attributes where required.
- Citation markers map to resolvable references.
- Unsafe HTML is escaped or stripped according to the renderer policy.
Security requirements you should make explicit
If AI output is user-visible, assume the model can emit content you do not want executed.
- Raw HTML should be sanitized unless your product explicitly allows a safe subset.
- JavaScript URLs, event handlers, and injected attributes must be blocked.
- Markdown links should be validated before rendering if they can affect navigation or tracking.
- If you support rendered HTML from markdown, verify the sanitizer configuration and not only the visual output.
A useful way to write requirements is to say what the renderer must do for each content class, for example: “render markdown text as text, allow fenced code blocks, allow ordinary HTTP(S) links, reject javascript: URLs, and preserve citation labels only when they resolve to known references.”
Build test cases from failure modes, not from syntax variety alone
Many teams start by sampling markdown syntax, headings, bold, italics, lists, tables, code fences, and links. That is necessary, but not sufficient. A stronger approach is to enumerate failure modes and write tests around them.
1. Parser and structure failures
These are cases where the renderer produces the wrong DOM structure or loses meaning.
Examples:
- Nested lists flatten incorrectly.
- A fenced block is parsed as inline code because the fence is malformed.
- Tables render, but header alignment is wrong.
- Blockquotes absorb the next paragraph due to an unclosed block.
2. Escaping and injection failures
These are cases where content is interpreted as executable or unsafe.
Examples:
- Markdown link text contains HTML entities that are decoded twice.
- A code block contains
<script>tags that leak into the DOM. - A renderer accepts raw HTML when your policy says to sanitize it.
- A citation label includes markup that should be treated as plain text.
3. Layout failures
These are cases where the content renders correctly in theory, but breaks the page in practice.
Examples:
- Long URLs cause overflow in mobile widths.
- Code blocks exceed the container and push the page horizontally.
- Large nested lists create unreadable indentation on small screens.
- Citation chips wrap awkwardly and obscure the sentence they annotate.
4. Link integrity failures
These are cases where the rendered link exists but is wrong, broken, or misleading.
Examples:
- The link target is a placeholder or stale citation ID.
- Relative links resolve against the wrong base URL.
- Footnote anchors do not match their references.
- A link exists in the DOM but is hidden behind a tooltip or expandable element.
Test the markdown renderer at two levels
A robust strategy usually needs both unit-level tests and browser-level tests.
Unit or component tests
Use these to validate the rendering pipeline before the browser gets involved. If your frontend uses a markdown library, test the conversion from markdown input to sanitized HTML or component tree.
Useful checks:
- Markdown source produces the expected node types.
- Dangerous HTML is removed or escaped.
- Link URLs are normalized or rejected according to policy.
- Code fences retain their text content exactly.
If you control the renderer, unit tests are often the cheapest place to catch regressions. They fail fast, are easier to debug, and do not require a full browser run.
Browser tests
Use these to validate the final user experience, because many bugs appear only after CSS, DOM structure, and browser layout interact.
Useful checks:
- The rendered text matches the source content.
- Code blocks scroll horizontally instead of wrapping or overflowing.
- Links are clickable and point to the expected destination.
- Visual spacing remains acceptable under long or malformed inputs.
- Mobile widths, dark mode, and zoom do not break the content area.
Browser tests are slower, but they catch the failures that users actually see. This is where test automation becomes economically justified: rendering regressions are repetitive, expensive to triage manually, and usually deterministic once you know the input set.
A practical Playwright pattern for rendered markdown
For frontends, Playwright is a reasonable default for browser-level content validation. The test does not need to assert every pixel. It should focus on semantic expectations and a few layout invariants.
import { test, expect } from '@playwright/test';
test('renders markdown, code blocks, and links safely', async ({ page }) => {
await page.goto('/preview');
await page.fill('[data-testid="prompt"]', 'show markdown sample');
await page.click('[data-testid="render"]');
await expect(page.locator(‘h2’, { hasText: ‘Example’ })).toBeVisible(); await expect(page.locator(‘pre code’)).toContainText(‘const value = 42;’); await expect(page.locator(‘a[href=”https://example.com/docs”]’)).toBeVisible(); });
That kind of test intentionally avoids brittle assertions about the exact HTML markup produced by the renderer. Instead, it checks the user-facing contract.
Add layout assertions where they matter
typescript
const codeBlock = page.locator('pre').first();
await expect(codeBlock).toHaveCSS('overflow-x', 'auto');
If your design system uses a different overflow strategy, adjust the assertion to match the policy. The point is to encode the expected behavior for long code samples, not to force every component into the same style.
Test code blocks like code, not like plain text
Code blocks are the easiest place to get false confidence. They may render visibly while still being damaged in subtle ways.
What to verify
- The exact content is preserved, including indentation and blank lines.
- Backticks inside the code sample do not terminate the fence incorrectly.
- The renderer handles language labels such as
json,bash, ortypescript. - Horizontal scrolling works for long lines.
- Copy buttons, if present, copy the raw text, not the syntax-highlighted DOM.
Common failure modes
- Leading spaces are trimmed by the markdown parser.
- Syntax highlighting wraps tokens in a way that changes copy-paste behavior.
- Long command lines overflow the viewport and hide important tokens.
- A nested triple-backtick sample needs escaping, or the renderer truncates it.
For example, if your product lets AI generate shell commands, a long one-liner can become unreadable unless code blocks scroll cleanly:
curl -X POST https://api.example.com/v1/query \
-H 'Authorization: Bearer TOKEN' \
-H 'Content-Type: application/json' \
-d '{"prompt":"summarize this","format":"markdown"}'
Your tests should cover both the rendered output and the raw clipboard content if your UI includes copy functionality.
Validate citation links separately from general hyperlinks
Citation links are often treated as normal anchors, but they deserve stricter checks. A citation can represent provenance, retrieval output, or a chain of evidence. If those links break, the user loses trust quickly.
What to validate
- Every citation marker maps to a valid reference item.
- Duplicate citations resolve consistently.
- The anchor text or citation label matches the underlying source identifier.
- The destination URL is reachable, or at least conforms to an approved pattern if offline validation is not possible.
- The rendered UI does not hide citation targets behind hover-only behavior if the product requires auditable references.
Link validation approaches
- Syntactic validation: confirm the URL parses and uses an allowed scheme.
- Referential validation: confirm the citation ID exists in the retrieved document set.
- Network validation: optionally issue HTTP checks for public URLs, with timeouts and allowlists.
The tradeoff is cost and stability. Network validation catches broken links, but it also adds slowness, external dependency, and flakiness. Many teams keep network checks in a separate job, run less frequently, or limit them to high-value pages.
For AI citation link validation, the most useful test is often not “does the internet respond right now,” but “does the UI preserve provenance faithfully from the content we already know is supposed to be there?”
Test escaping and sanitization explicitly
AI output is untrusted input. That statement is true even when the content originates from your own model, because prompt injection, retrieval contamination, and adversarial input can all influence the output.
A good test set includes payloads such as:
<script>alert(1)</script><img src=x onerror=alert(1)>[click](javascript:alert(1))- Raw HTML wrapped inside markdown paragraphs
- Broken nesting such as unclosed tags or mixed markdown and HTML
The goal is not merely to prove that the UI “looks okay.” The goal is to prove that the DOM does not contain executable surprises.
If your app sanitizes server-side, test that layer directly. If sanitization happens client-side, test the browser-rendered DOM. In both cases, verify that the allowed subset is intentional, because permissive configurations tend to expand quietly over time.
Use a fixture library of realistic AI outputs
Synthetic one-line examples are not enough. Build a fixture library of content classes that resemble real model output patterns.
A useful fixture set can include:
- A simple response with headings, bullets, and one link.
- A response with nested lists and multi-paragraph list items.
- A code-heavy response with bash, JSON, and TypeScript blocks.
- A citation-heavy response with repeated references.
- A malformed response containing unmatched fences, broken links, and raw HTML.
- A stress case with very long lines, repeated sections, and several embedded URLs.
Keep fixtures small enough that failures are readable, but broad enough to exercise the renderer’s edge cases. Store them as source-controlled markdown files so they can be reused across unit, component, and browser tests.
A minimal test matrix that is actually worth running
Do not try to test every permutation of markdown syntax against every browser and viewport. The cost explodes quickly. A practical matrix usually combines content classes with a few critical environments.
Content classes
- Short text with links
- Structured text with headings and lists
- Code-centric output
- Citation-centric output
- Adversarial or malformed output
Environments
- Desktop browser
- Mobile viewport
- Dark theme, if applicable
- One evergreen browser plus one secondary browser if your audience requires it
This is where software testing discipline matters more than raw test count. You want coverage that is justified by user risk, not by the abstract desire to enumerate every syntax corner.
Continuous integration, flakiness, and ownership economics
Markdown rendering tests are deceptively cheap to write and expensive to own if you let them sprawl.
The major cost drivers are:
- Engineering time, building fixtures, selectors, and assertions.
- CI runtime, especially if browser tests are included in every pull request.
- Debugging time, because failing renderer tests can originate in content, parser, sanitizer, CSS, or browser differences.
- Flaky-test triage, often caused by unstable waits, network checks, or animation timing.
- Onboarding cost, since new engineers need to understand both the content rules and the renderer implementation.
- Ownership concentration, if only one person understands the markdown stack.
Continuous integration helps because it makes regressions visible quickly, but it also exposes any weakness in your test isolation. If your tests depend on external URLs, random AI output, or live retrieval services, they will become harder to trust. A good rule is to separate deterministic rendering tests from slower integration checks and run them on different cadences when appropriate.
name: rendering-tests
on: [pull_request]
jobs:
browser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npm test -- --runInBand
- run: npx playwright test
That shape keeps the pipeline understandable. If the browser layer becomes too slow or unstable, the next step is usually not to delete tests, but to narrow the scope and improve the fixture discipline.
How to decide what belongs in unit tests, browser tests, or a manual review
A simple decision rule helps teams avoid over-testing low-risk details.
Put it in unit tests when
- The behavior is deterministic and isolated from browser layout.
- You want fast feedback on parsing and sanitization.
- The failure should be obvious from the source input and output.
Put it in browser tests when
- CSS, layout, or interaction behavior matters.
- The issue involves clickable links, scrolling, copy buttons, or responsive behavior.
- The content needs to be validated in real DOM conditions.
Keep a small manual review path when
- You are changing renderer policy, sanitizer rules, or allowed HTML.
- You are introducing a new content type, such as tables or callouts.
- You need product judgment on whether the rendered output is readable, not merely valid.
That manual path should be intentionally small. It is a control point, not a substitute for automation.
A good checklist for AI content rendering QA
Use this as a compact acceptance list before shipping a markdown rendering change:
- Markdown structure renders as intended across headings, lists, and paragraphs.
- Code blocks preserve whitespace, long lines, and language labels.
- Copy behavior returns raw source text, not highlighted HTML.
- Links are valid, allowed, and visually obvious.
- Citation references resolve to known targets.
- Dangerous HTML and unsafe URLs are sanitized.
- Long outputs do not break mobile layouts or container widths.
- Repeated or malformed output does not crash the renderer.
- Tests cover both semantic output and browser-visible behavior.
When a richer QA stack is justified
If your product generates content at scale, the rendering problem becomes operational, not cosmetic. A single malformed markdown response can affect support trust, legal review, documentation workflows, or any surface where users treat generated content as a source of record.
That is when a richer testing stack makes sense: fixture-based parser tests, browser rendering checks, link validation, and a small set of policy tests for sanitization. The right system is the one your team can maintain after the novelty wears off.
The core principle is straightforward: test the output the way users consume it, but keep the assertions focused on properties that matter. For AI-generated markdown, those properties are structure, safety, provenance, and layout. If your test suite covers those four areas well, it will catch the failures that cost real time in production while staying small enough to maintain.
Closing thought
Testing AI-generated markdown is less about memorizing every markdown feature and more about treating content rendering as an engineering interface. Once you do that, the shape of the solution becomes clearer: deterministic fixtures, semantic assertions, browser checks for the UI contract, and explicit validation for links and unsafe content. That combination gives frontend teams, QA engineers, and SDETs a practical way to ship rich text output without gambling on whatever the model decides to produce next.