first commit
This commit is contained in:
61
xterminal/tests/disposable.test.ts
Normal file
61
xterminal/tests/disposable.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
91
xterminal/tests/emitter.test.ts
Normal file
91
xterminal/tests/emitter.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
75
xterminal/tests/history.test.ts
Normal file
75
xterminal/tests/history.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
100
xterminal/tests/input.test.ts
Normal file
100
xterminal/tests/input.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
77
xterminal/tests/output.test.ts
Normal file
77
xterminal/tests/output.test.ts
Normal 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("<script>");
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
59
xterminal/tests/reactivity.test.ts
Normal file
59
xterminal/tests/reactivity.test.ts
Normal 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
14
xterminal/tests/setup.ts
Normal 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
|
||||
});
|
||||
193
xterminal/tests/xterminal.test.ts
Normal file
193
xterminal/tests/xterminal.test.ts
Normal 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("<script>");
|
||||
});
|
||||
});
|
||||
|
||||
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("<test>");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user