2026-08-08 18:48:41 -07:00
|
|
|
import { describe, test, expect } from 'vitest';
|
|
|
|
|
import { formatHtml } from './format-html';
|
|
|
|
|
|
|
|
|
|
describe('formatHtml', () => {
|
|
|
|
|
test('indents nested block elements two spaces per level', () => {
|
|
|
|
|
expect(formatHtml('<div><section><p>hi</p></section></div>')).toBe(
|
|
|
|
|
'<div>\n <section>\n <p>hi</p>\n </section>\n</div>',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('leaves inline tags on the same line as their text', () => {
|
|
|
|
|
expect(formatHtml('<p>hello <strong>world</strong> now</p>')).toBe(
|
|
|
|
|
'<p>hello <strong>world</strong> now</p>',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('void elements do not open an indent level', () => {
|
|
|
|
|
expect(formatHtml('<div><img src="a.png"><br><p>x</p></div>')).toBe(
|
|
|
|
|
'<div>\n <img src="a.png">\n <br>\n <p>x</p>\n</div>',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('preserves <pre> contents verbatim', () => {
|
|
|
|
|
const src = '<div><pre> keep\n this</pre></div>';
|
|
|
|
|
expect(formatHtml(src)).toBe('<div>\n <pre> keep\n this</pre>\n</div>');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('is idempotent', () => {
|
|
|
|
|
const once = formatHtml('<div><section><p>hi</p></section></div>');
|
|
|
|
|
expect(formatHtml(once)).toBe(once);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('empty and whitespace-only input round-trip to an empty string', () => {
|
|
|
|
|
expect(formatHtml('')).toBe('');
|
|
|
|
|
expect(formatHtml(' \n ')).toBe('');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('unbalanced markup never produces negative indent', () => {
|
|
|
|
|
expect(formatHtml('</div><p>x</p>')).toBe('</div>\n<p>x</p>');
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-08-08 18:59:57 -07:00
|
|
|
|
|
|
|
|
// Regression coverage from code review: only <pre> was originally exempted
|
|
|
|
|
// from the naive '<'/'>' tag-boundary scan, which let a '>' inside a quoted
|
|
|
|
|
// attribute value corrupt output, and let '<'/'>' inside <script>/<style>
|
|
|
|
|
// content be misparsed as tag boundaries.
|
|
|
|
|
describe('formatHtml - raw content and quoted attributes', () => {
|
|
|
|
|
test('a ">" inside a quoted attribute value does not split the tag', () => {
|
|
|
|
|
const src = '<div title="a>b"><p>hi</p></div><footer>bye</footer>';
|
|
|
|
|
expect(formatHtml(src)).toBe(
|
|
|
|
|
'<div title="a>b">\n <p>hi</p>\n</div>\n<footer>bye</footer>',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('"<" and ">" inside <script> content do not desync sibling nesting', () => {
|
|
|
|
|
const src = '<div><script>a < b;</script></div><div><script>c < d;</script></div>';
|
|
|
|
|
expect(formatHtml(src)).toBe(
|
|
|
|
|
'<div>\n <script>a < b;</script>\n</div>\n<div>\n <script>c < d;</script>\n</div>',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a CSS child combinator inside <style> content is not treated as markup', () => {
|
|
|
|
|
const src = '<div><style>div > p { color: red; }</style></div>';
|
|
|
|
|
expect(formatHtml(src)).toBe(
|
|
|
|
|
'<div>\n <style>div > p { color: red; }</style>\n</div>',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('is idempotent across a quoted ">" attribute and <script> content', () => {
|
|
|
|
|
const quotedAttr = formatHtml('<div title="a>b"><p>hi</p></div><footer>bye</footer>');
|
|
|
|
|
expect(formatHtml(quotedAttr)).toBe(quotedAttr);
|
|
|
|
|
|
|
|
|
|
const scriptSrc = '<div><script>a < b;</script></div><div><script>c < d;</script></div>';
|
|
|
|
|
const scripted = formatHtml(scriptSrc);
|
|
|
|
|
expect(formatHtml(scripted)).toBe(scripted);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('an unclosed <pre> is swallowed verbatim to the end of the document', () => {
|
|
|
|
|
expect(formatHtml('<div><pre>no closing tag here')).toBe(
|
|
|
|
|
'<div>\n <pre>no closing tag here',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|