chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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 } from "../../src/lib/api";
|
||||
import { render } from "../utils/render.tsx";
|
||||
|
||||
// Capture props passed to PortableTextEditor so the test can invoke the
|
||||
// block-sidebar callbacks the way the image node view does at runtime.
|
||||
const portableTextProps: { current: Record<string, unknown> | null } = { current: null };
|
||||
|
||||
vi.mock("../../src/components/PortableTextEditor", () => ({
|
||||
PortableTextEditor: (props: Record<string, unknown>) => {
|
||||
portableTextProps.current = props;
|
||||
return <div data-testid="portable-text-editor" />;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../src/components/MediaPickerModal", () => ({
|
||||
MediaPickerModal: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-router", async () => {
|
||||
const actual = await vi.importActual("@tanstack/react-router");
|
||||
return {
|
||||
...(actual as Record<string, unknown>),
|
||||
Link: ({ children, to, ...props }: { children: React.ReactNode; to?: string }) => (
|
||||
<a href={String(to ?? "")} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
useParams: () => ({ slug: "footer" }),
|
||||
useNavigate: () => vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockFetchSection = vi.fn<() => Promise<Section>>();
|
||||
const mockUpdateSection = vi.fn();
|
||||
|
||||
vi.mock("../../src/lib/api", async () => {
|
||||
const actual = await vi.importActual("../../src/lib/api");
|
||||
return {
|
||||
...(actual as Record<string, unknown>),
|
||||
fetchSection: (...args: unknown[]) => mockFetchSection(...(args as [])),
|
||||
updateSection: (...args: unknown[]) => mockUpdateSection(...(args as [])),
|
||||
};
|
||||
});
|
||||
|
||||
// Import after mocks so the module under test picks up the mocked deps.
|
||||
const { SectionEditor } = await import("../../src/components/SectionEditor");
|
||||
|
||||
function makeSection(overrides: Partial<Section> = {}): Section {
|
||||
return {
|
||||
id: "sec_footer",
|
||||
slug: "footer",
|
||||
title: "Footer",
|
||||
description: "All page footer",
|
||||
keywords: [],
|
||||
content: [],
|
||||
source: "user",
|
||||
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("SectionEditor", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
portableTextProps.current = null;
|
||||
mockFetchSection.mockResolvedValue(makeSection());
|
||||
});
|
||||
|
||||
it("opens the image settings panel when a block requests the sidebar", async () => {
|
||||
const screen = await render(<SectionEditor />, { wrapper: Wrapper });
|
||||
|
||||
// Editor must mount (and capture its props) before we can simulate.
|
||||
await expect.element(screen.getByTestId("portable-text-editor")).toBeInTheDocument();
|
||||
|
||||
const onBlockSidebarOpen = portableTextProps.current?.onBlockSidebarOpen as
|
||||
| ((panel: unknown) => void)
|
||||
| undefined;
|
||||
const onBlockSidebarClose = portableTextProps.current?.onBlockSidebarClose as
|
||||
| (() => void)
|
||||
| undefined;
|
||||
|
||||
// Both callbacks must be wired — without them, clicking the image-settings
|
||||
// icon in the editor is a silent no-op (#845).
|
||||
expect(typeof onBlockSidebarOpen).toBe("function");
|
||||
expect(typeof onBlockSidebarClose).toBe("function");
|
||||
|
||||
// Simulate the image node view asking for sidebar space, the same shape
|
||||
// it sends from ImageNode.openSidebar(). expect.element below polls until
|
||||
// React flushes the state update, so we don't need an explicit act wrapper.
|
||||
onBlockSidebarOpen!({
|
||||
type: "image",
|
||||
attrs: {
|
||||
src: "https://example.com/logo.png",
|
||||
alt: "Logo",
|
||||
mediaId: "media-1",
|
||||
width: 400,
|
||||
height: 200,
|
||||
},
|
||||
onUpdate: vi.fn(),
|
||||
onReplace: vi.fn(),
|
||||
onDelete: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
});
|
||||
|
||||
// The Image Settings panel should now be rendered in the sidebar slot.
|
||||
// The panel renders the image preview using the src we passed.
|
||||
await expect.element(screen.getByRole("img", { name: "Logo" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows both clean save actions as Saved", async () => {
|
||||
const screen = await render(<SectionEditor />, { wrapper: Wrapper });
|
||||
|
||||
await expect.element(screen.getByTestId("portable-text-editor")).toBeInTheDocument();
|
||||
const saveButtons = screen.getByRole("button", { name: "Saved", exact: true }).all();
|
||||
expect(saveButtons).toHaveLength(2);
|
||||
for (const button of saveButtons) await expect.element(button).toBeDisabled();
|
||||
expect(screen.getByRole("status").element().textContent).toBe("Saved");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user