* 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>
154 lines
4.8 KiB
TypeScript
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();
|
|
});
|
|
});
|