first commit

This commit is contained in:
douboer@gmail.com
2026-03-03 13:23:14 +08:00
commit 3b7c1d558a
161 changed files with 28120 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import { describe, it, expect, beforeEach } from "vitest";
import Disposable from "../source/base/disposable";
describe("Disposable", () => {
let disposable: Disposable;
beforeEach(() => {
disposable = new Disposable();
});
describe("constructor", () => {
it("should not be disposed initially", () => {
expect(disposable.isDisposed).toBe(false);
});
});
describe("register", () => {
it("should register disposable", () => {
let disposed = false;
const d = {
dispose: () => {
disposed = true;
}
};
disposable.register(d);
disposable.dispose();
expect(disposed).toBe(true);
});
it("should dispose immediately if already disposed", () => {
disposable.dispose();
let disposed = false;
const d = {
dispose: () => {
disposed = true;
}
};
disposable.register(d);
expect(disposed).toBe(true);
});
});
describe("dispose", () => {
it("should dispose all registered", () => {
let count = 0;
disposable.register({ dispose: () => count++ });
disposable.register({ dispose: () => count++ });
disposable.dispose();
expect(count).toBe(2);
expect(disposable.isDisposed).toBe(true);
});
it("should not dispose twice", () => {
let count = 0;
disposable.register({ dispose: () => count++ });
disposable.dispose();
disposable.dispose();
expect(count).toBe(1);
});
});
});

View File

@@ -0,0 +1,91 @@
import { describe, it, expect, beforeEach } from "vitest";
import XEventEmitter from "../source/emitter/index";
describe("XEventEmitter", () => {
let emitter: XEventEmitter;
beforeEach(() => {
emitter = new XEventEmitter();
});
describe("on", () => {
it("should add event listener", () => {
let called = false;
emitter.on("test", () => {
called = true;
});
emitter.emit("test");
expect(called).toBe(true);
});
it("should handle multiple listeners", () => {
let count = 0;
emitter.on("test", () => count++);
emitter.on("test", () => count++);
emitter.emit("test");
expect(count).toBe(2);
});
});
describe("once", () => {
it("should call listener only once", () => {
let count = 0;
emitter.once("test", () => count++);
emitter.emit("test");
emitter.emit("test");
expect(count).toBe(1);
});
});
describe("off", () => {
it("should remove event listener", () => {
let called = false;
const listener = () => {
called = true;
};
emitter.on("test", listener);
emitter.off("test", listener);
emitter.emit("test");
expect(called).toBe(false);
});
});
describe("emit", () => {
it("should emit events with arguments", () => {
let args: unknown[] = [];
emitter.on("test", (...a) => {
args = a;
});
emitter.emit("test", 1, "hello", { key: "value" });
expect(args).toEqual([1, "hello", { key: "value" }]);
});
it("should prevent recursive emits", () => {
let count = 0;
emitter.on("test", () => {
count++;
if (count < 2) emitter.emit("test");
});
emitter.emit("test");
expect(count).toBe(1);
});
it("should not emit if disposed", () => {
let called = false;
emitter.on("test", () => {
called = true;
});
emitter.dispose();
emitter.emit("test");
expect(called).toBe(false);
});
});
describe("dispose", () => {
it("should dispose and clear store", () => {
emitter.on("test", () => {});
emitter.dispose();
expect(emitter.isDisposed).toBe(true);
});
});
});

View File

@@ -0,0 +1,75 @@
import { describe, it, expect, beforeEach } from "vitest";
import XHistory from "../source/history/index";
describe("XHistory", () => {
let history: XHistory;
beforeEach(() => {
history = new XHistory();
});
describe("constructor", () => {
it("should create with empty array", () => {
expect(history.list).toEqual([]);
});
it("should create with initial state", () => {
const h = new XHistory(["cmd1", "cmd2"]);
expect(h.list).toEqual(["cmd2", "cmd1"]); // reversed
});
});
describe("add", () => {
it("should add new entry", () => {
history.add("cmd1");
expect(history.list).toEqual(["cmd1"]);
});
it("should not add duplicate consecutive entries", () => {
history.add("cmd1");
history.add("cmd1");
expect(history.list).toEqual(["cmd1"]);
});
it("should add different entries", () => {
history.add("cmd1");
history.add("cmd2");
expect(history.list).toEqual(["cmd1", "cmd2"]);
});
it("should not add empty string", () => {
history.add("");
expect(history.list).toEqual([]);
});
});
describe("previous and next", () => {
beforeEach(() => {
history.add("cmd1");
history.add("cmd2");
history.add("cmd3");
});
it("should navigate previous", () => {
expect(history.previous()).toBe("cmd3");
expect(history.previous()).toBe("cmd2");
expect(history.previous()).toBe("cmd1");
expect(history.previous()).toBe("cmd1"); // stays at last
});
it("should navigate next", () => {
history.previous(); // cmd3
history.previous(); // cmd2
expect(history.next()).toBe("cmd3");
expect(history.next()).toBe("");
});
});
describe("clear", () => {
it("should clear all entries", () => {
history.add("cmd1");
history.clear();
expect(history.list).toEqual([]);
});
});
});

