Commit ae7e5ae6 authored by Grant's avatar Grant
Browse files

add tests for banner, canvasUtils, constants, keybinds, and utils

Assisted-by: deepseek:v4-flash
parent 8ab0ea48
Loading
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

describe("banner", () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it("printBanner calls console.log", async () => {
    // Dynamic import so the template literal with __BUILD_YEAR__ resolves
    const { printBanner } = await import("../banner");
    printBanner();
    expect(consoleSpy).toHaveBeenCalled();
  });

  it("printBanner passes formatting arguments", async () => {
    const { printBanner } = await import("../banner");
    printBanner();
    // First arg should be the banner string, rest are formatting strings
    const call = consoleSpy.mock.calls[0];
    expect(call[0]).toContain("canvas.fediverse.events");
    // Should have at least 2 arguments (string + format specifiers)
    expect(call.length).toBeGreaterThanOrEqual(2);
  });

  it("banner includes copyright text", async () => {
    const { printBanner } = await import("../banner");
    printBanner();
    const call = consoleSpy.mock.calls[0];
    expect(call[0]).toMatch(/Copyright/);
  });
});
 No newline at end of file
+40 −0
Original line number Diff line number Diff line
import { describe, it, expect } from "vitest";
import { CanvasUtils } from "../canvas.utils";

describe("CanvasUtils", () => {
  describe("canvasToPanZoomTransform", () => {
    it("returns transform centered on the middle of the canvas", () => {
      const result = CanvasUtils.canvasToPanZoomTransform(50, 50, [100, 100]);
      expect(result.transformX).toBe(0);
      expect(result.transformY).toBe(0);
    });

    it("returns positive transform when coordinates are in the top-left", () => {
      const result = CanvasUtils.canvasToPanZoomTransform(0, 0, [100, 100]);
      expect(result.transformX).toBe(50);
      expect(result.transformY).toBe(50);
    });

    it("returns negative transform when coordinates are in the bottom-right", () => {
      const result = CanvasUtils.canvasToPanZoomTransform(100, 100, [100, 100]);
      expect(result.transformX).toBe(-50);
      expect(result.transformY).toBe(-50);
    });

    it("works with non-square canvas dimensions", () => {
      const result = CanvasUtils.canvasToPanZoomTransform(200, 50, [400, 100]);
      expect(result.transformX).toBe(0);
      expect(result.transformY).toBe(0);
    });

    it("handles large coordinates", () => {
      const result = CanvasUtils.canvasToPanZoomTransform(
        5000,
        3000,
        [10000, 6000],
      );
      expect(result.transformX).toBe(0);
      expect(result.transformY).toBe(0);
    });
  });
});
 No newline at end of file
+58 −0
Original line number Diff line number Diff line
import { describe, it, expect } from "vitest";
import {
  RECOMMENDED_INSTANCES,
  DISCORD_INVITE,
  MATRIX_ALIAS,
  MATRIX_INVITE,
  MATRIX_URL,
} from "../constants";

