From c3daa4a9aa620ec7c78a938220191beff6e93a76 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 8 Sep 2026 03:15:07 +0200 Subject: [PATCH] perf(webui): load optional password generation on demand --- docs/WEBUI_BUNDLE_BUDGETS.md | 18 +++++ webui/conformance/PasswordFieldMain.tsx | 64 +++++++++++++++ webui/conformance/password-field.html | 12 +++ .../tests/password-field-lazy.spec.ts | 77 +++++++++++++++++++ .../scripts/test-core-interface-patterns.mjs | 7 ++ webui/src/components/PasswordField.tsx | 38 +++++---- webui/tests/password-generator.test.tsx | 10 +++ 7 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 webui/conformance/PasswordFieldMain.tsx create mode 100644 webui/conformance/password-field.html create mode 100644 webui/conformance/tests/password-field-lazy.spec.ts diff --git a/docs/WEBUI_BUNDLE_BUDGETS.md b/docs/WEBUI_BUNDLE_BUDGETS.md index a976ee2..e00340b 100644 --- a/docs/WEBUI_BUNDLE_BUDGETS.md +++ b/docs/WEBUI_BUNDLE_BUDGETS.md @@ -89,3 +89,21 @@ The versioned default color document and its deep-clone helper live in does not load editor defaults or construct a draft. The default values and public helper names are unchanged; the theme regression checks independent draft clones as well as synchronous validation, application, and reset. + +`PasswordField` keeps ordinary input and reveal controls synchronous. Its +optional `PasswordGeneratorDialog` is imported only after an enabled, editable +generator is explicitly opened, not for every sign-in/password field. Loading +and failures use the shared resource boundary; the underlying field remains +usable. Closing or revoking generation while loading cannot apply a candidate. +The secure browser RNG, generation policy, public exports, and explicit +"Use password" confirmation remain unchanged. The isolated browser fixture +does not import the Core barrel, so it can verify that the generator is not +requested before opening it, along with cancel/use and focus restoration. + +Deutsch: Normale Passworteingabe und Sichtbarkeitssteuerung bleiben unmittelbar +verfügbar. Der optionale Generator wird erst beim bewussten Öffnen eines +aktivierten, bearbeitbaren Felds geladen; Lade- und Fehlerzustände nutzen die +gemeinsame Ressourcenanzeige. Ohne "Passwort verwenden" wird kein Kandidat +übernommen. Sichere Browser-Zufallszahlen, Richtlinien und öffentliche +Schnittstellen bleiben unverändert. Wird die Generierung während des Ladens +deaktiviert, öffnet eine verspätete Antwort keinen Dialog. diff --git a/webui/conformance/PasswordFieldMain.tsx b/webui/conformance/PasswordFieldMain.tsx new file mode 100644 index 0000000..086feb8 --- /dev/null +++ b/webui/conformance/PasswordFieldMain.tsx @@ -0,0 +1,64 @@ +import { StrictMode, useState } from "react"; +import { createRoot } from "react-dom/client"; +import Button from "../src/components/Button"; +import FormField from "../src/components/FormField"; +import PasswordField from "../src/components/PasswordField"; +import { PlatformLanguageProvider } from "../src/i18n/LanguageContext"; +import "../src/styles/tokens.css"; +import "../src/styles/layout.css"; +import "../src/styles/forms.css"; +import "../src/styles/components.css"; +import "../src/styles/dialogs.css"; +import "./conformance.css"; + +const GENERATOR_OPTIONS = { length: 24 }; + +function PasswordFieldScenario() { + const [password, setPassword] = useState("fixture-unchanged-password"); + const [disabled, setDisabled] = useState(false); + const [changes, setChanges] = useState(0); + + return ( +
+

Optional password generator

+ + { + setPassword(value); + setChanges((count) => count + 1); + }} + /> + + {changes} + + + undefined} /> + + + undefined} /> + + + undefined} /> + +
+ ); +} + +// Deliberately no Core barrel or other fixture imports: they could eagerly load +// the generator and mask a regression in this field's real lazy boundary. +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/webui/conformance/password-field.html b/webui/conformance/password-field.html new file mode 100644 index 0000000..2da649f --- /dev/null +++ b/webui/conformance/password-field.html @@ -0,0 +1,12 @@ + + + + + + Password field loading conformance + + +
+ + + diff --git a/webui/conformance/tests/password-field-lazy.spec.ts b/webui/conformance/tests/password-field-lazy.spec.ts new file mode 100644 index 0000000..3152852 --- /dev/null +++ b/webui/conformance/tests/password-field-lazy.spec.ts @@ -0,0 +1,77 @@ +import { expect, test } from "@playwright/test"; + +const generatorUrl = "**/PasswordGeneratorDialog.tsx*"; + +for (const language of ["en", "de"]) { + test(`password generation loads only when opened and preserves cancel/use/focus (${language})`, async ({ page }) => { + const generateLabel = language === "de" ? "Passwort generieren" : "Generate password"; + const cancelLabel = language === "de" ? "Abbrechen" : "Cancel"; + const useLabel = language === "de" ? "Passwort verwenden" : "Use password"; + const requests: string[] = []; + page.on("request", (request) => { + if (request.url().includes("/PasswordGeneratorDialog.tsx")) requests.push(request.url()); + }); + await page.goto(`/password-field.html?language=${language}`); + const trigger = page.getByRole("button", { name: generateLabel, exact: true }); + await expect(trigger).toHaveCount(1); + await expect(page.getByTestId("editable-password")).toHaveValue("fixture-unchanged-password"); + expect(requests).toHaveLength(0); + + await trigger.click(); + const dialog = page.getByRole("dialog", { name: generateLabel, exact: true }); + await expect(dialog).toBeVisible(); + await expect.poll(() => requests.length).toBe(1); + await expect(dialog.locator(".password-generator-result input")).toHaveValue(/^.{24}$/); + await expect(page.getByTestId("password-changes")).toHaveText("0"); + await dialog.getByRole("button", { name: cancelLabel, exact: true }).click(); + await expect(dialog).toBeHidden(); + await expect(trigger).toBeFocused(); + await expect(page.getByTestId("editable-password")).toHaveValue("fixture-unchanged-password"); + + await trigger.click(); + await expect(dialog.locator(".password-generator-result input")).toHaveValue(/^.{24}$/); + const candidate = await dialog.locator(".password-generator-result input").inputValue(); + await dialog.getByRole("button", { name: useLabel, exact: true }).click(); + await expect(dialog).toBeHidden(); + await expect(page.getByTestId("editable-password")).toHaveValue(candidate); + await expect(page.getByTestId("editable-password")).toHaveAttribute("type", "password"); + await expect(page.getByTestId("password-changes")).toHaveText("1"); + await expect(trigger).toBeFocused(); + expect(requests).toHaveLength(1); + }); +} + +test("a pending generator load cannot reopen after generation becomes unavailable", async ({ page }) => { + let releaseLoad!: () => void; + const pending = new Promise((resolve) => { releaseLoad = resolve; }); + await page.route(generatorUrl, async (route) => { + await pending; + await route.continue(); + }); + await page.goto("/password-field.html?language=en"); + await page.getByRole("button", { name: "Generate password", exact: true }).click(); + await expect(page.locator(".module-load-progress")).toBeVisible(); + await page.getByTestId("toggle-generator-access").click(); + releaseLoad(); + await expect(page.getByTestId("editable-password")).toBeDisabled(); + await expect(page.locator(".module-load-progress")).toBeHidden(); + await expect(page.getByRole("dialog")).toHaveCount(0); + await page.getByTestId("toggle-generator-access").click(); + await expect(page.getByRole("dialog")).toHaveCount(0); + await expect(page.getByTestId("password-changes")).toHaveText("0"); + await page.getByRole("button", { name: "Generate password", exact: true }).click(); + await expect(page.getByRole("dialog", { name: "Generate password", exact: true })).toBeVisible(); +}); + +test("generator chunk failure leaves ordinary password entry and reveal usable", async ({ page }) => { + await page.route(generatorUrl, (route) => route.abort("failed")); + await page.goto("/password-field.html?language=en"); + await page.getByRole("button", { name: "Generate password", exact: true }).click(); + await expect(page.locator(".module-load-error")).toBeVisible(); + await expect(page.getByTestId("editable-password")).toBeEnabled(); + await page.getByTestId("editable-password").fill("fixture-edited-password"); + await page.getByRole("button", { name: "Show password", exact: true }).first().click(); + await expect(page.getByTestId("editable-password")).toHaveAttribute("type", "text"); + await expect(page.getByTestId("editable-password")).toHaveValue("fixture-edited-password"); + await expect(page.getByRole("dialog")).toHaveCount(0); +}); diff --git a/webui/scripts/test-core-interface-patterns.mjs b/webui/scripts/test-core-interface-patterns.mjs index 0dd4b25..ff0c43e 100644 --- a/webui/scripts/test-core-interface-patterns.mjs +++ b/webui/scripts/test-core-interface-patterns.mjs @@ -12,6 +12,13 @@ const settings = read("src/features/settings/SettingsPage.tsx"); const retention = read("src/features/privacy/RetentionPolicyManagement.tsx"); const confirmDialog = read("src/components/ConfirmDialog.tsx"); const credentials = read("src/components/CredentialEnvelopeManager.tsx"); +const passwordField = read("src/components/PasswordField.tsx"); +assert.match(passwordField, /lazy\(\(\) => import\("\.\/PasswordGeneratorDialog"\)\)/, + "ordinary password fields must not eagerly load optional generator controls or randomness helpers"); +assert.doesNotMatch(passwordField, /import PasswordGeneratorDialog from/, + "the optional generator has no static import through the password field"); +assert.match(passwordField, /generatorOpen && canGenerate &&[\s\S]* import("./PasswordGeneratorDialog")); + export type PasswordFieldProps = Omit, "type" | "value" | "onChange"> & PlatformInterfaceIdentityProps & { value: string; onValueChange: (value: string) => void; @@ -53,6 +55,10 @@ export default function PasswordField({ const translatedRevealLabel = translateText(revealLabel); const translatedHideLabel = translateText(hideLabel); + useEffect(() => { + if (!canGenerate) setGeneratorOpen(false); + }, [canGenerate]); + return ( <>
- { - onValueChange(password); - setVisible(false); - }} - onClose={() => setGeneratorOpen(false)} - /> + {generatorOpen && canGenerate && ( + + { + onValueChange(password); + setVisible(false); + }} + onClose={() => setGeneratorOpen(false)} + /> + + )} ); } diff --git a/webui/tests/password-generator.test.tsx b/webui/tests/password-generator.test.tsx index 651f039..8b0ca8f 100644 --- a/webui/tests/password-generator.test.tsx +++ b/webui/tests/password-generator.test.tsx @@ -66,6 +66,16 @@ assert(!markup.includes("password-generator-dialog"), "the generator dialog stay assert(markup.includes('data-help-context-id="access.authentication.password"'), "the owner context reaches the password field and its actions"); assert(markup.includes('data-help-module-id="access"'), "the password field retains its documentation owner"); +for (const flags of [{}, { generator: true, disabled: true }, { generator: true, readOnly: true }]) { + const unavailableMarkup = renderToStaticMarkup( + + undefined} {...flags} /> + + ); + assert(!unavailableMarkup.includes('aria-label="Generate password"'), "unavailable generation cannot open the optional dialog"); + assert(!unavailableMarkup.includes("module-load-progress"), "closed generators never begin loading while rendering a field"); +} + const dialogMarkup = renderToStaticMarkup(