mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 02:36:10 +00:00
Add a small Studio selection channel so agents can ask a running preview server for the element the user selected in Studio. This keeps the UX on the existing npx hyperframes preview surface while giving agents a stable source file, target selector, timeline time, and thumbnail URL for follow-up edits.
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { Hono } from "hono";
|
|
import { registerSelectionRoutes } from "./selection";
|
|
import type { StudioApiAdapter, StudioSelectionSnapshot } from "../types";
|
|
|
|
function createAdapter(): StudioApiAdapter {
|
|
return {
|
|
listProjects: () => [],
|
|
resolveProject: async (id: string) =>
|
|
id === "demo" ? { id, dir: "/tmp/demo", title: "Demo" } : null,
|
|
bundle: async () => null,
|
|
lint: async () => ({ findings: [] }),
|
|
runtimeUrl: "/api/runtime.js",
|
|
rendersDir: () => "/tmp/renders",
|
|
startRender: () => ({
|
|
id: "job-1",
|
|
status: "rendering",
|
|
progress: 0,
|
|
outputPath: "/tmp/out.mp4",
|
|
}),
|
|
};
|
|
}
|
|
|
|
const selection = {
|
|
schemaVersion: 1,
|
|
projectId: "demo",
|
|
compositionPath: "index.html",
|
|
sourceFile: "index.html",
|
|
currentTime: 1.25,
|
|
target: { hfId: "hero-title", selector: ".title", selectorIndex: 0 },
|
|
label: "Hero title",
|
|
tagName: "h1",
|
|
boundingBox: { x: 10, y: 20, width: 300, height: 64 },
|
|
textContent: "Launch faster",
|
|
dataAttributes: { "data-hf-id": "hero-title" },
|
|
inlineStyles: { color: "white" },
|
|
computedStyles: { "font-size": "48px" },
|
|
textFields: [
|
|
{
|
|
key: "self",
|
|
label: "Text",
|
|
value: "Launch faster",
|
|
tagName: "h1",
|
|
source: "self",
|
|
},
|
|
],
|
|
capabilities: { canSelect: true, canEditStyles: true },
|
|
thumbnailUrl:
|
|
"/api/projects/demo/thumbnail/index.html?t=1.25&format=png&selector=.title&selectorIndex=0",
|
|
} satisfies StudioSelectionSnapshot;
|
|
|
|
describe("registerSelectionRoutes", () => {
|
|
it("stores and returns the latest Studio selection snapshot", async () => {
|
|
const app = new Hono();
|
|
registerSelectionRoutes(app, createAdapter());
|
|
|
|
const put = await app.request("http://localhost/projects/demo/selection", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ selection }),
|
|
});
|
|
expect(put.status).toBe(200);
|
|
|
|
const response = await app.request("http://localhost/projects/demo/selection");
|
|
const payload = (await response.json()) as {
|
|
selection?: StudioSelectionSnapshot | null;
|
|
updatedAt?: string | null;
|
|
};
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(payload.selection).toMatchObject({
|
|
projectId: "demo",
|
|
sourceFile: "index.html",
|
|
target: { hfId: "hero-title" },
|
|
thumbnailUrl: expect.stringContaining("/thumbnail/index.html"),
|
|
});
|
|
expect(payload.updatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
});
|
|
|
|
it("clears a stored selection when Studio posts null", async () => {
|
|
const app = new Hono();
|
|
registerSelectionRoutes(app, createAdapter());
|
|
|
|
await app.request("http://localhost/projects/demo/selection", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ selection }),
|
|
});
|
|
|
|
const clear = await app.request("http://localhost/projects/demo/selection", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ selection: null }),
|
|
});
|
|
expect(clear.status).toBe(200);
|
|
|
|
const response = await app.request("http://localhost/projects/demo/selection");
|
|
const payload = (await response.json()) as {
|
|
selection?: StudioSelectionSnapshot | null;
|
|
updatedAt?: string | null;
|
|
};
|
|
|
|
expect(payload.selection).toBeNull();
|
|
expect(payload.updatedAt).toBeNull();
|
|
});
|
|
|
|
it("rejects malformed selection payloads", async () => {
|
|
const app = new Hono();
|
|
registerSelectionRoutes(app, createAdapter());
|
|
|
|
const response = await app.request("http://localhost/projects/demo/selection", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ selection: { projectId: "demo" } }),
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
});
|
|
});
|