1import { EnvironmentId } from "@t3tools/contracts";
2import type { ReactElement } from "react";
3import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
4
5import { visitElements } from "../../test/reactElementTree";
6import { reactHookHarness as hooks } from "../../test/reactHookHarness";
7
8vi.mock("react", async (importOriginal) => {
9 const actual = await importOriginal<typeof import("react")>();
10 const { reactHookHarness } = await import("../../test/reactHookHarness");
11 return {
12 ...actual,
13 useMemo: reactHookHarness.useMemo,
14 useState: reactHookHarness.useState,
15 };
16});
17
18vi.mock("react/compiler-runtime", async () => {
19 const { reactHookHarness } = await import("../../test/reactHookHarness");
20 return { c: reactHookHarness.useMemoCache };
21});
22
23vi.mock("@effect/atom-react", () => ({
24 useAtomValue: () => ({}),
25}));
26
27vi.mock("~/state/server", () => ({
28 primaryServerKeybindingsAtom: Symbol("keybindings"),
29}));
30
31vi.mock("~/assets/assetUrls", () => ({
32 useAssetUrlState: () => ({ _tag: "Loading" }),
33}));
34
35vi.mock("../files/projectFilesQueryState", () => ({
36 useProjectFilePickerQuery: () => ({
37 entries: [],
38 error: null,
39 isPending: false,
40 matchedQuery: "",
41 }),
42}));
43
44vi.mock("../ui/toast", () => ({
45 toastManager: { add: vi.fn() },
46}));
47
48import { toastManager } from "../ui/toast";
49import {
50 canPickExternalProjectFavicon,
51 ProjectFaviconPickerDialog,
52} from "./ProjectFaviconPickerDialog";
53
54describe("ProjectFaviconPickerDialog", () => {
55 beforeEach(() => {
56 hooks.reset();
57 vi.stubGlobal("navigator", { platform: "MacIntel" });
58 });
59
60 it("selects an image from the native file picker", async () => {
61 const onOpenChange = vi.fn();
62 const onPickExternal = vi.fn().mockResolvedValue("/Users/me/Pictures/icon.png");
63 const onSelect = vi.fn();
64 hooks.beginRender();
65 const picker = ProjectFaviconPickerDialog({
66 cwd: "/Users/me/project",
67 environmentId: EnvironmentId.make("local"),
68 onOpenChange,
69 onPickExternal,
70 onSelect,
71 open: true,
72 projectName: "Project",
73 } as Parameters<typeof ProjectFaviconPickerDialog>[0] & {
74 readonly onPickExternal: () => Promise<string | null>;
75 }) as ReactElement<Record<string, unknown>>;
76
77 const button = visitElements(picker, (element) => element.props.children === "Open in Finder");
78 expect(button).not.toBeNull();
79
80 (button?.props.onClick as (() => void) | undefined)?.();
81 await Promise.resolve();
82 await Promise.resolve();
83
84 expect(onPickExternal).toHaveBeenCalledTimes(1);
85 expect(onOpenChange).toHaveBeenCalledWith(false);
86 expect(onSelect).toHaveBeenCalledWith("/Users/me/Pictures/icon.png");
87 });
88
89 it("hides the native picker for WSL project paths", () => {
90 expect(canPickExternalProjectFavicon("/home/me/project", "Win32")).toBe(false);
91 expect(canPickExternalProjectFavicon("C:\\Users\\me\\project", "Win32")).toBe(true);
92 });
93
94 it("keeps the dialog open when the native picker fails", async () => {
95 const onOpenChange = vi.fn();
96 const onSelect = vi.fn();
97 const props = {
98 cwd: "/Users/me/project",
99 environmentId: EnvironmentId.make("local"),
100 onOpenChange,
101 onPickExternal: vi.fn().mockRejectedValue(new Error("picker failed")),
102 onSelect,
103 open: true,
104 projectName: "Project",
105 } as Parameters<typeof ProjectFaviconPickerDialog>[0] & {
106 readonly onPickExternal: () => Promise<string | null>;
107 };
108
109 hooks.beginRender();
110 const picker = ProjectFaviconPickerDialog(props) as ReactElement<Record<string, unknown>>;
111 const button = visitElements(picker, (element) => element.props.children === "Open in Finder");
112
113 (button?.props.onClick as (() => void) | undefined)?.();
114 await Promise.resolve();
115 await Promise.resolve();
116
117 expect(onOpenChange).not.toHaveBeenCalled();
118 expect(onSelect).not.toHaveBeenCalled();
119 expect(toastManager.add).toHaveBeenCalledWith({
120 type: "error",
121 title: "Could not open image picker",
122 description: "picker failed",
123 });
124 });
125});