Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

210 lines
6.3 KiB
TypeScript

import { Toasty } from "@cloudflare/kumo";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import * as React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Section, SectionsResult } from "../../src/lib/api";
import { render } from "../utils/render.tsx";
// Mock router
vi.mock("@tanstack/react-router", async () => {
const actual = await vi.importActual("@tanstack/react-router");
return {
...actual,
Link: ({ children, to, ...props }: any) => (
<a href={to} {...props}>
{children}
</a>
),
useNavigate: () => vi.fn(),
};
});
const mockFetchSections = vi.fn<() => Promise<SectionsResult>>();
const mockCreateSection = vi.fn();
const mockDeleteSection = vi.fn();
vi.mock("../../src/lib/api", async () => {
const actual = await vi.importActual("../../src/lib/api");
return {
...actual,
fetchSections: (...args: unknown[]) => mockFetchSections(...(args as [])),
createSection: (...args: unknown[]) => mockCreateSection(...(args as [])),
deleteSection: (...args: unknown[]) => mockDeleteSection(...(args as [])),
};
});
// Import after mocks
const { Sections } = await import("../../src/components/Sections");
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const DELETE_SECTION_MSG_REGEX = /This will permanently delete/;
function makeSection(overrides: Partial<Section> = {}): Section {
return {
id: "sec_01",
slug: "hero",
title: "Hero Section",
description: "Main hero",
keywords: [],
content: [],
source: "theme",
createdAt: "2025-01-01T00:00:00Z",
updatedAt: "2025-01-02T00:00:00Z",
...overrides,
};
}
function Wrapper({ children }: { children: React.ReactNode }) {
const qc = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return (
<QueryClientProvider client={qc}>
<Toasty>{children}</Toasty>
</QueryClientProvider>
);
}
describe("Sections", () => {
beforeEach(() => {
vi.clearAllMocks();
mockFetchSections.mockResolvedValue({
items: [
makeSection({
id: "sec_01",
slug: "hero",
title: "Hero Section",
description: "Main hero",
source: "theme",
}),
makeSection({
id: "sec_02",
slug: "cta",
title: "Call to Action",
description: "CTA block",
source: "user",
}),
],
});
mockCreateSection.mockResolvedValue(makeSection({ slug: "new-section" }));
mockDeleteSection.mockResolvedValue(undefined);
});
it("displays sections with titles and descriptions", async () => {
const screen = await render(
<Wrapper>
<Sections />
</Wrapper>,
);
await expect.element(screen.getByText("Hero Section")).toBeInTheDocument();
await expect.element(screen.getByText("Call to Action")).toBeInTheDocument();
await expect.element(screen.getByText("Main hero")).toBeInTheDocument();
await expect.element(screen.getByText("CTA block")).toBeInTheDocument();
});
it("create button opens dialog with title/slug form", async () => {
const screen = await render(
<Wrapper>
<Sections />
</Wrapper>,
);
await screen.getByText("New Section").click();
await expect.element(screen.getByText("Create Section")).toBeInTheDocument();
// Check form fields exist — InputArea uses label prop but may not be associated via aria
await expect.element(screen.getByLabelText("Title")).toBeInTheDocument();
await expect.element(screen.getByLabelText("Slug")).toBeInTheDocument();
});
it("auto-generates slug from title in create dialog", async () => {
const screen = await render(
<Wrapper>
<Sections />
</Wrapper>,
);
await screen.getByText("New Section").click();
const titleInput = screen.getByLabelText("Title");
await titleInput.fill("My Great Section");
// Slug should be auto-generated
await expect.element(screen.getByLabelText("Slug")).toHaveValue("my-great-section");
});
it("search input filters sections", async () => {
const screen = await render(
<Wrapper>
<Sections />
</Wrapper>,
);
const searchInput = screen.getByPlaceholder("Search sections...");
await searchInput.fill("hero");
// fetchSections will be called again with search param
expect(mockFetchSections).toHaveBeenCalledWith(expect.objectContaining({ search: "hero" }));
});
it("delete button opens confirmation dialog", async () => {
mockFetchSections.mockResolvedValue({
items: [
makeSection({
id: "sec_02",
slug: "cta",
title: "Call to Action",
description: "CTA block",
source: "user",
}),
],
});
const screen = await render(
<Wrapper>
<Sections />
</Wrapper>,
);
await expect.element(screen.getByText("Call to Action")).toBeInTheDocument();
// Click delete on the user section. Kumo 2.x wraps `<Button title>` as
// a Tooltip popup rather than a DOM `title` attribute, so we locate the
// button by its aria-label instead.
const deleteButton = screen.getByLabelText("Delete Call to Action");
await deleteButton.click();
await expect.element(screen.getByText("Delete Section?")).toBeInTheDocument();
await expect.element(screen.getByText(DELETE_SECTION_MSG_REGEX)).toBeInTheDocument();
});
it("theme sections have disabled delete button", async () => {
mockFetchSections.mockResolvedValue({
items: [
makeSection({
id: "sec_01",
slug: "hero",
title: "Hero Section",
source: "theme",
}),
],
});
const screen = await render(
<Wrapper>
<Sections />
</Wrapper>,
);
await expect.element(screen.getByText("Hero Section")).toBeInTheDocument();
// Kumo 2.x's `<Button title>` is rendered as a Tooltip popup, not a DOM
// `title`. Locate the button by aria-label and assert the disabled state
// directly; the "Cannot delete theme sections" copy is now in the
// hover Tooltip rather than a queryable attribute.
const deleteButton = screen.getByLabelText("Delete Hero Section");
await expect.element(deleteButton).toBeDisabled();
});
it("each section has an edit button", async () => {
const screen = await render(
<Wrapper>
<Sections />
</Wrapper>,
);
await expect.element(screen.getByText("Hero Section")).toBeInTheDocument();
const editButtons = screen.getByText("Edit").all();
expect(editButtons.length).toBe(2);
});
});