import { test, expect, type Page } from "@playwright/test"; import { channelStage, generateReport, readJson, resetData, writeSite, } from "./helpers"; // The channel form's "Sites" membership section: per-site checkbox + group // dropdown (+ inline "+ New group…") on both the create and edit screens. type SiteFile = { groups: { id: string; name: string; selectedByDefault: boolean }[]; channels: { slug: string; groupId?: string; order?: number }[]; }; // Sentinel select value for "+ New group…" (SiteMembershipsSection.tsx). const NEW_GROUP = "__new__"; // Two sites over the shared two-channel pool. Alpha has a second group and // slow-b sits in it with an explicit order; beta has just the default group. async function seed() { await resetData("two-slow-channels"); await writeSite("alpha", { siteTitle: "Alpha", groups: [ { id: "default", name: "All channels", selectedByDefault: true }, { id: "news", name: "News", selectedByDefault: false }, ], channels: [ { slug: "slow-a" }, { slug: "slow-b", groupId: "news", order: 5 }, ], }); await writeSite("beta", { siteTitle: "Beta" }); } const readSite = (id: string) => readJson(`test-transcripts/sites/${id}/site.json`); // The membership inputs are controlled React state that the edit page also // server-renders, so a pre-hydration click can be silently swallowed (see the // charts-metadata lost-click incident). Both helpers verify each interaction // against React-rendered output (the group select / new-group input only // exist when state says so) and retry until it sticks. async function setSiteChecked(page: Page, site: string, on: boolean) { const groupSelect = page.getByLabel(`Group for ${site}`); await expect(async () => { if (((await groupSelect.count()) > 0) !== on) { await page.getByLabel(`Include on ${site}`).click(); } await expect(groupSelect).toHaveCount(on ? 1 : 0, { timeout: 1_000 }); }).toPass({ timeout: 15_000 }); } async function selectGroup(page: Page, site: string, value: string) { const groupSelect = page.getByLabel(`Group for ${site}`); const nameInput = page.getByLabel(`New group name for ${site}`); // Selecting "+ New group…" reveals a React-rendered input — proof the change // handler ran. Once that sticks, picking the real value is safe. await expect(async () => { await groupSelect.selectOption(NEW_GROUP); await expect(nameInput).toBeVisible({ timeout: 1_000 }); }).toPass({ timeout: 15_000 }); if (value !== NEW_GROUP) { await groupSelect.selectOption(value); await expect(nameInput).toHaveCount(0); } } test("create: active site pre-checked, explicit group + second site", async ({ page, }) => { await seed(); await page.goto("/channels/new?site=alpha"); // The pre-check happens client-side only (mount effect), so this assertion // doubles as a hydration gate for the plain interactions below. await expect(page.getByLabel("Include on Alpha")).toBeChecked(); await expect(page.getByLabel("Include on Beta")).not.toBeChecked(); await page.getByLabel("Group for Alpha").selectOption("news"); await page.getByLabel("Include on Beta").check(); await page.getByLabel(/^name/i).fill("Gamma Channel"); await page.getByLabel(/^slug/i).fill("gamma"); await page.getByRole("button", { name: /create channel/i }).click(); await page.waitForURL("**/channels/gamma", { timeout: 10_000 }); const alpha = await readSite("alpha"); expect(alpha.channels).toContainEqual({ slug: "gamma", groupId: "news" }); const beta = await readSite("beta"); expect(beta.channels).toContainEqual({ slug: "gamma" }); }); test("create: + New group creates the group and assigns the channel", async ({ page, }) => { await seed(); await page.goto("/channels/new?site=alpha"); await expect(page.getByLabel("Include on Alpha")).toBeChecked(); await selectGroup(page, "Alpha", NEW_GROUP); await page.getByLabel("New group name for Alpha").fill("Interviews"); await page.getByLabel(/^name/i).fill("Delta Channel"); await page.getByLabel(/^slug/i).fill("delta"); await page.getByRole("button", { name: /create channel/i }).click(); await page.waitForURL("**/channels/delta", { timeout: 10_000 }); const alpha = await readSite("alpha"); expect(alpha.groups).toContainEqual({ id: "interviews", name: "Interviews", selectedByDefault: false, }); expect(alpha.channels).toContainEqual({ slug: "delta", groupId: "interviews", }); }); test("edit: switching news → (default) keeps order, sibling untouched", async ({ page, }) => { await seed(); await generateReport(page, "slow-b"); await page.goto(channelStage("slow-b", "configure")); await expect(page.getByLabel("Include on Alpha")).toBeChecked(); await expect(page.getByLabel("Group for Alpha")).toHaveValue("news"); await selectGroup(page, "Alpha", ""); await page.getByRole("button", { name: /save changes/i }).click(); await expect .poll(async () => (await readSite("alpha")).channels.find((c) => c.slug === "slow-b"), ) .toEqual({ slug: "slow-b", order: 5 }); const alpha = await readSite("alpha"); expect(alpha.channels).toContainEqual({ slug: "slow-a" }); }); test("edit: unchecking removes the membership, sibling intact", async ({ page, }) => { await seed(); await generateReport(page, "slow-b"); await page.goto(channelStage("slow-b", "configure")); await expect(page.getByLabel("Include on Alpha")).toBeChecked(); await setSiteChecked(page, "Alpha", false); await page.getByRole("button", { name: /save changes/i }).click(); await expect .poll(async () => (await readSite("alpha")).channels.map((c) => c.slug)) .toEqual(["slow-a"]); }); test("edit: + New group on another site", async ({ page }) => { await seed(); await generateReport(page, "slow-a"); await page.goto(channelStage("slow-a", "configure")); await expect(page.getByLabel("Include on Alpha")).toBeChecked(); await expect(page.getByLabel("Include on Beta")).not.toBeChecked(); await setSiteChecked(page, "Beta", true); await selectGroup(page, "Beta", NEW_GROUP); await page.getByLabel("New group name for Beta").fill("Interviews"); await page.getByRole("button", { name: /save changes/i }).click(); await expect .poll(async () => (await readSite("beta")).groups) .toContainEqual({ id: "interviews", name: "Interviews", selectedByDefault: false, }); const beta = await readSite("beta"); expect(beta.channels).toContainEqual({ slug: "slow-a", groupId: "interviews", }); }); test("zero sites: informational note, create still succeeds", async ({ page, }) => { await resetData("empty"); await generateReport(page, "new"); await page.goto("/channels/new"); await expect(page.getByText(/no sites configured/i)).toBeVisible(); await page.getByLabel(/^name/i).fill("Solo Channel"); await page.getByLabel(/^slug/i).fill("solo"); await page.getByRole("button", { name: /create channel/i }).click(); await page.waitForURL("**/channels/solo", { timeout: 10_000 }); });