Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,510 changes: 240 additions & 1,270 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@vitest/coverage-v8": "^4.1.8",
"eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"husky": "^9.1.7",
"prettier": "^3.8.2",
"typescript-eslint": "^8.58.2"
"typescript-eslint": "^8.58.2",
"vitest": "^4.1.8"
},
"overrides": {
"vite": "^6.4.2",
"esbuild": "^0.25.0"
"esbuild": "^0.25.0",
"vitest": "^4.1.8",
"@vitest/coverage-v8": "^4.1.8"
}
}
2 changes: 1 addition & 1 deletion packages/bridge-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"html-webpack-plugin": "^5.6.0",
"ts-loader": "^9.5.0",
"typescript": "^5.5.0",
"vitest": "^2.1.0",
"vitest": "^4.1.8",
"webpack": "^5.90.0",
"webpack-cli": "^5.1.0"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// @vitest-environment happy-dom
import { describe, it, expect, beforeEach } from "vitest";
import { ActivityLog } from "../ui/activity-log.js";

function setupHost(): HTMLElement {
document.body.innerHTML = `<div id="activity-log"></div>`;
return document.getElementById("activity-log")!;
}

describe("ActivityLog integration", () => {
beforeEach(() => {
document.body.innerHTML = "";
});

it("renders the new empty-state copy when no entries", () => {
const host = setupHost();
const log = new ActivityLog(host);
log.render();
expect(host.textContent).toContain("No operations yet");
});

it("renders up to 10 entries (MAX_VISIBLE)", () => {
const host = setupHost();
const log = new ActivityLog(host);
for (let i = 0; i < 12; i++) {
log.push({
op: `op_${i}`,
status: "ok",
durationMs: 100,
params: {},
at: Date.now() - i * 1000,
});
}
log.render();
const rows = host.querySelectorAll(".activity-row");
expect(rows.length).toBe(10);
});

it("renders 5 entries when only 5 exist", () => {
const host = setupHost();
const log = new ActivityLog(host);
for (let i = 0; i < 5; i++) {
log.push({
op: `op_${i}`,
status: "ok",
durationMs: 100,
params: {},
at: Date.now() - i * 1000,
});
}
log.render();
const rows = host.querySelectorAll(".activity-row");
expect(rows.length).toBe(5);
});

it("error entries get the .err class", () => {
const host = setupHost();
const log = new ActivityLog(host);
log.push({
op: "bad_op",
status: "error",
durationMs: 50,
params: {},
error: "boom",
at: Date.now(),
});
log.render();
expect(host.querySelector(".activity-op.err")).not.toBeNull();
});
});
223 changes: 223 additions & 0 deletions packages/bridge-plugin/src/__tests__/render-ui.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// @vitest-environment happy-dom
import { describe, it, expect, beforeEach } from "vitest";
import { pillStateFor, pillTextFor, formatElapsed, type AppState } from "../ui/render-ui.js";
import { renderUI } from "../ui/render-ui.js";

describe("pillStateFor", () => {
it("returns kind for non-connected variants", () => {
expect(pillStateFor({ kind: "disconnected" })).toBe("disconnected");
expect(pillStateFor({ kind: "connecting", lastKnownPort: 9500 })).toBe("connecting");
expect(
pillStateFor({
kind: "mismatch",
reason: "x",
serverVersion: "0.4.3",
pluginVersion: "0.4.2",
})
).toBe("mismatch");
});

it("returns 'running' when connected with a running op", () => {
expect(
pillStateFor({
kind: "connected",
file: { name: "F", key: "k" },
port: 9500,
running: { name: "execute_figma", paramsPreview: "", startedAt: 0 },
})
).toBe("running");
});

it("returns 'connected' when connected and idle", () => {
expect(
pillStateFor({
kind: "connected",
file: { name: "F", key: "k" },
port: 9500,
running: null,
})
).toBe("connected");
});
});

describe("pillTextFor", () => {
it("returns user-facing strings per state", () => {
expect(pillTextFor({ kind: "disconnected" })).toBe("Not connected");
expect(pillTextFor({ kind: "connecting", lastKnownPort: null })).toBe("Connecting…");
expect(
pillTextFor({
kind: "connected",
file: { name: "F", key: "k" },
port: 9500,
running: null,
})
).toBe("Connected");
});

it("includes the op name when running", () => {
expect(
pillTextFor({
kind: "connected",
file: { name: "F", key: "k" },
port: 9500,
running: { name: "lint_styles", paramsPreview: "", startedAt: 0 },
})
).toBe("Running lint_styles");
});

it("returns 'Update needed' for mismatch", () => {
expect(
pillTextFor({
kind: "mismatch",
reason: "x",
serverVersion: "0.4.3",
pluginVersion: "0.4.2",
})
).toBe("Update needed");
});
});

describe("formatElapsed", () => {
it("shows seconds with one decimal under a minute", () => {
expect(formatElapsed(500)).toBe("0.5s elapsed");
expect(formatElapsed(12_345)).toBe("12.3s elapsed");
});

it("shows minutes + seconds at or above one minute", () => {
expect(formatElapsed(60_000)).toBe("1m 0s elapsed");
expect(formatElapsed(125_000)).toBe("2m 5s elapsed");
});

it("handles zero correctly", () => {
expect(formatElapsed(0)).toBe("0.0s elapsed");
});
});

