Files
olivv-cs-ComfyUI-FunPack/app/tests-layout/project_file.spec.js
T
digital-garbage b85c79a6a2 app, core: save/load a result, save/load a project, generate every scene
Player: Save to bin (whatever the viewer shows, if it has a real file
identity) and Save frame (canvas-capture the video on screen, upload
through ComfyUI's own /upload/image, no new route needed). Both add
without navigating the viewer -- an upload is a real round trip, and
the result is added to the bin, not shown as a stale finished pick.

Save/Load Project File: a real download route with Content-Disposition,
and an import route that never trusts an id in the file -- an import
always lands as its own project, never overwrites one already here.

Generate All: every scene in order, waiting for each to finish before
starting the next -- not a montage (v5 has no render/stitch stage),
N separate generations each landing on its own scene.

Both project-switching (open/newProject/importProject) and the earlier
run-reattach path shared a bug class this session kept finding: state
captured at one moment, applied at a later async moment with no check
that nothing had moved on in between. Closed it properly this time --
a generation counter every switch bumps and checks, including start()
(the page's own handlers are live before it resolves) and undo/redo
(also "a different project is open now"), and scheduleSave() captures
the object an edit actually touched instead of re-reading a variable
that can point somewhere else by the time its debounce timer fires.

Four adversarial rounds against this range, each fixed before landing.
2026-09-06 22:53:34 +03:00

87 lines
3.4 KiB
JavaScript

// Moving a project between machines: Save Project File downloads the real
// server route, Load Project File reads a real file back in. jsdom covers the
// store's own downloadUrl()/importProject() logic against a fake fetch; what
// only a browser can prove is that the menu items actually trigger a real
// download and a real file picker, against the real project routes.
import { test, expect } from "@playwright/test";
import { readFileSync } from "node:fs";
test.describe.configure({ mode: "serial" });
async function app(page) {
await page.goto("/funpack/");
await page.waitForFunction(() => window.FunPack !== undefined);
}
test("Save Project File downloads the current project as real JSON", async ({ page }) => {
await app(page);
const name = `Downloadable ${Date.now()}`;
await page.evaluate((n) => window.FunPack.project.newProject(n), name);
await page.getByRole("button", { name: "File" }).click();
const [download] = await Promise.all([
page.waitForEvent("download"),
page.getByRole("menuitem", { name: "Save Project File…" }).click(),
]);
expect(download.suggestedFilename()).toMatch(/\.funpack_project\.json$/);
const body = JSON.parse(readFileSync(await download.path(), "utf-8"));
expect(body.name).toBe(name);
expect(Array.isArray(body.scenes)).toBe(true);
});
test("Load Project File opens the file as a new project, not the one it came from", async ({ page }) => {
await app(page);
const original = await page.evaluate(async (n) => {
await window.FunPack.project.newProject(n);
window.FunPack.project.setVideo("width", 777);
await window.FunPack.project.flush();
return window.FunPack.project.project.id;
}, `Original ${Date.now()}`);
const fileContent = await page.evaluate(async () => {
const r = await fetch(window.FunPack.project.downloadUrl());
return r.text();
});
await page.getByRole("button", { name: "File" }).click();
const [chooser] = await Promise.all([
page.waitForEvent("filechooser"),
page.getByRole("menuitem", { name: "Load Project File…" }).click(),
]);
await chooser.setFiles({
name: "project.funpack_project.json",
mimeType: "application/json",
buffer: Buffer.from(fileContent),
});
await expect.poll(() => page.evaluate(() => window.FunPack.project.project.video.width)).toBe(777);
const imported = await page.evaluate(() => window.FunPack.project.project.id);
expect(imported).not.toBe(original);
// The original is untouched by the import -- a shared id would have
// overwritten it instead of landing as a project of its own.
const stillThere = await page.evaluate((id) => fetch(`/funpack/api/projects/${id}`).then((r) => r.status), original);
expect(stillThere).toBe(200);
});
test("a file that is not a project is refused, not silently opened", async ({ page }) => {
await app(page);
const before = await page.evaluate(() => window.FunPack.project.project.id);
await page.getByRole("button", { name: "File" }).click();
const [chooser] = await Promise.all([
page.waitForEvent("filechooser"),
page.getByRole("menuitem", { name: "Load Project File…" }).click(),
]);
await chooser.setFiles({
name: "not-a-project.json", mimeType: "application/json",
buffer: Buffer.from(JSON.stringify({ hello: "world" })),
});
await expect(page.locator(".cx-panel-status", { hasText: /could not load/i })).toBeVisible();
expect(await page.evaluate(() => window.FunPack.project.project.id)).toBe(before);
});