项目文件夹

文件
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

209 行
6.5 KiB
TypeScript

/**
* Byline avatar field tests (#1250).
*
* The `avatarMediaId` field is part of the byline data model and is
* accepted by the create/update API, but the admin editor never
* rendered a control for it — so editors had no way to set a byline
* avatar, and (worse) every UI edit coerced the field back to `null`,
* wiping any value set programmatically.
*
* These tests assert the avatar picker renders in the editor and that
* a hydrated `avatarMediaId` round-trips through the PATCH body on
* save. Driving the MediaPickerModal dialog itself is out of scope
* here (its overlay blocks Playwright clicks — see the custom-fields
* test for the same constraint); the round-trip proves the form state
* is wired into the update body, which is the regression that matters.
*/
import { Toast } 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 { BylineSummary } from "../src/lib/api/bylines";
import type { MediaItem } from "../src/lib/api/media";
import { render } from "./utils/render.tsx";
vi.mock("@tanstack/react-router", async () => {
const actual =
await vi.importActual<typeof import("@tanstack/react-router")>("@tanstack/react-router");
return {
...actual,
useSearch: () => ({ locale: undefined }),
useNavigate: () => vi.fn(),
};
});
vi.mock("../src/lib/api/bylines", async () => {
const actual =
await vi.importActual<typeof import("../src/lib/api/bylines")>("../src/lib/api/bylines");
return {
...actual,
fetchBylines: vi.fn(),
fetchByline: vi.fn(),
fetchBylineTranslations: vi.fn().mockResolvedValue({ items: [] }),
createByline: vi.fn(),
updateByline: vi.fn(),
deleteByline: vi.fn(),
createBylineTranslation: vi.fn(),
};
});
vi.mock("../src/lib/api/users", async () => {
const actual =
await vi.importActual<typeof import("../src/lib/api/users")>("../src/lib/api/users");
return {
...actual,
fetchUsers: vi.fn().mockResolvedValue({ items: [], nextCursor: undefined }),
};
});
vi.mock("../src/lib/api/byline-fields", async () => {
const actual = await vi.importActual<typeof import("../src/lib/api/byline-fields")>(
"../src/lib/api/byline-fields",
);
return {
...actual,
// No custom fields — keeps the editor to its fixed columns.
listBylineFields: vi.fn().mockResolvedValue({ items: [] }),
};
});
vi.mock("../src/lib/api/media", async () => {
const actual =
await vi.importActual<typeof import("../src/lib/api/media")>("../src/lib/api/media");
return {
...actual,
fetchMediaItem: vi.fn(),
};
});
vi.mock("../src/lib/api/client", async () => {
const actual =
await vi.importActual<typeof import("../src/lib/api/client")>("../src/lib/api/client");
return {
...actual,
fetchManifest: vi.fn().mockResolvedValue({
version: "0.0.0",
hash: "test",
collections: {},
plugins: {},
authMode: "passkey",
taxonomies: [],
}),
};
});
const { fetchBylines, fetchByline, updateByline } = await import("../src/lib/api/bylines");
const { fetchMediaItem } = await import("../src/lib/api/media");
const { BylinesPage } = await import("../src/routes/bylines");
function makeByline(overrides: Partial<BylineSummary> = {}): BylineSummary {
return {
id: "byline_01",
slug: "jane-doe",
displayName: "Jane Doe",
bio: null,
avatarMediaId: null,
websiteUrl: null,
userId: null,
isGuest: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
locale: "en",
translationGroup: null,
customFields: {},
...overrides,
};
}
function makeMediaItem(overrides: Partial<MediaItem> = {}): MediaItem {
return {
id: "media_avatar_01",
filename: "avatar.jpg",
mimeType: "image/jpeg",
url: "/_emdash/api/media/file/media_avatar_01.jpg",
storageKey: "media_avatar_01.jpg",
size: 1024,
createdAt: new Date().toISOString(),
...overrides,
};
}
function TestWrapper({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return (
<QueryClientProvider client={queryClient}>
<Toast.Provider>{children}</Toast.Provider>
</QueryClientProvider>
);
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("BylinesPage — avatar field (#1250)", () => {
it("renders the avatar picker in create mode", async () => {
vi.mocked(fetchBylines).mockResolvedValue({ items: [], nextCursor: undefined });
const screen = await render(
<TestWrapper>
<BylinesPage />
</TestWrapper>,
);
await expect.element(screen.getByText("Create byline")).toBeInTheDocument();
// The avatar field renders its label and an empty-state select button.
await expect.element(screen.getByText("Avatar")).toBeInTheDocument();
// No avatar set → no resolve query fires.
expect(vi.mocked(fetchMediaItem)).not.toHaveBeenCalled();
});
it("resolves the stored avatar media for preview when editing", async () => {
const byline = makeByline({ avatarMediaId: "media_avatar_01" });
vi.mocked(fetchBylines).mockResolvedValue({ items: [byline], nextCursor: undefined });
vi.mocked(fetchByline).mockResolvedValue(byline);
vi.mocked(fetchMediaItem).mockResolvedValue(makeMediaItem());
const screen = await render(
<TestWrapper>
<BylinesPage />
</TestWrapper>,
);
await screen.getByRole("button", { name: /Jane Doe/ }).click();
await expect.element(screen.getByText("Avatar")).toBeInTheDocument();
// The field resolves the stored id into a media item for display.
await vi.waitFor(() =>
expect(vi.mocked(fetchMediaItem)).toHaveBeenCalledWith("media_avatar_01"),
);
});
it("forwards avatarMediaId in the PATCH body on save", async () => {
const byline = makeByline({ avatarMediaId: "media_avatar_01" });
vi.mocked(fetchBylines).mockResolvedValue({ items: [byline], nextCursor: undefined });
vi.mocked(fetchByline).mockResolvedValue(byline);
vi.mocked(fetchMediaItem).mockResolvedValue(makeMediaItem());
vi.mocked(updateByline).mockResolvedValue(byline);
const screen = await render(
<TestWrapper>
<BylinesPage />
</TestWrapper>,
);
await screen.getByRole("button", { name: /Jane Doe/ }).click();
await screen.getByRole("button", { name: "Save" }).click();
await new Promise((resolve) => setTimeout(resolve, 50));
expect(vi.mocked(updateByline)).toHaveBeenCalledTimes(1);
const [bylineId, body] = vi.mocked(updateByline).mock.calls[0]!;
expect(bylineId).toBe(byline.id);
expect(body.avatarMediaId).toBe("media_avatar_01");
});
});