function setupDom(): void {
document.body.innerHTML = `
<div id="status-pill"><span id="status-text">—</span></div>
<section id="view-disconnected"></section>
<section id="view-connected" hidden>
<span id="file-name">—</span>
<span id="port-url">—</span>
<div id="running-block" hidden>
<span id="run-op">—</span>
<span id="run-params">—</span>
<span id="run-elapsed">—</span>
</div>
<div id="idle-block"></div>
</section>
<section id="view-mismatch" hidden>
<span id="mismatch-text">—</span>
</section>
`;
}

describe("renderUI", () => {
beforeEach(() => setupDom());

it("shows disconnected view + pill on disconnected state", () => {
renderUI({ kind: "disconnected" });
expect(document.getElementById("view-disconnected")!.hidden).toBe(false);
expect(document.getElementById("view-connected")!.hidden).toBe(true);
expect(document.getElementById("view-mismatch")!.hidden).toBe(true);
expect(document.getElementById("status-pill")!.dataset.state).toBe("disconnected");
expect(document.getElementById("status-text")!.textContent).toBe("Not connected");
});

it("shows disconnected view + 'Connecting…' pill on connecting state", () => {
renderUI({ kind: "connecting", lastKnownPort: 9500 });
expect(document.getElementById("view-disconnected")!.hidden).toBe(false);
expect(document.getElementById("status-pill")!.dataset.state).toBe("connecting");
expect(document.getElementById("status-text")!.textContent).toBe("Connecting…");
});

it("shows connected view with idle-block when running is null", () => {
renderUI({
kind: "connected",
file: { name: "MyFile", key: "abc" },
port: 9500,
running: null,
});
expect(document.getElementById("view-connected")!.hidden).toBe(false);
expect(document.getElementById("idle-block")!.hidden).toBe(false);
expect(document.getElementById("running-block")!.hidden).toBe(true);
expect(document.getElementById("file-name")!.textContent).toBe("MyFile");
expect(document.getElementById("port-url")!.textContent).toBe("localhost:9500");
});

it("shows connected view with running-block when running is set", () => {
const startedAt = Date.now() - 2500;
renderUI({
kind: "connected",
file: { name: "MyFile", key: "abc" },
port: 9500,
running: { name: "execute_figma", paramsPreview: "{ code: ... }", startedAt },
});
expect(document.getElementById("running-block")!.hidden).toBe(false);
expect(document.getElementById("idle-block")!.hidden).toBe(true);
expect(document.getElementById("run-op")!.textContent).toBe("execute_figma");
expect(document.getElementById("run-params")!.textContent).toBe("{ code: ... }");
expect(document.getElementById("run-elapsed")!.textContent).toMatch(/elapsed/);
expect(document.getElementById("status-text")!.textContent).toBe("Running execute_figma");
});

it("shows mismatch view with formatted text", () => {
renderUI({
kind: "mismatch",
reason: "Reinstall the plugin.",
serverVersion: "0.4.4",
pluginVersion: "0.4.2",
});
expect(document.getElementById("view-mismatch")!.hidden).toBe(false);
expect(document.getElementById("mismatch-text")!.textContent).toContain("0.4.4");
expect(document.getElementById("mismatch-text")!.textContent).toContain("0.4.2");
expect(document.getElementById("mismatch-text")!.textContent).toContain(
"Reinstall the plugin."
);
});

it("hides running-block defensively when not connected", () => {
renderUI({
kind: "connected",
file: { name: "F", key: "k" },
port: 9500,
running: { name: "op", paramsPreview: "", startedAt: Date.now() },
});
expect(document.getElementById("running-block")!.hidden).toBe(false);

renderUI({ kind: "disconnected" });
expect(document.getElementById("running-block")!.hidden).toBe(true);
});

it("regression: disconnect→reconnect cycle does not leak running-block visibility", () => {
renderUI({
kind: "connected",
file: { name: "F", key: "k" },
port: 9500,
running: { name: "op", paramsPreview: "", startedAt: Date.now() },
});
renderUI({ kind: "disconnected" });
renderUI({
kind: "connected",
file: { name: "F", key: "k" },
port: 9500,
running: null,
});
expect(document.getElementById("running-block")!.hidden).toBe(true);
expect(document.getElementById("idle-block")!.hidden).toBe(false);
});

it("is idempotent — calling with same state twice yields the same DOM", () => {
const state: AppState = {
kind: "connected",
file: { name: "F", key: "k" },
port: 9500,
running: null,
};
renderUI(state);
const firstHtml = document.body.innerHTML;
renderUI(state);
expect(document.body.innerHTML).toBe(firstHtml);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("ActivityLog", () => {

it("renders an empty state when no entries", () => {
new ActivityLog(host).render();
expect(host.textContent).toMatch(/no recent activity/i);
expect(host.textContent).toMatch(/no operations yet/i);
});

it("renders entries with success marker", () => {
Expand All @@ -30,12 +30,12 @@ describe("ActivityLog", () => {
expect(host.querySelector(".activity-op")?.classList.contains("err")).toBe(true);
});

it("caps visible entries to 5 but keeps up to 50 in memory", () => {
it("caps visible entries to 10 but keeps up to 50 in memory", () => {
const log = new ActivityLog(host);
for (let i = 0; i < 60; i++)
log.push({ op: `op_${i}`, status: "ok", durationMs: 10, params: {} });
log.render();
expect(host.querySelectorAll(".activity-row").length).toBe(5);
expect(host.querySelectorAll(".activity-row").length).toBe(10);
expect(log.size()).toBe(50);
});

Expand Down
Loading
Loading