diff --git a/marketing/product-film/src/captures.manifest.json b/marketing/product-film/src/captures.manifest.json new file mode 100644 index 0000000..7ff072d --- /dev/null +++ b/marketing/product-film/src/captures.manifest.json @@ -0,0 +1,24 @@ +{ + "viewport": { "width": 1920, "height": 1080, "deviceScaleFactor": 2 }, + "theme": "dark", + "baseUrl": "http://localhost:5173", + "captures": [ + { "id": "storm-intel", "role": "owner", "route": "/owner/storm-intel", "waitMs": 2000 }, + { "id": "territory-map", "role": "owner", "route": "/owner/maps", "waitMs": 2500 }, + { "id": "pro-canvas", "role": "owner", "route": "/owner/pro-canvas", "waitMs": 1800 }, + { "id": "lead-verification", "role": "owner", "route": "/lead-verification", "waitMs": 1800 }, + { "id": "dispatch", "role": "owner", "route": "/owner/dispatch", "waitMs": 2500 }, + { "id": "people-skills", "role": "owner", "route": "/owner/people", "waitMs": 1800 }, + { "id": "kanban", "role": "owner", "route": "/owner/kanban", "waitMs": 1800 }, + { "id": "settings-access", "role": "owner", "route": "/owner/settings", "waitMs": 1800 }, + { "id": "projects", "role": "owner", "route": "/owner/projects", "waitMs": 1800 }, + { "id": "estimates", "role": "owner", "route": "/owner/estimates", "waitMs": 1800 }, + { "id": "sub-tasks-owner", "role": "owner", "route": "/owner/subcontractor-tasks", "waitMs": 1800 }, + { "id": "owner-snapshot", "role": "owner", "route": "/owner/snapshot", "waitMs": 2000 }, + { "id": "leaderboard", "role": "owner", "route": "/admin/leaderboard", "waitMs": 1800 }, + { "id": "rep-leads", "role": "agentCody", "route": "/emp/fa/leads", "waitMs": 1800 }, + { "id": "rep-dashboard", "role": "agentCody", "route": "/emp/fa/dashboard", "waitMs": 1800 }, + { "id": "sub-dashboard", "role": "subCarlos", "route": "/subcontractor/dashboard", "waitMs": 1800 }, + { "id": "sub-projects", "role": "subCarlos", "route": "/subcontractor/projects", "waitMs": 1800 } + ] +} diff --git a/marketing/product-film/src/captures.test.ts b/marketing/product-film/src/captures.test.ts new file mode 100644 index 0000000..ce35710 --- /dev/null +++ b/marketing/product-film/src/captures.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest"; +import manifest from "./captures.manifest.json"; +import { validateManifest, type Manifest } from "./captures"; + +describe("captures manifest", () => { + it("passes validation", () => { + expect(validateManifest(manifest as Manifest)).toEqual([]); + }); + it("has unique ids", () => { + const ids = (manifest as Manifest).captures.map((c) => c.id); + expect(new Set(ids).size).toBe(ids.length); + }); + it("captures in dark theme", () => { + expect((manifest as Manifest).theme).toBe("dark"); + }); +}); diff --git a/marketing/product-film/src/captures.ts b/marketing/product-film/src/captures.ts new file mode 100644 index 0000000..e5da1d8 --- /dev/null +++ b/marketing/product-film/src/captures.ts @@ -0,0 +1,32 @@ +export type CaptureRole = "owner" | "agentCody" | "subCarlos"; + +export interface Capture { + id: string; + role: CaptureRole; + route: string; + waitMs?: number; +} + +export interface Manifest { + viewport: { width: number; height: number; deviceScaleFactor: number }; + theme: "dark" | "light"; + baseUrl: string; + captures: Capture[]; +} + +const VALID_ROLES: CaptureRole[] = ["owner", "agentCody", "subCarlos"]; + +export function validateManifest(m: Manifest): string[] { + const errors: string[] = []; + const seen = new Set(); + if (!m.baseUrl?.startsWith("http")) errors.push("baseUrl must be an http(s) URL"); + if (m.theme !== "dark" && m.theme !== "light") errors.push("theme must be dark|light"); + for (const c of m.captures) { + if (!c.id) errors.push("capture missing id"); + if (seen.has(c.id)) errors.push(`duplicate id: ${c.id}`); + seen.add(c.id); + if (!c.route?.startsWith("/")) errors.push(`${c.id}: route must start with "/"`); + if (!VALID_ROLES.includes(c.role)) errors.push(`${c.id}: unknown role "${c.role}"`); + } + return errors; +}