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
140 lines
5.6 KiB
TypeScript
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);
|
|
});
|
|
});
|