View File

@@ -0,0 +1,100 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import XInputComponent from "../source/input/index";
import type { IOutputInterface } from "../source/output/interface";
describe("XInputComponent", () => {
let input: XInputComponent;
let container: HTMLElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
input = new XInputComponent(container);
});
afterEach(() => {
input.dispose();
document.body.removeChild(container);
});
describe("constructor", () => {
it("should create input element", () => {
expect(input.el).toBeInstanceOf(HTMLTextAreaElement);
expect(container.contains(input.el)).toBe(true);
});
it("should mark terminal input as non-login field", () => {
expect(input.el.getAttribute("autocomplete")).toBe("off");
expect(input.el.getAttribute("data-form-type")).toBe("other");
expect(input.el.getAttribute("data-lpignore")).toBe("true");
expect(input.el.getAttribute("data-1p-ignore")).toBe("true");
expect(input.el.getAttribute("data-bwignore")).toBe("true");
});
it("should unlock readonly on first focus", () => {
expect(input.el.readOnly).toBe(true);
input.focus();
expect(input.el.readOnly).toBe(false);
});
});
describe("focus and blur", () => {
it("should focus the input", () => {
input.focus();
expect(document.activeElement).toBe(input.el);
});
it("should blur the input", () => {
input.focus();
input.blur();
expect(document.activeElement).not.toBe(input.el);
});
});
describe("pause and resume", () => {
it("should pause and resume", () => {
input.pause();
// @ts-expect-error testing private property
expect(input.isActive.value).toBe(false);
input.resume();
// @ts-expect-error testing private property
expect(input.isActive.value).toBe(true);
});
});
describe("setValue", () => {
it("should set input value", () => {
input.setValue("test");
expect(input.el.value).toBe("test");
// @ts-expect-error testing private property
expect(input.data.value).toBe("test");
});
});
describe("clear", () => {
it("should clear input value", () => {
input.setValue("test");
input.clear();
expect(input.el.value).toBe("");
// @ts-expect-error testing private property
expect(input.data.value).toBe("");
});
});
describe("pipe", () => {
it("should pipe to output", () => {
const output = {
el: document.createElement("div")
} as IOutputInterface;
input.pipe(output);
expect(output.el.children.length).toBe(3); // before, cursor, after
});
});
describe("dispose", () => {
it("should dispose", () => {
input.dispose();
expect(input.isDisposed).toBe(true);
});
});
});

View File

@@ -0,0 +1,77 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import XOutputComponent from "../source/output/index";
describe("XOutputComponent", () => {
let output: XOutputComponent;
let container: HTMLElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
output = new XOutputComponent(container);
});
afterEach(() => {
document.body.removeChild(container);
});
describe("constructor", () => {
it("should create output elements", () => {
expect(output.el).toBeInstanceOf(HTMLDivElement);
expect(container.contains(output.el)).toBe(true);
});
});
describe("write", () => {
it("should write data", () => {
output.write("Hello World");
expect(output.el.textContent).toContain("Hello World");
});
it("should parse newlines", () => {
output.write("line1\nline2");
expect(output.el.innerHTML).toContain("<br>");
});
it("should call callback", () => {
let called = false;
output.write("test", () => {
called = true;
});
expect(called).toBe(true);
});
it("should call onoutput", () => {
let called = false;
output.onoutput = () => {
called = true;
};
output.write("test");
expect(called).toBe(true);
});
});
describe("writeSafe", () => {
it("should escape HTML", () => {
output.writeSafe("<script>");
expect(output.el.innerHTML).toContain("&lt;script&gt;");
});
});
describe("clear", () => {
it("should clear output", () => {
output.write("test");
output.clear();
expect(output.el.innerHTML).toBe("<span></span>");
});
});
describe("clearLast", () => {
it("should clear last output", () => {
output.write("first");
output.write("second");
output.clearLast();
expect(output.el.textContent).toBe("first");
});
});
});

View File

@@ -0,0 +1,59 @@
import { describe, it, expect } from "vitest";
import { ref, createEffect } from "../source/base/reactivity";
describe("Reactivity", () => {
describe("ref", () => {
it("should create reactive value", () => {
const r = ref(10);
expect(r.value).toBe(10);
});
it("should update value", () => {
const r = ref("hello");
r.value = "world";
expect(r.value).toBe("world");
});
it("should notify effects", () => {
const r = ref(0);
let count = 0;
createEffect(() => {
count = r.value;
});
r.value = 5;
expect(count).toBe(5);
});
it("should dispose", () => {
const r = ref(0);
let count = 0;
createEffect(() => {
count = r.value;
});
r.dispose();
r.value = 10;
expect(count).toBe(0); // should not update
});
});
describe("createEffect", () => {
it("should run effect immediately", () => {
let ran = false;
createEffect(() => {
ran = true;
});
expect(ran).toBe(true);
});
it("should subscribe to reactive values", () => {
const r = ref(0);
let value = 0;
createEffect(() => {
value = r.value * 2;
});
expect(value).toBe(0);
r.value = 5;
expect(value).toBe(10);
});
});
});

