diff --git a/package-lock.json b/package-lock.json index 3a4e2c1..a358015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "devDependencies": { "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.0", - "@termix-ssh/plugin-sdk": "^1.0.25", + "@termix-ssh/plugin-sdk": "^1.1.0", "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", @@ -46,7 +46,7 @@ "node": ">=22.12.0" }, "peerDependencies": { - "@termix-ssh/plugin-sdk": "^1.0.25", + "@termix-ssh/plugin-sdk": "^1.1.0", "drizzle-orm": "^0.45.2", "express": "^5.2.1", "multer": "^2.3.0", @@ -2291,9 +2291,9 @@ } }, "node_modules/@termix-ssh/plugin-sdk": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@termix-ssh/plugin-sdk/-/plugin-sdk-1.0.25.tgz", - "integrity": "sha512-rJlbB/Wsas+0zRQabq4JXErU1dP8S0iYUi0J1LjHeecAirZr0Ffv4QsaagAuHOPXj3XSeYTJQPX0cAkwTR+NkA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@termix-ssh/plugin-sdk/-/plugin-sdk-1.1.1.tgz", + "integrity": "sha512-/bSnsIxvQBmu4qAtPLZH3gjaVSfGggTRRCI227RC/Ay/9UvwIDWMJhbCJeVCGWyG0SIb43fvI6USFi8NxCOPcA==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/src/frontend/local-terminal/LocalTerminal.tsx b/src/frontend/local-terminal/LocalTerminal.tsx index 4a32e0b..fedb192 100644 --- a/src/frontend/local-terminal/LocalTerminal.tsx +++ b/src/frontend/local-terminal/LocalTerminal.tsx @@ -341,8 +341,8 @@ export function LocalTerminal({ )} -
-
+
+
({ + ref: { current: null as HTMLElement | null }, +})); +vi.mock("react-xtermjs", () => ({ + useXTerm: () => ({ instance: null, ref: state.ref }), +})); +vi.mock("@termix-ssh/plugin-sdk/frontend", () => ({ + useTranslation: () => ({ t: (key: string) => key }), + usePluginApi: () => null, +})); +vi.mock("@termix-ssh/plugin-sdk/ui", () => ({ + useAppTheme: () => ({ theme: "dark" }), + ComponentSlot: () => null, + Select2: () => null, +})); +vi.mock("../../../src/frontend/terminal-api", () => ({ + getDefaultAppearance: () => new Promise(() => {}), +})); +vi.mock("../../../src/frontend/terminal-tools/TerminalContextMenu", () => ({ + TerminalContextMenu: () => null, +})); + +import { LocalTerminal } from "../../../src/frontend/local-terminal/LocalTerminal"; + +afterEach(() => { + vi.restoreAllMocks(); + cleanup(); +}); + +describe("local terminal fitting inside its padded pane", () => { + it.each([300, 301, 400, 600, 601, 800])( + "keeps every fitted row inside a %dpx pane", + (height) => { + const { container } = render( + , + ); + const style = document.createElement("style"); + style.textContent = "* { box-sizing: border-box } .p-2 { padding: 8px }"; + container.append(style); + const mount = state.ref.current!; + const wrapper = mount.parentElement!; + const element = document.createElement("div"); + element.style.padding = "0px"; + mount.append(element); + const computed = window.getComputedStyle.bind(window); + const inset = (node: Element) => { + const css = computed(node); + return ( + (parseFloat(css.paddingTop) || 0) + + (parseFloat(css.paddingBottom) || 0) + ); + }; + const mountHeight = height - inset(wrapper); + // jsdom has no layout engine. Supply the mount's border-box dimensions; + // keep production markup, padding and FitAddon measurement logic real. + vi.spyOn(window, "getComputedStyle").mockImplementation((node) => { + const css = computed(node); + if (node === mount) { + return new Proxy(css, { + get(target, property) { + if (property === "getPropertyValue") + return (name: string) => + name === "height" + ? `${mountHeight}px` + : name === "width" + ? "900px" + : target.getPropertyValue(name); + return Reflect.get(target, property); + }, + }); + } + return css; + }); + const terminal = { + element, + options: { scrollback: 1000 }, + _core: { + _renderService: { + dimensions: { css: { cell: { width: 8, height: 16 } } }, + }, + }, + }; + const fit = new FitAddon(); + fit.activate(terminal as unknown as Terminal); + const dimensions = fit.proposeDimensions()!; + expect(dimensions.rows * 16 + inset(mount)).toBeLessThanOrEqual( + mountHeight, + ); + expect(inset(wrapper) + inset(mount)).toBe(16); + expect(dimensions.rows).toBe(Math.floor((height - 16) / 16)); + }, + ); +});