commit 95ca71949f2ce9c4d86ac3c12a511315d5f42c83
parent b89d16fb8af83161b538ef58b826ef39d0b4b78d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 01:27:05 -0400
editor(e2e): forms-keep-input — a refused submit keeps what was typed, one case per form the slice changed
Eighteen cases: SiteForm (create and edit), SettingsForm (and the save after it
shows what was stored), ChannelForm (create and edit), RenameChannelForm,
DeleteChannelForm, DeleteVideoDirSection, HealthTimingForm, HomepageConfigForm,
CutReleaseForm, the four operation-settings forms, SchedulerSettingsForm,
SavedVideosControls and WorkersConfigForm. Each submits something its action
refuses and asserts the refusal and every field.
The operation, sync and backup forms cannot be refused from the page — their
actions fail only when settings.json cannot be written — so those cases make
that true for the length of the submit: the file is set aside and a directory
takes its path (the atomic rename fails EISDIR), and it is back before the test
ends.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
1 file changed, 719 insertions(+), 0 deletions(-)
diff --git a/editor/e2e/forms-keep-input.spec.ts b/editor/e2e/forms-keep-input.spec.ts
@@ -0,0 +1,719 @@
+import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
+import { test, expect, type Locator, type Page } from "@playwright/test";
+import {
+ channelStage,
+ generateReport,
+ readJson,
+ resetData,
+ resolvePath,
+ writeSettings,
+ writeSite,
+} from "./helpers";
+
+// A FORM THAT FAILS KEEPS WHAT WAS TYPED (release 16, slice FK).
+//
+// React resets a <form> when its action's transition commits, success or
+// failure, and an uncontrolled field goes back to its default. Every action
+// that can refuse now returns what was submitted and the fields seed from it
+// (app/lib/formState.ts). One case per form the slice changed: type, submit
+// something the action refuses, assert the refusal AND that every field still
+// holds what was typed.
+//
+// Fields are found by NAME where a label would be ambiguous: a Field's
+// accessible name includes its hint, and getByLabel matches substrings.
+//
+// THE FOUR OPERATION FORMS, THE SYNC FORM AND THE BACKUP FORM CANNOT BE REFUSED
+// FROM THE PAGE — their actions only fail when settings.json cannot be written.
+// `withSettingsUnwritable` makes that happen for real: the settings file is put
+// aside and a directory takes its path, so the atomic write's rename fails
+// (EISDIR) and the action returns its error. The file is back before the test
+// ends, whatever happens.
+
+const SETTINGS = resolvePath("test-settings.json");
+
+async function withSettingsUnwritable(fn: () => Promise<void>): Promise<void> {
+ const aside = `${SETTINGS}.fk-aside`;
+ await rename(SETTINGS, aside);
+ await mkdir(SETTINGS);
+ try {
+ await fn();
+ } finally {
+ await rm(SETTINGS, { recursive: true, force: true });
+ await rename(aside, SETTINGS);
+ }
+}
+
+// A click before React hydrates submits nothing a client action sees.
+async function hydrated(form: Locator): Promise<void> {
+ await expect(async () => {
+ const ok = await form.evaluate((el) =>
+ Object.keys(el).some((k) => k.startsWith("__reactProps")),
+ );
+ expect(ok).toBe(true);
+ }).toPass({ timeout: 15_000 });
+}
+
+const named = (scope: Locator | Page, name: string) =>
+ scope.locator(`[name="${name}"]`);
+
+// Next's route announcer is a second (empty) role=alert, so filter by text.
+const alert = (page: Page, text: RegExp) =>
+ page.getByRole("alert").filter({ hasText: text });
+
+test("SiteForm create: a refused site id keeps every field", async ({
+ page,
+}) => {
+ await resetData("one-youtube-channel-with-data");
+ await page.goto("/sites/new");
+ const form = page.locator("form").filter({
+ has: page.getByRole("button", { name: "Create site" }),
+ });
+ await hydrated(form);
+
+ await named(form, "siteId").fill("Not A Valid Id!");
+ await named(form, "siteTitle").fill("Typed Title");
+ await named(form, "headerTitle").fill("Typedalyzer");
+ await named(form, "wordmarkLead").fill("Typed");
+ await named(form, "siteDescription").fill("A typed description");
+ await named(form, "homeTagline").fill("A typed tagline");
+ await named(form, "cloudflareProject").fill("typed-project");
+ await named(form, "archiveMaxMB").fill("12");
+ // Two checkboxes toggled from their defaults (pwa off, archives on).
+ await named(form, "pwa").check();
+ await named(form, "archives").uncheck();
+ // Controlled, posted: the accent radio; controlled, not posted: membership.
+ await form.locator('input[name="accent"][value="brass"]').check();
+ await form
+ .getByRole("checkbox", { name: /include test youtube channel/i })
+ .check();
+
+ await form.getByRole("button", { name: "Create site" }).click();
+ await expect(alert(page, /site id must be/i)).toBeVisible();
+
+ await expect(named(form, "siteId")).toHaveValue("Not A Valid Id!");
+ await expect(named(form, "siteTitle")).toHaveValue("Typed Title");
+ await expect(named(form, "headerTitle")).toHaveValue("Typedalyzer");
+ await expect(named(form, "wordmarkLead")).toHaveValue("Typed");
+ await expect(named(form, "siteDescription")).toHaveValue(
+ "A typed description",
+ );
+ await expect(named(form, "homeTagline")).toHaveValue("A typed tagline");
+ await expect(named(form, "cloudflareProject")).toHaveValue("typed-project");
+ await expect(named(form, "archiveMaxMB")).toHaveValue("12");
+ await expect(named(form, "pwa")).toBeChecked();
+ await expect(named(form, "archives")).not.toBeChecked();
+ await expect(named(form, "listed")).toBeChecked();
+ await expect(
+ form.locator('input[name="accent"][value="brass"]'),
+ ).toBeChecked();
+ await expect(
+ form.getByRole("checkbox", { name: /include test youtube channel/i }),
+ ).toBeChecked();
+});
+
+test("SiteForm edit: a refused Public URL keeps every field", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await writeSite("alpha", { siteTitle: "Alpha Site" });
+ await page.goto("/sites/alpha");
+ const form = page.locator("form").filter({
+ has: page.getByRole("button", { name: "Save site" }),
+ });
+ await hydrated(form);
+
+ await named(form, "siteTitle").fill("Alpha Typed");
+ await named(form, "headerTitle").fill("Alphalyzer");
+ await named(form, "wordmarkLead").fill("Alpha");
+ await named(form, "siteDescription").fill("Typed description");
+ await named(form, "siteUrl").fill("not a url");
+ await named(form, "hubUrl").fill("https://hub.example");
+ await named(form, "duplicates").uncheck();
+ await named(form, "transcriptDownloads").uncheck();
+ await form.locator('input[name="accent"][value="violet"]').check();
+
+ await form.getByRole("button", { name: "Save site" }).click();
+ await expect(alert(page, /public url must be/i)).toBeVisible();
+
+ await expect(named(form, "siteTitle")).toHaveValue("Alpha Typed");
+ await expect(named(form, "headerTitle")).toHaveValue("Alphalyzer");
+ await expect(named(form, "wordmarkLead")).toHaveValue("Alpha");
+ await expect(named(form, "siteDescription")).toHaveValue(
+ "Typed description",
+ );
+ await expect(named(form, "siteUrl")).toHaveValue("not a url");
+ await expect(named(form, "hubUrl")).toHaveValue("https://hub.example");
+ await expect(named(form, "duplicates")).not.toBeChecked();
+ await expect(named(form, "transcriptDownloads")).not.toBeChecked();
+ await expect(
+ form.locator('input[name="accent"][value="violet"]'),
+ ).toBeChecked();
+ // Nothing was written.
+ const site = await readJson<{ siteTitle: string }>(
+ "test-transcripts/sites/alpha/site.json",
+ );
+ expect(site.siteTitle).toBe("Alpha Site");
+});
+
+test("SettingsForm: a refused page size keeps every field, and a save after it shows what was stored", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await page.goto("/settings");
+ const form = page.locator("form").filter({
+ has: page.getByRole("button", { name: "Save settings" }),
+ });
+ await hydrated(form);
+
+ await named(form, "adminTitle").fill("Typed Admin");
+ await named(form, "maxTranscriptPageBytes").fill("42");
+ await named(form, "cookiesFromBrowser").fill("firefox:Typed");
+ await named(form, "cookieMode").selectOption("defer");
+ await named(form, "sleepBetweenDownloadsSeconds").fill("17");
+ await named(form, "downloadFormat").selectOption("original");
+ await named(form, "minFreeDiskGB").fill("3");
+ await named(form, "skipLiveDownloads").uncheck();
+ await named(form, "inlineTranscribeOnFallback").check();
+ await named(form, "archiveStorageBucket").fill("typed-bucket");
+ await named(form, "reportDebouncePreset").selectOption("lazy");
+ await named(form, "maxParallelBuilds").fill("7");
+ await named(form, "dockerImage").fill("typed-image");
+ // A social link with its controlled "featured" box ticked.
+ await form.getByRole("button", { name: /add link/i }).click();
+ await form.getByPlaceholder(/label \(e\.g\. github\)/i).fill("GitHub");
+ const featured = form.getByRole("checkbox", {
+ name: "Keep in header on small screens",
+ });
+ await featured.check();
+
+ await form.getByRole("button", { name: "Save settings" }).click();
+ await expect(alert(page, /between/)).toBeVisible();
+
+ await expect(named(form, "adminTitle")).toHaveValue("Typed Admin");
+ await expect(named(form, "maxTranscriptPageBytes")).toHaveValue("42");
+ await expect(named(form, "cookiesFromBrowser")).toHaveValue("firefox:Typed");
+ await expect(named(form, "cookieMode")).toHaveValue("defer");
+ await expect(named(form, "sleepBetweenDownloadsSeconds")).toHaveValue("17");
+ await expect(named(form, "downloadFormat")).toHaveValue("original");
+ await expect(named(form, "minFreeDiskGB")).toHaveValue("3");
+ await expect(named(form, "skipLiveDownloads")).not.toBeChecked();
+ await expect(named(form, "inlineTranscribeOnFallback")).toBeChecked();
+ await expect(named(form, "archiveStorageBucket")).toHaveValue("typed-bucket");
+ await expect(named(form, "reportDebouncePreset")).toHaveValue("lazy");
+ await expect(named(form, "maxParallelBuilds")).toHaveValue("7");
+ await expect(named(form, "dockerImage")).toHaveValue("typed-image");
+ await expect(featured).toBeChecked();
+
+ // THE SUCCESS PATH: fix the one bad field and save. The selects show the
+ // stored choice afterwards (a select's default reached the DOM only at mount
+ // before this slice, so the reset put back the option it mounted with).
+ await form.getByRole("button", { name: /remove/i }).first().click();
+ await named(form, "maxTranscriptPageBytes").fill("8388608");
+ await form.getByRole("button", { name: "Save settings" }).click();
+ await expect(
+ form.getByRole("status").filter({ hasText: "Saved" }),
+ ).toBeVisible();
+ await expect(async () => {
+ const saved = await readJson<{ cookieMode: string; adminTitle: string }>(
+ "test-settings.json",
+ );
+ expect(saved.cookieMode).toBe("defer");
+ expect(saved.adminTitle).toBe("Typed Admin");
+ }).toPass({ timeout: 10_000 });
+ await expect(named(form, "cookieMode")).toHaveValue("defer");
+ await expect(named(form, "reportDebouncePreset")).toHaveValue("lazy");
+ await expect(named(form, "adminTitle")).toHaveValue("Typed Admin");
+});
+
+test("ChannelForm create: a refused slug keeps every field", async ({
+ page,
+}) => {
+ await resetData("one-youtube-channel");
+ await writeSite("alpha", { siteTitle: "Alpha Site" });
+ await generateReport(page, "new");
+ await page.goto("/channels/new");
+ const form = page.locator("form").filter({
+ has: page.getByRole("button", { name: /create channel/i }),
+ });
+ await hydrated(form);
+
+ await named(form, "name").fill("Typed Channel");
+ await named(form, "slug").fill("test-youtube"); // already exists
+ await named(form, "url").fill("https://www.youtube.com/@typed/videos");
+ const member = form.getByRole("checkbox", { name: "Include on Alpha Site" });
+ const memberWas = await member.isChecked();
+ await member.setChecked(!memberWas);
+ await named(form, "fetchPlaylist").uncheck();
+ await named(form, "prioritizeDownload").check();
+ await named(form, "audioFormat").selectOption("opus");
+ await named(form, "downloadFormat").selectOption("original");
+ await named(form, "keepSourceVideo").check();
+ await named(form, "audioCheckEnabled").check();
+ await named(form, "audioCheckIntervalSeconds").fill("45");
+ await named(form, "keepLatest").fill("5");
+ await named(form, "extractionMode").selectOption("app");
+ await named(form, "savedVideosDir").fill("/tmp/typed-dir");
+ await form.getByText("Advanced", { exact: true }).click();
+ await named(form, "ytdlpExtraArgs").fill("--typed\n--args");
+ await named(form, "downloadFilterInclude").fill("typed-include");
+ await named(form, "downloadFilterIncludeLivestreams").check();
+ await named(form, "downloadFilterRejectedLivestreams").selectOption(
+ "chat-only",
+ );
+ await named(form, "cookiesFromBrowser").fill("firefox");
+ await named(form, "cookieMode").selectOption("always");
+ await named(form, "sleepBetweenDownloadsSeconds").fill("9");
+
+ await form.getByRole("button", { name: /create channel/i }).click();
+ await expect(form.getByText(/already exists/i)).toBeVisible();
+
+ await expect(named(form, "name")).toHaveValue("Typed Channel");
+ await expect(named(form, "slug")).toHaveValue("test-youtube");
+ await expect(named(form, "url")).toHaveValue(
+ "https://www.youtube.com/@typed/videos",
+ );
+ await expect(member).toBeChecked({ checked: !memberWas });
+ await expect(named(form, "fetchPlaylist")).not.toBeChecked();
+ await expect(named(form, "prioritizeDownload")).toBeChecked();
+ await expect(named(form, "audioFormat")).toHaveValue("opus");
+ await expect(named(form, "downloadFormat")).toHaveValue("original");
+ await expect(named(form, "keepSourceVideo")).toBeChecked();
+ await expect(named(form, "audioCheckEnabled")).toBeChecked();
+ await expect(named(form, "audioCheckIntervalSeconds")).toHaveValue("45");
+ await expect(named(form, "keepLatest")).toHaveValue("5");
+ await expect(named(form, "extractionMode")).toHaveValue("app");
+ await expect(named(form, "savedVideosDir")).toHaveValue("/tmp/typed-dir");
+ await expect(named(form, "ytdlpExtraArgs")).toHaveValue("--typed\n--args");
+ await expect(named(form, "downloadFilterInclude")).toHaveValue(
+ "typed-include",
+ );
+ await expect(named(form, "downloadFilterIncludeLivestreams")).toBeChecked();
+ await expect(named(form, "downloadFilterRejectedLivestreams")).toHaveValue(
+ "chat-only",
+ );
+ await expect(named(form, "cookiesFromBrowser")).toHaveValue("firefox");
+ await expect(named(form, "cookieMode")).toHaveValue("always");
+ await expect(named(form, "sleepBetweenDownloadsSeconds")).toHaveValue("9");
+});
+
+test("ChannelForm edit: a refused filter keeps every field", async ({
+ page,
+}) => {
+ await resetData("one-youtube-channel");
+ await generateReport(page, "test-youtube");
+ await page.goto(channelStage("test-youtube", "configure"));
+ const form = page.locator("form").filter({
+ has: page.getByRole("button", { name: /save changes/i }),
+ });
+ await hydrated(form);
+
+ await named(form, "name").fill("Edited Name");
+ await named(form, "url").fill("https://www.youtube.com/@edited/videos");
+ await named(form, "platform").selectOption("rumble");
+ await form.locator('input[name="handling"][value="transcribe"]').check();
+ await named(form, "audioFormat").selectOption("m4a");
+ await named(form, "keepLatest").fill("7");
+ await named(form, "keepSourceVideo").check();
+ await form.getByText("Advanced", { exact: true }).click();
+ await named(form, "ytdlpExtraArgs").fill("--edited");
+ await named(form, "downloadFilterExclude").fill("(unclosed");
+ await named(form, "cookieMode").selectOption("defer");
+
+ await form.getByRole("button", { name: /save changes/i }).click();
+ await expect(
+ form.getByText(/not a valid regular expression/i),
+ ).toBeVisible();
+
+ await expect(named(form, "name")).toHaveValue("Edited Name");
+ await expect(named(form, "url")).toHaveValue(
+ "https://www.youtube.com/@edited/videos",
+ );
+ await expect(named(form, "platform")).toHaveValue("rumble");
+ await expect(
+ form.locator('input[name="handling"][value="transcribe"]'),
+ ).toBeChecked();
+ await expect(named(form, "audioFormat")).toHaveValue("m4a");
+ await expect(named(form, "keepLatest")).toHaveValue("7");
+ await expect(named(form, "keepSourceVideo")).toBeChecked();
+ await expect(named(form, "ytdlpExtraArgs")).toHaveValue("--edited");
+ await expect(named(form, "downloadFilterExclude")).toHaveValue("(unclosed");
+ await expect(named(form, "cookieMode")).toHaveValue("defer");
+ const config = await readJson<{ name: string }>(
+ "test-transcripts/channels/test-youtube/config.json",
+ );
+ expect(config.name).not.toBe("Edited Name");
+});
+
+test("RenameChannelForm: a mismatched confirm keeps both boxes", async ({
+ page,
+}) => {
+ await resetData("one-youtube-channel");
+ await generateReport(page, "test-youtube");
+ await page.goto(channelStage("test-youtube", "danger"));
+ const form = page.locator("form").filter({
+ has: page.getByRole("button", { name: "Rename channel" }),
+ });
+ await hydrated(form);
+
+ await form.getByLabel("new slug").fill("renamed-typed");
+ await form.getByLabel("confirm current slug").fill("not-the-slug");
+ await form.getByRole("button", { name: "Rename channel" }).click();
+ await expect(form.getByText(/exactly to confirm the rename/i)).toBeVisible();
+
+ await expect(form.getByLabel("new slug")).toHaveValue("renamed-typed");
+ await expect(form.getByLabel("confirm current slug")).toHaveValue(
+ "not-the-slug",
+ );
+});
+
+test("DeleteChannelForm: a mismatched confirm keeps the box", async ({
+ page,
+}) => {
+ await resetData("one-youtube-channel");
+ await generateReport(page, "test-youtube");
+ await page.goto(channelStage("test-youtube", "danger"));
+ const form = page.locator("form").filter({
+ has: page.getByRole("button", { name: "Delete channel" }),
+ });
+ await hydrated(form);
+
+ await form.getByLabel("confirm slug to delete").fill("wrong-slug");
+ await form.getByRole("button", { name: "Delete channel" }).click();
+ await expect(form.getByText(/exactly to confirm deletion/i)).toBeVisible();
+ await expect(form.getByLabel("confirm slug to delete")).toHaveValue(
+ "wrong-slug",
+ );
+});
+
+test("DeleteVideoDirSection: a mismatched id keeps the box", async ({
+ page,
+}) => {
+ await resetData("one-youtube-channel-with-data");
+ await page.goto("/channels/test-youtube/videos/20240101_test1234567");
+ const form = page.getByLabel("delete video directory 20240101_test1234567");
+ await hydrated(form);
+
+ await form.getByLabel("confirm video id").fill("nope-typed");
+ await form.getByRole("button", { name: "Delete directory" }).click();
+ await expect(page.getByLabel("delete directory error")).toContainText(
+ "Type the video id",
+ );
+ await expect(form.getByLabel("confirm video id")).toHaveValue("nope-typed");
+});
+
+test("HealthTimingForm: an out-of-range value keeps the five fields", async ({
+ page,
+}) => {
+ await resetData("one-youtube-channel-with-data");
+ await writeSettings({ adminTitle: "Test Admin", minFreeDiskGB: 0 });
+ await page.goto("/storage");
+ const block = page.getByLabel("drive health timing");
+ const form = block.locator("form");
+ await hydrated(form);
+ await block.locator("summary").click();
+
+ await block.getByLabel("read budget").fill("200");
+ await block.getByLabel("health check interval").fill("20000");
+ await block.getByLabel("health check timeout").fill("4000");
+ await block.getByLabel("clean checks to clear").fill("3");
+ await block.getByLabel("reads at once per drive").fill("2");
+ await block.getByLabel("save timing").click();
+ await expect(block.getByLabel("timing error")).toContainText(
+ "Read budget must be between",
+ );
+
+ await expect(block.getByLabel("read budget")).toHaveValue("200");
+ await expect(block.getByLabel("health check interval")).toHaveValue("20000");
+ await expect(block.getByLabel("health check timeout")).toHaveValue("4000");
+ await expect(block.getByLabel("clean checks to clear")).toHaveValue("3");
+ await expect(block.getByLabel("reads at once per drive")).toHaveValue("2");
+});
+
+test("HomepageConfigForm: a refused Public URL keeps every field", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await page.goto("/sites");
+ const form = page.locator("form").filter({
+ has: page.getByRole("button", { name: "Save config" }),
+ });
+ await hydrated(form);
+
+ await named(form, "siteTitle").fill("Typed Hub");
+ await named(form, "headerTitle").fill("Typed Header");
+ await named(form, "siteDescription").fill("Typed hub description");
+ await named(form, "homeTagline").fill("Typed hub tagline");
+ await named(form, "siteUrl").fill("not a url");
+ await named(form, "cloudflareProject").fill("typed-hub");
+ await named(form, "transcriptDownloads").uncheck();
+ await form.getByRole("button", { name: "Save config" }).click();
+ await expect(form.getByText(/public url must be/i)).toBeVisible();
+
+ await expect(named(form, "siteTitle")).toHaveValue("Typed Hub");
+ await expect(named(form, "headerTitle")).toHaveValue("Typed Header");
+ await expect(named(form, "siteDescription")).toHaveValue(
+ "Typed hub description",
+ );
+ await expect(named(form, "homeTagline")).toHaveValue("Typed hub tagline");
+ await expect(named(form, "siteUrl")).toHaveValue("not a url");
+ await expect(named(form, "cloudflareProject")).toHaveValue("typed-hub");
+ await expect(named(form, "transcriptDownloads")).not.toBeChecked();
+});
+
+test("CutReleaseForm: a bad version keeps it and the commit box", async ({
+ page,
+}) => {
+ // The disposable test changelog cut-release.spec.ts drives (never the
+ // repo's own); left with no [Unreleased] heading, as that spec leaves it.
+ const changelog = resolvePath("test-changelog.md");
+ const before = await readFile(changelog, "utf8").catch(() => null);
+ await writeFile(
+ changelog,
+ "# Changelog\n\n## [Unreleased]\n- a pending bullet\n\n## [9.9.9] - 2024-01-01\n- old\n",
+ );
+ try {
+ await page.goto("/changelog");
+ const form = page.locator("form").filter({
+ has: page.locator("#version-editor"),
+ });
+ await hydrated(form);
+
+ await page.locator("#version-editor").fill("not-semver");
+ await form.getByRole("checkbox", { name: "Commit changelog" }).uncheck();
+ await form.getByRole("button", { name: /^cut release$/i }).click();
+ await expect(alert(page, /not a valid semver/i)).toBeVisible();
+
+ await expect(page.locator("#version-editor")).toHaveValue("not-semver");
+ await expect(
+ form.getByRole("checkbox", { name: "Commit changelog" }),
+ ).not.toBeChecked();
+ // Refused before anything was written.
+ expect(await readFile(changelog, "utf8")).toContain("## [Unreleased]");
+ } finally {
+ await writeFile(
+ changelog,
+ before ??
+ "# Changelog\n\n## [9.9.9] - 2024-01-01\n- old released bullet\n",
+ );
+ }
+});
+
+test("AttributionSettingsForm: a refused write keeps every field", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await page.goto("/operations/attribution-text");
+ const form = page.locator('form[data-settings-block="attribution"]');
+ await hydrated(form);
+
+ const enabled = await named(form, "attributionEnabled").isChecked();
+ await named(form, "attributionEnabled").setChecked(!enabled);
+ await named(form, "attributionTextOnly").check();
+ const app = named(form, "attributionAppId");
+ await app.selectOption({ index: 1 });
+ const appValue = await app.inputValue();
+ await named(form, "attributionModel").fill("typed-model:7b");
+ await named(form, "attributionPromptVersion").fill("99");
+
+ await withSettingsUnwritable(async () => {
+ await form
+ .getByRole("button", { name: "Save attribution settings" })
+ .click();
+ await expect(form.getByRole("alert")).toBeVisible();
+ });
+
+ await expect(named(form, "attributionEnabled")).toBeChecked({
+ checked: !enabled,
+ });
+ await expect(named(form, "attributionTextOnly")).toBeChecked();
+ await expect(app).toHaveValue(appValue);
+ await expect(named(form, "attributionModel")).toHaveValue("typed-model:7b");
+ await expect(named(form, "attributionPromptVersion")).toHaveValue("99");
+});
+
+test("DiarizationSettingsForm: a refused write keeps every field", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await page.goto("/operations/diarization");
+ const form = page.locator('form[data-settings-block="diarization"]');
+ await hydrated(form);
+
+ await named(form, "diarizationEnabled").check();
+ await named(form, "diarizationInlineAfterTranscribe").check();
+ await named(form, "diarizationEngine").selectOption("sortformer");
+ await named(form, "diarizationBackend").selectOption("cpu");
+ await named(form, "diarizationSortformerBin").fill("/opt/typed/diarize");
+ await named(form, "diarizationPython").fill("/opt/typed/python");
+ await named(form, "diarizationThreshold").fill("0.42");
+ await named(form, "diarizationThreads").fill("3");
+ await named(form, "diarizationMaxAudioHours").fill("2.5");
+
+ await withSettingsUnwritable(async () => {
+ await form
+ .getByRole("button", { name: "Save diarization settings" })
+ .click();
+ await expect(form.getByRole("alert")).toBeVisible();
+ });
+
+ await expect(named(form, "diarizationEnabled")).toBeChecked();
+ await expect(named(form, "diarizationInlineAfterTranscribe")).toBeChecked();
+ await expect(named(form, "diarizationEngine")).toHaveValue("sortformer");
+ await expect(named(form, "diarizationBackend")).toHaveValue("cpu");
+ await expect(named(form, "diarizationSortformerBin")).toHaveValue(
+ "/opt/typed/diarize",
+ );
+ await expect(named(form, "diarizationPython")).toHaveValue(
+ "/opt/typed/python",
+ );
+ await expect(named(form, "diarizationThreshold")).toHaveValue("0.42");
+ await expect(named(form, "diarizationThreads")).toHaveValue("3");
+ await expect(named(form, "diarizationMaxAudioHours")).toHaveValue("2.5");
+});
+
+test("DigestSettingsForm: a refused write keeps every field, the section boxes too", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await page.goto("/operations/digest");
+ const form = page.locator('form[data-settings-block="digest"]');
+ await hydrated(form);
+
+ const box = (section: string) =>
+ form.locator(`input[name="digestSections"][value="${section}"]`);
+ await box("tags").check();
+ await box("chapters").uncheck();
+ await named(form, "digestTimestampMode").selectOption("absolute");
+ await named(form, "digestPromptVariant").fill("typed-variant");
+ await named(form, "digestYieldToCpuWorkers").check();
+ await named(form, "digestRemoteEnabled").check();
+ await named(form, "digestLongTailSeconds").fill("7200");
+ await named(form, "digestSpendCapUsd").fill("3.25");
+
+ await withSettingsUnwritable(async () => {
+ await form.getByRole("button", { name: "Save digest settings" }).click();
+ await expect(form.getByRole("alert")).toBeVisible();
+ });
+
+ await expect(box("tags")).toBeChecked();
+ await expect(box("chapters")).not.toBeChecked();
+ await expect(named(form, "digestTimestampMode")).toHaveValue("absolute");
+ await expect(named(form, "digestPromptVariant")).toHaveValue("typed-variant");
+ await expect(named(form, "digestYieldToCpuWorkers")).toBeChecked();
+ await expect(named(form, "digestRemoteEnabled")).toBeChecked();
+ await expect(named(form, "digestLongTailSeconds")).toHaveValue("7200");
+ await expect(named(form, "digestSpendCapUsd")).toHaveValue("3.25");
+});
+
+test("LaneSettingsForm: a refused write keeps every field", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await page.goto("/operations/diarization");
+ const form = page.locator('form[data-settings-block="backfill"]');
+ await hydrated(form);
+
+ const run = await named(form, "backfillEnabled").isChecked();
+ await named(form, "backfillEnabled").setChecked(!run);
+ await named(form, "backfillConcurrency").fill("3");
+ await named(form, "backfillAllowRedownload").check();
+
+ await withSettingsUnwritable(async () => {
+ await form.getByRole("button", { name: "Save lane settings" }).click();
+ await expect(form.getByRole("alert")).toBeVisible();
+ });
+
+ await expect(named(form, "backfillEnabled")).toBeChecked({ checked: !run });
+ await expect(named(form, "backfillConcurrency")).toHaveValue("3");
+ await expect(named(form, "backfillAllowRedownload")).toBeChecked();
+});
+
+test("SchedulerSettingsForm: a refused write keeps every field, a cadence select too", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await page.goto("/operations/sync");
+ const form = page.locator('form[data-settings-block="syncScheduler"]');
+ await hydrated(form);
+
+ const on = await named(form, "syncSchedulerEnabled").isChecked();
+ await named(form, "syncSchedulerEnabled").setChecked(!on);
+ await named(form, "syncSchedulerFullSweepConfirmMaxSuspects").fill("6");
+ await named(form, "syncSchedulerFullSweepShrinkGuardPercent").fill("33");
+ await named(form, "syncSchedulerMaxConcurrentSyncs").fill("4");
+ await named(form, "syncSchedulerQuietHoursStart").fill("22");
+ await named(form, "syncSchedulerQuietHoursEnd").fill("6");
+ // A DurationField: a controlled select inside the action form.
+ const sweep = form.getByLabel("Default full sweep interval", { exact: true });
+ await sweep.selectOption("4320");
+
+ await withSettingsUnwritable(async () => {
+ await form.getByRole("button", { name: "Save controls" }).click();
+ await expect(form.getByRole("alert")).toBeVisible();
+ });
+
+ await expect(named(form, "syncSchedulerEnabled")).toBeChecked({
+ checked: !on,
+ });
+ await expect(
+ named(form, "syncSchedulerFullSweepConfirmMaxSuspects"),
+ ).toHaveValue("6");
+ await expect(
+ named(form, "syncSchedulerFullSweepShrinkGuardPercent"),
+ ).toHaveValue("33");
+ await expect(named(form, "syncSchedulerMaxConcurrentSyncs")).toHaveValue("4");
+ await expect(named(form, "syncSchedulerQuietHoursStart")).toHaveValue("22");
+ await expect(named(form, "syncSchedulerQuietHoursEnd")).toHaveValue("6");
+ await expect(sweep).toHaveValue("4320");
+});
+
+test("SavedVideosControls: a refused write keeps the box and the cadence", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await page.goto("/saved-videos");
+ const form = page.getByLabel("backup config");
+ await hydrated(form);
+
+ await page.getByLabel("backup destination").fill("/tmp/ttb-fk-dest");
+ await page.getByLabel("scheduled backup enabled").check();
+ await form.getByLabel("Backup interval amount").fill("3");
+ await form.getByLabel("Backup interval unit").selectOption("days");
+
+ await withSettingsUnwritable(async () => {
+ await form.getByRole("button", { name: "Save backup config" }).click();
+ await expect(form.getByRole("alert")).toBeVisible();
+ });
+
+ await expect(page.getByLabel("backup destination")).toHaveValue(
+ "/tmp/ttb-fk-dest",
+ );
+ await expect(page.getByLabel("scheduled backup enabled")).toBeChecked();
+ await expect(form.getByLabel("Backup interval amount")).toHaveValue("3");
+ await expect(form.getByLabel("Backup interval unit")).toHaveValue("days");
+});
+
+test("WorkersConfigForm: a refused list keeps the Enabled boxes as set", async ({
+ page,
+}) => {
+ await resetData("empty");
+ await writeSettings({
+ workers: [
+ { id: "gpu", name: "GPU", kind: "local", enabled: true, priority: 0, appId: "whisper-cpp", config: {} },
+ { id: "cpu", name: "CPU", kind: "local", enabled: true, priority: 1, appId: "whisper-cpp", config: {} },
+ ],
+ });
+ await page.goto("/workers");
+ const form = page.locator('form[data-settings-block="workers"]');
+ await hydrated(form);
+
+ await form.getByLabel("worker 1 enabled").uncheck();
+ await form.getByLabel("worker 2 enabled").uncheck();
+ await form.getByRole("button", { name: "Save workers" }).click();
+ await expect(alert(page, /at least one worker must be enabled/i)).toBeVisible();
+
+ // Controlled boxes: before slice FK the reset put them back to ticked while
+ // the list still said off.
+ await expect(form.getByLabel("worker 1 enabled")).not.toBeChecked();
+ await expect(form.getByLabel("worker 2 enabled")).not.toBeChecked();
+});