describe("constants", () => {
  describe("RECOMMENDED_INSTANCES", () => {
    it("each instance has required fields", () => {
      for (const instance of RECOMMENDED_INSTANCES) {
        expect(instance.name).toBeTruthy();
        expect(instance.url).toMatch(/^https:\/\//);
        expect(instance.software).toBeTruthy();
        expect(instance.software.name).toBeTruthy();
        expect(instance.software.like).toBeTruthy();
      }
    });

    it("all instance URLs are valid https URLs", () => {
      for (const instance of RECOMMENDED_INSTANCES) {
        expect(() => new URL(instance.url)).not.toThrow();
        expect(instance.url.startsWith("https://")).toBe(true);
      }
    });
  });

  describe("DISCORD_INVITE", () => {
    it("is a valid discord.gg URL", () => {
      expect(DISCORD_INVITE).toMatch(/^https:\/\/discord\.gg\//);
    });
  });

  describe("MATRIX_ALIAS", () => {
    it("valid matrix alias", () => {
      expect(MATRIX_ALIAS.startsWith("#")).toBe(true);
      expect(MATRIX_ALIAS.includes(":"));
    });
  });

  describe("MATRIX_INVITE", () => {
    it("is a matrix.to URL", () => {
      expect(MATRIX_INVITE).toMatch(/^https:\/\/matrix\.to\/#\//);
    });

    it("includes the alias", () => {
      expect(MATRIX_INVITE).toContain(MATRIX_ALIAS);
    });
  });

  describe("MATRIX_URL", () => {
    it("is a matrix:r/ URI", () => {
      expect(MATRIX_URL).toMatch(/^matrix:r\//);
    });
  });
});
+249 −0
Original line number Diff line number Diff line
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// KeybindManager is a singleton whose constructor registers document listeners.
// We test handleInteraction directly — it's the pure keybind-matching logic.

describe("KeybindManager", () => {
  let manager: typeof import("../keybinds").KeybindManager;
  let emitSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(async () => {
    // Dynamic import to avoid interference from other tests
    const mod = await import("../keybinds");
    manager = mod.KeybindManager;
    emitSpy = vi.spyOn(manager, "emit").mockImplementation(() => true);
  });

  afterEach(() => {
    emitSpy.mockRestore();
  });

  /* ─── Keyboard keybinds ─── */

  it("TOGGLE_TEMPLATE on pressing T", () => {
    manager.handleInteraction(
      { key: "KeyT" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("TOGGLE_TEMPLATE", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("TOGGLE_TEMPLATE with extra shift modifier still matches", () => {
    // Keybind doesn't specify shift, so shift doesn't block the match
    manager.handleInteraction(
      { key: "KeyT", shift: true },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("TOGGLE_TEMPLATE", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("TOGGLE_BLANK on pressing V", () => {
    manager.handleInteraction(
      { key: "KeyV" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("TOGGLE_BLANK", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("TOGGLE_BLANK on pressing B (alternative keybind)", () => {
    manager.handleInteraction(
      { key: "KeyB" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("TOGGLE_BLANK", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("TOGGLE_HEATMAP on pressing H", () => {
    manager.handleInteraction(
      { key: "KeyH" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("TOGGLE_HEATMAP", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("TOGGLE_GRID on pressing G", () => {
    manager.handleInteraction(
      { key: "KeyG" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("TOGGLE_GRID", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("TOGGLE_MOD_MENU on pressing M", () => {
    manager.handleInteraction(
      { key: "KeyM" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("TOGGLE_MOD_MENU", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("DESELECT_COLOR on pressing Escape", () => {
    manager.handleInteraction(
      { key: "Escape" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("DESELECT_COLOR", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("SNAPSHOT on pressing P", () => {
    manager.handleInteraction(
      { key: "KeyP" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("SNAPSHOT", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("KEYBINDS on pressing Shift+/", () => {
    manager.handleInteraction(
      { key: "Slash", shift: true },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("KEYBINDS", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("Slash without shift does not trigger KEYBINDS", () => {
    manager.handleInteraction(
      { key: "Slash" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).not.toHaveBeenCalled();
  });

  /* ─── Mouse keybinds ─── */

  it("PIXEL_WHOIS on Shift+LCLICK", () => {
    manager.handleInteraction(
      { key: "LCLICK", shift: true },
      { clientX: 100, clientY: 200 },
    );
    expect(emitSpy).toHaveBeenCalledWith("PIXEL_WHOIS", {
      clientX: 100,
      clientY: 200,
    });
  });

  it("PIXEL_WHOIS on LONG_PRESS", () => {
    manager.handleInteraction(
      { key: "LONG_PRESS" },
      { clientX: 50, clientY: 60 },
    );
    expect(emitSpy).toHaveBeenCalledWith("PIXEL_WHOIS", {
      clientX: 50,
      clientY: 60,
    });
  });

  it("plain LCLICK does not trigger PIXEL_WHOIS", () => {
    manager.handleInteraction(
      { key: "LCLICK" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).not.toHaveBeenCalledWith(
      "PIXEL_WHOIS",
      expect.anything(),
    );
  });

  it("TEMPLATE_MOVE on Alt+LCLICK", () => {
    manager.handleInteraction(
      { key: "LCLICK", alt: true },
      { clientX: 30, clientY: 40 },
    );
    expect(emitSpy).toHaveBeenCalledWith("TEMPLATE_MOVE", {
      clientX: 30,
      clientY: 40,
    });
  });

  it("PICK_COLOR on MCLICK (middle click)", () => {
    manager.handleInteraction(
      { key: "MCLICK" },
      { clientX: -1, clientY: -1 },
    );
    expect(emitSpy).toHaveBeenCalledWith("PICK_COLOR", {
      clientX: -1,
      clientY: -1,
    });
  });

  it("MOD_SELECT on Ctrl+LCLICK", () => {
    manager.handleInteraction(
      { key: "LCLICK", ctrl: true },
      { clientX: 10, clientY: 20 },
    );
    expect(emitSpy).toHaveBeenCalledWith("MOD_SELECT", {
      clientX: 10,
      clientY: 20,
    });
  });

  /* ─── Modifier-specific tests ─── */

  it("Shift+LCLICK does not also trigger TEMPLATE_MOVE (no alt)", () => {
    manager.handleInteraction(
      { key: "LCLICK", shift: true },
      { clientX: -1, clientY: -1 },
    );
    // PIXEL_WHOIS requires shift
    expect(emitSpy).toHaveBeenCalledWith("PIXEL_WHOIS", expect.anything());
    // TEMPLATE_MOVE requires alt — should not fire
    expect(emitSpy).not.toHaveBeenCalledWith(
      "TEMPLATE_MOVE",
      expect.anything(),
    );
    // MOD_SELECT requires ctrl — should not fire
    expect(emitSpy).not.toHaveBeenCalledWith(
      "MOD_SELECT",
      expect.anything(),
    );
  });

  /* ─── Unknown / non-matching ─── */

  it("unknown key returns false and emits nothing", () => {
    const result = manager.handleInteraction(
      { key: "F12" },
      { clientX: -1, clientY: -1 },
    );
    expect(result).toBe(false);
    expect(emitSpy).not.toHaveBeenCalled();
  });

  it("returns true when at least one keybind matches", () => {
    const result = manager.handleInteraction(
      { key: "KeyT" },
      { clientX: -1, clientY: -1 },
    );
    expect(result).toBe(true);
  });
});
 No newline at end of file
+42 −0
Original line number Diff line number Diff line
import { describe, it, expect } from "vitest";
import { rgbToHex } from "../utils";

describe("rgbToHex", () => {
  it('returns "#FF0000" for red', () => {
    expect(rgbToHex(255, 0, 0)).toBe("#FF0000");
  });

  it('returns "#00FF00" for green', () => {
    expect(rgbToHex(0, 255, 0)).toBe("#00FF00");
  });

  it('returns "#0000FF" for blue', () => {
    expect(rgbToHex(0, 0, 255)).toBe("#0000FF");
  });

  it('returns "#000000" for black', () => {
    expect(rgbToHex(0, 0, 0)).toBe("#000000");
  });

  it('returns "#FFFFFF" for white', () => {
    expect(rgbToHex(255, 255, 255)).toBe("#FFFFFF");
  });

  it("handles leading zeros in components", () => {
    expect(rgbToHex(15, 15, 15)).toBe("#0F0F0F");
  });

  it("returns uppercase hex", () => {
    const result = rgbToHex(171, 205, 239);
    expect(result).toBe("#ABCDEF");
    expect(result).toEqual(result.toUpperCase());
  });

  it("pads single-digit hex components with leading zero", () => {
    expect(rgbToHex(1, 2, 3)).toBe("#010203");
  });

  it("handles mid-range values", () => {
    expect(rgbToHex(128, 128, 128)).toBe("#808080");
  });
});
 No newline at end of file
Loading