Feature: expanded and custom task icons (Lucide + Font Awesome + emoji) (#248)
Co-authored-by: dev_agent <17+dev_agent@code.cubecraftcreations.com> Co-committed-by: dev_agent <17+dev_agent@code.cubecraftcreations.com>
This commit was merged in pull request #248.
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import DefinitionsTab from "./DefinitionsTab";
|
||||
import { DEFINITION_ICONS, DefinitionIcon } from "./definitionIcons";
|
||||
import {
|
||||
DEFINITION_ICONS,
|
||||
DefinitionIcon,
|
||||
FA_DEFINITION_ICONS,
|
||||
emojiIconKey,
|
||||
faIconName,
|
||||
findDefinitionIcon,
|
||||
normalizeDefinitionIconName,
|
||||
} from "./definitionIcons";
|
||||
import type { AdminChild, DefinitionRule, QuestDefinition } from "../api/definitions";
|
||||
import { storeTokens } from "../auth/oidc";
|
||||
|
||||
@@ -153,6 +161,10 @@ function fakeApi(initial: QuestDefinition[]) {
|
||||
}
|
||||
const match = /^\/api\/v1\/admin\/quest-definitions\/(\d+)$/.exec(path);
|
||||
if (method === "PATCH" && match) {
|
||||
// Like the API's strict validate_icon: a Lucide name never has a colon.
|
||||
if (typeof body.icon === "string" && /^lucide:.*:/.test(body.icon)) {
|
||||
return jsonResponse({ detail: "icon is not a valid icon key" }, 422);
|
||||
}
|
||||
const id = Number(match[1]);
|
||||
definitions = definitions.map((d) =>
|
||||
d.id === id
|
||||
@@ -455,7 +467,6 @@ describe("DefinitionsTab", () => {
|
||||
path: "/api/v1/admin/quest-definitions/11",
|
||||
body: {
|
||||
title: "Take out the bins",
|
||||
icon: null,
|
||||
rule: {
|
||||
rule_type: "monthly_day",
|
||||
interval: 1,
|
||||
@@ -1052,7 +1063,7 @@ describe("DefinitionsTab icon picker", () => {
|
||||
|
||||
expect(api.writes()).toHaveLength(1);
|
||||
expect(api.writes()[0].method).toBe("POST");
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("bed");
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("lucide:bed");
|
||||
expect(rowIcon(await screen.findByTestId("definition-99"))).toBe("bed");
|
||||
});
|
||||
|
||||
@@ -1073,13 +1084,13 @@ describe("DefinitionsTab icon picker", () => {
|
||||
const { method, path, body } = api.writes()[0];
|
||||
expect(method).toBe("PATCH");
|
||||
expect(path).toBe("/api/v1/admin/quest-definitions/11");
|
||||
expect((body as { icon: string }).icon).toBe("trash-2");
|
||||
expect((body as { icon: string }).icon).toBe("lucide:trash-2");
|
||||
// Inactive "Old" stays assigned: the unchanged selection is still omitted.
|
||||
expect(body).not.toHaveProperty("assignee_child_ids");
|
||||
await waitFor(() => expect(rowIcon(screen.getByTestId("definition-11"))).toBe("trash-2"));
|
||||
});
|
||||
|
||||
it("an unchanged icon is still sent on edit and shows as selected", async () => {
|
||||
it("an unchanged icon is omitted on edit and shows as selected", async () => {
|
||||
await renderReady();
|
||||
expect(rowIcon(screen.getByTestId("definition-10"))).toBe("smile");
|
||||
fireEvent.click(screen.getByTestId("definition-10"));
|
||||
@@ -1093,7 +1104,7 @@ describe("DefinitionsTab icon picker", () => {
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
|
||||
});
|
||||
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("smile");
|
||||
expect(api.writes()[0].body).not.toHaveProperty("icon");
|
||||
});
|
||||
|
||||
it("an unknown legacy icon falls back to the generic glyph and is kept on save", async () => {
|
||||
@@ -1113,12 +1124,431 @@ describe("DefinitionsTab icon picker", () => {
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
|
||||
});
|
||||
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("rocket-ship-9000");
|
||||
expect(api.writes()[0].body).not.toHaveProperty("icon");
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
expect(rowIcon(screen.getByTestId("definition-20"))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefinitionsTab namespaced icons", () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
storeTokens({ access_token: "at-123", expires_in: 600 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function rowIcon(row: HTMLElement): string | null {
|
||||
const svg = row.querySelector(".defs-row-glyph svg");
|
||||
expect(svg).not.toBeNull();
|
||||
return svg!.getAttribute("data-icon");
|
||||
}
|
||||
|
||||
function iconRadio(s: HTMLElement, label: string): HTMLElement {
|
||||
return within(within(s).getByRole("radiogroup", { name: "Icon" })).getByRole("radio", {
|
||||
name: label,
|
||||
});
|
||||
}
|
||||
|
||||
it("normalizes lucide-namespaced and legacy bare values to one Lucide name", () => {
|
||||
expect(normalizeDefinitionIconName("lucide:dog")).toBe("dog");
|
||||
expect(normalizeDefinitionIconName("dog")).toBe("dog");
|
||||
expect(normalizeDefinitionIconName("fa:dog")).toBeNull();
|
||||
expect(normalizeDefinitionIconName("emoji:🐶")).toBeNull();
|
||||
expect(normalizeDefinitionIconName("mystery:dog")).toBeNull();
|
||||
expect(normalizeDefinitionIconName(null)).toBeNull();
|
||||
});
|
||||
|
||||
for (const icon of ["lucide:dog", "dog"]) {
|
||||
it(`renders and selects the Dog glyph for ${icon}`, async () => {
|
||||
await renderReady([{ ...BRUSH, id: 30, icon }]);
|
||||
const row = screen.getByTestId("definition-30");
|
||||
expect(rowIcon(row)).toBe("dog");
|
||||
|
||||
fireEvent.click(row);
|
||||
const s = sheet();
|
||||
expect(iconRadio(s, "Dog").getAttribute("aria-checked")).toBe("true");
|
||||
expect(iconRadio(s, "Dog").className).toContain("defs-icon--on");
|
||||
expect(iconRadio(s, "No icon").getAttribute("aria-checked")).toBe("false");
|
||||
});
|
||||
}
|
||||
|
||||
for (const icon of ["mystery:dog", "lucide:not-an-icon", "fa:not-an-icon", "emoji:a b"]) {
|
||||
it(`renders the fallback tile and selects nothing for ${icon}`, async () => {
|
||||
await renderReady([{ ...BRUSH, id: 31, icon }]);
|
||||
const row = screen.getByTestId("definition-31");
|
||||
expect(rowIcon(row)).toBeNull();
|
||||
|
||||
fireEvent.click(row);
|
||||
const s = sheet();
|
||||
for (const name of ["Icon", "Font Awesome"]) {
|
||||
const group = within(s).getByRole("radiogroup", { name });
|
||||
for (const radio of within(group).getAllByRole("radio")) {
|
||||
expect(radio.getAttribute("aria-checked")).toBe("false");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it("tapping a Lucide choice on a namespaced task PATCHes lucide:<name>, and No icon clears it", async () => {
|
||||
await renderReady([{ ...BRUSH, id: 32, icon: "lucide:dog" }]);
|
||||
fireEvent.click(screen.getByTestId("definition-32"));
|
||||
let s = sheet();
|
||||
fireEvent.click(iconRadio(s, "Cat"));
|
||||
expect(iconRadio(s, "Cat").getAttribute("aria-checked")).toBe("true");
|
||||
expect(iconRadio(s, "Dog").getAttribute("aria-checked")).toBe("false");
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
|
||||
});
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("lucide:cat");
|
||||
await waitFor(() => expect(rowIcon(screen.getByTestId("definition-32"))).toBe("cat"));
|
||||
|
||||
fireEvent.click(screen.getByTestId("definition-32"));
|
||||
s = sheet();
|
||||
expect(iconRadio(s, "Cat").getAttribute("aria-checked")).toBe("true");
|
||||
fireEvent.click(iconRadio(s, "No icon"));
|
||||
expect(iconRadio(s, "No icon").getAttribute("aria-checked")).toBe("true");
|
||||
await act(async () => {
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
|
||||
});
|
||||
expect((api.writes()[1].body as { icon: string | null }).icon).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefinitionsTab Font Awesome and emoji icons", () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
storeTokens({ access_token: "at-123", expires_in: 600 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
/** What a row draws: `svg:<data-icon>`, `emoji:<text>`, or `fallback`. */
|
||||
function rowGlyph(row: HTMLElement): string {
|
||||
const glyph = row.querySelector(".defs-row-glyph");
|
||||
const emoji = glyph!.querySelector("[data-emoji]");
|
||||
if (emoji) return `emoji:${emoji.textContent}`;
|
||||
const svg = glyph!.querySelector("svg");
|
||||
expect(svg).not.toBeNull();
|
||||
const name = svg!.getAttribute("data-icon");
|
||||
if (name === null) return "fallback";
|
||||
return `${svg!.getAttribute("fill") === "none" ? "lucide" : "fa"}:${name}`;
|
||||
}
|
||||
|
||||
function radio(s: HTMLElement, group: "Icon" | "Font Awesome", label: string): HTMLElement {
|
||||
return within(within(s).getByRole("radiogroup", { name: group })).getByRole("radio", {
|
||||
name: label,
|
||||
});
|
||||
}
|
||||
|
||||
function emojiInput(s: HTMLElement): HTMLInputElement {
|
||||
return within(s).getByRole("textbox", { name: "Emoji" }) as HTMLInputElement;
|
||||
}
|
||||
|
||||
async function save(s: HTMLElement) {
|
||||
await act(async () => {
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
|
||||
});
|
||||
}
|
||||
|
||||
function fillNewTask(s: HTMLElement) {
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Title" }), {
|
||||
target: { value: "Walk the dog" },
|
||||
});
|
||||
fireEvent.click(within(s).getByRole("button", { name: /Assigned children/ }));
|
||||
fireEvent.click(within(s).getByRole("checkbox", { name: "Declan" }));
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Morning" }));
|
||||
}
|
||||
|
||||
it("ships the curated Font Awesome subset, each drawn as a filled glyph", () => {
|
||||
expect(FA_DEFINITION_ICONS.length).toBeGreaterThan(0);
|
||||
for (const entry of FA_DEFINITION_ICONS) {
|
||||
expect(entry.kind).toBe("fa");
|
||||
expect(findDefinitionIcon(`fa:${entry.name}`)).toBe(entry);
|
||||
const { container, unmount } = render(<DefinitionIcon name={`fa:${entry.name}`} />);
|
||||
const svg = container.querySelector("svg");
|
||||
expect(svg, entry.name).not.toBeNull();
|
||||
expect(svg!.getAttribute("data-icon")).toBe(entry.name);
|
||||
expect(svg!.getAttribute("fill")).toBe("currentColor");
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("builds and validates emoji keys like the registry", () => {
|
||||
expect(emojiIconKey("🐶")).toBe("emoji:🐶");
|
||||
expect(emojiIconKey("👨👩👧👦")).toBe("emoji:👨👩👧👦");
|
||||
expect(emojiIconKey("⭐".repeat(8))).toBe(`emoji:${"⭐".repeat(8)}`);
|
||||
for (const bad of ["", " ", " 🐶 ", "🐶 ", "\t🐶", "⭐".repeat(9), "a b", "<b>", "a&b", "🐶\u0007"]) {
|
||||
expect(emojiIconKey(bad), JSON.stringify(bad)).toBeNull();
|
||||
}
|
||||
expect(faIconName("fa:dog")).toBe("dog");
|
||||
expect(faIconName("lucide:dog")).toBeNull();
|
||||
expect(faIconName(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("renders lucide, legacy, fa and emoji rows, and the fallback for none or unknown", async () => {
|
||||
await renderReady([
|
||||
{ ...BRUSH, id: 40, icon: "lucide:dog" },
|
||||
{ ...BRUSH, id: 41, title: "Task 41", icon: "dog" },
|
||||
{ ...BRUSH, id: 42, title: "Task 42", icon: "fa:dog" },
|
||||
{ ...BRUSH, id: 43, title: "Task 43", icon: "emoji:🐶" },
|
||||
{ ...BRUSH, id: 44, title: "Task 44", icon: null },
|
||||
{ ...BRUSH, id: 45, title: "Task 45", icon: "" },
|
||||
{ ...BRUSH, id: 46, title: "Task 46", icon: "mystery:dog" },
|
||||
]);
|
||||
expect(rowGlyph(screen.getByTestId("definition-40"))).toBe("lucide:dog");
|
||||
expect(rowGlyph(screen.getByTestId("definition-41"))).toBe("lucide:dog");
|
||||
expect(rowGlyph(screen.getByTestId("definition-42"))).toBe("fa:dog");
|
||||
expect(rowGlyph(screen.getByTestId("definition-43"))).toBe("emoji:🐶");
|
||||
expect(rowGlyph(screen.getByTestId("definition-44"))).toBe("fallback");
|
||||
expect(rowGlyph(screen.getByTestId("definition-45"))).toBe("fallback");
|
||||
expect(rowGlyph(screen.getByTestId("definition-46"))).toBe("fallback");
|
||||
});
|
||||
|
||||
it("a Font Awesome choice is POSTed as fa:<name> and renders on its row", async () => {
|
||||
await renderReady();
|
||||
fireEvent.click(screen.getByRole("button", { name: "New" }));
|
||||
const s = sheet();
|
||||
fillNewTask(s);
|
||||
fireEvent.click(radio(s, "Font Awesome", "Dog"));
|
||||
expect(radio(s, "Font Awesome", "Dog").getAttribute("aria-checked")).toBe("true");
|
||||
expect(radio(s, "Font Awesome", "Dog").className).toContain("defs-icon--on");
|
||||
expect(radio(s, "Icon", "Dog").getAttribute("aria-checked")).toBe("false");
|
||||
expect(radio(s, "Icon", "No icon").getAttribute("aria-checked")).toBe("false");
|
||||
|
||||
await save(s);
|
||||
expect(api.writes()).toHaveLength(1);
|
||||
expect(api.writes()[0].method).toBe("POST");
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("fa:dog");
|
||||
expect(rowGlyph(await screen.findByTestId("definition-99"))).toBe("fa:dog");
|
||||
});
|
||||
|
||||
it("a typed emoji is POSTed as emoji:<grapheme> and renders on its row", async () => {
|
||||
await renderReady();
|
||||
fireEvent.click(screen.getByRole("button", { name: "New" }));
|
||||
const s = sheet();
|
||||
fillNewTask(s);
|
||||
fireEvent.change(emojiInput(s), { target: { value: "🐶" } });
|
||||
expect(radio(s, "Icon", "No icon").getAttribute("aria-checked")).toBe("false");
|
||||
|
||||
await save(s);
|
||||
expect(api.writes()).toHaveLength(1);
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("emoji:🐶");
|
||||
expect(rowGlyph(await screen.findByTestId("definition-99"))).toBe("emoji:🐶");
|
||||
});
|
||||
|
||||
it("a Lucide choice is POSTed as lucide:<name> after an emoji was typed", async () => {
|
||||
await renderReady();
|
||||
fireEvent.click(screen.getByRole("button", { name: "New" }));
|
||||
const s = sheet();
|
||||
fillNewTask(s);
|
||||
fireEvent.change(emojiInput(s), { target: { value: "🐶" } });
|
||||
fireEvent.click(radio(s, "Icon", "Dog"));
|
||||
expect(emojiInput(s).value).toBe("");
|
||||
|
||||
await save(s);
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("lucide:dog");
|
||||
expect(rowGlyph(await screen.findByTestId("definition-99"))).toBe("lucide:dog");
|
||||
});
|
||||
|
||||
for (const bad of [" 🐶 ", "🐶 ", " ", "a b", "<b>", "a&b", "⭐".repeat(9)]) {
|
||||
it(`an invalid emoji entry ${JSON.stringify(bad)} is not saved`, async () => {
|
||||
await renderReady([{ ...BRUSH, id: 50, icon: "lucide:dog" }]);
|
||||
fireEvent.click(screen.getByTestId("definition-50"));
|
||||
const s = sheet();
|
||||
fireEvent.change(emojiInput(s), { target: { value: bad } });
|
||||
|
||||
await save(s);
|
||||
expect(api.writes()).toHaveLength(0);
|
||||
expect(within(s).getByText(/Enter a single emoji/)).toBeTruthy();
|
||||
expect(screen.getByRole("dialog")).toBe(s);
|
||||
});
|
||||
}
|
||||
|
||||
it("prefills a fa:<name> task and PATCHes an emoji replacement", async () => {
|
||||
await renderReady([{ ...BRUSH, id: 51, icon: "fa:dog" }]);
|
||||
fireEvent.click(screen.getByTestId("definition-51"));
|
||||
const s = sheet();
|
||||
expect(radio(s, "Font Awesome", "Dog").getAttribute("aria-checked")).toBe("true");
|
||||
expect(radio(s, "Icon", "Dog").getAttribute("aria-checked")).toBe("false");
|
||||
expect(radio(s, "Icon", "No icon").getAttribute("aria-checked")).toBe("false");
|
||||
expect(emojiInput(s).value).toBe("");
|
||||
|
||||
fireEvent.change(emojiInput(s), { target: { value: "🐱" } });
|
||||
expect(radio(s, "Font Awesome", "Dog").getAttribute("aria-checked")).toBe("false");
|
||||
await save(s);
|
||||
const { method, path, body } = api.writes()[0];
|
||||
expect(method).toBe("PATCH");
|
||||
expect(path).toBe("/api/v1/admin/quest-definitions/51");
|
||||
expect((body as { icon: string }).icon).toBe("emoji:🐱");
|
||||
await waitFor(() => expect(rowGlyph(screen.getByTestId("definition-51"))).toBe("emoji:🐱"));
|
||||
});
|
||||
|
||||
it("prefills an emoji:<grapheme> task and PATCHes a Font Awesome replacement", async () => {
|
||||
await renderReady([{ ...BRUSH, id: 52, icon: "emoji:🐶" }]);
|
||||
fireEvent.click(screen.getByTestId("definition-52"));
|
||||
const s = sheet();
|
||||
expect(emojiInput(s).value).toBe("🐶");
|
||||
expect(radio(s, "Icon", "No icon").getAttribute("aria-checked")).toBe("false");
|
||||
for (const name of ["Icon", "Font Awesome"]) {
|
||||
const group = within(s).getByRole("radiogroup", { name });
|
||||
expect(within(group).queryAllByRole("radio", { checked: true })).toHaveLength(0);
|
||||
}
|
||||
|
||||
fireEvent.click(radio(s, "Font Awesome", "Pets"));
|
||||
expect(emojiInput(s).value).toBe("");
|
||||
await save(s);
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("fa:paw");
|
||||
await waitFor(() => expect(rowGlyph(screen.getByTestId("definition-52"))).toBe("fa:paw"));
|
||||
});
|
||||
|
||||
it("an unchanged emoji is omitted on edit", async () => {
|
||||
await renderReady([{ ...BRUSH, id: 53, icon: "emoji:🐶" }]);
|
||||
fireEvent.click(screen.getByTestId("definition-53"));
|
||||
const s = sheet();
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Title" }), {
|
||||
target: { value: "Feed the dog" },
|
||||
});
|
||||
await save(s);
|
||||
expect(api.writes()[0].body).not.toHaveProperty("icon");
|
||||
});
|
||||
|
||||
it("No icon and an emptied emoji field both clear the icon to null", async () => {
|
||||
await renderReady([
|
||||
{ ...BRUSH, id: 54, icon: "emoji:🐶" },
|
||||
{ ...BRUSH, id: 55, title: "Task 55", icon: "fa:dog" },
|
||||
]);
|
||||
fireEvent.click(screen.getByTestId("definition-54"));
|
||||
let s = sheet();
|
||||
fireEvent.change(emojiInput(s), { target: { value: "" } });
|
||||
expect(radio(s, "Icon", "No icon").getAttribute("aria-checked")).toBe("true");
|
||||
await save(s);
|
||||
expect((api.writes()[0].body as { icon: string | null }).icon).toBeNull();
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
|
||||
fireEvent.click(screen.getByTestId("definition-55"));
|
||||
s = sheet();
|
||||
fireEvent.click(radio(s, "Icon", "No icon"));
|
||||
expect(radio(s, "Font Awesome", "Dog").getAttribute("aria-checked")).toBe("false");
|
||||
await save(s);
|
||||
expect((api.writes()[1].body as { icon: string | null }).icon).toBeNull();
|
||||
await waitFor(() => expect(rowGlyph(screen.getByTestId("definition-55"))).toBe("fallback"));
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Legacy icons ───────────────────────────────────────────────────── */
|
||||
|
||||
describe("DefinitionsTab legacy icon edits", () => {
|
||||
// The API reads a stored legacy `mdi:tooth` back as `lucide:mdi:tooth`,
|
||||
// which its strict write validation rejects if sent back.
|
||||
const LEGACY: QuestDefinition = { ...BRUSH, id: 60, icon: "lucide:mdi:tooth" };
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
storeTokens({ access_token: "at-123", expires_in: 600 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function radio(s: HTMLElement, group: "Icon" | "Font Awesome", label: string): HTMLElement {
|
||||
return within(within(s).getByRole("radiogroup", { name: group })).getByRole("radio", {
|
||||
name: label,
|
||||
});
|
||||
}
|
||||
|
||||
async function save(s: HTMLElement) {
|
||||
await act(async () => {
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
|
||||
});
|
||||
}
|
||||
|
||||
it("editing only the title omits the legacy icon and saves", async () => {
|
||||
await renderReady([LEGACY]);
|
||||
fireEvent.click(screen.getByTestId("definition-60"));
|
||||
const s = sheet();
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Title" }), {
|
||||
target: { value: "Brush teeth well" },
|
||||
});
|
||||
await save(s);
|
||||
|
||||
expect(api.writes()).toHaveLength(1);
|
||||
expect(api.writes()[0].method).toBe("PATCH");
|
||||
expect(api.writes()[0].body).not.toHaveProperty("icon");
|
||||
expect((api.writes()[0].body as { title: string }).title).toBe("Brush teeth well");
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
expect(await screen.findByText("Brush teeth well")).toBeTruthy();
|
||||
});
|
||||
|
||||
for (const [label, choose, sent] of [
|
||||
["a Lucide", (s: HTMLElement) => fireEvent.click(radio(s, "Icon", "Dog")), "lucide:dog"],
|
||||
["a Font Awesome", (s: HTMLElement) => fireEvent.click(radio(s, "Font Awesome", "Dog")), "fa:dog"],
|
||||
[
|
||||
"an emoji",
|
||||
(s: HTMLElement) =>
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Emoji" }), {
|
||||
target: { value: "🦷" },
|
||||
}),
|
||||
"emoji:🦷",
|
||||
],
|
||||
["No icon", (s: HTMLElement) => fireEvent.click(radio(s, "Icon", "No icon")), null],
|
||||
] as const) {
|
||||
it(`choosing ${label} replacement for the legacy icon sends ${JSON.stringify(sent)}`, async () => {
|
||||
await renderReady([LEGACY]);
|
||||
fireEvent.click(screen.getByTestId("definition-60"));
|
||||
const s = sheet();
|
||||
choose(s);
|
||||
await save(s);
|
||||
|
||||
expect(api.writes()).toHaveLength(1);
|
||||
expect(api.writes()[0].body).toHaveProperty("icon", sent);
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
});
|
||||
}
|
||||
|
||||
it("an invalid emoji replacing the legacy icon is still rejected before the request", async () => {
|
||||
await renderReady([LEGACY]);
|
||||
fireEvent.click(screen.getByTestId("definition-60"));
|
||||
const s = sheet();
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Emoji" }), {
|
||||
target: { value: "a b" },
|
||||
});
|
||||
await save(s);
|
||||
|
||||
expect(api.writes()).toHaveLength(0);
|
||||
expect(within(s).getByText(/Enter a single emoji/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("creating a task still sends icon, even when none is chosen", async () => {
|
||||
await renderReady([LEGACY]);
|
||||
fireEvent.click(screen.getByRole("button", { name: "New" }));
|
||||
const s = sheet();
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Title" }), {
|
||||
target: { value: "Floss" },
|
||||
});
|
||||
fireEvent.click(within(s).getByRole("button", { name: /Assigned children/ }));
|
||||
fireEvent.click(within(s).getByRole("checkbox", { name: "Declan" }));
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Morning" }));
|
||||
await save(s);
|
||||
|
||||
expect(api.writes()).toHaveLength(1);
|
||||
expect(api.writes()[0].method).toBe("POST");
|
||||
expect(api.writes()[0].body).toHaveProperty("icon", null);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Start date ─────────────────────────────────────────────────────── */
|
||||
|
||||
function startDateInput(s: HTMLElement): HTMLInputElement {
|
||||
|
||||
Reference in New Issue
Block a user