Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc32c167fb |
No files matched your search
@@ -0,0 +1,109 @@
|
|||||||
|
import { act, createElement } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { MantineProvider } from "@mantine/core";
|
||||||
|
|
||||||
|
import type { Root } from "react-dom/client";
|
||||||
|
|
||||||
|
const { findIconsQuery } = vi.hoisted(() => ({
|
||||||
|
findIconsQuery: vi.fn((_query: { searchText: string }) => ({
|
||||||
|
data: { icons: [], countIcons: 0 },
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
refetch: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@homarr/api/client", () => ({
|
||||||
|
clientApi: { icon: { findIcons: { useQuery: findIconsQuery } } },
|
||||||
|
}));
|
||||||
|
vi.mock("@homarr/auth/client", () => ({
|
||||||
|
useSession: () => ({ data: { user: { permissions: [] } } }),
|
||||||
|
}));
|
||||||
|
vi.mock("@homarr/translation", () => ({ supportedLanguages: [] }));
|
||||||
|
vi.mock("@homarr/translation/client", () => ({
|
||||||
|
useScopedI18n: () => (key: string) => key,
|
||||||
|
}));
|
||||||
|
vi.mock("../upload-media/upload-media", () => ({ UploadMedia: () => null }));
|
||||||
|
|
||||||
|
import { IconPicker } from "./icon-picker";
|
||||||
|
|
||||||
|
const directIconUrl = "https://cdn.example.com/shopify.svg";
|
||||||
|
|
||||||
|
const setInputValue = (input: HTMLInputElement, value: string) => {
|
||||||
|
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
||||||
|
if (!setter) throw new Error("Input value setter was not found");
|
||||||
|
setter.call(input, value);
|
||||||
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("IconPicker", () => {
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let root: Root;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", { value: true, writable: true });
|
||||||
|
Object.defineProperty(window, "matchMedia", {
|
||||||
|
writable: true,
|
||||||
|
value: (query: string) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: () => undefined,
|
||||||
|
removeListener: () => undefined,
|
||||||
|
addEventListener: () => undefined,
|
||||||
|
removeEventListener: () => undefined,
|
||||||
|
dispatchEvent: () => false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
findIconsQuery.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
root.unmount();
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderPicker = async (onChange: (value: string) => void) => {
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
await act(async () => {
|
||||||
|
root.render(createElement(MantineProvider, null, createElement(IconPicker, { value: "", onChange })));
|
||||||
|
});
|
||||||
|
const input = container.querySelector("input");
|
||||||
|
if (!input) throw new Error("Icon picker input was not rendered");
|
||||||
|
return input;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("keeps a direct URL visible without searching the icon repositories", async () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const input = await renderPicker(onChange);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
setInputValue(input, directIconUrl);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith(directIconUrl);
|
||||||
|
expect(input.value).toBe(directIconUrl);
|
||||||
|
expect(findIconsQuery.mock.calls.some(([query]) => query.searchText === directIconUrl)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps icon names in repository search mode", async () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const input = await renderPicker(onChange);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
setInputValue(input, "shopify");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChange).not.toHaveBeenCalled();
|
||||||
|
expect(input.value).toBe("shopify");
|
||||||
|
expect(findIconsQuery.mock.calls.some(([query]) => query.searchText === "shopify")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -63,7 +63,7 @@ export const IconPicker = ({
|
|||||||
const tCommon = useScopedI18n("common");
|
const tCommon = useScopedI18n("common");
|
||||||
const [debouncedQuery] = useDebouncedValue(query, 100);
|
const [debouncedQuery] = useDebouncedValue(query, 100);
|
||||||
|
|
||||||
const searchText = (debouncedQuery !== (value ?? "") && debouncedQuery) || "";
|
const searchText = !isDirectIconUrl(debouncedQuery) && debouncedQuery !== (value ?? "") ? debouncedQuery : "";
|
||||||
|
|
||||||
const { data, isLoading, isError, refetch } = clientApi.icon.findIcons.useQuery(
|
const { data, isLoading, isError, refetch } = clientApi.icon.findIcons.useQuery(
|
||||||
{ searchText },
|
{ searchText },
|
||||||
@@ -167,15 +167,12 @@ export const IconPicker = ({
|
|||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const nextValue = event.currentTarget.value;
|
const nextValue = event.currentTarget.value;
|
||||||
const trimmedValue = nextValue.trim();
|
const trimmedValue = nextValue.trim();
|
||||||
if (/^https?:\/\/[^\s/]+(?:\/\S*)?$/i.test(trimmedValue)) {
|
setQuery(nextValue);
|
||||||
startTransition(() => {
|
if (isDirectIconUrl(trimmedValue)) {
|
||||||
setValue(trimmedValue);
|
setValue(trimmedValue);
|
||||||
setQuery("");
|
combobox.closeDropdown();
|
||||||
combobox.closeDropdown();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
combobox.openDropdown();
|
combobox.openDropdown();
|
||||||
setQuery(nextValue);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClick={() => combobox.openDropdown()}
|
onClick={() => combobox.openDropdown()}
|
||||||
@@ -230,3 +227,12 @@ const shouldShowPreview = (value: string | null | undefined): value is string =>
|
|||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
return !localizationPathRegex.test(value);
|
return !localizationPathRegex.test(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDirectIconUrl = (value: string) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(value.trim());
|
||||||
|
return (url.protocol === "http:" || url.protocol === "https:") && Boolean(url.hostname);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in new issue
Block a user