14
xterminal/tests/setup.ts Normal file
View File

@@ -0,0 +1,14 @@
import { beforeAll } from "vitest";
// Mock scrollTo for jsdom
Object.defineProperty(HTMLElement.prototype, "scrollTo", {
writable: true,
value: function (x: number, y: number) {
this.scrollTop = y;
this.scrollLeft = x;
}
});
beforeAll(() => {
// Any global setup
});

View File

@@ -0,0 +1,193 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import XTerminal from "../source/index";
describe("XTerminal", () => {
let terminal: XTerminal;
let container: HTMLElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
terminal = new XTerminal();
});
afterEach(() => {
if (terminal.isMounted) {
terminal.dispose();
}
document.body.removeChild(container);
});
describe("constructor", () => {
it("should create a new instance", () => {
expect(terminal).toBeInstanceOf(XTerminal);
expect(terminal.isMounted).toBe(false);
});
it("should mount if target is provided", () => {
const term = new XTerminal({ target: container });
expect(term.isMounted).toBe(true);
term.dispose();
});
});
describe("mount", () => {
it("should mount to a valid HTMLElement", () => {
terminal.mount(container);
expect(terminal.isMounted).toBe(true);
});
it("should mount to a selector string", () => {
container.id = "test-terminal";
terminal.mount("#test-terminal");
expect(terminal.isMounted).toBe(true);
});
it("should throw error for invalid target", () => {
// @ts-expect-error testing invalid input
expect(() => terminal.mount(null)).toThrow();
});
it("should not mount twice", () => {
terminal.mount(container);
terminal.mount(container);
expect(terminal.isMounted).toBe(true);
});
});
describe("write", () => {
beforeEach(() => {
terminal.mount(container);
});
it("should write string data", () => {
terminal.write("Hello World");
// Check if output contains the text
expect(container.textContent).toContain("Hello World");
});
it("should write number data", () => {
terminal.write(123);
expect(container.textContent).toContain("123");
});
it("should call callback after write", () => {
let called = false;
terminal.write("test", () => {
called = true;
});
expect(called).toBe(true);
});
});
describe("writeln", () => {
beforeEach(() => {
terminal.mount(container);
});
it("should write with newline", () => {
terminal.writeln("Hello");
expect(container.innerHTML).toContain("<br>");
});
});
describe("writeSafe", () => {
beforeEach(() => {
terminal.mount(container);
});
it("should escape HTML", () => {
terminal.writeSafe('<script>alert("xss")</script>');
expect(container.innerHTML).toContain("&lt;script&gt;");
});
});
describe("clear", () => {
beforeEach(() => {
terminal.mount(container);
terminal.write("test");
});
it("should clear the terminal", () => {
terminal.clear();
expect(container.textContent.trim()).toBe("");
});
});
describe("history", () => {
beforeEach(() => {
terminal.mount(container);
});
it("should have empty history initially", () => {
expect(terminal.history).toEqual([]);
});
it("should allow setting history", () => {
terminal.history = ["cmd1", "cmd2"];
expect(terminal.history).toEqual(["cmd1", "cmd2"]);
});
it("should clear history", () => {
terminal.history = ["cmd1"];
terminal.clearHistory();
expect(terminal.history).toEqual([]);
});
});
describe("setCompleter", () => {
beforeEach(() => {
terminal.mount(container);
});
it("should set completer function", () => {
const completer = (data: string) => data + " completed";
terminal.setCompleter(completer);
// Completer is internal, hard to test directly
expect(true).toBe(true); // Placeholder
});
it("should not set non-function", () => {
// @ts-expect-error testing invalid input
terminal.setCompleter("not a function");
expect(true).toBe(true);
});
});
describe("pause and resume", () => {
beforeEach(() => {
terminal.mount(container);
});
it("should pause and resume", () => {
terminal.pause();
terminal.resume();
expect(true).toBe(true); // Events are emitted, but hard to test without spying
});
});
describe("dispose", () => {
beforeEach(() => {
terminal.mount(container);
});
it("should dispose the terminal", () => {
terminal.dispose();
expect(terminal.isMounted).toBe(false);
});
});
describe("static methods", () => {
it("should have version", () => {
expect(typeof XTerminal.version).toBe("string");
});
it("should have XEventEmitter", () => {
expect(XTerminal.XEventEmitter).toBeDefined();
});
it("should escape HTML", () => {
expect(XTerminal.escapeHTML("<test>")).toBe("&lt;test&gt;");
});
});
});