diff --git a/package-lock.json b/package-lock.json
index 0912f37..6da283c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,7 +21,6 @@
"@11ty/eleventy": "^3.1.5",
"@11ty/eleventy-navigation": "^1.0.5",
"@stylistic/eslint-plugin": "latest",
- "chalk": "^5.6.2",
"eleventy-plugin-toc": "^1.1.5",
"eslint": "latest",
"globals": "latest",
diff --git a/package.json b/package.json
index c7dfa92..bc1ec1e 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,6 @@
"@11ty/eleventy": "^3.1.5",
"@11ty/eleventy-navigation": "^1.0.5",
"@stylistic/eslint-plugin": "latest",
- "chalk": "^5.6.2",
"eleventy-plugin-toc": "^1.1.5",
"eslint": "latest",
"globals": "latest",
diff --git a/src/classes/TestResult.js b/src/classes/TestResult.js
index 3b29c9f..6a27753 100644
--- a/src/classes/TestResult.js
+++ b/src/classes/TestResult.js
@@ -1,6 +1,6 @@
import Test from "./Test.js";
import BubblingEventTarget from "./BubblingEventTarget.js";
-import { stripFormatting } from "../format-console.js";
+import { stripFormatting } from "../util/format-console.js";
import { delay, formatDuration, interceptConsole, pluralize, stringify } from "../util.js";
import { formatDiff } from "../util/format-diff.js";
@@ -358,11 +358,11 @@ ${ this.error.stack }`);
* @returns {string}
*/
getResult (o) {
- let color = this.pass ? "green" : this.skipped ? "yellow" : "red";
let label = this.pass ? "PASS" : this.skipped ? "SKIP" : "FAIL";
+ let color = label.toLowerCase();
let ret = [
- ` ${ label } `,
- `${this.name ?? "(Anonymous)"}`,
+ ` ${ label } `,
+ `${this.name ?? "(Anonymous)"}`,
].join(" ");
if (this.messages?.length > 0) {
@@ -392,11 +392,11 @@ ${ this.error.stack }`);
];
if (stats.pass > 0) {
- ret.push(`${ stats.pass }/${ stats.total } PASS`);
+ ret.push(`${ stats.pass }/${ stats.total } PASS`);
}
if (stats.fail > 0) {
- ret.push(`${ stats.fail }/${ stats.total } FAIL`);
+ ret.push(`${ stats.fail }/${ stats.total } FAIL`);
}
if (stats.pending > 0) {
@@ -431,7 +431,7 @@ ${ this.error.stack }`);
* @returns {string}
*/
getMessages (o = {}) {
- let ret = new String("(Messages)");
+ let ret = new String("(Messages)");
ret.children = this.messages.map(m => `(${ m.method }) ${ m.args.map(a => stringify(a)).join(" ") }`);
return o?.format === "rich" ? ret : stripFormatting(ret);
diff --git a/src/env/console.js b/src/env/console.js
index 987bff2..38ac6ca 100644
--- a/src/env/console.js
+++ b/src/env/console.js
@@ -1,15 +1,15 @@
-import format from "../format-console.js";
+import format from "../util/format-console.js";
function printTree (str, parent) {
if (str.children?.length > 0) {
- console["group" + (parent ? "Collapsed" : "")](format(str));
+ console["group" + (parent ? "Collapsed" : "")](...format(str, "css"));
for (let child of str.children) {
printTree(child, str);
}
console.groupEnd();
}
else {
- console.log(format(str));
+ console.log(...format(str, "css"));
}
}
diff --git a/src/env/node.js b/src/env/node.js
index b96c764..94f17ee 100644
--- a/src/env/node.js
+++ b/src/env/node.js
@@ -10,7 +10,7 @@ import { AsciiTree } from "oo-ascii-tree";
import { globSync } from "glob";
// Internal modules
-import format from "../format-console.js";
+import format from "../util/format-console.js";
import { getType } from "../util.js";
/**
@@ -60,7 +60,7 @@ function getTree (msg, i) {
let icon = collapsed ? icons.collapsed : icons.expanded;
if (highlighted) {
- icon = `${ collapsed ? icons.collapsedHighlighted : icons.expandedHighlighted }`;
+ icon = `${ collapsed ? icons.collapsedHighlighted : icons.expandedHighlighted }`;
msg = `${ msg }`;
}
msg = icon + " " + msg;
diff --git a/src/format-console.js b/src/format-console.js
deleted file mode 100644
index 879df41..0000000
--- a/src/format-console.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * Format console text with HTML-like tags
- */
-// https://stackoverflow.com/a/41407246/90826
-let modifiers = {
- reset: "\x1b[0m",
- b: "\x1b[1m",
- dim: "\x1b[2m",
- i: "\x1b[3m",
-};
-
-let hues = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"];
-
-let colors = Object.fromEntries(hues.map((hue, i) => [hue, `\x1b[3${i}m`]));
-let bgColors = Object.fromEntries(hues.map((hue, i) => [hue, `\x1b[4${i}m`]));
-
-function getColorCode (hue, {light, bg} = {}) {
- if (!hue) {
- return "";
- }
- if (hue.startsWith("light")) {
- hue = hue.replace("light", "");
- light = true;
- }
- let i = hues.indexOf(hue);
-
- if (i === -1) {
- return "";
- }
-
- if (light) {
- return `\x1b[${ bg ? 10 : 9 }${i}m`;
- }
-
- return `\x1b[${ light ? "1;" : ""}${ bg ? 4 : 3 }${i}m`;
-}
-
-let tags = [
- Object.keys(modifiers).map(tag => `?${tag}>`),
- ``, ``,
- ``, ``,
-];
-let tagRegex = RegExp(tags.flat().join("|"), "gi");
-
-export default function format (str) {
- if (!str) {
- return str;
- }
-
- str = str + "";
- // Iterate over all regex matches in str
- let active = new Set();
- let colorStack = [];
- let bgStack = [];
- return str.replace(tagRegex, tag => {
- let isClosing = tag[1] === "/";
- let name = tag.match(/<\/?(\w+)/)[1];
- let color = tag.match(/<(?:bg|c)\s+(\w+)>/)?.[1];
-
- if (isClosing) {
- if (name === "c") {
- colorStack.pop();
- }
- else if (name === "bg") {
- bgStack.pop();
- }
- else if (active.has(name)) {
- active.delete(name);
- }
- else {
- // Closing tag for formatting that wasn't active
- return "";
- }
-
- let activeColor = colorStack.at(-1);
- let colorModifier = getColorCode(activeColor);
- let activeBg = bgStack.at(-1);
- let bgColorModifier = getColorCode(activeBg, {bg: true});
- return modifiers.reset + [...active].map(name => modifiers[name]).join("") + colorModifier + bgColorModifier;
- }
- else {
- if (name === "c") {
- colorStack.push(color);
- return getColorCode(color);
- }
- else if (name === "bg") {
- bgStack.push(color);
- return getColorCode(color, {bg: true});
- }
- else {
- active.add(name);
- return modifiers[name];
- }
- }
- });
-}
-
-export function stripFormatting (str) {
- return str.replace(tagRegex, "");
-}
-
-// /**
-// * Platform agnostic console formatting
-// * @param {*} str
-// * @param {*} format
-// */
-// export default function format (str, format) {
-// if (typeof format === "string") {
-// format = Object.fromEntries(format.split(/\s+/).map(type => [type, true]));
-// }
-
-// for (let type in format) {
-// str = formats[type] ? formats[type](str) : str;
-// }
-// str = str.replaceAll("\x1b", "\\x1b");
-// return str;
-// }
diff --git a/src/render.js b/src/render.js
index f44be94..1e9fd78 100644
--- a/src/render.js
+++ b/src/render.js
@@ -7,7 +7,7 @@ import TestResult from "./classes/TestResult.js";
import RefTest from "https://html.htest.dev/src/classes/RefTest.js";
import { create, output } from "https://html.htest.dev/src/util.js";
import { formatDuration } from "./util.js";
-import format from "./format-console.js";
+import format from "./util/format-console.js";
export default function render (test) {
let root = new Test(test);
@@ -83,7 +83,7 @@ export default function render (test) {
}
else if (!target.pass) {
cell.classList.add("details");
- cell.onclick = () => console.log(target.details.map(format).join("\n"));
+ cell.onclick = () => console.log(...format(target.details.join("\n")));
}
tr.dataset.time = formatDuration(target.timeTaken);
}
diff --git a/src/util/format-console.js b/src/util/format-console.js
new file mode 100644
index 0000000..104edc4
--- /dev/null
+++ b/src/util/format-console.js
@@ -0,0 +1,262 @@
+/**
+ * Format console text with HTML-like tags.
+ *
+ * Modes:
+ * - "truecolor" / "256" / "strip" — ANSI backend (default on Node).
+ * - "css" — returns [text, ...styles] for console.log spread (default on browser).
+ */
+
+import { IS_NODEJS } from "../util.js";
+import palette from "./palette.js";
+
+// `reset` is internal-only — not matched by tagRegex, used by emitAnsi's replay.
+const modifiers = {
+ reset: { ansi: "\x1b[0m" },
+ b: { ansi: "\x1b[1m", css: "font-weight: bold" },
+ i: { ansi: "\x1b[3m", css: "font-style: italic" },
+ dim: { ansi: "\x1b[2m", css: "opacity: 0.6" },
+};
+
+const tagRegex = /<\/?(b|i|dim|c|bg)(?:\s+([\w#-]+))?\s*>/gi;
+
+/**
+ * Detect format mode from an env-like object. Browser falls back to "css" (CSS backend).
+ * Pure — takes env as argument for testability; default path is cached in `detectedMode`.
+ * @param {Record | null} [env]
+ * @returns {"truecolor" | "256" | "strip" | "css"}
+ */
+function detectMode (env = IS_NODEJS ? process.env : null) {
+ if (!env) {
+ return "css";
+ }
+
+ let ret = "256"; // env.FORCE_COLOR === "2" || env.FORCE_COLOR === "1" || no env;
+
+ if (env.NO_COLOR || env.FORCE_COLOR === "0") {
+ ret = "strip";
+ }
+ else if (
+ env.FORCE_COLOR === "3" ||
+ env.COLORTERM === "truecolor" ||
+ env.COLORTERM === "24bit" ||
+ /-truecolor|-direct|-24bit/.test(env.TERM ?? "")
+ ) {
+ ret = "truecolor";
+ }
+
+ return ret;
+}
+
+const detectedMode = detectMode();
+
+/**
+ * Resolve a color name to a hex value.
+ * Accepts semantic tokens, base names, and hex literals.
+ */
+function resolveColor (name) {
+ if (palette[name]) {
+ return palette[name];
+ }
+ if (/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(name)) {
+ return name;
+ }
+ return null;
+}
+
+function parseHex (hex) {
+ hex = hex.replace("#", "");
+ if (hex.length === 3) {
+ hex = hex
+ .split("")
+ .map(c => c + c)
+ .join("");
+ }
+ return {
+ r: parseInt(hex.slice(0, 2), 16),
+ g: parseInt(hex.slice(2, 4), 16),
+ b: parseInt(hex.slice(4, 6), 16),
+ };
+}
+
+export function ansiTruecolor (hex, { bg } = {}) {
+ let { r, g, b } = parseHex(hex);
+ return `\x1b[${bg ? 48 : 38};2;${r};${g};${b}m`;
+}
+
+// Reference points of the xterm 6×6×6 color cube (indices 16–231).
+const cubeLevels = [0, 95, 135, 175, 215, 255];
+
+function quantize (value) {
+ let best = 0;
+ let bestDelta = Infinity;
+ for (let i = 0; i < cubeLevels.length; i++) {
+ let delta = Math.abs(value - cubeLevels[i]);
+ if (delta < bestDelta) {
+ bestDelta = delta;
+ best = i;
+ }
+ }
+ return best;
+}
+
+export function ansi256 (hex, { bg } = {}) {
+ let { r, g, b } = parseHex(hex);
+ let index = 16 + 36 * quantize(r) + 6 * quantize(g) + quantize(b);
+ return `\x1b[${bg ? 48 : 38};5;${index}m`;
+}
+
+/**
+ * Tokenize a tagged string into a stream of { type, tag, value } records.
+ */
+function tokenize (str) {
+ let tokens = [];
+ let lastIndex = 0;
+ for (let match of str.matchAll(tagRegex)) {
+ if (match.index > lastIndex) {
+ tokens.push({ type: "text", value: str.slice(lastIndex, match.index) });
+ }
+ let isClose = match[0].startsWith("");
+ tokens.push({
+ type: isClose ? "close" : "open",
+ tag: match[1].toLowerCase(),
+ value: match[2],
+ });
+ lastIndex = match.index + match[0].length;
+ }
+ if (lastIndex < str.length) {
+ tokens.push({ type: "text", value: str.slice(lastIndex) });
+ }
+ return tokens;
+}
+
+function emitAnsi (tokens, mode) {
+ let output = "";
+ let activeModifiers = new Set();
+ let colorStack = [];
+ let bgStack = [];
+
+ let emitColor =
+ mode === "truecolor" ? ansiTruecolor : mode === "256" ? ansi256 : () => "";
+
+ let replay = () => {
+ output += modifiers.reset.ansi;
+ for (let modifier of activeModifiers) {
+ output += modifiers[modifier].ansi;
+ }
+ let color = colorStack.findLast(Boolean);
+ if (color) {
+ output += emitColor(color);
+ }
+ let bg = bgStack.findLast(Boolean);
+ if (bg) {
+ output += emitColor(bg, { bg: true });
+ }
+ };
+
+ for (let token of tokens) {
+ if (token.type === "text") {
+ output += token.value;
+ continue;
+ }
+
+ let isBg = token.tag === "bg";
+ let isColor = token.tag === "c" || isBg;
+ let stack = isBg ? bgStack : colorStack;
+
+ if (token.type === "open") {
+ if (isColor) {
+ let hex = resolveColor(token.value);
+ // Push even when null so close's pop stays balanced.
+ stack.push(hex);
+ if (hex) {
+ output += emitColor(hex, { bg: isBg });
+ }
+ }
+ else {
+ activeModifiers.add(token.tag);
+ output += modifiers[token.tag].ansi;
+ }
+ }
+ else {
+ if (isColor) {
+ stack.pop();
+ }
+ else {
+ activeModifiers.delete(token.tag);
+ }
+ // ANSI has no "close this color" code, so reset and replay remaining state.
+ replay();
+ }
+ }
+ return output;
+}
+
+function emitCss (tokens) {
+ let text = "";
+ let styles = [];
+ let activeModifiers = new Set();
+ let colorStack = [];
+ let bgStack = [];
+
+ let pushStyle = () => {
+ let parts = [];
+ for (let modifier of activeModifiers) {
+ parts.push(modifiers[modifier].css);
+ }
+ let color = colorStack.findLast(Boolean);
+ if (color) {
+ parts.push(`color: ${color}`);
+ }
+ let bg = bgStack.findLast(Boolean);
+ if (bg) {
+ parts.push(`background: ${bg}`);
+ }
+ text += "%c";
+ styles.push(parts.join("; "));
+ };
+
+ for (let token of tokens) {
+ if (token.type === "text") {
+ text += token.value;
+ continue;
+ }
+
+ let isBg = token.tag === "bg";
+ let isColor = token.tag === "c" || isBg;
+ let isOpen = token.type === "open";
+
+ if (isColor) {
+ let stack = isBg ? bgStack : colorStack;
+ if (isOpen) {
+ stack.push(resolveColor(token.value));
+ }
+ else {
+ stack.pop();
+ }
+ }
+ else if (isOpen) {
+ activeModifiers.add(token.tag);
+ }
+ else {
+ activeModifiers.delete(token.tag);
+ }
+ pushStyle();
+ }
+
+ return [text, ...styles];
+}
+
+/**
+ * Format a tagged string for the target mode.
+ * @param {string} str
+ * @param {"truecolor" | "256" | "strip" | "css"} [mode]
+ * @returns {string | [string, ...string[]]}
+ */
+export default function format (str, mode = detectedMode) {
+ let tokens = tokenize(String(str ?? ""));
+ return mode === "css" ? emitCss(tokens) : emitAnsi(tokens, mode);
+}
+
+export function stripFormatting (str) {
+ return String(str).replace(tagRegex, "");
+}
diff --git a/src/util/format-diff.js b/src/util/format-diff.js
index aebb98e..3de6e14 100644
--- a/src/util/format-diff.js
+++ b/src/util/format-diff.js
@@ -1,5 +1,5 @@
import { IS_NODEJS, getType, pluralize, stringify } from "../util.js";
-import { stripFormatting } from "../format-console.js";
+import { stripFormatting } from "./format-console.js";
// Dual Node/browser import. Kept version in the CDN URL in sync with package.json.
// `diffWordsWithSpace` keeps whitespace as part of token boundaries so the
@@ -18,10 +18,10 @@ const CONTEXT = 2;
/** Longer single-line text switches to two-line word-diff layout. */
const INLINE_MAX = 40;
-/** Per-side color, change-action key, and output label for diff formatting. */
+/** Per-side chunk/line bg tokens, change-action key, and output label for diff formatting. */
const sides = {
- actual: { color: "red", action: "removed", label: " Actual: " },
- expected: { color: "green", action: "added", label: " Expected: " },
+ actual: { chunk: "diff-removed-tint", line: "diff-removed", action: "removed", label: " Actual: " },
+ expected: { chunk: "diff-added-tint", line: "diff-added", action: "added", label: " Expected: " },
};
/**
@@ -246,16 +246,16 @@ function lineDiff (actualString, expectedString, unmapped) {
}
/**
- * Format one side of a change array. Every changed run gets ``
+ * Format one side of a change array. Every changed run gets ``
* so all diffs — whitespace, token, or char — carry the same visual primitive.
*
* Without `prefix`: returns mixed common/changed text; caller decides line framing.
- * With `prefix`: wraps the whole line in neutral `` with `prefix`
- * in front; chunk bgs inside stack over the neutral line bg so changed chars
- * pop while the rest of the line keeps a faint "this line changed" tint.
+ * With `prefix`: wraps the whole line in `` with `prefix` in front;
+ * chunk bgs inside stack over the line bg so changed chars pop while the rest
+ * of the line keeps a faint "this line changed" tint.
*/
function colorize (changes, side, prefix) {
- let { color, action } = sides[side];
+ let { chunk, line, action } = sides[side];
let ret = "";
for (let change of changes) {
@@ -264,14 +264,14 @@ function colorize (changes, side, prefix) {
}
if (change[action]) {
- ret += `${ change.value }`;
+ ret += `${ change.value }`;
}
else {
ret += change.value;
}
}
- return prefix ? `${ prefix } ${ ret }` : ret;
+ return prefix ? `${ prefix } ${ ret }` : ret;
}
/**
diff --git a/src/util/palette.js b/src/util/palette.js
new file mode 100644
index 0000000..0b923ee
--- /dev/null
+++ b/src/util/palette.js
@@ -0,0 +1,43 @@
+/**
+ * Color palette: base 16 ANSI-named colors + semantic tokens.
+ * All values as hex literals (no runtime conversion).
+ */
+
+const base = {
+ black: "#1e1e2e",
+ red: "#f38ba8",
+ green: "#4ade80",
+ yellow: "#f9e2af",
+ blue: "#89b4fa",
+ magenta: "#cba6f7",
+ cyan: "#94e2d5",
+ white: "#cdd6f4",
+ lightblack: "#585b70",
+ lightred: "#eba0ac",
+ lightgreen: "#b5e3a7",
+ lightyellow: "#faedc4",
+ lightblue: "#a0bff9",
+ lightmagenta: "#d4b8f7",
+ lightcyan: "#a9e3d3",
+ lightwhite: "#e6edf5",
+};
+
+const semantic = {
+ pass: base.green,
+ "pass-tint": base.lightgreen,
+ fail: base.red,
+ "fail-tint": base.lightred,
+ skip: "#a0a8b4",
+ "skip-tint": "#c4c9d2",
+ message: base.yellow,
+ "message-tint": base.lightyellow,
+ highlight: base.green,
+ text: base.black,
+ "diff-added": "#2e4b3a",
+ "diff-added-tint": base.lightgreen,
+ "diff-removed": "#4b2e38",
+ "diff-removed-tint": base.lightred,
+ gutter: "#313244",
+};
+
+export default { ...base, ...semantic };
diff --git a/tests/format-console.js b/tests/format-console.js
index acc0747..be39afc 100644
--- a/tests/format-console.js
+++ b/tests/format-console.js
@@ -1,61 +1,182 @@
-import format, { stripFormatting } from "../src/format-console.js";
-import chalk from "chalk";
+import format, {
+ stripFormatting,
+ ansiTruecolor,
+ ansi256,
+} from "../src/util/format-console.js";
+import palette from "../src/util/palette.js";
-// We don't want to use map because it will output unmapped values on fail as well, causing a mess in this very special case
+// Escape ANSI escape codes so failure output shows them as visible characters.
+// We don't want to use map because it will output unmapped values on fail as
+// well, causing a mess in this very special case.
function escape (str) {
return str.replaceAll("\x1b", "\\x1b");
}
+const RESET = "\\x1b[0m";
+const color = hex => escape(ansiTruecolor(hex));
+const bgColor = hex => escape(ansiTruecolor(hex, { bg: true }));
+const color256 = hex => escape(ansi256(hex));
+
export default {
- name: "Console formatting tests",
+ name: "format-console",
tests: [
{
- name: "Formatting",
- run (str) {
- return escape(format(str));
+ name: "format()",
+ run (arg) {
+ let result = format(arg, this.data?.mode);
+ return typeof result === "string" ? escape(result) : result;
},
tests: [
{
- name: "Bold",
- args: "bold",
- expect: "\\x1b[1mbold\\x1b[0m",
- },
- {
- name: "Text color",
- args: "red",
- expect: "\\x1b[31mred\\x1b[0m",
+ name: "Truecolor",
+ data: { mode: "truecolor" },
+ tests: [
+ {
+ name: "Bold modifier",
+ arg: "x",
+ expect: `\\x1b[1mx${RESET}`,
+ },
+ {
+ name: "Semantic token",
+ arg: "x",
+ expect: `${color(palette.pass)}x${RESET}`,
+ },
+ {
+ name: "Hex literal",
+ arg: "x",
+ expect: "\\x1b[38;2;255;0;0mx\\x1b[0m",
+ },
+ {
+ name: "Background",
+ arg: "x",
+ expect: `${bgColor(palette.pass)}x${RESET}`,
+ },
+ {
+ name: "Nested preserves outer",
+ arg: "xy",
+ expect: `${color(palette.pass)}${color(palette.fail)}x${RESET}${color(palette.pass)}y${RESET}`,
+ },
+ {
+ name: "Diff-style",
+ arg: " + added - removed",
+ expect: `${bgColor(palette.gutter)} ${color(palette["diff-added"])}+ added${RESET}${bgColor(palette.gutter)} ${color(palette["diff-removed"])}- removed${RESET}${bgColor(palette.gutter)}${RESET}`,
+ },
+ {
+ name: "Unknown color ignored",
+ arg: "x",
+ expect: `x${RESET}`,
+ },
+ {
+ name: "Nested unknown preserves outer",
+ arg: "x",
+ expect: `${color(palette.pass)}\\x1b[1mx${RESET}${color(palette.pass)}${RESET}${color(palette.pass)}${RESET}`,
+ },
+ {
+ name: "3-char hex expansion",
+ arg: "x",
+ expect: "\\x1b[38;2;255;0;0mx\\x1b[0m",
+ },
+ {
+ name: "Empty string",
+ arg: "",
+ expect: "",
+ },
+ ],
},
{
- name: "Background color",
- args: "red",
- expect: "\\x1b[41mred\\x1b[0m",
+ name: "256",
+ data: { mode: "256" },
+ tests: [
+ {
+ name: "Semantic token via 256 cube",
+ arg: "x",
+ expect: `${color256(palette.pass)}x${RESET}`,
+ },
+ {
+ name: "Hex literal red → index 196",
+ arg: "x",
+ expect: "\\x1b[38;5;196mx\\x1b[0m",
+ },
+ ],
},
{
- name: "Light color",
- args: "light red",
- // expect: "\\x1b[91mlight red\\x1b[0m"
- expect: escape(chalk.redBright("light red")),
+ name: "Strip",
+ data: { mode: "strip" },
+ tests: [
+ {
+ name: "Semantic token stripped",
+ arg: "x",
+ expect: `x${RESET}`,
+ },
+ {
+ name: "Hex literal stripped",
+ arg: "x",
+ expect: `x${RESET}`,
+ },
+ {
+ name: "Colors stripped, modifiers kept",
+ arg: "x",
+ expect: `\\x1b[1mx${RESET}\\x1b[1m${RESET}`,
+ },
+ ],
},
{
- name: "Light background color",
- args: "light red",
- // expect: "\\x1b[101mlight red\\x1b[0m"
- expect: escape(chalk.bgRedBright("light red")),
+ name: "CSS",
+ data: { mode: "css" },
+ tests: [
+ {
+ name: "Single foreground",
+ arg: "x",
+ expect: ["%cx%c", `color: ${palette.pass}`, ""],
+ },
+ {
+ name: "Nested foreground",
+ arg: "x",
+ expect: [
+ "%c%cx%c%c",
+ `color: ${palette.pass}`,
+ `color: ${palette.fail}`,
+ `color: ${palette.pass}`,
+ "",
+ ],
+ },
+ {
+ name: "Background plus bold",
+ arg: "x",
+ expect: [
+ "%c%cx%c%c",
+ "font-weight: bold",
+ `font-weight: bold; background: ${palette.pass}`,
+ "font-weight: bold",
+ "",
+ ],
+ },
+ {
+ name: "Diff-style",
+ arg: " + added - removed",
+ expect: [
+ "%c %c+ added%c %c- removed%c%c",
+ `background: ${palette.gutter}`,
+ `color: ${palette["diff-added"]}; background: ${palette.gutter}`,
+ `background: ${palette.gutter}`,
+ `color: ${palette["diff-removed"]}; background: ${palette.gutter}`,
+ `background: ${palette.gutter}`,
+ "",
+ ],
+ },
+ ],
},
],
},
{
- name: "Strip formatting",
+ name: "stripFormatting()",
run: stripFormatting,
tests: [
- {
- args: "bold",
- expect: "bold",
- },
+ { arg: "x", expect: "x" },
{
name: "Malformed tags",
- args: "bold red?",
- expect: "bold red?",
+ arg: "bold x?",
+ expect: "bold x?",
},
],
},
diff --git a/tests/format-diff.js b/tests/format-diff.js
index 6cdd060..ddf2b80 100644
--- a/tests/format-diff.js
+++ b/tests/format-diff.js
@@ -32,31 +32,31 @@ export default {
{
name: "Inline char diff",
args: ["abc", "adc"],
- expect: `Got "abc", expected "adc"`,
+ expect: `Got "abc", expected "adc"`,
},
{
name: "Inline char diff with unmapped values",
args: ["abc", "adc", { actual: "foo", expected: "bar" }],
- expect: `Got "abc" ("foo" unmapped), expected "adc" ("bar" unmapped)`,
+ expect: `Got "abc" ("foo" unmapped), expected "adc" ("bar" unmapped)`,
},
{
name: "Two-line word diff",
args: [`${longPrefix} eta`, `${longPrefix} theta`],
- expect: ` Actual: "${longPrefix} eta"
- Expected: "${longPrefix} theta"`,
+ expect: ` Actual: "${longPrefix} eta"
+ Expected: "${longPrefix} theta"`,
},
{
name: "Two-line word diff with unmapped values",
args: [`${longPrefix} eta`, `${longPrefix} theta`, { actual: "foo", expected: "bar" }],
- expect: ` Actual: "${longPrefix} eta"
+ expect: ` Actual: "${longPrefix} eta"
"foo" unmapped
- Expected: "${longPrefix} theta"
+ Expected: "${longPrefix} theta"
"bar" unmapped`,
},
{
name: "Inline multiline string diff",
args: ["one\ntwo\nthree", "one\nTWO\nthree"],
- expect: `Got "one\\ntwo\\nthree", expected "one\\nTWO\\nthree"`,
+ expect: `Got "one\\ntwo\\nthree", expected "one\\nTWO\\nthree"`,
},
{
name: "Elided array hunks",
@@ -72,15 +72,15 @@ export default {
… 5 matching lines …
"line4",
"line5",
-- \t"X",
-+ \t"x",
+- \t"X",
++ \t"x",
"line7",
"line8",
… 3 matching lines …
"line12",
"line13",
-- \t"Y",
-+ \t"y",
+- \t"Y",
++ \t"y",
"line15",
"line16",
]`,
@@ -100,8 +100,8 @@ export default {
args: ["foo\nbar\n", "foo\nbaz\n", { actual: "bar", expected: "yolo" }],
expect: ` Actual ↔ Expected:
foo
-- bar
-+ baz
+- bar
++ baz
Actual unmapped: "bar"
Expected unmapped: "yolo"`,
},
@@ -109,39 +109,39 @@ export default {
name: "multiple paired lines",
args: ["foo13\nbar42\n", "foo25\nbar47\n"],
expect: ` Actual ↔ Expected:
-- foo13
-+ foo25
-- bar42
-+ bar47`,
+- foo13
++ foo25
+- bar42
++ bar47`,
},
{
name: "noisy swap collapses via cleanup",
args: ["fix: button alignment\n", "fix: button padding\n"],
expect: ` Actual ↔ Expected:
-- fix: button alignment
-+ fix: button padding`,
+- fix: button alignment
++ fix: button padding`,
},
{
name: "unequal counts stay plain",
args: ["foo\nbar\n", "baz\n"],
expect: ` Actual ↔ Expected:
-- foo
-- bar
-+ baz`,
+- foo
+- bar
++ baz`,
},
{
name: "added line",
args: ["foo\n", "foo\nbar\n"],
expect: ` Actual ↔ Expected:
foo
-+ bar`,
++ bar`,
},
{
name: "removed line",
args: ["foo\nbar\n", "foo\n"],
expect: ` Actual ↔ Expected:
foo
-- bar`,
+- bar`,
},
],
},