Emdash source with visual editor image upload fix

Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
2026-05-03 10:44:54 +07:00
parent 78f81bebb6
commit 2d1be52177
2352 changed files with 662964 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import * as React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render } from "../utils/render.tsx";
// Mock API
let mockSeedInfo: any = null;
vi.mock("../../src/lib/api/client", async () => {
const actual = await vi.importActual("../../src/lib/api/client");
return {
...actual,
apiFetch: vi.fn().mockImplementation((url: string) => {
if (url.includes("/setup/status")) {
return Promise.resolve(
new Response(
JSON.stringify({
data: {
needsSetup: true,
authMode: "passkey",
...(mockSeedInfo ? { seedInfo: mockSeedInfo } : {}),
},
}),
{
status: 200,
},
),
);
}
if (url.includes("/setup/admin")) {
return Promise.resolve(
new Response(JSON.stringify({ data: { success: true } }), { status: 200 }),
);
}
if (url.includes("/setup") && !url.includes("status")) {
return Promise.resolve(
new Response(JSON.stringify({ data: { success: true } }), { status: 200 }),
);
}
return Promise.resolve(new Response(JSON.stringify({ data: {} }), { status: 200 }));
}),
};
});
// Mock WebAuthn so PasskeyRegistration doesn't bail out
Object.defineProperty(window, "PublicKeyCredential", {
value: function PublicKeyCredential() {},
writable: true,
});
// Import after mocks
const { SetupWizard } = await import("../../src/components/SetupWizard");
function QueryWrapper({ children }: { children: React.ReactNode }) {
const qc = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
}
describe("SetupWizard", () => {
beforeEach(() => {
vi.clearAllMocks();
mockSeedInfo = null;
});
it("shows site setup step first with title input", async () => {
const screen = await render(
<QueryWrapper>
<SetupWizard />
</QueryWrapper>,
);
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
await expect.element(screen.getByPlaceholder("My Awesome Blog")).toBeInTheDocument();
});
it("empty title prevents advancing and shows validation error", async () => {
const screen = await render(
<QueryWrapper>
<SetupWizard />
</QueryWrapper>,
);
// Wait for setup status to load
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
// Click Continue without filling title
await screen.getByText("Continue →").click();
await expect.element(screen.getByText("Site title is required")).toBeInTheDocument();
// Should still be on site step
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
});
it("filling title and clicking Next advances to admin step", async () => {
const screen = await render(
<QueryWrapper>
<SetupWizard />
</QueryWrapper>,
);
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
// Fill in the title
await screen.getByPlaceholder("My Awesome Blog").fill("Test Site");
// Click continue
await screen.getByText("Continue →").click();
// Should advance to admin step
await expect.element(screen.getByText("Create your account")).toBeInTheDocument();
});
it("admin step shows email input", async () => {
const screen = await render(
<QueryWrapper>
<SetupWizard />
</QueryWrapper>,
);
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
// Fill title and advance
await screen.getByPlaceholder("My Awesome Blog").fill("Test Site");
await screen.getByText("Continue →").click();
// Should see email input on admin step
await expect.element(screen.getByText("Create your account")).toBeInTheDocument();
await expect.element(screen.getByPlaceholder("you@example.com")).toBeInTheDocument();
});
it("empty email prevents advancing on admin step", async () => {
const screen = await render(
<QueryWrapper>
<SetupWizard />
</QueryWrapper>,
);
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
// Fill title and advance
await screen.getByPlaceholder("My Awesome Blog").fill("Test Site");
await screen.getByText("Continue →").click();
await expect.element(screen.getByText("Create your account")).toBeInTheDocument();
// Click Continue without filling email
await screen.getByText("Continue →").click();
// Should show validation error
await expect.element(screen.getByText("Email is required")).toBeInTheDocument();
});
it("step indicator shows three steps", async () => {
const screen = await render(
<QueryWrapper>
<SetupWizard />
</QueryWrapper>,
);
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
// Step indicator labels - use exact matching via role
await expect.element(screen.getByText("Account")).toBeInTheDocument();
await expect.element(screen.getByText("Sign In")).toBeInTheDocument();
});
it("prefills title and tagline from seedInfo", async () => {
mockSeedInfo = {
name: "Blog Template",
description: "A blog template",
collections: 2,
hasContent: true,
title: "My Awesome Blog",
tagline: "Thoughts and tutorials",
};
const screen = await render(
<QueryWrapper>
<SetupWizard />
</QueryWrapper>,
);
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
const titleInput = screen.getByPlaceholder("My Awesome Blog");
const taglineInput = screen.getByPlaceholder("Thoughts, tutorials, and more");
await vi.waitFor(() => {
expect((titleInput.element() as HTMLInputElement).value).toBe("My Awesome Blog");
});
await vi.waitFor(() => {
expect((taglineInput.element() as HTMLInputElement).value).toBe("Thoughts and tutorials");
});
});
it("uses empty string for title and tagline when not in seedInfo", async () => {
mockSeedInfo = {
name: "Blank Template",
description: "A blank template",
collections: 0,
hasContent: false,
// title and tagline not provided
};
const screen = await render(
<QueryWrapper>
<SetupWizard />
</QueryWrapper>,
);
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
const titleInput = screen.getByPlaceholder("My Awesome Blog");
const taglineInput = screen.getByPlaceholder("Thoughts, tutorials, and more");
await vi.waitFor(() => {
expect((titleInput.element() as HTMLInputElement).value).toBe("");
});
await vi.waitFor(() => {
expect((taglineInput.element() as HTMLInputElement).value).toBe("");
});
});
it("prefilled title can be edited and submitted", async () => {
mockSeedInfo = {
name: "Blog Template",
description: "A blog template",
collections: 2,
hasContent: true,
title: "My Awesome Blog",
tagline: "Thoughts and tutorials",
};
const screen = await render(
<QueryWrapper>
<SetupWizard />
</QueryWrapper>,
);
await expect.element(screen.getByText("Set up your site")).toBeInTheDocument();
const titleInput = screen.getByPlaceholder("My Awesome Blog");
await vi.waitFor(() => {
expect((titleInput.element() as HTMLInputElement).value).toBe("My Awesome Blog");
});
// Edit the title
await titleInput.fill("My Custom Blog");
await vi.waitFor(() => {
expect((screen.getByPlaceholder("My Awesome Blog").element() as HTMLInputElement).value).toBe(
"My Custom Blog",
);
});
// Should be able to advance with the edited value
await screen.getByText("Continue →").click();
await expect.element(screen.getByText("Create your account")).toBeInTheDocument();
});
});