Files
paseo/packages/app/src/projects/edit-form.test.ts
T
800bd2bef1 feat: add host-local custom project icons (#2335)
* feat(projects): add local icons and colors

* refactor(projects): simplify appearance handling

* fix(projects): serialize appearance saves

* test(app): update project appearance expectations

* test: fix project appearance CI expectations

* refactor(projects): organize appearance code

* fix(server): adopt main's reconcile model in project appearance updates

* fix(projects): preserve host-local appearance targets

* fix(desktop): restore Linux development startup

Ubuntu can block Electron's unprivileged user-namespace fallback while the npm-installed sandbox helper is not SUID. Disable the Chromium process sandbox only in the Linux development runner so packaged desktop policy remains unchanged.

* feat(projects): add custom project icons

Keep URL acquisition in the client so the host daemon only validates uploaded bytes. Project name and icon changes share one adaptive edit sheet.

* refactor(settings): scope projects by host

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-01 01:34:42 +08:00

154 lines
4.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { openProjectEditForm, type ProjectEditFormSnapshot } from "./edit-form";
const derivedName: ProjectEditFormSnapshot = {
projectName: "grumpy-turtle",
projectCustomName: null,
hasCustomIcon: false,
currentIconDataUri: "data:image/png;base64,ZGVyaXZlZA==",
};
const customIcon: ProjectEditFormSnapshot = {
projectName: "Turtle",
projectCustomName: "Turtle",
hasCustomIcon: true,
currentIconDataUri: "data:image/png;base64,Y3VzdG9t",
};
describe("project edit form model", () => {
it("has nothing to submit until the user changes something", () => {
expect(openProjectEditForm(derivedName).getState()).toMatchObject({
name: "",
canSubmit: false,
canUseAutomatic: false,
submission: { rename: null, icon: null },
});
});
it("submits only the name when only the name changed", () => {
const model = openProjectEditForm(derivedName);
model.setName(" Turtle ");
expect(model.getState()).toMatchObject({
canSubmit: true,
submission: { rename: { customName: "Turtle" }, icon: null },
});
});
it("clears the override when the name is emptied", () => {
const model = openProjectEditForm(customIcon);
model.setName(" ");
expect(model.getState().submission.rename).toEqual({ customName: null });
});
it("treats a retyped identical name as no change", () => {
const model = openProjectEditForm(customIcon);
model.setName("Turtle");
expect(model.getState()).toMatchObject({ canSubmit: false, submission: { rename: null } });
});
it("previews a picked image and submits it as an upload", () => {
const model = openProjectEditForm(derivedName);
model.setPickedImage({ fileName: "logo.png", mimeType: "image/png", data: "aW1hZ2U=" });
expect(model.getState()).toMatchObject({
previewDataUri: "data:image/png;base64,aW1hZ2U=",
pickedFileName: "logo.png",
canUseAutomatic: true,
submission: { rename: null, icon: { type: "upload", data: "aW1hZ2U=" } },
});
});
it("keeps a URL client-side and leaves the preview on the current icon", () => {
const model = openProjectEditForm(derivedName);
model.setImageUrl(" https://example.com ");
expect(model.getState()).toMatchObject({
previewDataUri: derivedName.currentIconDataUri,
pickedFileName: null,
submission: { icon: { type: "url", url: "https://example.com" } },
});
});
it("falls back to the earlier pick when the URL box is emptied", () => {
const model = openProjectEditForm(derivedName);
model.setPickedImage({ fileName: "logo.png", mimeType: "image/png", data: "aW1hZ2U=" });
model.setImageUrl("https://example.com");
expect(model.getState().submission.icon).toEqual({
type: "url",
url: "https://example.com",
});
model.setImageUrl("");
expect(model.getState().submission.icon).toEqual({ type: "upload", data: "aW1hZ2U=" });
});
it("resets a stored custom icon back to automatic", () => {
const model = openProjectEditForm(customIcon);
expect(model.getState().canUseAutomatic).toBe(true);
model.useAutomaticIcon();
expect(model.getState()).toMatchObject({
previewDataUri: null,
canUseAutomatic: false,
submission: { rename: null, icon: { type: "automatic" } },
});
});
it("discards a pick without asking the daemon for an icon it already has", () => {
const model = openProjectEditForm(derivedName);
model.setPickedImage({ fileName: "logo.png", mimeType: "image/png", data: "aW1hZ2U=" });
model.useAutomaticIcon();
expect(model.getState()).toMatchObject({
previewDataUri: derivedName.currentIconDataUri,
canSubmit: false,
submission: { rename: null, icon: null },
});
});
it("clears the URL box when the user falls back to automatic", () => {
const model = openProjectEditForm(customIcon);
model.setImageUrl("https://example.com");
const before = model.getState().urlResetKey;
model.useAutomaticIcon();
expect(model.getState().urlResetKey).not.toBe(before);
});
it("submits both fields together when both changed", () => {
const model = openProjectEditForm(customIcon);
model.setName("Renamed");
model.setPickedImage({ fileName: "logo.png", mimeType: "image/png", data: "aW1hZ2U=" });
expect(model.getState().submission).toEqual({
rename: { customName: "Renamed" },
icon: { type: "upload", data: "aW1hZ2U=" },
});
});
it("drops a submit failure as soon as the user edits again", () => {
const model = openProjectEditForm(derivedName);
model.setError({ scope: "icon", message: "Enter a valid website or image URL" });
expect(model.getState().error).toMatchObject({ scope: "icon" });
model.setImageUrl("https://example.com");
expect(model.getState().error).toBeNull();
});
});