551 lines
16 KiB
JavaScript
551 lines
16 KiB
JavaScript
/**
|
|
* Modern Terminal Features Tests
|
|
* Tests for true color, enhanced Unicode, and mouse interaction support
|
|
* Requirements: 12.1, 12.2, 12.3
|
|
*/
|
|
|
|
const {
|
|
TerminalCapabilities,
|
|
TrueColorUtils,
|
|
UnicodeChars,
|
|
MouseUtils,
|
|
FeatureDetection,
|
|
} = require("../../src/tui/utils/modernTerminal.js");
|
|
|
|
// Mock environment variables for testing
|
|
const mockEnv = (envVars) => {
|
|
const originalEnv = { ...process.env };
|
|
Object.assign(process.env, envVars);
|
|
return () => {
|
|
process.env = originalEnv;
|
|
};
|
|
};
|
|
|
|
describe("Terminal Capabilities", () => {
|
|
describe("True Color Support Detection", () => {
|
|
test("should detect true color via COLORTERM=truecolor", () => {
|
|
const restore = mockEnv({ COLORTERM: "truecolor" });
|
|
expect(TerminalCapabilities.supportsTrueColor()).toBe(true);
|
|
restore();
|
|
});
|
|
|
|
test("should detect true color via COLORTERM=24bit", () => {
|
|
const restore = mockEnv({ COLORTERM: "24bit" });
|
|
expect(TerminalCapabilities.supportsTrueColor()).toBe(true);
|
|
restore();
|
|
});
|
|
|
|
test("should detect true color via modern terminal programs", () => {
|
|
const restore = mockEnv({ TERM_PROGRAM: "iTerm.app" });
|
|
expect(TerminalCapabilities.supportsTrueColor()).toBe(true);
|
|
restore();
|
|
});
|
|
|
|
test("should detect true color via Windows Terminal", () => {
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
const restore = mockEnv({ WT_SESSION: "12345" });
|
|
|
|
expect(TerminalCapabilities.supportsTrueColor()).toBe(true);
|
|
|
|
restore();
|
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
});
|
|
|
|
test("should detect true color via TERM variable", () => {
|
|
const restore = mockEnv({ TERM: "xterm-256color" });
|
|
expect(TerminalCapabilities.supportsTrueColor()).toBe(true);
|
|
restore();
|
|
});
|
|
|
|
test("should not detect true color when no indicators present", () => {
|
|
const restore = mockEnv({
|
|
COLORTERM: undefined,
|
|
TERM_PROGRAM: undefined,
|
|
TERMINAL_EMULATOR: undefined,
|
|
TERM: "xterm",
|
|
WT_SESSION: undefined,
|
|
});
|
|
expect(TerminalCapabilities.supportsTrueColor()).toBe(false);
|
|
restore();
|
|
});
|
|
});
|
|
|
|
describe("Enhanced Unicode Support Detection", () => {
|
|
test("should detect Unicode via UTF-8 locale", () => {
|
|
const restore = mockEnv({ LC_ALL: "en_US.UTF-8" });
|
|
expect(TerminalCapabilities.supportsEnhancedUnicode()).toBe(true);
|
|
restore();
|
|
});
|
|
|
|
test("should detect Unicode via LANG variable", () => {
|
|
const restore = mockEnv({ LANG: "en_US.utf8" });
|
|
expect(TerminalCapabilities.supportsEnhancedUnicode()).toBe(true);
|
|
restore();
|
|
});
|
|
|
|
test("should fallback to true color detection for Unicode", () => {
|
|
const restore = mockEnv({
|
|
LC_ALL: undefined,
|
|
LC_CTYPE: undefined,
|
|
LANG: undefined,
|
|
COLORTERM: "truecolor",
|
|
});
|
|
expect(TerminalCapabilities.supportsEnhancedUnicode()).toBe(true);
|
|
restore();
|
|
});
|
|
|
|
test("should not detect Unicode when no indicators present", () => {
|
|
const restore = mockEnv({
|
|
LC_ALL: undefined,
|
|
LC_CTYPE: undefined,
|
|
LANG: undefined,
|
|
COLORTERM: undefined,
|
|
TERM_PROGRAM: undefined,
|
|
});
|
|
expect(TerminalCapabilities.supportsEnhancedUnicode()).toBe(false);
|
|
restore();
|
|
});
|
|
});
|
|
|
|
describe("Mouse Interaction Support Detection", () => {
|
|
test("should detect mouse via TERM_FEATURES", () => {
|
|
const restore = mockEnv({ TERM_FEATURES: "mouse,color" });
|
|
expect(TerminalCapabilities.supportsMouseInteraction()).toBe(true);
|
|
restore();
|
|
});
|
|
|
|
test("should detect mouse via modern terminal programs", () => {
|
|
const restore = mockEnv({ TERM_PROGRAM: "Windows Terminal" });
|
|
expect(TerminalCapabilities.supportsMouseInteraction()).toBe(true);
|
|
restore();
|
|
});
|
|
|
|
test("should detect mouse via Windows Terminal", () => {
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
const restore = mockEnv({ WT_SESSION: "12345" });
|
|
|
|
expect(TerminalCapabilities.supportsMouseInteraction()).toBe(true);
|
|
|
|
restore();
|
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
});
|
|
|
|
test("should not detect mouse when no indicators present", () => {
|
|
const restore = mockEnv({
|
|
TERM_FEATURES: undefined,
|
|
TERM_PROGRAM: undefined,
|
|
TERMINAL_EMULATOR: undefined,
|
|
WT_SESSION: undefined,
|
|
});
|
|
expect(TerminalCapabilities.supportsMouseInteraction()).toBe(false);
|
|
restore();
|
|
});
|
|
});
|
|
|
|
describe("Terminal Information", () => {
|
|
test("should return terminal information", () => {
|
|
const restore = mockEnv({
|
|
TERM_PROGRAM: "iTerm.app",
|
|
TERM: "xterm-256color",
|
|
});
|
|
|
|
const info = TerminalCapabilities.getTerminalInfo();
|
|
|
|
expect(info).toHaveProperty("width");
|
|
expect(info).toHaveProperty("height");
|
|
expect(info).toHaveProperty("colorDepth");
|
|
expect(info).toHaveProperty("supportsUnicode");
|
|
expect(info).toHaveProperty("supportsMouse");
|
|
expect(info).toHaveProperty("platform");
|
|
expect(info).toHaveProperty("termProgram");
|
|
expect(info).toHaveProperty("termType");
|
|
|
|
expect(info.termProgram).toBe("iTerm.app");
|
|
expect(info.termType).toBe("xterm-256color");
|
|
|
|
restore();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("True Color Utils", () => {
|
|
describe("RGB Color Generation", () => {
|
|
test("should generate RGB true color escape sequence", () => {
|
|
// Mock true color support
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(true);
|
|
|
|
const result = TrueColorUtils.rgb(255, 128, 64);
|
|
expect(result).toBe("\x1b[38;2;255;128;64m");
|
|
});
|
|
|
|
test("should generate RGB background true color escape sequence", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(true);
|
|
|
|
const result = TrueColorUtils.rgbBg(255, 128, 64);
|
|
expect(result).toBe("\x1b[48;2;255;128;64m");
|
|
});
|
|
|
|
test("should fallback to 8-bit color when true color not supported", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(false);
|
|
|
|
const result = TrueColorUtils.rgb(255, 0, 0);
|
|
expect(result).toMatch(/\x1b\[38;5;\d+m/);
|
|
});
|
|
});
|
|
|
|
describe("Hex Color Conversion", () => {
|
|
test("should convert hex to RGB true color", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(true);
|
|
|
|
const result = TrueColorUtils.hex("#FF8040");
|
|
expect(result).toBe("\x1b[38;2;255;128;64m");
|
|
});
|
|
|
|
test("should convert hex to RGB background true color", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(true);
|
|
|
|
const result = TrueColorUtils.hexBg("#FF8040");
|
|
expect(result).toBe("\x1b[48;2;255;128;64m");
|
|
});
|
|
|
|
test("should handle hex colors without # prefix", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(true);
|
|
|
|
const result = TrueColorUtils.hex("FF8040");
|
|
expect(result).toBe("\x1b[38;2;255;128;64m");
|
|
});
|
|
});
|
|
|
|
describe("Ink Color Compatibility", () => {
|
|
test("should return hex color for true color terminals", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(true);
|
|
|
|
const result = TrueColorUtils.getInkColor("#FF0000");
|
|
expect(result).toBe("#FF0000");
|
|
});
|
|
|
|
test("should return standard color names for fallback", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(false);
|
|
|
|
const result = TrueColorUtils.getInkColor("#FF0000");
|
|
expect(result).toBe("red");
|
|
});
|
|
|
|
test("should fallback to white for unknown colors", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(false);
|
|
|
|
const result = TrueColorUtils.getInkColor("#123456");
|
|
expect(result).toBe("white");
|
|
});
|
|
});
|
|
|
|
describe("Color Reset", () => {
|
|
test("should provide reset escape sequence", () => {
|
|
const result = TrueColorUtils.reset();
|
|
expect(result).toBe("\x1b[0m");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Unicode Characters", () => {
|
|
describe("Character Categories", () => {
|
|
test("should provide box drawing characters", () => {
|
|
expect(UnicodeChars.box.horizontal).toBe("─");
|
|
expect(UnicodeChars.box.vertical).toBe("│");
|
|
expect(UnicodeChars.box.topLeft).toBe("┌");
|
|
expect(UnicodeChars.box.roundedTopLeft).toBe("╭");
|
|
});
|
|
|
|
test("should provide progress characters", () => {
|
|
expect(UnicodeChars.progress.full).toBe("█");
|
|
expect(UnicodeChars.progress.empty).toBe("░");
|
|
expect(UnicodeChars.progress.spinner).toBeInstanceOf(Array);
|
|
expect(UnicodeChars.progress.spinner.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("should provide symbol characters", () => {
|
|
expect(UnicodeChars.symbols.checkMark).toBe("✓");
|
|
expect(UnicodeChars.symbols.crossMark).toBe("✗");
|
|
expect(UnicodeChars.symbols.rightArrow).toBe("→");
|
|
});
|
|
|
|
test("should provide emoji characters", () => {
|
|
expect(UnicodeChars.emoji.gear).toBe("⚙");
|
|
expect(UnicodeChars.emoji.rocket).toBe("🚀");
|
|
});
|
|
});
|
|
|
|
describe("Character Selection with Fallbacks", () => {
|
|
test("should return Unicode character when supported", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsEnhancedUnicode")
|
|
.mockReturnValue(true);
|
|
|
|
const result = UnicodeChars.getChar("box", "horizontal");
|
|
expect(result).toBe("─");
|
|
});
|
|
|
|
test("should return ASCII fallback when Unicode not supported", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsEnhancedUnicode")
|
|
.mockReturnValue(false);
|
|
|
|
const result = UnicodeChars.getChar("box", "horizontal");
|
|
expect(result).toBe("-");
|
|
});
|
|
|
|
test("should return custom fallback when provided", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsEnhancedUnicode")
|
|
.mockReturnValue(false);
|
|
|
|
const result = UnicodeChars.getChar("box", "nonexistent", "X");
|
|
expect(result).toBe("X");
|
|
});
|
|
|
|
test("should return default fallback for unknown characters", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsEnhancedUnicode")
|
|
.mockReturnValue(true);
|
|
|
|
const result = UnicodeChars.getChar("unknown", "unknown");
|
|
expect(result).toBe("?");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Mouse Utils", () => {
|
|
let originalStdout;
|
|
|
|
beforeEach(() => {
|
|
originalStdout = process.stdout.write;
|
|
process.stdout.write = jest.fn();
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.stdout.write = originalStdout;
|
|
});
|
|
|
|
describe("Mouse Tracking Control", () => {
|
|
test("should enable mouse tracking when supported", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsMouseInteraction")
|
|
.mockReturnValue(true);
|
|
|
|
const result = MouseUtils.enableMouse();
|
|
expect(result).toBe(true);
|
|
expect(process.stdout.write).toHaveBeenCalledWith("\x1b[?1000h");
|
|
expect(process.stdout.write).toHaveBeenCalledWith("\x1b[?1002h");
|
|
expect(process.stdout.write).toHaveBeenCalledWith("\x1b[?1015h");
|
|
expect(process.stdout.write).toHaveBeenCalledWith("\x1b[?1006h");
|
|
});
|
|
|
|
test("should not enable mouse tracking when not supported", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsMouseInteraction")
|
|
.mockReturnValue(false);
|
|
|
|
const result = MouseUtils.enableMouse();
|
|
expect(result).toBe(false);
|
|
expect(process.stdout.write).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should disable mouse tracking when supported", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsMouseInteraction")
|
|
.mockReturnValue(true);
|
|
|
|
const result = MouseUtils.disableMouse();
|
|
expect(result).toBe(true);
|
|
expect(process.stdout.write).toHaveBeenCalledWith("\x1b[?1006l");
|
|
expect(process.stdout.write).toHaveBeenCalledWith("\x1b[?1015l");
|
|
expect(process.stdout.write).toHaveBeenCalledWith("\x1b[?1002l");
|
|
expect(process.stdout.write).toHaveBeenCalledWith("\x1b[?1000l");
|
|
});
|
|
});
|
|
|
|
describe("Mouse Event Parsing", () => {
|
|
test("should parse SGR mouse format press event", () => {
|
|
const data = "\x1b[<0;10;5M";
|
|
const result = MouseUtils.parseMouseEvent(data);
|
|
|
|
expect(result).toEqual({
|
|
button: 0,
|
|
x: 10,
|
|
y: 5,
|
|
action: "press",
|
|
type: "mouse",
|
|
});
|
|
});
|
|
|
|
test("should parse SGR mouse format release event", () => {
|
|
const data = "\x1b[<0;10;5m";
|
|
const result = MouseUtils.parseMouseEvent(data);
|
|
|
|
expect(result).toEqual({
|
|
button: 0,
|
|
x: 10,
|
|
y: 5,
|
|
action: "release",
|
|
type: "mouse",
|
|
});
|
|
});
|
|
|
|
test("should parse basic mouse format", () => {
|
|
const data = "\x1b[M" + String.fromCharCode(32, 42, 37); // button=0, x=10, y=5
|
|
const result = MouseUtils.parseMouseEvent(data);
|
|
|
|
expect(result).toEqual({
|
|
button: 0,
|
|
x: 10,
|
|
y: 5,
|
|
action: "press",
|
|
type: "mouse",
|
|
});
|
|
});
|
|
|
|
test("should return null for invalid mouse data", () => {
|
|
const data = "invalid mouse data";
|
|
const result = MouseUtils.parseMouseEvent(data);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("Bounds Checking", () => {
|
|
test("should detect coordinates within bounds", () => {
|
|
const bounds = { x: 5, y: 5, width: 10, height: 5 };
|
|
|
|
expect(MouseUtils.isWithinBounds(10, 7, bounds)).toBe(true);
|
|
expect(MouseUtils.isWithinBounds(5, 5, bounds)).toBe(true);
|
|
expect(MouseUtils.isWithinBounds(14, 9, bounds)).toBe(true);
|
|
});
|
|
|
|
test("should detect coordinates outside bounds", () => {
|
|
const bounds = { x: 5, y: 5, width: 10, height: 5 };
|
|
|
|
expect(MouseUtils.isWithinBounds(4, 7, bounds)).toBe(false);
|
|
expect(MouseUtils.isWithinBounds(15, 7, bounds)).toBe(false);
|
|
expect(MouseUtils.isWithinBounds(10, 4, bounds)).toBe(false);
|
|
expect(MouseUtils.isWithinBounds(10, 10, bounds)).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Feature Detection", () => {
|
|
describe("Available Features", () => {
|
|
test("should return all available features", () => {
|
|
const features = FeatureDetection.getAvailableFeatures();
|
|
|
|
expect(features).toHaveProperty("trueColor");
|
|
expect(features).toHaveProperty("enhancedUnicode");
|
|
expect(features).toHaveProperty("mouseInteraction");
|
|
expect(features).toHaveProperty("terminalInfo");
|
|
|
|
expect(typeof features.trueColor).toBe("boolean");
|
|
expect(typeof features.enhancedUnicode).toBe("boolean");
|
|
expect(typeof features.mouseInteraction).toBe("boolean");
|
|
expect(typeof features.terminalInfo).toBe("object");
|
|
});
|
|
});
|
|
|
|
describe("Optimal Configuration", () => {
|
|
test("should return optimal configuration based on capabilities", () => {
|
|
const config = FeatureDetection.getOptimalConfig();
|
|
|
|
expect(config).toHaveProperty("colors");
|
|
expect(config).toHaveProperty("characters");
|
|
expect(config).toHaveProperty("interaction");
|
|
expect(config).toHaveProperty("performance");
|
|
|
|
expect(config.colors).toHaveProperty("useTrue");
|
|
expect(config.colors).toHaveProperty("palette");
|
|
expect(config.characters).toHaveProperty("useUnicode");
|
|
expect(config.interaction).toHaveProperty("enableMouse");
|
|
});
|
|
|
|
test("should configure for enhanced features when available", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(true);
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsEnhancedUnicode")
|
|
.mockReturnValue(true);
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsMouseInteraction")
|
|
.mockReturnValue(true);
|
|
|
|
const config = FeatureDetection.getOptimalConfig();
|
|
|
|
expect(config.colors.useTrue).toBe(true);
|
|
expect(config.colors.palette).toBe("extended");
|
|
expect(config.characters.useUnicode).toBe(true);
|
|
expect(config.characters.boxStyle).toBe("rounded");
|
|
expect(config.interaction.enableMouse).toBe(true);
|
|
expect(config.performance.animationLevel).toBe("full");
|
|
});
|
|
|
|
test("should configure for basic features when not available", () => {
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsTrueColor")
|
|
.mockReturnValue(false);
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsEnhancedUnicode")
|
|
.mockReturnValue(false);
|
|
jest
|
|
.spyOn(TerminalCapabilities, "supportsMouseInteraction")
|
|
.mockReturnValue(false);
|
|
|
|
const config = FeatureDetection.getOptimalConfig();
|
|
|
|
expect(config.colors.useTrue).toBe(false);
|
|
expect(config.colors.palette).toBe("basic");
|
|
expect(config.characters.useUnicode).toBe(false);
|
|
expect(config.characters.boxStyle).toBe("basic");
|
|
expect(config.interaction.enableMouse).toBe(false);
|
|
expect(config.performance.animationLevel).toBe("reduced");
|
|
});
|
|
});
|
|
|
|
describe("Capability Testing", () => {
|
|
test("should test terminal capabilities", () => {
|
|
const results = FeatureDetection.testCapabilities();
|
|
|
|
expect(results).toHaveProperty("trueColor");
|
|
expect(results).toHaveProperty("unicode");
|
|
expect(results).toHaveProperty("mouse");
|
|
expect(results).toHaveProperty("errors");
|
|
|
|
expect(typeof results.trueColor).toBe("boolean");
|
|
expect(typeof results.unicode).toBe("boolean");
|
|
expect(typeof results.mouse).toBe("boolean");
|
|
expect(Array.isArray(results.errors)).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Cleanup mocks
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|