fix(proxy): isolate registry credentials from autofill (#8883)

Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
This commit is contained in:
小妍儿 ✨
2026-08-08 07:52:06 +08:00
committed by GitHub
parent 124f64a6c0
commit 3f4f2000b6
3 changed files with 177 additions and 0 deletions

View File

@@ -0,0 +1 @@
- **fix(proxy):** isolate new proxy credential fields from browser and password-manager autofill after form reset ([#8883](https://github.com/diegosouzapw/OmniRoute/pull/8883)) — thanks @xiaoyaner0201

View File

@@ -1014,6 +1014,9 @@ export default function ProxyRegistryManager({
<input
className="w-full px-3 py-2 rounded bg-bg-subtle border border-border"
value={form.username}
autoComplete="off"
data-1p-ignore="true"
data-lpignore="true"
placeholder={editingId ? t("usernamePlaceholderEdit") : ""}
onChange={(e) => setForm((prev) => ({ ...prev, username: e.target.value }))}
/>
@@ -1024,6 +1027,9 @@ export default function ProxyRegistryManager({
type="password"
className="w-full px-3 py-2 rounded bg-bg-subtle border border-border"
value={form.password}
autoComplete="new-password"
data-1p-ignore="true"
data-lpignore="true"
placeholder={editingId ? t("passwordPlaceholderEdit") : ""}
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
/>

View File

@@ -0,0 +1,170 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const translate = (key: string) => key;
vi.mock("next-intl", () => ({
useTranslations: () => translate,
}));
const SEEDED_PROXY = {
id: "proxy-8855",
name: "Seeded proxy",
type: "http",
host: "127.0.0.1",
port: 8080,
username: "stored-user",
password: "stored-password",
status: "active",
family: "auto",
};
let root: Root;
let container: HTMLDivElement;
let postBody: Record<string, unknown> | undefined;
function jsonResponse(body: unknown): Response {
return { ok: true, json: async () => body } as Response;
}
function findButton(text: string): HTMLButtonElement {
const button = Array.from(container.querySelectorAll("button")).find((candidate) =>
candidate.textContent?.includes(text)
);
if (!button) throw new Error(`Button not found: ${text}`);
return button;
}
function findCredentialInput(label: string): HTMLInputElement {
const labelNode = Array.from(container.querySelectorAll("label")).find(
(candidate) => candidate.textContent?.trim() === label
);
const input = labelNode?.parentElement?.querySelector<HTMLInputElement>("input");
if (!input) throw new Error(`Credential input not found: ${label}`);
return input;
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
if (!setter) throw new Error("HTMLInputElement value setter is unavailable");
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function click(element: HTMLElement) {
await act(async () => {
element.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
}
async function waitFor(assertion: () => void, timeoutMs = 2000) {
const startedAt = Date.now();
let lastError: unknown;
while (Date.now() - startedAt <= timeoutMs) {
try {
assertion();
return;
} catch (error) {
lastError = error;
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 20));
});
}
}
throw lastError;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
postBody = undefined;
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url === "/api/settings/proxies" && init?.method === "POST") {
postBody = JSON.parse(String(init.body));
return jsonResponse({ item: { ...SEEDED_PROXY, ...postBody } });
}
if (url === "/api/settings/proxies") {
return jsonResponse({ items: [SEEDED_PROXY] });
}
if (url.startsWith("/api/settings/proxies/health")) {
return jsonResponse({ items: [] });
}
if (url.startsWith("/api/settings/proxies/assignments")) {
return jsonResponse({ items: [] });
}
throw new Error(`Unexpected fetch: ${url}`);
})
);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
vi.unstubAllGlobals();
vi.clearAllMocks();
});
describe("ProxyRegistryManager credential autofill regression #8855", () => {
it("keeps Edit → close → Add credentials blank and isolates both fields from autofill", async () => {
const { default: ProxyRegistryManager } =
await import("@/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager");
await act(async () => {
root.render(<ProxyRegistryManager />);
});
await waitFor(() => expect(container.textContent).toContain(SEEDED_PROXY.name));
await click(findButton("edit"));
const editUsername = findCredentialInput("labelUsername");
const editPassword = findCredentialInput("labelPassword");
expect(editUsername.value).toBe("");
expect(editPassword.value).toBe("");
setInputValue(editUsername, "edit-user-sentinel");
setInputValue(editPassword, "edit-password-sentinel");
await click(container.querySelector<HTMLButtonElement>('button[aria-label="close"]')!);
await click(
container.querySelector<HTMLButtonElement>('[data-testid="proxy-registry-open-create"]')!
);
const createUsername = findCredentialInput("labelUsername");
const createPassword = findCredentialInput("labelPassword");
expect(createUsername.value).toBe("");
expect(createPassword.value).toBe("");
expect.soft(createUsername.getAttribute("autocomplete")).toBe("off");
expect.soft(createPassword.getAttribute("autocomplete")).toBe("new-password");
for (const input of [createUsername, createPassword]) {
expect.soft(input.getAttribute("data-1p-ignore")).toBe("true");
expect.soft(input.getAttribute("data-lpignore")).toBe("true");
}
setInputValue(
container.querySelector<HTMLInputElement>('[data-testid="proxy-registry-name-input"]')!,
"New proxy"
);
setInputValue(
container.querySelector<HTMLInputElement>('[data-testid="proxy-registry-host-input"]')!,
"proxy.example.test"
);
await click(findButton("save"));
await waitFor(() => expect(postBody).toBeDefined());
expect([undefined, ""]).toContain(postBody?.username);
expect([undefined, ""]).toContain(postBody?.password);
expect(postBody?.username).not.toBe("edit-user-sentinel");
expect(postBody?.password).not.toBe("edit-password-sentinel");
});
});