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
138 lines
4.1 KiB
TypeScript
138 lines
4.1 KiB
TypeScript
import * as React from "react";
|
|
import { describe, it, expect, vi } from "vitest";
|
|
|
|
import { RepeaterField } from "../../src/components/RepeaterField";
|
|
import { render } from "../utils/render.tsx";
|
|
|
|
describe("RepeaterField", () => {
|
|
describe("datetime sub-field", () => {
|
|
it("displays a stored ISO datetime in the datetime-local input", async () => {
|
|
// Mirrors the top-level datetime widget contract: full ISO 8601
|
|
// values must round-trip through `<input type="datetime-local">`,
|
|
// which only accepts `YYYY-MM-DDTHH:mm`.
|
|
const screen = await render(
|
|
<RepeaterField
|
|
label="Recalls"
|
|
id="recalls"
|
|
value={[{ recall_date: "2026-02-26T09:30:00.000Z" }]}
|
|
onChange={vi.fn()}
|
|
subFields={[{ slug: "recall_date", type: "datetime", label: "Recall date" }]}
|
|
/>,
|
|
);
|
|
const input = screen.getByLabelText("Recall date");
|
|
await expect.element(input).toHaveValue("2026-02-26T09:30");
|
|
});
|
|
|
|
it("emits a full ISO 8601 value with Z and milliseconds on change", async () => {
|
|
const onChange = vi.fn();
|
|
const screen = await render(
|
|
<RepeaterField
|
|
label="Recalls"
|
|
id="recalls"
|
|
value={[{ recall_date: "" }]}
|
|
onChange={onChange}
|
|
subFields={[{ slug: "recall_date", type: "datetime", label: "Recall date" }]}
|
|
/>,
|
|
);
|
|
const input = screen.getByLabelText("Recall date");
|
|
await input.fill("2026-02-26T09:30");
|
|
|
|
expect(onChange).toHaveBeenLastCalledWith([
|
|
expect.objectContaining({ recall_date: "2026-02-26T09:30:00.000Z" }),
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Image sub-field support (issue #1424): rows render the media picker
|
|
* (ImageFieldRenderer) instead of falling through to a plain text input.
|
|
*/
|
|
describe("RepeaterField sub-field types", () => {
|
|
it("renders the media picker for image sub-fields", async () => {
|
|
const screen = await render(
|
|
<RepeaterField
|
|
label="Gallery"
|
|
id="gallery"
|
|
value={[{ image: null, caption: "" }]}
|
|
onChange={vi.fn()}
|
|
subFields={[
|
|
{ slug: "image", type: "image", label: "Image" },
|
|
{ slug: "caption", type: "string", label: "Caption" },
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
// Image sub-field → picker button, not a text input.
|
|
await expect.element(screen.getByRole("button", { name: /Select image/ })).toBeVisible();
|
|
// Scalar sub-fields keep their plain inputs.
|
|
await expect.element(screen.getByRole("textbox", { name: "Caption" })).toBeVisible();
|
|
});
|
|
|
|
it("shows the existing image preview for media values", async () => {
|
|
const screen = await render(
|
|
<RepeaterField
|
|
label="Gallery"
|
|
id="gallery"
|
|
value={[
|
|
{
|
|
image: {
|
|
id: "m1",
|
|
provider: "local",
|
|
alt: "",
|
|
meta: { storageKey: "01ABC.png" },
|
|
},
|
|
},
|
|
]}
|
|
onChange={vi.fn()}
|
|
subFields={[{ slug: "image", type: "image", label: "Image" }]}
|
|
/>,
|
|
);
|
|
|
|
// MediaValue with a storageKey renders the local-media preview image.
|
|
await expect
|
|
.element(screen.container.querySelector('img[src="/_emdash/api/media/file/01ABC.png"]'))
|
|
.toBeInTheDocument();
|
|
});
|
|
|
|
it("initializes image sub-fields as null when adding an item", async () => {
|
|
const onChange = vi.fn();
|
|
const screen = await render(
|
|
<RepeaterField
|
|
label="Gallery"
|
|
id="gallery"
|
|
value={[]}
|
|
onChange={onChange}
|
|
subFields={[
|
|
{ slug: "image", type: "image", label: "Image" },
|
|
{ slug: "caption", type: "string", label: "Caption" },
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
await screen.getByRole("button", { name: /Add First Item/ }).click();
|
|
|
|
expect(onChange).toHaveBeenCalledWith([{ image: null, caption: "" }]);
|
|
});
|
|
|
|
it("renders a searchable combobox for select sub-fields", async () => {
|
|
const screen = await render(
|
|
<RepeaterField
|
|
label="Services"
|
|
id="services"
|
|
value={[{ service: "CT" }]}
|
|
onChange={vi.fn()}
|
|
subFields={[
|
|
{ slug: "service", type: "select", label: "Service", options: ["MRI", "CT", "PET"] },
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
// Select sub-field → searchable typeahead input (role "combobox"),
|
|
// not a plain native select, and it reflects the stored value.
|
|
const input = screen.getByRole("combobox");
|
|
await expect.element(input).toBeVisible();
|
|
await expect.element(input).toHaveValue("CT");
|
|
});
|
|
});
|