refactor(test): route SettingsDialog panel test through a public seam

Replace the SettingsDialog.general.test.ts markup-scraping assertion (which
scraped the rendered TemplateResult for '<settings-general-panel', 'scope-note',
'This tab edits:') with an exported pure routing seam activeSettingsPanelTag()
and assert the section->panel contract directly. Delete the now-unused
collectTemplateStrings helper and its private template-reflection cluster from
SettingsDialog.testSupport.ts.

The .packages/.plugins/.sessiond siblings inspect no TemplateResult internals
(orchestration reflection only), so no migration was needed for them.
This commit is contained in:
Federico Jaramillo Martinez
2026-07-17 20:48:17 +02:00
parent 946f7612ae
commit e7926e173b
3 changed files with 44 additions and 50 deletions
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { configApi, type PiWebConfigResponse } from "../api"; import { configApi, type PiWebConfigResponse } from "../api";
import { SettingsDialog } from "./SettingsDialog"; import { activeSettingsPanelTag, SettingsDialog } from "./SettingsDialog";
import { callDialogPromise, callDialogUpdated, collectTemplateStrings, configResponse, deferred, getDialogProperty, remoteMachine, secondRemoteMachine, setDialogProperty, stubWindowTimers } from "./SettingsDialog.testSupport"; import { callDialogPromise, callDialogUpdated, configResponse, deferred, getDialogProperty, remoteMachine, secondRemoteMachine, setDialogProperty, stubWindowTimers } from "./SettingsDialog.testSupport";
afterEach(() => { afterEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
@@ -9,16 +9,15 @@ afterEach(() => {
}); });
describe("settings-dialog general settings machine targeting", () => { describe("settings-dialog general settings machine targeting", () => {
it("renders the active settings panel without the old global scope note", () => { it("routes each section to a single settings panel with no per-tab scope-note wrapper", () => {
const dialog = new SettingsDialog(); // The old global "scope-note"/"This tab edits:" wrapper is gone: each section
dialog.section = "general"; // now maps to exactly one panel element. Assert that public routing contract
dialog.machine = remoteMachine; // (`activeSettingsPanelTag`) instead of scraping the rendered template markup.
expect(activeSettingsPanelTag("general")).toBe("settings-general-panel");
const strings = collectTemplateStrings(dialog.render()).join(""); expect(activeSettingsPanelTag("sessiond")).toBe("settings-sessiond-panel");
expect(activeSettingsPanelTag("packages")).toBe("settings-packages-panel");
expect(strings).toContain("<settings-general-panel"); expect(activeSettingsPanelTag("plugins")).toBe("settings-plugins-panel");
expect(strings).not.toContain("scope-note"); expect(activeSettingsPanelTag("shortcuts")).toBe("settings-shortcuts-panel");
expect(strings).not.toContain("This tab edits:");
}); });
it("keeps gateway server config saves on the gateway config endpoint", async () => { it("keeps gateway server config saves on the gateway config endpoint", async () => {
@@ -1,4 +1,3 @@
import type { TemplateResult } from "lit";
import { vi } from "vitest"; import { vi } from "vitest";
import { PI_WEB_CAPABILITIES } from "../../../shared/capabilities"; import { PI_WEB_CAPABILITIES } from "../../../shared/capabilities";
import type { Machine, MachineRuntime, PiPackageInfo, PiPackageMutationResponse, PiWebConfigResponse, PiWebConfigValues, PiWebPluginInfo, PiWebPluginsResponse } from "../api"; import type { Machine, MachineRuntime, PiPackageInfo, PiPackageMutationResponse, PiWebConfigResponse, PiWebConfigValues, PiWebPluginInfo, PiWebPluginsResponse } from "../api";
@@ -58,43 +57,6 @@ function isDialogMethod(value: unknown): value is (this: SettingsDialog, ...args
return typeof value === "function"; return typeof value === "function";
} }
export function collectTemplateStrings(template: TemplateResult): string[] {
const strings: string[] = [];
visitTemplate(template);
return strings;
function visitTemplate(current: TemplateResult): void {
strings.push(...templateStrings(current));
for (const value of templateValues(current)) {
if (Array.isArray(value)) {
for (const item of value) if (isTemplateResult(item)) visitTemplate(item);
} else if (isTemplateResult(value)) {
visitTemplate(value);
}
}
}
}
function templateStrings(template: TemplateResult): readonly string[] {
const strings = Reflect.get(template, "strings");
if (!isStringArray(strings)) throw new Error("TemplateResult strings were unavailable");
return strings;
}
function templateValues(template: TemplateResult): readonly unknown[] {
const values = Reflect.get(template, "values");
if (!Array.isArray(values)) throw new Error("TemplateResult values were unavailable");
return values.map((value: unknown) => value);
}
function isTemplateResult(value: unknown): value is TemplateResult {
return typeof value === "object" && value !== null && isStringArray(Reflect.get(value, "strings")) && Array.isArray(Reflect.get(value, "values"));
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item: unknown) => typeof item === "string");
}
export function configResponse(config: PiWebConfigValues): PiWebConfigResponse { export function configResponse(config: PiWebConfigValues): PiWebConfigResponse {
return { return {
path: "/tmp/pi-web/config.json", path: "/tmp/pi-web/config.json",
@@ -129,6 +129,9 @@ export class SettingsDialog extends LitElement {
} }
private renderActiveSection(): TemplateResult { private renderActiveSection(): TemplateResult {
// Keep the section -> panel routing in sync with the public
// `activeSettingsPanelTag` seam below, which tests assert against instead of
// scraping this template's markup.
if (this.section === "sessiond") { if (this.section === "sessiond") {
return html` return html`
<settings-sessiond-panel <settings-sessiond-panel
@@ -677,3 +680,33 @@ export class SettingsDialog extends LitElement {
function errorMessage(error: unknown): string { function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error); return error instanceof Error ? error.message : String(error);
} }
export type SettingsPanelTag =
| "settings-general-panel"
| "settings-sessiond-panel"
| "settings-packages-panel"
| "settings-plugins-panel"
| "settings-shortcuts-panel";
/**
* The single custom-element panel the settings dialog renders for a section.
*
* This is the public routing contract behind `renderActiveSection`: each section
* maps to exactly one panel element and nothing else (no per-tab "scope note"
* wrapper). Tests assert this mapping instead of inspecting the rendered
* `TemplateResult`'s markup.
*/
export function activeSettingsPanelTag(section: SettingsSection): SettingsPanelTag {
switch (section) {
case "sessiond":
return "settings-sessiond-panel";
case "packages":
return "settings-packages-panel";
case "plugins":
return "settings-plugins-panel";
case "shortcuts":
return "settings-shortcuts-panel";
case "general":
return "settings-general-panel";
}
}