From 92e9530adbfe5948c47b0a7b2f9b4933630dbf2c Mon Sep 17 00:00:00 2001 From: Rolando Santamaria Maso Date: Wed, 28 Oct 2026 16:53:09 +0100 Subject: [PATCH 1/4] Raise test coverage from 74% to 97% of lines New tests for the server (http, index, main, git, settings, watch, update, pty, files, browser) and the client (api, util, palette, menu, auth, updates, icons, git, highlight, terminal, boxdraw). Client DOM modules run against a small dependency-free fake DOM in test/helpers/fakedom.ts. Co-Authored-By: Claude Opus 5.5 --- test/boxdraw.test.ts | 141 +++++ test/browser.coverage.test.ts | 152 ++++++ test/client.api.test.ts | 99 ++++ test/client.auth.test.ts | 67 +++ test/client.git.test.ts | 99 ++++ test/client.icons.test.ts | 42 ++ test/client.menu.test.ts | 130 +++++ test/client.palette.test.ts | 326 +++++++++++ test/client.updates.test.ts | 250 +++++++++ test/client.util.test.ts | 219 ++++++++ test/files.coverage.test.ts | 26 + test/helpers/fakedom.ts | 350 ++++++++++++ test/highlight.coverage.test.ts | 234 ++++++++ test/http.coverage.test.ts | 477 ++++++++++++++++ test/index.coverage.test.ts | 140 +++++ test/main.test.ts | 358 ++++++++++++ test/pty.coverage.test.ts | 196 +++++++ test/servergit.coverage.test.ts | 248 +++++++++ test/settings.coverage.test.ts | 73 +++ test/terminal.coverage.test.ts | 937 ++++++++++++++++++++++++++++++++ test/update.coverage.test.ts | 271 +++++++++ test/watch.coverage.test.ts | 83 +++ 22 files changed, 4918 insertions(+) create mode 100644 test/boxdraw.test.ts create mode 100644 test/browser.coverage.test.ts create mode 100644 test/client.api.test.ts create mode 100644 test/client.auth.test.ts create mode 100644 test/client.git.test.ts create mode 100644 test/client.icons.test.ts create mode 100644 test/client.menu.test.ts create mode 100644 test/client.palette.test.ts create mode 100644 test/client.updates.test.ts create mode 100644 test/client.util.test.ts create mode 100644 test/files.coverage.test.ts create mode 100644 test/helpers/fakedom.ts create mode 100644 test/highlight.coverage.test.ts create mode 100644 test/http.coverage.test.ts create mode 100644 test/index.coverage.test.ts create mode 100644 test/main.test.ts create mode 100644 test/pty.coverage.test.ts create mode 100644 test/servergit.coverage.test.ts create mode 100644 test/settings.coverage.test.ts create mode 100644 test/terminal.coverage.test.ts create mode 100644 test/update.coverage.test.ts create mode 100644 test/watch.coverage.test.ts diff --git a/test/boxdraw.test.ts b/test/boxdraw.test.ts new file mode 100644 index 0000000..3c5bc15 --- /dev/null +++ b/test/boxdraw.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, test } from "bun:test"; +import { drawProcedural, isProcedural } from "../src/client/term/boxdraw.ts"; + +type Op = [string, ...number[]]; + +function fakeCtx() { + const ops: Op[] = []; + const stack: number[] = []; + const ctx = { + fillStyle: "#fff", + strokeStyle: "", + lineWidth: 0, + globalAlpha: 1, + save: () => (stack.push(ctx.globalAlpha), ops.push(["save"])), + restore: () => (ctx.globalAlpha = stack.pop()!, ops.push(["restore"])), + beginPath: () => ops.push(["begin"]), + stroke: () => ops.push(["stroke"]), + moveTo: (x: number, y: number) => ops.push(["moveTo", x, y]), + lineTo: (x: number, y: number) => ops.push(["lineTo", x, y]), + arcTo: (...a: number[]) => ops.push(["arcTo", ...a]), + fillRect: (x: number, y: number, w: number, h: number) => ops.push(["rect", x, y, w, h]), + }; + return { ctx: ctx as unknown as CanvasRenderingContext2D, raw: ctx, ops }; +} + +const draw = (cp: number, w = 10, h = 20, dpr = 1) => { + const f = fakeCtx(); + drawProcedural(f.ctx, cp, 0, 0, w, h, dpr); + return f; +}; + +describe("boxdraw: isProcedural", () => { + test("covers lines and blocks, excludes dashed/diagonals and other ranges", () => { + expect(isProcedural(0x2500)).toBe(true); // ─ + expect(isProcedural(0x254c)).toBe(false); // dashed + expect(isProcedural(0x2571)).toBe(false); // diagonal + expect(isProcedural(0x256d)).toBe(true); // rounded + expect(isProcedural(0x2580)).toBe(true); + expect(isProcedural(0x259f)).toBe(true); + expect(isProcedural(0x25a0)).toBe(false); + expect(isProcedural(0x41)).toBe(false); + }); +}); + +describe("boxdraw: lines", () => { + test("light horizontal is two arms overlapping at the center, spanning the cell", () => { + expect(draw(0x2500).ops).toEqual([["rect", 0, 9, 5, 1], ["rect", 4, 9, 6, 1]]); + }); + + test("light vertical", () => { + expect(draw(0x2502).ops).toEqual([["rect", 4, 0, 1, 10], ["rect", 4, 9, 1, 11]]); + }); + + test("heavy line is twice as thick", () => { + expect(draw(0x2501).ops).toEqual([["rect", 0, 9, 5, 2], ["rect", 4, 9, 6, 2]]); + }); + + test("dpr scales thickness", () => { + const { ops } = draw(0x2502, 10, 20, 2); + expect(ops[0]![3]).toBe(2); + }); + + test("corner draws two segments that meet", () => { + expect(draw(0x250c).ops).toEqual([["rect", 4, 9, 1, 11], ["rect", 4, 9, 6, 1]]); // ┌ + }); + + test("cross draws four segments", () => { + expect(draw(0x253c).ops.length).toBe(4); + }); + + test("double lines draw two parallel rects per arm", () => { + expect(draw(0x2550).ops).toEqual([["rect", 0, 8, 6, 1], ["rect", 0, 10, 6, 1], ["rect", 3, 8, 7, 1], ["rect", 3, 10, 7, 1]]); + expect(draw(0x2551).ops.length).toBe(4); + }); + + test("mixed heavy/light and half lines", () => { + expect(draw(0x257a).ops).toEqual([["rect", 4, 9, 6, 2]]); // heavy right + expect(draw(0x2574).ops.length).toBe(1); // light left + expect(draw(0x2578).ops.length).toBe(1); // heavy left + expect(draw(0x257c).ops).toEqual([["rect", 0, 9, 5, 1], ["rect", 4, 9, 6, 2]]); // light left, heavy right + }); + + test("rounded corners stroke an arc with the foreground color", () => { + for (const cp of [0x256d, 0x256e, 0x256f, 0x2570]) { + const { ops, raw } = draw(cp); + expect(ops[0]![0]).toBe("save"); + expect(ops.some((o) => o[0] === "arcTo")).toBe(true); + expect(ops.some((o) => o[0] === "stroke")).toBe(true); + expect(ops.at(-1)![0]).toBe("restore"); + expect(raw.strokeStyle).toBe("#fff"); + } + }); +}); + +describe("boxdraw: blocks", () => { + test("half blocks", () => { + expect(draw(0x2580).ops).toEqual([["rect", 0, 0, 10, 10]]); + expect(draw(0x2584).ops).toEqual([["rect", 0, 10, 10, 10]]); + expect(draw(0x258c).ops).toEqual([["rect", 0, 0, 5, 20]]); + expect(draw(0x2590).ops).toEqual([["rect", 5, 0, 5, 20]]); + }); + + test("full block and eighths", () => { + expect(draw(0x2588).ops).toEqual([["rect", 0, 0, 10, 20]]); + const low = draw(0x2581, 8, 16).ops[0]!; + expect(low.slice(1)).toEqual([0, 14, 8, 2]); + const left = draw(0x258f, 8, 16).ops[0]!; + expect(left.slice(1)).toEqual([0, 0, 1, 16]); + }); + + test("shades use alpha and restore it", () => { + const f = fakeCtx(); + const alphas: number[] = []; + const orig = f.raw.fillRect; + f.raw.fillRect = (...a: [number, number, number, number]) => { + alphas.push(f.raw.globalAlpha); + orig(...a); + }; + for (const cp of [0x2591, 0x2592, 0x2593]) drawProcedural(f.ctx, cp, 0, 0, 10, 20, 1); + expect(alphas).toEqual([0.25, 0.5, 0.75]); + expect(f.raw.globalAlpha).toBe(1); + }); + + test("upper and right eighths", () => { + expect(draw(0x2594, 8, 16).ops[0]!.slice(1)).toEqual([0, 0, 8, 2]); + expect(draw(0x2595, 8, 16).ops[0]!.slice(1)).toEqual([7, 0, 1, 16]); + }); + + test("quadrants", () => { + expect(draw(0x2596).ops).toEqual([["rect", 0, 10, 5, 10]]); // lower left + expect(draw(0x2597).ops).toEqual([["rect", 5, 10, 5, 10]]); // lower right + expect(draw(0x2598).ops).toEqual([["rect", 0, 0, 5, 10]]); // upper left + expect(draw(0x259b).ops.length).toBe(3); // UL+UR+LL + expect(draw(0x259f).ops.length).toBe(3); // UR+LL+LR + expect(draw(0x259a).ops.length).toBe(2); // UL+LR + expect(draw(0x259e).ops.length).toBe(2); // UR+LL + expect(draw(0x259c).ops.length).toBe(3); + expect(draw(0x2599).ops.length).toBe(3); + expect(draw(0x259d).ops).toEqual([["rect", 5, 0, 5, 10]]); + }); +}); diff --git a/test/browser.coverage.test.ts b/test/browser.coverage.test.ts new file mode 100644 index 0000000..774b50b --- /dev/null +++ b/test/browser.coverage.test.ts @@ -0,0 +1,152 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { chmod, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { appWindowArgs, bootPage, closeAppWindow, findAppBrowser, findMacApp, pickFolder, runAppWindow } from "../src/server/browser.ts"; + +let tmp = ""; +const env = { PATH: process.env.PATH, B: process.env.CODEVIEWER_BROWSER }; +beforeEach(async () => { + tmp = await mkdtemp(path.join(tmpdir(), "cv-browser-")); +}); +afterEach(async () => { + process.env.PATH = env.PATH; + if (env.B === undefined) delete process.env.CODEVIEWER_BROWSER; + else process.env.CODEVIEWER_BROWSER = env.B; + await rm(tmp, { recursive: true, force: true }); +}); + +async function script(name: string, body: string) { + const p = path.join(tmp, name); + await writeFile(p, `#!/bin/sh\n${body}\n`); + await chmod(p, 0o755); + return p; +} + +test("appWindowArgs isolates the window: own profile, dead proxy, no resolver", () => { + const a = appWindowArgs("file:///x.html", "/tmp/prof"); + expect(a).toContain("--app=file:///x.html"); + expect(a).toContain("--user-data-dir=/tmp/prof"); + expect(a).toContain("--proxy-server=127.0.0.1:9"); + expect(a.some((x) => x.startsWith("--host-resolver-rules=MAP * ~NOTFOUND"))).toBe(true); + expect(a).toContain("--disable-extensions"); + expect(a.every((x) => x.startsWith("--"))).toBe(true); + expect(a).not.toContain("--no-sandbox"); +}); + +test("bootPage writes a private redirect page, strips unsafe chars, and cleans up once", async () => { + const url = 'http://127.0.0.1:1234/?t=abc">#x'; + const b = await bootPage(url); + const file = fileURLToPath(b.fileUrl); + expect(existsSync(file)).toBe(true); + const html = await readFile(file, "utf8"); + expect(html).toContain('http-equiv="refresh"'); + expect(html).not.toContain("