/* Copyright 2026 Marimo. All rights reserved. */ import { render } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useCellErrors } from "@/core/cells/cells"; import { DynamicFavicon } from "../dynamic-favicon"; // Mock useCellErrors hook vi.mock("@/core/cells/cells", () => ({ useCellErrors: vi.fn(), })); describe("DynamicFavicon", () => { let favicon: HTMLLinkElement; beforeEach(() => { // Mock favicon element favicon = document.createElement("link"); favicon.rel = "icon"; favicon.href = "./favicon.ico"; document.head.append(favicon); // Mock document.hasFocus vi.spyOn(document, "hasFocus").mockReturnValue(true); // Mock useCellErrors to return no errors by default (useCellErrors as ReturnType).mockReturnValue([]); }); afterEach(() => { favicon.remove(); vi.clearAllMocks(); vi.useRealTimers(); }); it("should update favicon when running state changes", async () => { render(); // Wait for async favicon update await new Promise((resolve) => setTimeout(resolve, 0)); const faviconElement = document.querySelector("link[rel~='icon']")!; expect(faviconElement.href.endsWith("circle-play.ico")).toBe(true); }); it("should show success favicon when run completes without errors", async () => { const { rerender } = render(); // Wait for the running favicon to be set await new Promise((resolve) => setTimeout(resolve, 0)); rerender(); // Wait for async favicon update await new Promise((resolve) => setTimeout(resolve, 0)); const faviconElement = document.querySelector("link[rel~='icon']")!; expect(faviconElement.href.endsWith("circle-check.ico")).toBe(true); }); it("should show error favicon when run completes with errors", async () => { (useCellErrors as ReturnType).mockReturnValue([ { error: "mock error" }, ]); const { rerender } = render(); // Wait for the running favicon to be set await new Promise((resolve) => setTimeout(resolve, 0)); rerender(); // Wait for async favicon update await new Promise((resolve) => setTimeout(resolve, 0)); const faviconElement = document.querySelector("link[rel~='icon']")!; expect(faviconElement.href.endsWith("circle-x.ico")).toBe(true); }); it("should not reset favicon when not in focus", async () => { vi.spyOn(document, "hasFocus").mockReturnValue(false); vi.useFakeTimers(); const { rerender } = render(); // Wait for the running favicon to be set await vi.advanceTimersByTimeAsync(0); rerender(); // Wait for async favicon update await vi.advanceTimersByTimeAsync(0); const faviconElement = document.querySelector("link[rel~='icon']")!; expect(faviconElement.href.endsWith("circle-check.ico")).toBe(true); // Advance timers beyond the 3-second reset timeout await vi.advanceTimersByTimeAsync(3000); // Favicon should still be the success one since document is not in focus expect(faviconElement.href.endsWith("circle-check.ico")).toBe(true); }); it("should create favicon link if none exists", () => { favicon.remove(); render(); const newFavicon = document.querySelector("link[rel~='icon']"); expect(newFavicon).not.toBeNull(); }); describe("notifications", () => { beforeEach(() => { vi.spyOn(document, "visibilityState", "get").mockReturnValue("hidden"); // @ts-expect-error ok in tests global.Notification = vi.fn(); // @ts-expect-error ok in tests global.Notification.permission = "granted"; }); it("should send success notification when run completes without errors", () => { // @ts-expect-error ok in tests global.Notification = vi.fn().mockImplementation((title, options) => { expect(title).toBe("Execution completed"); expect(options).toEqual({ body: "Your notebook run completed successfully.", icon: "/src/assets/circle-check.ico", }); }); // @ts-expect-error ok in tests global.Notification.permission = "granted"; const { rerender } = render(); rerender(); }); it("should send error notification when run completes with errors", () => { (useCellErrors as ReturnType).mockReturnValue([ { error: "mock error" }, ]); // @ts-expect-error ok in tests global.Notification = vi.fn().mockImplementation((title, options) => { expect(title).toBe("Execution failed"); expect(options).toEqual({ body: "Your notebook run encountered 1 error(s).", icon: "/src/assets/circle-x.ico", }); }); // @ts-expect-error ok in tests global.Notification.permission = "granted"; const { rerender } = render(); rerender(); }); it("should not send notification when document is visible", () => { vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); const { rerender } = render(); rerender(); expect(Notification).not.toHaveBeenCalled(); }); it("should request permission if not granted", () => { // @ts-expect-error ok in tests global.Notification.permission = "default"; global.Notification.requestPermission = vi .fn() .mockResolvedValue("granted"); const { rerender } = render(); rerender(); // oxlint-disable-next-line typescript/unbound-method expect(Notification.requestPermission).toHaveBeenCalled(); }); }); });