Skip to content
Source
Sign inSign up
Open History to see the last commit

ThemeImportDialog.browser.test.tsx

218 lines7.0 KB

1import { StrictMode } from "react";
2import { describe, expect, it, vi } from "vite-plus/test";
3import { page, userEvent } from "vite-plus/test/browser";
4​
5import { render } from "../../../test/render";
6import {
7 CUSTOM_THEMES_STORAGE_KEY,
8 getCustomThemes,
9 installCustomTheme,
10 invalidateCustomThemes,
11 parseThemeFile,
12 themeColorToHex,
13 THEME_FILE_VERSION,
14} from "../../themePalette";
15import type { OpenVsxThemeExtension } from "../../openVsxThemes";
16import { ThemeImportDialog } from "./ThemeImportDialog";
17​
18const searchOpenVsxThemes = vi.hoisted(() => vi.fn());
19​
20vi.mock("../../openVsxThemes", () => ({
21 searchOpenVsxThemes,
22 importOpenVsxThemeExtension: vi.fn(),
23}));
24​
25const extension: OpenVsxThemeExtension = {
26 id: "acme.nightfall",
27 collectionId: "acme.nightfall",
28 name: "Nightfall",
29 publisher: "acme",
30 description: "A dark theme",
31 downloadCount: 10,
32 iconUrl: null,
33 sourceUrl: null,
34 manifestUrl: "https://example.test/manifest",
35 sha256Url: "https://example.test/sha256",
36 vsixUrl: "https://example.test/vsix",
37 version: "1.0.0",
38 license: "MIT",
39};
40​
41describe("ThemeImportDialog", () => {
42 // StrictMode replays ref attach/detach on mount, which must not end the session.
43 it("searches Open VSX under StrictMode", async () => {
44 searchOpenVsxThemes.mockReset();
45 searchOpenVsxThemes.mockResolvedValue([extension]);
46 await using _screen = await render(
47 <StrictMode>
48 <ThemeImportDialog
49 open
50 onOpenChange={() => {}}
51 onImported={() => true}
52 onImportedMany={() => {}}
53 />
54 </StrictMode>,
55 );
56​
57 // One input event: keystrokes more than the search debounce apart would search a prefix first.
58 await userEvent.fill(page.getByLabelText("Search Open VSX themes"), "night");
59​
60 await expect.element(page.getByRole("heading", { name: "Nightfall" })).toBeVisible();
61 expect(searchOpenVsxThemes).toHaveBeenCalledWith(
62 "night",
63 expect.objectContaining({ sortBy: "downloadCount" }),
64 );
65 });
66});
67​
68const theme = (name: string, accent = "#3b82f6") => ({
69 version: THEME_FILE_VERSION,
70 name,
71 appearance: "dark",
72 colors: { accent },
73});
74​
75const themeFile = (name: string, accent?: string) =>
76 new File([JSON.stringify(theme(name, accent))], `${name}.json`, { type: "application/json" });
77​
78it.each(["Update existing", "Keep both"])(
79 "keeps batch failures visible after %s",
80 async (choice) => {
81 using _themes = {
82 [Symbol.dispose]() {
83 window.localStorage.removeItem(CUSTOM_THEMES_STORAGE_KEY);
84 invalidateCustomThemes();
85 },
86 };
87 installCustomTheme(parseThemeFile(theme("Dusk")));
88 const onOpenChange = vi.fn();
89 await using _screen = await render(
90 <ThemeImportDialog
91 open
92 onOpenChange={onOpenChange}
93 onImported={() => true}
94 onImportedMany={() => {}}
95 />,
96 );
97 const input = document.querySelector<HTMLInputElement>('input[type="file"]')!;
98 await userEvent.upload(input, [
99 themeFile("Dawn"),
100 themeFile("Dusk", "#f97316"),
101 new File(["{"], "broken.json", { type: "application/json" }),
102 ]);
103​
104 await expect.element(page.getByText("Already installed")).toBeVisible();
105 await expect.element(page.getByText(/^broken\.json:/)).toBeVisible();
106 expect(getCustomThemes().map((theme) => theme.label)).toContain("Dawn");
107 await page.getByRole("button", { name: choice }).click();
108 invalidateCustomThemes();
109 const label = choice === "Update existing" ? "Dusk" : "Dusk (1)";
110 expect(getCustomThemes().find((theme) => theme.label === label)?.colors).toEqual(
111 parseThemeFile(theme("Dusk", "#f97316")).colors,
112 );
113 await expect.element(page.getByText(/^broken\.json:/)).toBeVisible();
114 expect(onOpenChange).not.toHaveBeenCalledWith(false);
115 },
116);
117​
118function isolateThemes() {
119 const previous = localStorage.getItem(CUSTOM_THEMES_STORAGE_KEY);
120 localStorage.removeItem(CUSTOM_THEMES_STORAGE_KEY);
121 invalidateCustomThemes();
122 return {
123 [Symbol.dispose]() {
124 if (previous === null) localStorage.removeItem(CUSTOM_THEMES_STORAGE_KEY);
125 else localStorage.setItem(CUSTOM_THEMES_STORAGE_KEY, previous);
126 invalidateCustomThemes();
127 },
128 };
130​
131it("sizes an oversized theme file just under 1 MB as 1.00 MB", async () => {
132 using _themes = isolateThemes();
133 await using _screen = await render(
134 <ThemeImportDialog
135 open
136 onOpenChange={() => {}}
137 onImported={() => true}
138 onImportedMany={() => {}}
139 />,
140 );
141 const input = document.querySelector<HTMLInputElement>('input[type="file"]')!;
142 await userEvent.upload(
143 input,
144 new File(["x".repeat(1_048_100)], "huge.json", { type: "application/json" }),
145 );
146​
147 await expect
148 .element(page.getByText(/^That file is/))
149 .toHaveTextContent(
150 "That file is 1.00 MB. Theme files are only a few KB, so this one was not read (limit 256 KB).",
151 );
152});
153​
154function themeJsonc(name: string, background: string) {
155 return `{
156 // VS Code themes use JSONC even with a .json extension.
157 "name": "${name}",
158 "colors": {
159 "editor.background": "${background}",
160 },
161 "tokenColors": [],
162 }`;
164​
165describe("ThemeImportDialog JSONC imports", () => {
166 it.each(["paste", "file"])("imports a VS Code JSONC theme from %s", async (source) => {
167 using _themes = isolateThemes();
168 await using _screen = await render(
169 <ThemeImportDialog
170 open
171 onOpenChange={() => {}}
172 onImported={() => true}
173 onImportedMany={() => {}}
174 />,
175 );
176 const json = themeJsonc("JSONC Night", "#002b36");
177 if (source === "paste") {
178 await page.getByRole("textbox", { name: "Theme JSON" }).fill(json);
179 } else {
180 const input = document.querySelector<HTMLInputElement>('input[type="file"]')!;
181 await userEvent.upload(input, new File([json], "night.json", { type: "application/json" }));
182 await expect.element(page.getByRole("textbox", { name: "Theme JSON" })).toHaveValue(json);
183 }
184 await page.getByRole("button", { name: "Add theme", exact: true }).click();
185​
186 expect(getCustomThemes().map((theme) => theme.label)).toEqual(["JSONC Night"]);
187 expect(themeColorToHex(getCustomThemes()[0]!.colors.canvas)).toBe("#002b36");
188 });
189​
190 it("imports each VS Code JSONC theme in a file batch", async () => {
191 using _themes = isolateThemes();
192 await using _screen = await render(
193 <ThemeImportDialog
194 open
195 onOpenChange={() => {}}
196 onImported={() => true}
197 onImportedMany={() => {}}
198 />,
199 );
200 const input = document.querySelector<HTMLInputElement>('input[type="file"]')!;
201 await userEvent.upload(input, [
202 new File([themeJsonc("JSONC Night", "#002b36")], "night.json", {
203 type: "application/json",
204 }),
205 new File([themeJsonc("JSONC Day", "#fdf6e3")], "day.json", {
206 type: "application/json",
207 }),
208 ]);
209​
210 await vi.waitFor(() => {
211 expect(getCustomThemes().map((theme) => theme.label)).toEqual(["JSONC Night", "JSONC Day"]);
212 });
213 expect(getCustomThemes().map((theme) => themeColorToHex(theme.colors.canvas))).toEqual([
214 "#002b36",
215 "#fdf6e3",
216 ]);
217 });
218});