perf(webui): load optional password generation on demand

This commit is contained in:
2026-09-08 03:15:07 +02:00
parent c209b3c27d
commit c3daa4a9aa
7 changed files with 212 additions and 14 deletions
+18
View File
@@ -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.
+64
View File
@@ -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 (
<main className="conformance-root">
<h1>Optional password generator</h1>
<FormField label="Editable password">
<PasswordField
data-testid="editable-password"
value={password}
disabled={disabled}
generator
generatorOptions={GENERATOR_OPTIONS}
helpContextId="access.authentication.password"
helpModuleId="access"
onValueChange={(value) => {
setPassword(value);
setChanges((count) => count + 1);
}}
/>
</FormField>
<output data-testid="password-changes">{changes}</output>
<Button data-testid="toggle-generator-access" onClick={() => setDisabled((value) => !value)}>
{disabled ? "Enable generation" : "Disable generation"}
</Button>
<FormField label="Sign-in password">
<PasswordField value="fixture-sign-in-password" onValueChange={() => undefined} />
</FormField>
<FormField label="Disabled password">
<PasswordField generator disabled value="" onValueChange={() => undefined} />
</FormField>
<FormField label="Read-only password">
<PasswordField generator readOnly value="" onValueChange={() => undefined} />
</FormField>
</main>
);
}
// 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(
<StrictMode>
<PlatformLanguageProvider preferredLanguageCode={new URLSearchParams(window.location.search).get("language") ?? "en"}>
<PasswordFieldScenario />
</PlatformLanguageProvider>
</StrictMode>,
);
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Password field loading conformance</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./PasswordFieldMain.tsx"></script>
</body>
</html>
@@ -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<void>((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);
});
@@ -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]*<ModuleLoadBoundary[\s\S]*<PasswordGeneratorDialog/,
"the generator is requested only when opened and permitted, with shared loading/error feedback");
const iconRail = read("src/layout/IconRail.tsx");
const moduleLoadBoundary = read("src/components/ModuleLoadBoundary.tsx");
const moduleLoading = read("src/platform/modules.ts");
+24 -14
View File
@@ -1,10 +1,12 @@
import { useId, useState, type InputHTMLAttributes } from "react";
import { lazy, useEffect, useId, useState, type InputHTMLAttributes } from "react";
import { Dice5, Eye, EyeOff } from "lucide-react";
import PasswordGeneratorDialog from "./PasswordGeneratorDialog";
import ModuleLoadBoundary from "./ModuleLoadBoundary";
import type { PasswordGeneratorOptions } from "./passwordGenerator";
import { usePlatformLanguage } from "../i18n/LanguageContext";
import type { PlatformInterfaceIdentityProps } from "../types";
const PasswordGeneratorDialog = lazy(() => import("./PasswordGeneratorDialog"));
export type PasswordFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "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 (
<>
<div
@@ -113,18 +119,22 @@ export default function PasswordField({
) : null}
</div>
<PasswordGeneratorDialog
open={generatorOpen}
initialOptions={generatorOptions}
helpContextId={helpContextId}
helpModuleId={helpModuleId}
helpTopicId={helpTopicId}
onUse={(password) => {
onValueChange(password);
setVisible(false);
}}
onClose={() => setGeneratorOpen(false)}
/>
{generatorOpen && canGenerate && (
<ModuleLoadBoundary resetKey="password-generator">
<PasswordGeneratorDialog
open
initialOptions={generatorOptions}
helpContextId={helpContextId}
helpModuleId={helpModuleId}
helpTopicId={helpTopicId}
onUse={(password) => {
onValueChange(password);
setVisible(false);
}}
onClose={() => setGeneratorOpen(false)}
/>
</ModuleLoadBoundary>
)}
</>
);
}
+10
View File
@@ -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(
<PlatformLanguageProvider>
<PasswordField value="fixture-password" onValueChange={() => undefined} {...flags} />
</PlatformLanguageProvider>
);
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(
<PlatformLanguageProvider>
<PasswordGeneratorDialog