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

140 lines
5.6 KiB
TypeScript

/**
* Byline Custom Fields E2E (Phase 7 of Discussion #1174)
*
* Proves the full round-trip: an admin registers a custom field via
* `/byline-schema`, a byline is given a value through the edit form,
* and the GET response on `/api/admin/bylines/{id}` returns the value
* via `customFields.{slug}` — exercising every layer the PR touches
* (registry, repository hydration, admin API, schema management UI,
* byline edit form).
*
* Workers are pinned to 1 in `playwright.config.ts`, and
* `global-setup.ts` rebuilds the fixture DB per `pnpm test:e2e`
* invocation — so a fixed slug works without conflicting with itself
* across runs. We still suffix with a timestamp because the same
* `pnpm test:e2e` invocation may run this spec alongside others that
* touch the bylines table.
*/
import { test, expect } from "../fixtures";
function apiHeaders(token: string, baseUrl: string): Record<string, string> {
return {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
"X-EmDash-Request": "1",
Origin: baseUrl,
};
}
test.describe("Byline custom fields", () => {
test.beforeEach(async ({ admin }) => {
await admin.devBypassAuth();
});
test("custom field value round-trips through the API end to end", async ({
admin,
page,
serverInfo,
}) => {
const unique = Date.now();
const fieldSlug = `job_title_${unique}`;
const fieldLabel = `Job title ${unique}`;
const bylineDisplayName = `Jane Custom ${unique}`;
const bylineSlug = `jane-custom-${unique}`;
const fieldValue = "Editor";
// ---------------------------------------------------------------
// 1. Register a custom field via /byline-schema
// ---------------------------------------------------------------
await admin.goto("/byline-schema");
await admin.waitForLoading();
await page.getByRole("button", { name: "New field" }).click();
// `BylineFieldEditor` auto-fills the slug from the label
// (Phase 5). Filling label first matches the production UX
// flow; overriding slug afterwards proves the input is editable.
await page.getByLabel("Label").fill(fieldLabel);
await page.getByLabel("Slug").fill(fieldSlug);
// Type defaults to `string` — leave it. `translatable` defaults
// to true — leave it. Required stays off.
await page.getByRole("button", { name: "Create field" }).click();
// The new field row should show up in the schema table. Scope
// the assertion to the table because the success toast also
// contains the field label ("Created \"{label}\"."), and
// Playwright's strict-mode locator rejects ambiguous matches.
await expect(page.locator("table").getByText(fieldLabel)).toBeVisible({
timeout: 5000,
});
// ---------------------------------------------------------------
// 2. Create a byline via /bylines and select it
// ---------------------------------------------------------------
await admin.goto("/bylines");
await admin.waitForLoading();
await page.getByRole("button", { name: "New" }).click();
await page.getByLabel("Display name").fill(bylineDisplayName);
await page.getByLabel("Slug").fill(bylineSlug);
// Guest byline keeps the form simple — no user-link side quest.
await page.getByRole("switch", { name: "Guest byline" }).click();
await page.getByRole("button", { name: "Create" }).click();
// After create, the form moves to edit mode and the sidebar list
// re-renders with the new byline highlighted. Custom-field inputs
// are gated on `selected`, so they appear only after create lands.
await expect(page.getByRole("button", { name: bylineDisplayName })).toBeVisible({
timeout: 5000,
});
// ---------------------------------------------------------------
// 3. Fill the custom field input and save
// ---------------------------------------------------------------
await expect(page.getByLabel(fieldLabel)).toBeVisible();
await page.getByLabel(fieldLabel).fill(fieldValue);
await page.getByRole("button", { name: "Save" }).click();
// ---------------------------------------------------------------
// 4. Verify the round-trip via the REST API
// ---------------------------------------------------------------
// Find the byline id via the list endpoint (the sidebar's selected
// row is keyed by id internally; reading via API is easier than
// scraping the DOM). Filter by slug to avoid pagination concerns.
const headers = apiHeaders(serverInfo.token, serverInfo.baseUrl);
const listResponse = await fetch(
`${serverInfo.baseUrl}/_emdash/api/admin/bylines?search=${encodeURIComponent(bylineSlug)}`,
{ headers },
);
expect(listResponse.ok).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const listBody: any = await listResponse.json();
const created = (listBody.data?.items ?? []).find(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(b: any) => b.slug === bylineSlug,
);
expect(created).toBeTruthy();
expect(created.displayName).toBe(bylineDisplayName);
const getResponse = await fetch(
`${serverInfo.baseUrl}/_emdash/api/admin/bylines/${created.id}`,
{ headers },
);
expect(getResponse.ok).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getBody: any = await getResponse.json();
const fetched = getBody.data;
// The core assertion: the value we typed in the form is what
// the API returns for the field slug. Goes through every layer:
// admin UI form state → PATCH body → handler → BylineRepository
// .update → translatable value table → hydration → GET response.
expect(fetched.customFields).toBeTruthy();
expect(fetched.customFields[fieldSlug]).toBe(fieldValue);
});
});