const { test, expect } = require("./fixtures.cjs");
const { execSync } = require("child_process");
const BASE = process.env.MOBUX_URL || "https://localhost:5151";
const USER = process.env.MOBUX_USER || "";
const PASS = process.env.MOBUX_PASS || "";
const AUTH =
USER && PASS
? "Basic " + Buffer.from(`${USER}:${PASS}`).toString("base64")
: null;
const SESSION = process.env.MOBUX_TEST_SESSION || "mobux-conformance";
const { createTmuxRunner } = require("./lib/tmux.cjs");
const SANDBOX_HOME = process.env.MOBUX_TEST_HOME || "/tmp/mobux-smoke/home";
const SHELL_ENV = `-e HISTFILE=/dev/null -e HOME=${SANDBOX_HOME}`;
const tmux = createTmuxRunner("mobux-test");
test.use({
...(AUTH ? { extraHTTPHeaders: { Authorization: AUTH } } : {}),
});
test.beforeAll(() => {
try {
tmux(`kill-session -t ${SESSION}`);
} catch (_) {}
tmux(`new-session -d -s ${SESSION} ${SHELL_ENV} "bash --norc --noprofile"`);
tmux(`send-keys -t ${SESSION} "PS1='\\$ '" Enter`);
tmux(`send-keys -t ${SESSION} "clear" Enter`);
execSync("sleep 0.3");
});
test.afterAll(() => {
try {
tmux(`kill-session -t ${SESSION}`);
} catch (_) {}
});
async function boot(page) {
await page.goto(`${BASE}/app#/s/${SESSION}`);
await page.waitForFunction(() => typeof window.__mobuxView !== "undefined", {
timeout: 8000,
});
await page.waitForFunction(
() => window.__mobuxView?.test?.wsReady?.() === true,
{ timeout: 15000 },
);
}
async function bufferHasText(page, marker) {
return page.evaluate((m) => {
const len = window.__mobuxView.test.bufferLength();
for (let y = 0; y < len; y++) {
const t = window.__mobuxView.test.lineText(y);
if (t && t.includes(m)) return true;
}
return false;
}, marker);
}
test("R1: create mounts the engine, navigation disposes it", async ({
page,
}) => {
await boot(page);
expect(await page.evaluate(() => !!window.__mobuxView)).toBe(true);
await page.goto(`${BASE}/app#/`);
await page.waitForFunction(() => window.__mobuxView === undefined, {
timeout: 8000,
});
expect(await page.evaluate(() => window.__mobuxView === undefined)).toBe(
true,
);
});
test("R2: write resolves after the buffer reflects the data", async ({
page,
}) => {
await boot(page);
await page.evaluate(() =>
window.__mobuxView.test.inject("CONFORMANCE_R2_MARKER\n"),
);
expect(await bufferHasText(page, "CONFORMANCE_R2_MARKER")).toBe(true);
});
test("R3/R4: measure and current grid are positive", async ({ page }) => {
await boot(page);
const dims = await page.evaluate(() => ({
m: window.__mobuxView.test.measure(),
cell: window.__mobuxView.test.cellMetrics(),
cols: window.__mobuxView.test.cols(),
rows: window.__mobuxView.test.rows(),
}));
expect(dims.m.cols).toBeGreaterThan(0);
expect(dims.m.rows).toBeGreaterThan(0);
expect(dims.m.cellWidth).toBeGreaterThan(0);
expect(dims.m.cellHeight).toBeGreaterThan(0);
expect(dims.cell.width).toBeGreaterThan(0);
expect(dims.cell.height).toBeGreaterThan(0);
expect(dims.cols).toBeGreaterThan(0);
expect(dims.rows).toBeGreaterThan(0);
});
test("R5: keystrokes typed into the renderer reach the PTY", async ({
page,
}) => {
await boot(page);
await page.evaluate(() => {
window.__mobuxView.test.setNativeInputEnabled(true);
window.__mobuxView.test.focus();
});
await page.keyboard.type("echo R5_CONFORMANCE_OK\r");
await page.waitForFunction(
() => {
const len = window.__mobuxView.test.bufferLength();
for (let y = 0; y < len; y++) {
const t = window.__mobuxView.test.lineText(y);
if (t && t.includes("R5_CONFORMANCE_OK")) return true;
}
return false;
},
{ timeout: 15000 },
);
});
test("R6: scrollLines and scrollToBottom move the viewport", async ({
page,
}) => {
await boot(page);
await page.evaluate(() => window.__mobuxView.test.injectLines(300, "r6seed"));
await page.waitForFunction(
() => window.__mobuxView.test.bufferLength() > 200,
{ timeout: 10000 },
);
await page.evaluate(() => window.__mobuxView.test.scrollToBottom());
const bottom = await page.evaluate(() => window.__mobuxView.test.viewportY());
await page.evaluate(() => window.__mobuxView.test.scrollLines(-10));
const scrolled = await page.evaluate(() =>
window.__mobuxView.test.viewportY(),
);
expect(scrolled).toBeLessThan(bottom);
await page.evaluate(() => window.__mobuxView.test.scrollToBottom());
const back = await page.evaluate(() => window.__mobuxView.test.viewportY());
expect(back).toBe(bottom);
});
test("R7: buffer exposes lines and styled cells", async ({ page }) => {
await boot(page);
await page.evaluate(() =>
window.__mobuxView.test.inject("R7_CELL_SURFACE_XYZ\n"),
);
const info = await page.evaluate((m) => {
const len = window.__mobuxView.test.bufferLength();
for (let y = 0; y < len; y++) {
const t = window.__mobuxView.test.lineText(y);
if (t && t.includes(m)) {
const x = t.indexOf(m);
return { text: t, cell: window.__mobuxView.test.cellInfo(y, x) };
}
}
return null;
}, "R7_CELL_SURFACE_XYZ");
expect(info).not.toBeNull();
expect(info.cell).not.toBeNull();
expect(info.cell.chars).toBe("R");
expect(info.cell.code).toBe("R".charCodeAt(0));
expect(info.cell.bold).toBeDefined();
expect(info.cell.fgDefault).toBeDefined();
expect(info.cell.bgDefault).toBeDefined();
});
test("R8: onBufferChanged fires after a write", async ({ page }) => {
await boot(page);
const observed = await page.evaluate(
() =>
new Promise((resolve) => {
let done = false;
const settle = (v) => {
if (!done) {
done = true;
resolve(v);
}
};
window.__mobuxView.test.awaitBufferChange().then(() => settle(true));
setTimeout(() => settle(false), 5000);
window.__mobuxView.test.writeData("R8_CHANGE\r\n");
}),
);
expect(observed).toBe(true);
});
test("R9/R16: reports the normal (non-alternate) screen", async ({ page }) => {
await boot(page);
await page.evaluate(() =>
window.__mobuxView.test.inject("R9_NORMAL_SCREEN\n"),
);
expect(await page.evaluate(() => window.__mobuxView.test.isAlternate())).toBe(
false,
);
});
test("R16: mixed DEC private modes suppress the blocked mode, keep the allowed one", async ({
page,
}) => {
await boot(page);
await page.evaluate(() => window.__mobuxView.test.inject(""));
await page.evaluate(() =>
window.__mobuxView.test.writeData("\x1b[?1049;25h"),
);
expect(await page.evaluate(() => window.__mobuxView.test.isAlternate())).toBe(
false,
);
await page.evaluate(() =>
window.__mobuxView.test.writeData(
"\x1b[2J\x1b[HR16SAVE\x1b[?1002;1048h\r\nR16OTHR\x1b[?1048lZ",
),
);
const restored = await page.evaluate(() => {
const len = window.__mobuxView.test.bufferLength();
for (let y = 0; y < len; y++) {
const t = window.__mobuxView.test.lineText(y);
if (t && t.includes("R16SAVEZ")) return true;
}
return false;
});
expect(restored).toBe(true);
expect(await page.evaluate(() => window.__mobuxView.test.isAlternate())).toBe(
false,
);
});
test("R10: OSC 133 markers are detected via registerOscHandler", async ({
page,
}) => {
await boot(page);
expect(await page.evaluate(() => window.__mobuxView.test.oscDetected())).toBe(
false,
);
await page.evaluate(() =>
window.__mobuxView.test.writeData("\x1b]133;A\x07prompt$ \r\n"),
);
await page.waitForFunction(
() => window.__mobuxView.test.oscDetected() === true,
{ timeout: 5000 },
);
expect(
await page.evaluate(() => window.__mobuxView.test.oscMarkerCount()),
).toBeGreaterThan(0);
});
test("R11: setFontSize / getFontSize round-trip", async ({ page }) => {
await boot(page);
const result = await page.evaluate(() => {
const before = window.__mobuxView.test.getFontSize();
window.__mobuxView.test.setFontSize(before + 3);
const after = window.__mobuxView.test.getFontSize();
window.__mobuxView.test.setFontSize(before);
return { before, after, restored: window.__mobuxView.test.getFontSize() };
});
expect(result.after).toBe(result.before + 3);
expect(result.restored).toBe(result.before);
});
test("R15: focus and setNativeInputEnabled are callable", async ({ page }) => {
await boot(page);
const ok = await page.evaluate(() => {
window.__mobuxView.test.setNativeInputEnabled(false);
window.__mobuxView.test.setNativeInputEnabled(true);
window.__mobuxView.test.setNativeInputEnabled(false);
window.__mobuxView.test.focus();
return true;
});
expect(ok).toBe(true);
});
test("R12: selectAll, getSelection, onSelectionChange, clearSelection", async ({
page,
}) => {
await boot(page);
await page.evaluate(() =>
window.__mobuxView.test.inject("R12_SELECTION_MARKER\n"),
);
await page.waitForFunction(() => window.__mobuxView.test.bufferLength() > 0, {
timeout: 8000,
});
const changePromise = page.evaluate(
() =>
new Promise((resolve) => {
let done = false;
const settle = (v) => {
if (!done) {
done = true;
resolve(v);
}
};
window.__mobuxView.test.awaitSelectionChange().then(() => settle(true));
setTimeout(() => settle(false), 5000);
}),
);
await page.waitForTimeout(50);
await page.evaluate(() => window.__mobuxView.test.selectAll());
expect(await changePromise).toBe(true);
expect(
await page.evaluate(() => window.__mobuxView.test.hasSelection()),
).toBe(true);
const selected = await page.evaluate(() =>
window.__mobuxView.test.getSelection(),
);
expect(selected).toContain("R12_SELECTION_MARKER");
await page.evaluate(() => window.__mobuxView.test.clearSelection());
expect(
await page.evaluate(() => window.__mobuxView.test.hasSelection()),
).toBe(false);
});
test("R13: URL activation is reported through onLink", async ({ page }) => {
await boot(page);
const URL = "https://example.com/r13-conformance";
await page.evaluate(() => {
const o = document.getElementById("touchOverlay");
if (o) o.style.pointerEvents = "none";
const q = document.getElementById("loadquote");
if (q) q.remove();
});
await page.evaluate(
(u) => window.__mobuxView.test.inject(`open ${u}\x1b[K\n`),
URL,
);
await page.evaluate(() => window.__mobuxView.test.scrollToBottom());
await page.waitForFunction(
(u) => {
const len = window.__mobuxView.test.bufferLength();
for (let y = 0; y < len; y++) {
const t = window.__mobuxView.test.lineText(y);
if (t && t.includes(u)) return true;
}
return false;
},
URL,
{ timeout: 10000 },
);
const point = await page.evaluate((u) => {
const len = window.__mobuxView.test.bufferLength();
const vy = window.__mobuxView.test.viewportY();
const rows = window.__mobuxView.test.rows();
const cell = window.__mobuxView.test.cellMetrics();
const rect = document.getElementById("terminal").getBoundingClientRect();
for (let y = vy; y < Math.min(len, vy + rows); y++) {
const t = window.__mobuxView.test.lineText(y);
const idx = t ? t.indexOf(u) : -1;
if (idx >= 0) {
const col = idx + Math.floor(u.length / 2);
return {
x: rect.left + (col + 0.5) * cell.width,
y: rect.top + (y - vy + 0.5) * cell.height,
};
}
}
return null;
}, URL);
expect(point).not.toBeNull();
const linkPromise = page.evaluate(
() =>
new Promise((resolve) => {
let done = false;
const settle = (v) => {
if (!done) {
done = true;
resolve(v);
}
};
window.__mobuxView.test.awaitLink().then((uri) => settle(uri));
setTimeout(() => settle(null), 6000);
}),
);
await page.waitForTimeout(100);
await page.mouse.move(point.x, point.y);
await page.mouse.move(point.x, point.y);
await page.mouse.click(point.x, point.y);
expect(await linkPromise).toBe(URL);
});
test("R13: trailing punctuation is not part of the link", async ({ page }) => {
await boot(page);
const URL = "https://example.com/r13-boundary";
await page.evaluate(() => {
const o = document.getElementById("touchOverlay");
if (o) o.style.pointerEvents = "none";
const q = document.getElementById("loadquote");
if (q) q.remove();
});
await page.evaluate(
(u) => window.__mobuxView.test.inject(`see ${u}.\x1b[K\n`),
URL,
);
await page.evaluate(() => window.__mobuxView.test.scrollToBottom());
await page.waitForFunction(
(u) => {
const len = window.__mobuxView.test.bufferLength();
for (let y = 0; y < len; y++) {
const t = window.__mobuxView.test.lineText(y);
if (t && t.includes(`${u}.`)) return true;
}
return false;
},
URL,
{ timeout: 10000 },
);
const point = await page.evaluate((u) => {
const len = window.__mobuxView.test.bufferLength();
const vy = window.__mobuxView.test.viewportY();
const rows = window.__mobuxView.test.rows();
const cell = window.__mobuxView.test.cellMetrics();
const rect = document.getElementById("terminal").getBoundingClientRect();
for (let y = vy; y < Math.min(len, vy + rows); y++) {
const t = window.__mobuxView.test.lineText(y);
const idx = t ? t.indexOf(u) : -1;
if (idx >= 0) {
const col = idx + Math.floor(u.length / 2);
return {
x: rect.left + (col + 0.5) * cell.width,
y: rect.top + (y - vy + 0.5) * cell.height,
};
}
}
return null;
}, URL);
expect(point).not.toBeNull();
const linkPromise = page.evaluate(
() =>
new Promise((resolve) => {
let done = false;
const settle = (v) => {
if (!done) {
done = true;
resolve(v);
}
};
window.__mobuxView.test.awaitLink().then((uri) => settle(uri));
setTimeout(() => settle(null), 6000);
}),
);
await page.waitForTimeout(100);
await page.mouse.move(point.x, point.y);
await page.mouse.move(point.x, point.y);
await page.mouse.click(point.x, point.y);
expect(await linkPromise).toBe(URL);
});
test("R14: terminal BEL is reported through onBell", async ({ page }) => {
await boot(page);
const observed = await page.evaluate(
() =>
new Promise((resolve) => {
let done = false;
const settle = (v) => {
if (!done) {
done = true;
resolve(v);
}
};
window.__mobuxView.test.awaitBell().then(() => settle(true));
setTimeout(() => settle(false), 5000);
window.__mobuxView.test.writeData("\x07");
}),
);
expect(observed).toBe(true);
});
test("bell: a raw client-side BEL never triggers the chime", async ({
page,
}) => {
await boot(page);
await page.waitForFunction(() => typeof window.__mobuxChime === "function", {
timeout: 8000,
});
const chimed = await page.evaluate(
() =>
new Promise((resolve) => {
let called = false;
const original = window.__mobuxChime;
window.__mobuxChime = (...args) => {
called = true;
return original(...args);
};
window.__mobuxView.test.awaitBell().then(() => {
setTimeout(() => resolve(called), 200);
});
window.__mobuxView.test.writeData("\x07");
}),
);
expect(chimed).toBe(false);
});