import type { Page } from "@playwright/test"; import { expect, test } from "@playwright/test"; import { captureScreenshot, completeOnboarding, openUserSettings, signup } from "./helpers"; type Draft = { comfyui: { baseUrl: string; apiKey?: string; imageModel?: string; imageWorkflow?: string; videoWorkflow?: string; } | null; transcription: { baseUrl: string; model?: string; apiKey?: string } | null; }; /** The server keeps keys; the settings screen only learns whether one is saved. */ function view(draft: Draft, previous: { comfyui: boolean; transcription: boolean }) { const hasKey = (key: string | undefined, had: boolean) => (key === undefined ? had : key !== ""); const text = (value: string | undefined) => (value ? value : undefined); return { comfyui: draft.comfyui ? { baseUrl: draft.comfyui.baseUrl, hasApiKey: hasKey(draft.comfyui.apiKey, previous.comfyui), imageModel: text(draft.comfyui.imageModel), imageWorkflow: text(draft.comfyui.imageWorkflow), videoWorkflow: text(draft.comfyui.videoWorkflow), } : null, transcription: draft.transcription ? { baseUrl: draft.transcription.baseUrl, model: text(draft.transcription.model), hasApiKey: hasKey(draft.transcription.apiKey, previous.transcription), } : null, }; } type Source = { id: string; path: string; status: "idle" | "syncing" | "failed"; error: string | null; assetCount: number; lastSyncedAt: string | null; }; /** Library folders on the fake server: a new folder indexes on its next list. */ async function fakeLibrary(page: Page, initial: Source[] = []) { let sources = initial; const calls: { proc: string; input: unknown }[] = []; await page.route("**/rpc/media/library/**", (route) => { const proc = new URL(route.request().url()).pathname.split("/").at(-1) ?? ""; const input = (route.request().postDataJSON() as { json?: unknown } | null)?.json; calls.push({ proc, input }); if (proc === "list") { const listed = sources; sources = sources.map((source) => source.status === "syncing" ? { ...source, status: "idle", assetCount: 1284, lastSyncedAt: new Date().toISOString() } : source, ); return route.fulfill({ json: { json: listed } }); } if (proc === "add") { const { path } = input as { path: string }; if (!path.startsWith("/")) { return route.fulfill({ status: 400, json: { json: { defined: false, code: "BAD_REQUEST", status: 400, message: "Use an absolute folder path.", }, }, }); } const source: Source = { id: `source-${sources.length + 1}`, path, status: "syncing", error: null, assetCount: 0, lastSyncedAt: null, }; sources = [...sources, source]; return route.fulfill({ json: { json: source } }); } if (proc === "remove") { const { sourceId } = input as { sourceId: string }; sources = sources.filter((source) => source.id !== sourceId); return route.fulfill({ json: { json: { ok: true } } }); } return route.fallback(); }); return calls; } async function asDeploymentOwner(page: Page, isDeploymentOwner: boolean) { await page.route("**/rpc/bootstrap", async (route) => { const response = await route.fetch(); const body = await response.json(); await route.fulfill({ response, json: { json: { ...body.json, me: { ...body.json.me, isDeploymentOwner } } }, }); }); } test("the deployment owner points media at their own servers", async ({ page }, testInfo) => { await asDeploymentOwner(page, true); await fakeLibrary(page); let current: ReturnType = { comfyui: null, transcription: null }; const saved: Draft[] = []; await page.route("**/rpc/media/settings/get", (route) => route.fulfill({ json: { json: current } }), ); await page.route("**/rpc/media/settings/update", (route) => { const draft = (route.request().postDataJSON() as { json: Draft }).json; saved.push(draft); current = view(draft, { comfyui: Boolean(current.comfyui?.hasApiKey), transcription: Boolean(current.transcription?.hasApiKey), }); return route.fulfill({ json: { json: current } }); }); await signup(page, `media-settings-${Date.now()}@rakazo.test`, "password12", "Media Owner"); await completeOnboarding(page); const settings = await openUserSettings(page); await settings.getByTestId("settings-nav-media").click(); const panel = settings.getByTestId("media-settings"); await expect(panel).toBeVisible(); const comfy = panel.locator("section").filter({ hasText: "ComfyUI" }); const transcription = panel.locator("section").filter({ hasText: "Transcription" }); await comfy.getByLabel("Server URL", { exact: true }).fill("http://127.0.0.1:8188"); await comfy.getByLabel("API key", { exact: true }).fill("fake-comfy-key"); await transcription.getByLabel("Server URL", { exact: true }).fill("http://127.0.0.1:8080/v1"); await transcription.getByLabel("Model", { exact: true }).fill("whisper-1"); // The ComfyUI model and workflows stay folded away until asked for. await expect(comfy.getByLabel("Image model", { exact: true })).toBeHidden(); await comfy.scrollIntoViewIfNeeded(); await captureScreenshot(page, testInfo, "media-settings"); await comfy.getByText("Advanced", { exact: true }).click(); await comfy.getByLabel("Image model", { exact: true }).fill("sd_xl_base_1.0.safetensors"); await panel.getByRole("button", { name: "Save", exact: true }).click(); await expect(panel.getByText("Saved", { exact: true })).toBeVisible(); expect(saved).toEqual([ { comfyui: { baseUrl: "http://127.0.0.1:8188", apiKey: "fake-comfy-key", imageModel: "sd_xl_base_1.0.safetensors", imageWorkflow: "", videoWorkflow: "", }, transcription: { baseUrl: "http://127.0.0.1:8080/v1", model: "whisper-1" }, }, ]); // The key never comes back; the field shows that one is saved and offers to remove it. const comfyKey = comfy.getByLabel("API key", { exact: true }); await expect(comfyKey).toHaveValue(""); await expect(comfyKey).toHaveAttribute("placeholder", "Saved"); await comfy.getByRole("button", { name: "Remove", exact: true }).click(); await expect(comfyKey).toBeFocused(); await transcription.getByLabel("Server URL", { exact: true }).fill(""); await panel.getByRole("button", { name: "Save", exact: true }).click(); await expect.poll(() => saved.length).toBe(2); expect(saved[1]).toMatchObject({ comfyui: { apiKey: "" }, transcription: null }); await expect(comfyKey).toHaveAttribute("placeholder", "Optional"); }); test("the deployment owner adds a photo folder to the library", async ({ page }, testInfo) => { await asDeploymentOwner(page, true); const calls = await fakeLibrary(page, [ { id: "source-0", path: "/srv/media/Videos", status: "failed", error: "Folder not found.", assetCount: 0, lastSyncedAt: null, }, ]); await page.route("**/rpc/media/settings/get", (route) => route.fulfill({ json: { json: { comfyui: null, transcription: null } } }), ); await signup(page, `media-library-${Date.now()}@rakazo.test`, "password12", "Library Owner"); await completeOnboarding(page); const settings = await openUserSettings(page); await settings.getByTestId("settings-nav-media").click(); const library = settings.getByTestId("media-library"); await expect(library.getByText("Folder not found.")).toBeVisible(); const folder = library.getByLabel("Folder", { exact: true }); await folder.fill("photos"); await library.getByRole("button", { name: "Add", exact: true }).click(); await expect(library.getByRole("alert")).toHaveText("Use an absolute folder path."); await folder.fill("/srv/media/Photos"); await library.getByRole("button", { name: "Add", exact: true }).click(); await expect(folder).toHaveValue(""); await expect(library.getByText("/srv/media/Photos")).toBeVisible(); // The new folder indexes in the background; the list catches up on its own. await expect(library.getByText("1,284 files")).toBeVisible({ timeout: 10_000 }); await captureScreenshot(page, testInfo, "media-library"); await library .getByRole("listitem") .filter({ hasText: "/srv/media/Videos" }) .getByRole("button", { name: "Remove folder" }) .click(); await expect(library.getByText("/srv/media/Videos")).toHaveCount(0); expect(calls.filter((call) => call.proc !== "list")).toEqual([ { proc: "add", input: { path: "photos" } }, { proc: "add", input: { path: "/srv/media/Photos" } }, { proc: "remove", input: { sourceId: "source-0" } }, ]); }); test("members who do not own the deployment have no media settings", async ({ page }) => { await asDeploymentOwner(page, false); await signup(page, `media-member-${Date.now()}@rakazo.test`, "password12", "Media Member"); await completeOnboarding(page); const settings = await openUserSettings(page); await expect(settings.getByTestId("settings-nav-voice")).toBeVisible(); await expect(settings.getByTestId("settings-nav-media")).toHaveCount(0); });