mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix(session-sharing): synchronize shared SSH terminal dimensions (#1502)
This commit is contained in:
1 parent
c265758cdd
commit
0f6e48a95c
7 files changed
+341
-15
No files matched your search
@@ -135,7 +135,11 @@ export function GuestTerminalView({
|
|||||||
terminal.open(xtermRef.current);
|
terminal.open(xtermRef.current);
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => fitAddon.fit());
|
let sharedSize: { cols: number; rows: number } | null = null;
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (sharedSize) terminal.resize(sharedSize.cols, sharedSize.rows);
|
||||||
|
else fitAddon.fit();
|
||||||
|
});
|
||||||
resizeObserver.observe(xtermRef.current);
|
resizeObserver.observe(xtermRef.current);
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -163,6 +167,21 @@ export function GuestTerminalView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
|
case "resized": {
|
||||||
|
const { cols, rows } = msg;
|
||||||
|
if (
|
||||||
|
typeof cols === "number" &&
|
||||||
|
typeof rows === "number" &&
|
||||||
|
Number.isInteger(cols) &&
|
||||||
|
Number.isInteger(rows) &&
|
||||||
|
cols > 0 &&
|
||||||
|
rows > 0
|
||||||
|
) {
|
||||||
|
sharedSize = { cols, rows };
|
||||||
|
terminal.resize(cols, rows);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "data":
|
case "data":
|
||||||
if (typeof msg.data === "string") terminal.write(msg.data);
|
if (typeof msg.data === "string") terminal.write(msg.data);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { act, cleanup, render, waitFor } from "@testing-library/react";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { GuestTerminalView } from "../../src/frontend/SharedSessionView";
|
||||||
|
|
||||||
|
const f = vi.hoisted(() => ({
|
||||||
|
terminal: {
|
||||||
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
options: {},
|
||||||
|
loadAddon: vi.fn(),
|
||||||
|
open: vi.fn(),
|
||||||
|
write: vi.fn(),
|
||||||
|
resize: vi.fn(),
|
||||||
|
},
|
||||||
|
fit: vi.fn(),
|
||||||
|
onResize: null as (() => void) | null,
|
||||||
|
}));
|
||||||
|
vi.mock("@termix/plugin-sdk/frontend", () => ({
|
||||||
|
useTranslation: () => ({ t: (key: string) => key }),
|
||||||
|
}));
|
||||||
|
vi.mock("react-xtermjs", () => ({
|
||||||
|
useXTerm: () => ({ instance: f.terminal, ref: { current: null } }),
|
||||||
|
}));
|
||||||
|
vi.mock("@xterm/addon-fit", () => ({
|
||||||
|
FitAddon: class {
|
||||||
|
fit() {
|
||||||
|
f.fit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("../../src/frontend/shared", () => ({
|
||||||
|
wsUrlForPath: async () => "ws://termix.test/terminal",
|
||||||
|
RemoteDisplay: () => null,
|
||||||
|
Loader: () => null,
|
||||||
|
}));
|
||||||
|
class Socket {
|
||||||
|
static OPEN = 1;
|
||||||
|
static latest: Socket;
|
||||||
|
readyState = 1;
|
||||||
|
onopen?: () => void;
|
||||||
|
onmessage?: (event: { data: string }) => void;
|
||||||
|
onclose?: () => void;
|
||||||
|
onerror?: () => void;
|
||||||
|
constructor() {
|
||||||
|
Socket.latest = this;
|
||||||
|
}
|
||||||
|
send() {}
|
||||||
|
close() {}
|
||||||
|
message(value: object) {
|
||||||
|
this.onmessage?.({ data: JSON.stringify(value) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
f.terminal.cols = 80;
|
||||||
|
f.terminal.rows = 24;
|
||||||
|
f.terminal.resize.mockImplementation((cols: number, rows: number) => {
|
||||||
|
f.terminal.cols = cols;
|
||||||
|
f.terminal.rows = rows;
|
||||||
|
});
|
||||||
|
f.fit.mockImplementation(() => {
|
||||||
|
f.terminal.cols = 80;
|
||||||
|
f.terminal.rows = 24;
|
||||||
|
});
|
||||||
|
vi.stubGlobal("WebSocket", Socket);
|
||||||
|
vi.stubGlobal(
|
||||||
|
"ResizeObserver",
|
||||||
|
class {
|
||||||
|
constructor(fn: () => void) {
|
||||||
|
f.onResize = fn;
|
||||||
|
}
|
||||||
|
observe() {}
|
||||||
|
disconnect() {}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the presenter's grid while the guest window changes size", async () => {
|
||||||
|
render(
|
||||||
|
<GuestTerminalView
|
||||||
|
share={{ permissionLevel: "read-only" }}
|
||||||
|
wsPath="/plugin-ws/ssh-terminal/terminal?shareToken=test"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await waitFor(() => expect(Socket.latest.onmessage).toBeTypeOf("function"));
|
||||||
|
act(() => Socket.latest.message({ type: "resized", cols: 132, rows: 40 }));
|
||||||
|
expect([f.terminal.cols, f.terminal.rows]).toEqual([132, 40]);
|
||||||
|
act(() => f.onResize?.());
|
||||||
|
expect([f.terminal.cols, f.terminal.rows]).toEqual([132, 40]);
|
||||||
|
act(() => Socket.latest.message({ type: "resized", cols: 100, rows: 30 }));
|
||||||
|
expect([f.terminal.cols, f.terminal.rows]).toEqual([100, 30]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("invalid shared terminal dimensions", () => {
|
||||||
|
it.each([0, -1, 1.5, "132"])("ignores cols=%s", async (cols) => {
|
||||||
|
render(
|
||||||
|
<GuestTerminalView
|
||||||
|
share={{ permissionLevel: "read-only" }}
|
||||||
|
wsPath="/plugin-ws/ssh-terminal/terminal?shareToken=test"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await waitFor(() => expect(Socket.latest.onmessage).toBeTypeOf("function"));
|
||||||
|
act(() => Socket.latest.message({ type: "resized", cols, rows: 24 }));
|
||||||
|
expect(f.terminal.resize).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -444,6 +444,15 @@ export class TerminalSessionManager {
|
|||||||
tabInstanceId: opts.tabInstanceId,
|
tabInstanceId: opts.tabInstanceId,
|
||||||
joinedViaShareId: opts.shareId,
|
joinedViaShareId: opts.shareId,
|
||||||
});
|
});
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "resized",
|
||||||
|
cols: session.cols,
|
||||||
|
rows: session.rows,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
this.broadcastParticipants(sessionId);
|
this.broadcastParticipants(sessionId);
|
||||||
|
|
||||||
this.log.info("Participant joined shared session", {
|
this.log.info("Participant joined shared session", {
|
||||||
@@ -812,6 +821,15 @@ export class TerminalSessionManager {
|
|||||||
this.recordSessionEvent(session, "i", data);
|
this.recordSessionEvent(session, "i", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resizeSession(sessionId: string, cols: number, rows: number): void {
|
||||||
|
const session = this.sessions.get(sessionId);
|
||||||
|
if (!session) return;
|
||||||
|
session.cols = cols;
|
||||||
|
session.rows = rows;
|
||||||
|
this.broadcast(sessionId, { type: "resized", cols, rows });
|
||||||
|
this.bufferResize(sessionId, cols, rows);
|
||||||
|
}
|
||||||
|
|
||||||
bufferResize(sessionId: string, cols: number, rows: number): void {
|
bufferResize(sessionId: string, cols: number, rows: number): void {
|
||||||
const session = this.sessions.get(sessionId);
|
const session = this.sessions.get(sessionId);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|||||||
@@ -568,8 +568,11 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
|||||||
attachRows,
|
attachRows,
|
||||||
attachCols,
|
attachCols,
|
||||||
);
|
);
|
||||||
session.cols = attachCols;
|
sessionManager.resizeSession(
|
||||||
session.rows = attachRows;
|
session.id,
|
||||||
|
attachCols,
|
||||||
|
attachRows,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.send(
|
ws.send(
|
||||||
@@ -2871,11 +2874,8 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
|||||||
resizeStream.setWindow(rows, cols, rows, cols);
|
resizeStream.setWindow(rows, cols, rows, cols);
|
||||||
const session = sessionManager.getSession(currentSessionId);
|
const session = sessionManager.getSession(currentSessionId);
|
||||||
if (session) {
|
if (session) {
|
||||||
session.cols = cols;
|
sessionManager.resizeSession(session.id, cols, rows);
|
||||||
session.rows = rows;
|
|
||||||
sessionManager.bufferResize(session.id, cols, rows);
|
|
||||||
}
|
}
|
||||||
ws.send(JSON.stringify({ type: "resized", cols, rows }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -910,6 +910,33 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sharedSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||||
|
|
||||||
|
function applySharedSize(msg: { cols?: unknown; rows?: unknown }) {
|
||||||
|
if (!hostConfig.joinShareId || !terminal) return;
|
||||||
|
const { cols, rows } = msg;
|
||||||
|
if (
|
||||||
|
typeof cols !== "number" ||
|
||||||
|
typeof rows !== "number" ||
|
||||||
|
!Number.isInteger(cols) ||
|
||||||
|
!Number.isInteger(rows) ||
|
||||||
|
cols < 1 ||
|
||||||
|
rows < 1
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
sharedSizeRef.current = { cols, rows };
|
||||||
|
terminal.resize(cols, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitTerminal() {
|
||||||
|
const size = sharedSizeRef.current;
|
||||||
|
if (hostConfig.joinShareId && size) {
|
||||||
|
terminal?.resize(size.cols, size.rows);
|
||||||
|
} else {
|
||||||
|
fitAddonRef.current?.fit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function performFit() {
|
function performFit() {
|
||||||
if (
|
if (
|
||||||
!fitAddonRef.current ||
|
!fitAddonRef.current ||
|
||||||
@@ -923,7 +950,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
isFittingRef.current = true;
|
isFittingRef.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fitAddonRef.current.fit();
|
fitTerminal();
|
||||||
if (terminal && terminal.cols > 0 && terminal.rows > 0) {
|
if (terminal && terminal.cols > 0 && terminal.rows > 0) {
|
||||||
const lastSize = lastFittedSizeRef.current;
|
const lastSize = lastFittedSizeRef.current;
|
||||||
if (
|
if (
|
||||||
@@ -1180,6 +1207,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNotify(cols: number, rows: number) {
|
function scheduleNotify(cols: number, rows: number) {
|
||||||
|
if (hostConfig.joinShareId) return;
|
||||||
if (!(cols > 0 && rows > 0)) return;
|
if (!(cols > 0 && rows > 0)) return;
|
||||||
pendingSizeRef.current = { cols, rows };
|
pendingSizeRef.current = { cols, rows };
|
||||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||||
@@ -1390,7 +1418,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
if (!fitAddonRef.current || !terminal || isFittingRef.current) return;
|
if (!fitAddonRef.current || !terminal || isFittingRef.current) return;
|
||||||
isFittingRef.current = true;
|
isFittingRef.current = true;
|
||||||
try {
|
try {
|
||||||
fitAddonRef.current.fit();
|
fitTerminal();
|
||||||
if (terminal.cols > 0 && terminal.rows > 0) {
|
if (terminal.cols > 0 && terminal.rows > 0) {
|
||||||
const lastSize = lastFittedSizeRef.current;
|
const lastSize = lastFittedSizeRef.current;
|
||||||
if (
|
if (
|
||||||
@@ -1839,7 +1867,9 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
pongReceivedRef.current = true;
|
pongReceivedRef.current = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.type === "data") {
|
if (msg.type === "resized") {
|
||||||
|
applySharedSize(msg);
|
||||||
|
} else if (msg.type === "data") {
|
||||||
if (typeof msg.data === "string") {
|
if (typeof msg.data === "string") {
|
||||||
outputListenersRef.current.forEach((listener) =>
|
outputListenersRef.current.forEach((listener) =>
|
||||||
listener(msg.data),
|
listener(msg.data),
|
||||||
@@ -2674,7 +2704,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
});
|
});
|
||||||
document.fonts.ready.then(() => {
|
document.fonts.ready.then(() => {
|
||||||
terminal.refresh(0, terminal.rows - 1);
|
terminal.refresh(0, terminal.rows - 1);
|
||||||
fitAddon.fit();
|
fitTerminal();
|
||||||
});
|
});
|
||||||
|
|
||||||
terminal.attachCustomWheelEventHandler((ev) => {
|
terminal.attachCustomWheelEventHandler((ev) => {
|
||||||
@@ -2699,12 +2729,12 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
fitAddonRef.current?.fit();
|
fitTerminal();
|
||||||
// Double-rAF ensures layout is fully settled (fonts, flexbox, etc.) before
|
// Double-rAF ensures layout is fully settled (fonts, flexbox, etc.) before
|
||||||
// committing the fitted size, preventing the "terminal too short" glitch.
|
// committing the fitted size, preventing the "terminal too short" glitch.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
fitAddonRef.current?.fit();
|
fitTerminal();
|
||||||
setIsFitted(true);
|
setIsFitted(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3383,10 +3413,10 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
fitAddonRef.current?.fit();
|
fitTerminal();
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
fitAddonRef.current?.fit();
|
fitTerminal();
|
||||||
if (terminal.cols > 0 && terminal.rows > 0) {
|
if (terminal.cols > 0 && terminal.rows > 0) {
|
||||||
scheduleNotify(terminal.cols, terminal.rows);
|
scheduleNotify(terminal.cols, terminal.rows);
|
||||||
connectToHost(terminal.cols, terminal.rows);
|
connectToHost(terminal.cols, terminal.rows);
|
||||||
|
|||||||
@@ -182,6 +182,46 @@ describe("TerminalSessionManager - multiplayer participants", () => {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it("sends the presenter dimensions before a participant replays output", () => {
|
||||||
|
const id = createConnectedSession();
|
||||||
|
const session = sessionManager.getSession(id)!;
|
||||||
|
session.cols = 132;
|
||||||
|
session.rows = 40;
|
||||||
|
const ws = makeFakeWs();
|
||||||
|
sessionManager.joinAsParticipant(id, ws, {
|
||||||
|
userId: "viewer",
|
||||||
|
permissionLevel: "read-only",
|
||||||
|
});
|
||||||
|
expect(JSON.parse(vi.mocked(ws.send).mock.calls[0][0] as string)).toEqual({
|
||||||
|
type: "resized",
|
||||||
|
cols: 132,
|
||||||
|
rows: 40,
|
||||||
|
});
|
||||||
|
sessionManager.destroySession(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("broadcasts later size changes and retains them for new participants", () => {
|
||||||
|
const id = createConnectedSession();
|
||||||
|
const owner = makeFakeWs();
|
||||||
|
const viewer = makeFakeWs();
|
||||||
|
sessionManager.attachWs(id, "owner-1", owner);
|
||||||
|
sessionManager.joinAsParticipant(id, viewer, {
|
||||||
|
userId: "viewer",
|
||||||
|
permissionLevel: "read-only",
|
||||||
|
});
|
||||||
|
sessionManager.resizeSession(id, 100, 35);
|
||||||
|
const message = JSON.stringify({ type: "resized", cols: 100, rows: 35 });
|
||||||
|
expect(owner.send).toHaveBeenCalledWith(message);
|
||||||
|
expect(viewer.send).toHaveBeenCalledWith(message);
|
||||||
|
const newcomer = makeFakeWs();
|
||||||
|
sessionManager.joinAsParticipant(id, newcomer, {
|
||||||
|
userId: null,
|
||||||
|
permissionLevel: "read-only",
|
||||||
|
});
|
||||||
|
expect(vi.mocked(newcomer.send).mock.calls[0][0]).toBe(message);
|
||||||
|
sessionManager.destroySession(id);
|
||||||
|
});
|
||||||
|
|
||||||
it("joinAsParticipant adds a participant without evicting the owner", () => {
|
it("joinAsParticipant adds a participant without evicting the owner", () => {
|
||||||
const id = createConnectedSession();
|
const id = createConnectedSession();
|
||||||
const ownerWs = makeFakeWs();
|
const ownerWs = makeFakeWs();
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import ts from "typescript";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import sourceText from "../../../src/frontend/terminal/Terminal.tsx?raw";
|
||||||
|
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
"Terminal.tsx",
|
||||||
|
sourceText,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true,
|
||||||
|
ts.ScriptKind.TSX,
|
||||||
|
);
|
||||||
|
const names = [
|
||||||
|
"setupWebSocketListeners",
|
||||||
|
"applySharedSize",
|
||||||
|
"fitTerminal",
|
||||||
|
"scheduleNotify",
|
||||||
|
];
|
||||||
|
const functions: string[] = [];
|
||||||
|
function visit(node: ts.Node) {
|
||||||
|
if (
|
||||||
|
ts.isFunctionDeclaration(node) &&
|
||||||
|
node.name &&
|
||||||
|
names.includes(node.name.text)
|
||||||
|
)
|
||||||
|
functions.push(node.getText(source));
|
||||||
|
ts.forEachChild(node, visit);
|
||||||
|
}
|
||||||
|
visit(source);
|
||||||
|
if (functions.length !== names.length)
|
||||||
|
throw new Error("Missing terminal size handlers");
|
||||||
|
const create = new Function(
|
||||||
|
"state",
|
||||||
|
ts.transpileModule(
|
||||||
|
`
|
||||||
|
const { terminal, hostConfig, sharedSizeRef, fitAddonRef, webSocketRef, emitSessionMessage } = state;
|
||||||
|
const connectionAttemptIdRef = { current: 1 };
|
||||||
|
${functions.join("\n")}
|
||||||
|
setupWebSocketListeners(webSocketRef.current, 80, 24, hostConfig);
|
||||||
|
return { fit: fitTerminal, notify: scheduleNotify };
|
||||||
|
`,
|
||||||
|
{ compilerOptions: { target: ts.ScriptTarget.ES2022 } },
|
||||||
|
).outputText,
|
||||||
|
) as (state: Record<string, unknown>) => {
|
||||||
|
fit: () => void;
|
||||||
|
notify: (cols: number, rows: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function fixture(shared = true) {
|
||||||
|
const events = new Map<string, (event: { data: string }) => void>();
|
||||||
|
const terminal = {
|
||||||
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
resize: vi.fn((cols: number, rows: number) => {
|
||||||
|
terminal.cols = cols;
|
||||||
|
terminal.rows = rows;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const fit = vi.fn(() => {
|
||||||
|
terminal.cols = 80;
|
||||||
|
terminal.rows = 24;
|
||||||
|
});
|
||||||
|
const socket = {
|
||||||
|
addEventListener: (name: string, fn: (event: { data: string }) => void) =>
|
||||||
|
events.set(name, fn),
|
||||||
|
send: vi.fn(),
|
||||||
|
};
|
||||||
|
const handlers = create({
|
||||||
|
terminal,
|
||||||
|
hostConfig: shared ? { joinShareId: "share" } : {},
|
||||||
|
sharedSizeRef: { current: null },
|
||||||
|
fitAddonRef: { current: { fit } },
|
||||||
|
webSocketRef: { current: socket },
|
||||||
|
emitSessionMessage: vi.fn(),
|
||||||
|
});
|
||||||
|
const resize = (cols: unknown, rows: unknown) =>
|
||||||
|
events.get("message")!({
|
||||||
|
data: JSON.stringify({ type: "resized", cols, rows }),
|
||||||
|
});
|
||||||
|
return { terminal, fit, handlers, socket, resize };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("in-app shared terminal sizing", () => {
|
||||||
|
it("uses presenter sizes and does not refit or resize the remote shell from the viewer", () => {
|
||||||
|
const f = fixture();
|
||||||
|
f.resize(132, 40);
|
||||||
|
expect([f.terminal.cols, f.terminal.rows]).toEqual([132, 40]);
|
||||||
|
f.handlers.fit();
|
||||||
|
expect(f.fit).not.toHaveBeenCalled();
|
||||||
|
expect([f.terminal.cols, f.terminal.rows]).toEqual([132, 40]);
|
||||||
|
f.handlers.notify(80, 24);
|
||||||
|
expect(f.socket.send).not.toHaveBeenCalled();
|
||||||
|
f.resize(100, 30);
|
||||||
|
expect([f.terminal.cols, f.terminal.rows]).toEqual([100, 30]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues fitting the owner's terminal normally", () => {
|
||||||
|
const f = fixture(false);
|
||||||
|
f.resize(132, 40);
|
||||||
|
expect(f.terminal.resize).not.toHaveBeenCalled();
|
||||||
|
f.handlers.fit();
|
||||||
|
expect(f.fit).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([0, -1, 1.5, "132"])("rejects an invalid column count %s", (cols) => {
|
||||||
|
const f = fixture();
|
||||||
|
f.resize(cols, 24);
|
||||||
|
expect(f.terminal.resize).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user