1import { renderToStaticMarkup } from "react-dom/server";
2import { afterEach, describe, expect, it, vi } from "vite-plus/test";
3
4import { scrollToSettingsTarget, SettingsRow, SettingsUnavailableGroup } from "./settingsLayout";
5
6afterEach(() => {
7 vi.unstubAllGlobals();
8});
9
10describe("unavailable settings", () => {
11 it("groups disabled controls under one reason", () => {
12 const markup = renderToStaticMarkup(
13 <SettingsUnavailableGroup message="Only available in the desktop app.">
14 <SettingsRow title="Window capture" description="Capture a window." />
15 </SettingsUnavailableGroup>,
16 );
17
18 expect(markup).toContain("Only available in the desktop app.");
19 expect(markup).toContain("border-border/60");
20 expect(markup).toContain("[&amp;_h3]:opacity-64");
21 });
22});
23
24describe("settings search targets", () => {
25 it("scrolls directly to a section header and restarts the destination pulse", () => {
26 const sectionScrollIntoView = vi.fn();
27 const headerScrollIntoView = vi.fn();
28 const focus = vi.fn();
29 const remove = vi.fn();
30 const add = vi.fn();
31 const addEventListener = vi.fn();
32 const target = {
33 tagName: "SECTION",
34 firstElementChild: { scrollIntoView: headerScrollIntoView },
35 scrollIntoView: sectionScrollIntoView,
36 focus,
37 classList: { remove, add },
38 addEventListener,
39 offsetWidth: 100,
40 } as unknown as HTMLElement;
41 vi.stubGlobal("document", {
42 getElementById: vi.fn(() => target),
43 });
44 vi.stubGlobal("window", {
45 matchMedia: vi.fn(() => ({ matches: false })),
46 });
47
48 expect(scrollToSettingsTarget("providers")).toBe(true);
49 expect(headerScrollIntoView).toHaveBeenCalledWith({
50 behavior: "smooth",
51 block: "center",
52 });
53 expect(sectionScrollIntoView).not.toHaveBeenCalled();
54 expect(focus).toHaveBeenCalledWith({ preventScroll: true });
55 expect(remove).toHaveBeenCalledWith("settings-search-target-pulse");
56 expect(add).toHaveBeenCalledWith("settings-search-target-pulse");
57 expect(addEventListener).toHaveBeenCalledWith("blur", expect.any(Function), { once: true });
58 });
59
60 it("does not animate the destination when reduced motion is requested", () => {
61 const scrollIntoView = vi.fn();
62 const focus = vi.fn();
63 const remove = vi.fn();
64 const add = vi.fn();
65 const target = {
66 tagName: "DIV",
67 firstElementChild: null,
68 scrollIntoView,
69 focus,
70 classList: { remove, add },
71 offsetWidth: 100,
72 } as unknown as HTMLElement;
73 vi.stubGlobal("document", {
74 getElementById: vi.fn(() => target),
75 });
76 vi.stubGlobal("window", {
77 matchMedia: vi.fn(() => ({ matches: true })),
78 });
79
80 expect(scrollToSettingsTarget("word-wrap")).toBe(true);
81 expect(scrollIntoView).toHaveBeenCalledWith({
82 behavior: "auto",
83 block: "center",
84 });
85 expect(focus).toHaveBeenCalledWith({ preventScroll: true });
86 expect(remove).toHaveBeenCalledWith("settings-search-target-pulse");
87 expect(add).not.toHaveBeenCalled();
88 });
89
90 it("leaves not-yet-mounted destinations to their mount lifecycle", () => {
91 vi.stubGlobal("document", {
92 getElementById: vi.fn(() => null),
93 });
94
95 expect(scrollToSettingsTarget("archive")).toBe(false);
96 });
97});