BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import { fireEvent, render, screen } from "@solidjs/testing-library";
2import { createSignal } from "solid-js";
3import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4import { LoginPanel } from "../LoginPanel";
5
6const invokeMock = vi.hoisted(() => vi.fn());
7
8vi.mock("@tauri-apps/api/core", () => ({ invoke: invokeMock }));
9
10function renderPanel(overrides: Partial<Parameters<typeof LoginPanel>[0]> = {}) {
11 const defaults = { value: "", pending: false, shakeCount: 0, onInput: vi.fn(), onSubmit: vi.fn() };
12
13 return render(() => <LoginPanel {...{ ...defaults, ...overrides }} />);
14}
15
16function renderInteractivePanel() {
17 const onSubmit = vi.fn();
18
19 return {
20 onSubmit,
21 ...render(() => {
22 const [value, setValue] = createSignal("");
23 return <LoginPanel value={value()} pending={false} shakeCount={0} onInput={setValue} onSubmit={onSubmit} />;
24 }),
25 };
26}
27
28describe("LoginPanel", () => {
29 beforeEach(() => {
30 vi.useFakeTimers();
31 invokeMock.mockReset();
32 invokeMock.mockResolvedValue([]);
33 });
34
35 afterEach(() => {
36 vi.useRealTimers();
37 });
38
39 it("renders branded header with Lazurite logo", () => {
40 renderPanel();
41
42 expect(screen.getByText("Lazurite")).toBeInTheDocument();
43 expect(screen.getByText("Powered by Bluesky")).toBeInTheDocument();
44 expect(screen.getByText(/sign in with your/i)).toBeInTheDocument();
45 expect(screen.getByRole("link", { name: "Internet Handle" })).toBeInTheDocument();
46
47 const svg = document.querySelector("svg");
48 expect(svg).toBeInTheDocument();
49 expect(svg).toHaveAttribute("fill", "currentColor");
50 });
51
52 it("uses solid primary background on submit button (no gradient)", () => {
53 renderPanel();
54
55 const button = screen.getByRole("button", { name: /continue/i });
56 expect(button.className).toContain("bg-primary");
57 expect(button.className).not.toContain("gradient");
58 });
59
60 it("uses rounded-xl on input (not rounded-full)", () => {
61 renderPanel();
62
63 const input = screen.getByPlaceholderText("alice.bsky.social");
64 expect(input.className).toContain("rounded-xl");
65 expect(input.className).not.toContain("rounded-full");
66 });
67
68 it("shows loading state when pending", () => {
69 renderPanel({ pending: true });
70
71 expect(screen.getByText("Opening sign-in...")).toBeInTheDocument();
72 expect(screen.getByRole("button")).toBeDisabled();
73 });
74
75 it("requests autocomplete suggestions for handle-like input", async () => {
76 invokeMock.mockResolvedValue([{
77 did: "did:plc:alice",
78 handle: "alice.bsky.social",
79 displayName: "Alice Example",
80 avatar: null,
81 }]);
82
83 renderInteractivePanel();
84
85 const input = screen.getByPlaceholderText("alice.bsky.social");
86 input.focus();
87 fireEvent.input(input, { target: { value: "ali" } });
88 await vi.advanceTimersByTimeAsync(200);
89
90 expect(invokeMock).toHaveBeenCalledWith("search_login_suggestions", { query: "ali" });
91 expect(await screen.findByText("Alice Example")).toBeInTheDocument();
92 expect(screen.getByText("@alice.bsky.social")).toBeInTheDocument();
93 });
94
95 it("applies the highlighted suggestion on enter instead of submitting immediately", async () => {
96 const { onSubmit } = renderInteractivePanel();
97 invokeMock.mockResolvedValue([{
98 did: "did:plc:alice",
99 handle: "alice.bsky.social",
100 displayName: "Alice Example",
101 avatar: null,
102 }]);
103
104 const input = screen.getByPlaceholderText("alice.bsky.social");
105 input.focus();
106 fireEvent.input(input, { target: { value: "ali" } });
107 await vi.advanceTimersByTimeAsync(200);
108
109 fireEvent.keyDown(input, { key: "Enter" });
110
111 expect(screen.getByDisplayValue("alice.bsky.social")).toBeInTheDocument();
112 expect(onSubmit).not.toHaveBeenCalled();
113 });
114});