Add unit tests for ANSI-to-HTML converter
This commit is contained in:
parent
5bc2d4fe1b
commit
a90da23cbc
1 changed files with 426 additions and 0 deletions
426
src/ansi.test.ts
Normal file
426
src/ansi.test.ts
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
import { expect, test } from "bun:test";
|
||||
import { ansiToHtml, trimLineEndPreserveAnsi } from "./ansi";
|
||||
|
||||
// 1. XSS/HTML escaping tests
|
||||
|
||||
test("escapes < to <", () => {
|
||||
const result = ansiToHtml("<div>");
|
||||
expect(result).toBe("<div>");
|
||||
});
|
||||
|
||||
test("escapes > to >", () => {
|
||||
const result = ansiToHtml("a > b");
|
||||
expect(result).toBe("a > b");
|
||||
});
|
||||
|
||||
test("escapes & to &", () => {
|
||||
const result = ansiToHtml("foo & bar");
|
||||
expect(result).toBe("foo & bar");
|
||||
});
|
||||
|
||||
test("escapes <script> tags", () => {
|
||||
const result = ansiToHtml("<script>alert('xss')</script>");
|
||||
expect(result).toBe("<script>alert('xss')</script>");
|
||||
});
|
||||
|
||||
test("escapes HTML in styled text", () => {
|
||||
const result = ansiToHtml("\x1b[31m<script>alert(1)</script>\x1b[0m");
|
||||
expect(result).toContain("<script>");
|
||||
expect(result).toContain("</script>");
|
||||
expect(result).not.toContain("<script>");
|
||||
});
|
||||
|
||||
// 2. Basic 16 colors tests
|
||||
|
||||
test("red foreground (31)", () => {
|
||||
const result = ansiToHtml("\x1b[31mred\x1b[0m");
|
||||
expect(result).toBe('<span style="color:#f85149">red</span>');
|
||||
});
|
||||
|
||||
test("green foreground (32)", () => {
|
||||
const result = ansiToHtml("\x1b[32mgreen\x1b[0m");
|
||||
expect(result).toBe('<span style="color:#3fb950">green</span>');
|
||||
});
|
||||
|
||||
test("blue foreground (34)", () => {
|
||||
const result = ansiToHtml("\x1b[34mblue\x1b[0m");
|
||||
expect(result).toBe('<span style="color:#58a6ff">blue</span>');
|
||||
});
|
||||
|
||||
test("yellow foreground (33)", () => {
|
||||
const result = ansiToHtml("\x1b[33myellow\x1b[0m");
|
||||
expect(result).toBe('<span style="color:#d29922">yellow</span>');
|
||||
});
|
||||
|
||||
test("bright red foreground (91)", () => {
|
||||
const result = ansiToHtml("\x1b[91mbright red\x1b[0m");
|
||||
expect(result).toBe('<span style="color:#ff7b72">bright red</span>');
|
||||
});
|
||||
|
||||
test("bright green foreground (92)", () => {
|
||||
const result = ansiToHtml("\x1b[92mbright green\x1b[0m");
|
||||
expect(result).toBe('<span style="color:#7ee787">bright green</span>');
|
||||
});
|
||||
|
||||
test("red background (41)", () => {
|
||||
const result = ansiToHtml("\x1b[41mred bg\x1b[0m");
|
||||
expect(result).toBe('<span style="background-color:#f85149">red bg</span>');
|
||||
});
|
||||
|
||||
test("green background (42)", () => {
|
||||
const result = ansiToHtml("\x1b[42mgreen bg\x1b[0m");
|
||||
expect(result).toBe('<span style="background-color:#3fb950">green bg</span>');
|
||||
});
|
||||
|
||||
test("bright blue background (104)", () => {
|
||||
const result = ansiToHtml("\x1b[104mbright blue bg\x1b[0m");
|
||||
expect(result).toBe(
|
||||
'<span style="background-color:#79c0ff">bright blue bg</span>',
|
||||
);
|
||||
});
|
||||
|
||||
test("foreground and background together", () => {
|
||||
const result = ansiToHtml("\x1b[31;42mred on green\x1b[0m");
|
||||
expect(result).toContain("color:#f85149");
|
||||
expect(result).toContain("background-color:#3fb950");
|
||||
});
|
||||
|
||||
// 3. 256-color palette tests
|
||||
|
||||
test("256-color basic colors (0-15): red", () => {
|
||||
const result = ansiToHtml("\x1b[38;5;1mred\x1b[0m");
|
||||
expect(result).toContain("color:#cd3131");
|
||||
});
|
||||
|
||||
test("256-color basic colors (0-15): bright white", () => {
|
||||
const result = ansiToHtml("\x1b[38;5;15mwhite\x1b[0m");
|
||||
expect(result).toContain("color:#ffffff");
|
||||
});
|
||||
|
||||
test("256-color 6x6x6 cube: first color (16)", () => {
|
||||
const result = ansiToHtml("\x1b[38;5;16mcolor\x1b[0m");
|
||||
expect(result).toContain("rgb(0,0,0)");
|
||||
});
|
||||
|
||||
test("256-color 6x6x6 cube: mid-range color", () => {
|
||||
// Color 196 = index 180 (196-16) in cube
|
||||
// ri=5, gi=0, bi=0 -> r=255, g=0, b=0
|
||||
const result = ansiToHtml("\x1b[38;5;196mred\x1b[0m");
|
||||
expect(result).toContain("rgb(255,0,0)");
|
||||
});
|
||||
|
||||
test("256-color grayscale ramp (232-255): dark gray", () => {
|
||||
// Color 232: gray = (232-232)*10+8 = 8
|
||||
const result = ansiToHtml("\x1b[38;5;232mgray\x1b[0m");
|
||||
expect(result).toContain("rgb(8,8,8)");
|
||||
});
|
||||
|
||||
test("256-color grayscale ramp (232-255): light gray", () => {
|
||||
// Color 255: gray = (255-232)*10+8 = 238
|
||||
const result = ansiToHtml("\x1b[38;5;255mgray\x1b[0m");
|
||||
expect(result).toContain("rgb(238,238,238)");
|
||||
});
|
||||
|
||||
test("256-color background", () => {
|
||||
const result = ansiToHtml("\x1b[48;5;21mbg\x1b[0m");
|
||||
expect(result).toContain("background-color");
|
||||
});
|
||||
|
||||
// 4. 24-bit RGB tests
|
||||
|
||||
test("24-bit RGB foreground", () => {
|
||||
const result = ansiToHtml("\x1b[38;2;255;128;64mrgb\x1b[0m");
|
||||
expect(result).toBe('<span style="color:rgb(255,128,64)">rgb</span>');
|
||||
});
|
||||
|
||||
test("24-bit RGB background", () => {
|
||||
const result = ansiToHtml("\x1b[48;2;100;150;200mbg\x1b[0m");
|
||||
expect(result).toBe(
|
||||
'<span style="background-color:rgb(100,150,200)">bg</span>',
|
||||
);
|
||||
});
|
||||
|
||||
test("24-bit RGB foreground and background", () => {
|
||||
const result = ansiToHtml("\x1b[38;2;255;0;0;48;2;0;255;0mtext\x1b[0m");
|
||||
expect(result).toContain("color:rgb(255,0,0)");
|
||||
expect(result).toContain("background-color:rgb(0,255,0)");
|
||||
});
|
||||
|
||||
// 5. Text attributes tests
|
||||
|
||||
test("bold (1)", () => {
|
||||
const result = ansiToHtml("\x1b[1mbold\x1b[0m");
|
||||
expect(result).toBe('<span style="font-weight:bold">bold</span>');
|
||||
});
|
||||
|
||||
test("dim (2)", () => {
|
||||
const result = ansiToHtml("\x1b[2mdim\x1b[0m");
|
||||
expect(result).toBe('<span style="opacity:0.5">dim</span>');
|
||||
});
|
||||
|
||||
test("italic (3)", () => {
|
||||
const result = ansiToHtml("\x1b[3mitalic\x1b[0m");
|
||||
expect(result).toBe('<span style="font-style:italic">italic</span>');
|
||||
});
|
||||
|
||||
test("underline (4)", () => {
|
||||
const result = ansiToHtml("\x1b[4munderline\x1b[0m");
|
||||
expect(result).toBe(
|
||||
'<span style="text-decoration:underline">underline</span>',
|
||||
);
|
||||
});
|
||||
|
||||
test("inverse (7) swaps foreground and background", () => {
|
||||
const result = ansiToHtml("\x1b[7minverse\x1b[0m");
|
||||
expect(result).toContain("color:#ffffff");
|
||||
expect(result).toContain("background-color:#0d1117");
|
||||
});
|
||||
|
||||
test("inverse with colors swaps them", () => {
|
||||
const result = ansiToHtml("\x1b[31;42;7minverse\x1b[0m");
|
||||
// Original: fg=#f85149, bg=#3fb950
|
||||
// Swapped: fg=#3fb950, bg=#f85149
|
||||
expect(result).toContain("color:#3fb950");
|
||||
expect(result).toContain("background-color:#f85149");
|
||||
});
|
||||
|
||||
test("multiple attributes combined", () => {
|
||||
const result = ansiToHtml("\x1b[1;3;4mtext\x1b[0m");
|
||||
expect(result).toContain("font-weight:bold");
|
||||
expect(result).toContain("font-style:italic");
|
||||
expect(result).toContain("text-decoration:underline");
|
||||
});
|
||||
|
||||
test("bold and color", () => {
|
||||
const result = ansiToHtml("\x1b[1;31mbold red\x1b[0m");
|
||||
expect(result).toContain("font-weight:bold");
|
||||
expect(result).toContain("color:#f85149");
|
||||
});
|
||||
|
||||
// 6. Reset codes tests
|
||||
|
||||
test("reset (0) clears all styles", () => {
|
||||
const result = ansiToHtml("\x1b[1;31;42mbold red on green\x1b[0mplain");
|
||||
expect(result).toBe(
|
||||
'<span style="color:#f85149;background-color:#3fb950;font-weight:bold">bold red on green</span>plain',
|
||||
);
|
||||
});
|
||||
|
||||
test("reset bold/dim (22)", () => {
|
||||
const result = ansiToHtml("\x1b[1mbold\x1b[22mnormal\x1b[0m");
|
||||
expect(result).toBe('<span style="font-weight:bold">bold</span>normal');
|
||||
});
|
||||
|
||||
test("reset italic (23)", () => {
|
||||
const result = ansiToHtml("\x1b[3mitalic\x1b[23mnormal\x1b[0m");
|
||||
expect(result).toBe('<span style="font-style:italic">italic</span>normal');
|
||||
});
|
||||
|
||||
test("reset underline (24)", () => {
|
||||
const result = ansiToHtml("\x1b[4munderline\x1b[24mnormal\x1b[0m");
|
||||
expect(result).toBe(
|
||||
'<span style="text-decoration:underline">underline</span>normal',
|
||||
);
|
||||
});
|
||||
|
||||
test("reset inverse (27)", () => {
|
||||
const result = ansiToHtml("\x1b[7minverse\x1b[27mnormal\x1b[0m");
|
||||
expect(result).toBe(
|
||||
'<span style="color:#ffffff;background-color:#0d1117">inverse</span>normal',
|
||||
);
|
||||
});
|
||||
|
||||
test("reset foreground (39)", () => {
|
||||
const result = ansiToHtml("\x1b[31mred\x1b[39mdefault\x1b[0m");
|
||||
expect(result).toBe('<span style="color:#f85149">red</span>default');
|
||||
});
|
||||
|
||||
test("reset background (49)", () => {
|
||||
const result = ansiToHtml("\x1b[41mred bg\x1b[49mdefault\x1b[0m");
|
||||
expect(result).toBe(
|
||||
'<span style="background-color:#f85149">red bg</span>default',
|
||||
);
|
||||
});
|
||||
|
||||
// 7. Multiple codes in one sequence tests
|
||||
|
||||
test("bold and red in one sequence", () => {
|
||||
const result = ansiToHtml("\x1b[1;31mbold red\x1b[0m");
|
||||
expect(result).toContain("font-weight:bold");
|
||||
expect(result).toContain("color:#f85149");
|
||||
});
|
||||
|
||||
test("multiple attributes in one sequence", () => {
|
||||
const result = ansiToHtml("\x1b[1;3;4;31;42mtext\x1b[0m");
|
||||
expect(result).toContain("font-weight:bold");
|
||||
expect(result).toContain("font-style:italic");
|
||||
expect(result).toContain("text-decoration:underline");
|
||||
expect(result).toContain("color:#f85149");
|
||||
expect(result).toContain("background-color:#3fb950");
|
||||
});
|
||||
|
||||
test("empty sequence defaults to reset", () => {
|
||||
const result = ansiToHtml("\x1b[31mred\x1b[mdefault\x1b[0m");
|
||||
expect(result).toBe('<span style="color:#f85149">red</span>default');
|
||||
});
|
||||
|
||||
// 8. Newline handling tests
|
||||
|
||||
test("newline closes and reopens spans", () => {
|
||||
const result = ansiToHtml("\x1b[31mred\nstill red\x1b[0m");
|
||||
expect(result).toBe(
|
||||
'<span style="color:#f85149">red</span>\n<span style="color:#f85149">still red</span>',
|
||||
);
|
||||
});
|
||||
|
||||
test("newline without styling", () => {
|
||||
const result = ansiToHtml("line1\nline2");
|
||||
expect(result).toBe("line1\nline2");
|
||||
});
|
||||
|
||||
test("multiple newlines with styling", () => {
|
||||
const result = ansiToHtml("\x1b[31mred\n\nstill red\x1b[0m");
|
||||
expect(result).toBe(
|
||||
'<span style="color:#f85149">red</span>\n<span style="color:#f85149"></span>\n<span style="color:#f85149">still red</span>',
|
||||
);
|
||||
});
|
||||
|
||||
test("carriage return is stripped", () => {
|
||||
const result = ansiToHtml("line1\r\nline2");
|
||||
expect(result).toBe("line1\nline2");
|
||||
});
|
||||
|
||||
test("standalone carriage return is stripped", () => {
|
||||
const result = ansiToHtml("text\rmore");
|
||||
expect(result).toBe("textmore");
|
||||
});
|
||||
|
||||
// 9. trimLineEndPreserveAnsi tests
|
||||
|
||||
test("trimLineEndPreserveAnsi trims trailing spaces", () => {
|
||||
const result = trimLineEndPreserveAnsi("hello world ");
|
||||
expect(result).toBe("hello world");
|
||||
});
|
||||
|
||||
test("trimLineEndPreserveAnsi preserves content", () => {
|
||||
const result = trimLineEndPreserveAnsi("no trailing spaces");
|
||||
expect(result).toBe("no trailing spaces");
|
||||
});
|
||||
|
||||
test("trimLineEndPreserveAnsi preserves trailing ANSI reset", () => {
|
||||
const result = trimLineEndPreserveAnsi("text \x1b[0m");
|
||||
expect(result).toBe("text\x1b[0m");
|
||||
});
|
||||
|
||||
test("trimLineEndPreserveAnsi preserves trailing ANSI color", () => {
|
||||
const result = trimLineEndPreserveAnsi("text \x1b[31m");
|
||||
expect(result).toBe("text\x1b[31m");
|
||||
});
|
||||
|
||||
test("trimLineEndPreserveAnsi handles lines without ANSI codes", () => {
|
||||
const result = trimLineEndPreserveAnsi("plain text ");
|
||||
expect(result).toBe("plain text");
|
||||
});
|
||||
|
||||
test("trimLineEndPreserveAnsi handles empty string", () => {
|
||||
const result = trimLineEndPreserveAnsi("");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
test("trimLineEndPreserveAnsi handles only spaces", () => {
|
||||
const result = trimLineEndPreserveAnsi(" ");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
test("trimLineEndPreserveAnsi preserves multiple trailing ANSI sequences", () => {
|
||||
const result = trimLineEndPreserveAnsi("text \x1b[0m\x1b[31m");
|
||||
expect(result).toBe("text\x1b[0m\x1b[31m");
|
||||
});
|
||||
|
||||
test("ansiToHtml uses trimLineEndPreserveAnsi on each line", () => {
|
||||
const result = ansiToHtml("\x1b[41mtext \x1b[0m\nmore ");
|
||||
// Trailing spaces should be trimmed
|
||||
expect(result).not.toContain("text </span>");
|
||||
expect(result).not.toContain("more ");
|
||||
});
|
||||
|
||||
// 10. Edge cases tests
|
||||
|
||||
test("empty string returns empty", () => {
|
||||
const result = ansiToHtml("");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
test("no ANSI codes (plain text)", () => {
|
||||
const result = ansiToHtml("plain text");
|
||||
expect(result).toBe("plain text");
|
||||
});
|
||||
|
||||
test("malformed ANSI sequence (incomplete)", () => {
|
||||
const result = ansiToHtml("\x1b[31incomplete");
|
||||
// Incomplete sequence consumes the 3, but outputs the rest
|
||||
expect(result).toContain("ncomplete");
|
||||
});
|
||||
|
||||
test("ANSI sequence without parameters", () => {
|
||||
const result = ansiToHtml("\x1b[mtext");
|
||||
expect(result).toBe("text");
|
||||
});
|
||||
|
||||
test("unknown ANSI codes are ignored", () => {
|
||||
const result = ansiToHtml("\x1b[999mtext\x1b[0m");
|
||||
expect(result).toBe("text");
|
||||
});
|
||||
|
||||
test("non-SGR escape sequences are skipped", () => {
|
||||
// Cursor positioning sequences like ESC[H should be ignored
|
||||
const result = ansiToHtml("\x1b[Htext");
|
||||
expect(result).toBe("text");
|
||||
});
|
||||
|
||||
test("consecutive ANSI codes", () => {
|
||||
const result = ansiToHtml("\x1b[31m\x1b[1mred bold\x1b[0m");
|
||||
expect(result).toContain("color:#f85149");
|
||||
expect(result).toContain("font-weight:bold");
|
||||
});
|
||||
|
||||
test("ANSI code at end without text", () => {
|
||||
const result = ansiToHtml("text\x1b[31m");
|
||||
// Opens a span even though there's no text after
|
||||
expect(result).toBe('text<span style="color:#f85149"></span>');
|
||||
});
|
||||
|
||||
test("ANSI code at start", () => {
|
||||
const result = ansiToHtml("\x1b[31mtext");
|
||||
expect(result).toBe('<span style="color:#f85149">text</span>');
|
||||
});
|
||||
|
||||
test("multiple resets", () => {
|
||||
const result = ansiToHtml("\x1b[31mred\x1b[0m\x1b[0m\x1b[0m");
|
||||
expect(result).toBe('<span style="color:#f85149">red</span>');
|
||||
});
|
||||
|
||||
test("style change without reset", () => {
|
||||
const result = ansiToHtml("\x1b[31mred\x1b[32mgreen");
|
||||
expect(result).toBe(
|
||||
'<span style="color:#f85149">red</span><span style="color:#3fb950">green</span>',
|
||||
);
|
||||
});
|
||||
|
||||
test("mixed escaped characters and ANSI codes", () => {
|
||||
const result = ansiToHtml("\x1b[31m<script>&alert</script>\x1b[0m");
|
||||
expect(result).toContain("<script>");
|
||||
expect(result).toContain("&alert");
|
||||
expect(result).toContain("color:#f85149");
|
||||
});
|
||||
|
||||
test("preserves internal spaces", () => {
|
||||
const result = ansiToHtml("hello world");
|
||||
expect(result).toBe("hello world");
|
||||
});
|
||||
|
||||
test("handles only ANSI codes with no text", () => {
|
||||
const result = ansiToHtml("\x1b[31m\x1b[0m");
|
||||
// Opens and closes span even with no text
|
||||
expect(result).toBe('<span style="color:#f85149"></span>');
|
||||
});
|
||||
Loading…
Reference in a new issue