feat: xray support

This commit is contained in:
zhom
2026-07-31 01:04:58 +04:00
parent 064bf297dd
commit 0a7d7803f2
112 changed files with 10291 additions and 772 deletions
+11 -4
View File
@@ -7,7 +7,11 @@ import path from "node:path";
import test from "node:test";
import { appFromEnvironment } from "../lib/app.mjs";
import { CdpClient } from "../lib/cdp.mjs";
import { defaultWayfernPath, prepareWayfern } from "../lib/fixtures.mjs";
import {
defaultWayfernPath,
inspectWayfern,
prepareWayfern,
} from "../lib/fixtures.mjs";
const fixtureUrl = process.env.DONUT_E2E_FIXTURE_URL;
@@ -133,11 +137,14 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
assert.ok(process.env.WAYFERN_TEST_TOKEN, "WAYFERN_TEST_TOKEN is required");
const realTermsFile = realWayfernTermsPath();
const realTermsBefore = await snapshotFile(realTermsFile);
const hasLocalWayfern = existsSync(
defaultWayfernPath(process.env.DONUT_E2E_PROJECT_ROOT),
const localWayfernPath = defaultWayfernPath(
process.env.DONUT_E2E_PROJECT_ROOT,
);
const localWayfernVersion = existsSync(localWayfernPath)
? inspectWayfern(localWayfernPath).version
: null;
const app = appFromEnvironment("browser-wayfern", {
seedVersionCache: hasLocalWayfern,
seedVersionCache: localWayfernVersion ?? false,
wayfernTermsAccepted: false,
});
let cdp;
+131
View File
@@ -4,6 +4,9 @@ import path from "node:path";
import test from "node:test";
import { withApp } from "../lib/app.mjs";
const VLESS_URI =
"vless://6d6e21a1-4829-4d2b-bc7f-1b25707b61e4@127.0.0.1:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.example.com&fp=chrome&pbk=BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc&sid=0123456789abcdef&spx=%2F&type=tcp&headerType=none#MCP";
async function jsonRequest(
url,
{ method = "GET", token, body, headers = {} } = {},
@@ -139,6 +142,49 @@ test("authenticated REST API serves its complete OpenAPI contract and CRUD lifec
token: saved.api_token,
});
assert.equal(fetchedProxy.value.name, "REST Proxy");
const createdVless = await jsonRequest(`${base}/v1/proxies`, {
method: "POST",
token: saved.api_token,
body: {
name: "REST VLESS Reality",
proxy_settings: {
proxy_type: "vless",
host: "127.0.0.1",
port: 443,
username: null,
password: null,
vless_uri: VLESS_URI,
},
},
});
assert.equal(createdVless.response.status, 200);
assert.equal(createdVless.value.proxy_settings.vless_uri, VLESS_URI);
assert.equal(createdVless.value.proxy_settings.host, "127.0.0.1");
assert.equal(createdVless.value.proxy_settings.port, 443);
const vlessProxyId = createdVless.value.id;
const invalidVless = await jsonRequest(
`${base}/v1/proxies/${vlessProxyId}`,
{
method: "PUT",
token: saved.api_token,
body: {
proxy_settings: {
...createdVless.value.proxy_settings,
vless_uri: VLESS_URI.replace("security=reality", "security=tls"),
},
},
},
);
assert.equal(invalidVless.response.status, 400);
assert.match(JSON.stringify(invalidVless.value), /VLESS_CONFIG_INVALID/);
assert.equal(
(
await jsonRequest(`${base}/v1/proxies/${vlessProxyId}`, {
token: saved.api_token,
})
).value.proxy_settings.vless_uri,
VLESS_URI,
);
const imported = await jsonRequest(`${base}/v1/proxies/import`, {
method: "POST",
token: saved.api_token,
@@ -171,6 +217,15 @@ test("authenticated REST API serves its complete OpenAPI contract and CRUD lifec
).response.status,
204,
);
assert.equal(
(
await jsonRequest(`${base}/v1/proxies/${vlessProxyId}`, {
method: "DELETE",
token: saved.api_token,
})
).response.status,
204,
);
for (const importedProxy of imported.value.proxies) {
await jsonRequest(`${base}/v1/proxies/${importedProxy.id}`, {
method: "DELETE",
@@ -257,6 +312,8 @@ test("MCP Streamable HTTP initialization, auth, discovery, calls, and isolated a
"create_profile",
"run_profile",
"list_proxies",
"create_proxy",
"update_proxy",
"get_page_content",
"get_interactive_elements",
]) {
@@ -276,6 +333,80 @@ test("MCP Streamable HTTP initialization, auth, discovery, calls, and isolated a
assert.equal(listed.value.error, undefined);
assert.ok(listed.value.result);
const createdVless = await jsonRequest(`${base}/mcp/${config.token}`, {
method: "POST",
headers: mcpHeaders,
body: {
jsonrpc: "2.0",
id: 4,
method: "tools/call",
params: {
name: "create_proxy",
arguments: {
name: "MCP VLESS Reality",
proxy_type: "vless",
vless_uri: VLESS_URI,
},
},
},
});
assert.equal(createdVless.response.status, 200);
assert.equal(createdVless.value.error, undefined);
let vlessProxy = (await app.invoke("get_stored_proxies")).find(
(proxy) => proxy.name === "MCP VLESS Reality",
);
assert.ok(vlessProxy);
assert.equal(vlessProxy.proxy_settings.proxy_type, "vless");
assert.equal(vlessProxy.proxy_settings.vless_uri, VLESS_URI);
const updatedVless = await jsonRequest(`${base}/mcp/${config.token}`, {
method: "POST",
headers: mcpHeaders,
body: {
jsonrpc: "2.0",
id: 5,
method: "tools/call",
params: {
name: "update_proxy",
arguments: {
proxy_id: vlessProxy.id,
name: "MCP VLESS Updated",
vless_uri: VLESS_URI,
},
},
},
});
assert.equal(updatedVless.value.error, undefined);
vlessProxy = (await app.invoke("get_stored_proxies")).find(
(proxy) => proxy.id === vlessProxy.id,
);
assert.equal(vlessProxy.name, "MCP VLESS Updated");
const invalidVless = await jsonRequest(`${base}/mcp/${config.token}`, {
method: "POST",
headers: mcpHeaders,
body: {
jsonrpc: "2.0",
id: 6,
method: "tools/call",
params: {
name: "update_proxy",
arguments: {
proxy_id: vlessProxy.id,
vless_uri: VLESS_URI.replace("security=reality", "security=tls"),
},
},
},
});
assert.match(invalidVless.value.error.message, /VLESS_CONFIG_INVALID/);
assert.equal(
(await app.invoke("get_stored_proxies")).find(
(proxy) => proxy.id === vlessProxy.id,
).proxy_settings.vless_uri,
VLESS_URI,
);
await app.invoke("delete_stored_proxy", { proxyId: vlessProxy.id });
const agents = await app.invoke("list_mcp_agents");
assert.ok(agents.some((agent) => agent.id === "cursor"));
await assertCommandErrorCode(app, "add_mcp_to_agent", "MCP_AGENT_UNKNOWN", {
+325 -1
View File
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readdir, readFile, writeFile } from "node:fs/promises";
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
import { isIP } from "node:net";
import path from "node:path";
import test from "node:test";
@@ -406,6 +406,330 @@ function wireGuardTargetWasReached() {
);
}
function processIsRunning(pid) {
if (!Number.isInteger(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error?.code === "EPERM";
}
}
async function createXrayProfile(app, version) {
return app.invoke("create_browser_profile_new", {
name: "Xray Reality Profile",
browserStr: "wayfern",
version,
releaseType: "stable",
proxyId: null,
vpnId: null,
wayfernConfig: {
fingerprint: null,
randomize_fingerprint_on_launch: false,
geoip: false,
},
groupId: null,
ephemeral: false,
dnsBlocklist: null,
launchHook: null,
});
}
test("VLESS Reality persists, imports, routes through Xray-core, records traffic, and cleans up", async () => {
const vlessUri = process.env.DONUT_E2E_VLESS_URI;
const accessLog = process.env.DONUT_E2E_XRAY_ACCESS_LOG;
assert.ok(vlessUri, "The network harness must provide a VLESS Reality URI");
assert.ok(accessLog, "The network harness must provide an Xray access log");
const app = appFromEnvironment("network-xray-reality", {
seedVersionCache: false,
wayfernTermsAccepted: false,
});
let apiPort;
let activeCdp;
let profile;
let worker;
try {
const prepared = await prepareWayfern(
app,
process.env.DONUT_E2E_PROJECT_ROOT,
);
if (!app.session) await app.start();
if (!(await app.invoke("check_wayfern_terms_accepted"))) {
await app.invoke("accept_wayfern_terms");
await app.restart();
}
const invalidUri = vlessUri.replace("security=reality", "security=tls");
const invalidCreate = await app.invokeError("create_stored_proxy", {
name: "Invalid VLESS",
proxySettings: {
proxy_type: "vless",
host: "ignored.invalid",
port: 1,
username: "must-be-cleared",
password: "must-be-cleared",
vless_uri: invalidUri,
},
});
assert.match(invalidCreate, /VLESS_CONFIG_INVALID/);
const created = await app.invoke("create_stored_proxy", {
name: "Local VLESS Reality",
proxySettings: {
proxy_type: "VLESS",
host: "ignored.invalid",
port: 1,
username: "must-be-cleared",
password: "must-be-cleared",
vless_uri: vlessUri,
},
});
assert.equal(created.proxy_settings.proxy_type, "vless");
assert.equal(created.proxy_settings.host, "127.0.0.1");
assert.equal(created.proxy_settings.port, Number(new URL(vlessUri).port));
assert.equal(created.proxy_settings.username, null);
assert.equal(created.proxy_settings.password, null);
assert.equal(created.proxy_settings.vless_uri, vlessUri);
const updated = await app.invoke("update_stored_proxy", {
proxyId: created.id,
name: "Updated VLESS Reality",
proxySettings: {
proxy_type: "vless",
host: "still-ignored.invalid",
port: 2,
username: "still-cleared",
password: "still-cleared",
vless_uri: vlessUri,
},
});
assert.equal(updated.name, "Updated VLESS Reality");
assert.equal(updated.proxy_settings.host, "127.0.0.1");
assert.equal(updated.proxy_settings.username, null);
assert.equal(updated.proxy_settings.password, null);
const exportedJson = await app.invoke("export_proxies", {
format: "json",
});
const exported = JSON.parse(exportedJson);
assert.equal(exported.proxies.length, 1);
assert.deepEqual(exported.proxies[0], {
name: "Updated VLESS Reality",
type: "vless",
host: "127.0.0.1",
port: Number(new URL(vlessUri).port),
vless_uri: vlessUri,
});
assert.equal(
await app.invoke("export_proxies", { format: "txt" }),
vlessUri,
);
const parsed = await app.invoke("parse_txt_proxies", {
content: `${vlessUri}\n${invalidUri}\n`,
});
assert.equal(parsed.length, 2);
assert.equal(parsed[0].status, "parsed");
assert.equal(parsed[0].proxy_type, "vless");
assert.equal(parsed[0].vless_uri, vlessUri);
assert.equal(parsed[1].status, "invalid");
await app.restart();
const persisted = (await app.invoke("get_stored_proxies")).find(
(candidate) => candidate.id === created.id,
);
assert.ok(persisted, "VLESS proxy did not survive an app restart");
assert.equal(persisted.proxy_settings.vless_uri, vlessUri);
await app.invoke("delete_stored_proxy", { proxyId: created.id });
const imported = await app.invoke("import_proxies_json", {
content: exportedJson,
});
assert.equal(imported.imported_count, 1);
assert.equal(imported.skipped_count, 0);
assert.deepEqual(imported.errors, []);
assert.equal(imported.proxies[0].proxy_settings.vless_uri, vlessUri);
const proxy = imported.proxies[0];
const invalidImport = await app.invoke("import_proxies_json", {
content: JSON.stringify({
version: "1.0",
source: "DonutBrowser",
exported_at: new Date().toISOString(),
proxies: [
{
name: "Rejected VLESS",
type: "vless",
host: "127.0.0.1",
port: 443,
vless_uri: invalidUri,
},
],
}),
});
assert.equal(invalidImport.imported_count, 0);
assert.equal(invalidImport.errors.length, 1);
assert.match(invalidImport.errors[0], /VLESS_CONFIG_INVALID/);
profile = await createXrayProfile(app, prepared.version);
await app.invoke("update_profile_proxy", {
profileId: profile.id,
proxyId: proxy.id,
});
const assigned = (await app.invoke("list_browser_profiles")).find(
(candidate) => candidate.id === profile.id,
);
assert.equal(assigned.proxy_id, proxy.id);
const settings = await app.invoke("get_app_settings");
const saved = await app.invoke("save_app_settings", {
settings: {
...settings,
api_enabled: true,
api_port: 0,
api_token: null,
},
});
apiPort = await app.invoke("start_api_server", { port: 0 });
const base = `http://127.0.0.1:${apiPort}`;
const launched = await runProfile(
app,
base,
saved.api_token,
profile.id,
"https://api.ipify.org/",
);
activeCdp = launched.cdp;
const exitIp = await activeCdp.waitFor(
`(() => {
const value = document.body?.innerText?.trim() ?? "";
return /^[0-9a-f:.]+$/i.test(value) ? value : false;
})()`,
{ timeoutMs: 30_000, description: "VLESS Reality exit IP" },
);
assert.ok(isIP(exitIp), `Unexpected exit IP response: ${exitIp}`);
const workerDirectory = path.join(app.dataRoot, "cache", "proxy_workers");
await app.waitFor(
async () => {
const workerFiles = await readdir(workerDirectory).catch(() => []);
const workerFile = workerFiles.find(
(file) => file.startsWith("xray_worker_") && file.endsWith(".json"),
);
if (!workerFile) return false;
worker = JSON.parse(
await readFile(path.join(workerDirectory, workerFile), "utf8"),
);
return worker.profile_id === profile.id;
},
{ description: "profile-scoped Xray worker configuration" },
);
assert.ok(processIsRunning(worker.pid), "Xray supervisor is not running");
assert.ok(processIsRunning(worker.xray_pid), "Xray-core is not running");
assert.equal(worker.vless_uri, vlessUri);
const workerPath = path.join(
workerDirectory,
`xray_worker_${worker.id}.json`,
);
const runtimePath = path.join(
workerDirectory,
`xray_runtime_${worker.id}.json`,
);
if (process.platform !== "win32") {
assert.equal((await stat(workerPath)).mode & 0o777, 0o600);
assert.equal((await stat(runtimePath)).mode & 0o777, 0o600);
}
const runtime = JSON.parse(await readFile(runtimePath, "utf8"));
assert.equal(runtime.inbounds[0].protocol, "socks");
assert.equal(runtime.inbounds[0].listen, "127.0.0.1");
assert.equal(runtime.outbounds[0].protocol, "vless");
assert.equal(runtime.outbounds[0].streamSettings.security, "reality");
assert.equal(
runtime.outbounds[0].settings.vnext[0].users[0].flow,
"xtls-rprx-vision",
);
await app.waitFor(
async () =>
(await readFile(accessLog, "utf8").catch(() => "")).includes(
"api.ipify.org:443",
),
{
timeoutMs: 15_000,
description: "request in Xray-core server access log",
},
);
const liveTraffic = await app.waitFor(
async () => {
const snapshot = await app.invoke("get_profile_traffic_snapshot", {
profileId: profile.id,
});
return snapshot?.total_bytes_sent > 0 &&
snapshot?.total_bytes_received > 0
? snapshot
: false;
},
{
timeoutMs: 15_000,
description: "local traffic snapshot for VLESS profile",
},
);
assert.ok(liveTraffic.total_requests > 0);
const supervisorPid = worker.pid;
const xrayPid = worker.xray_pid;
await stopProfile(app, base, saved.api_token, profile.id, activeCdp);
activeCdp = null;
await app.waitFor(
async () => {
const files = await readdir(workerDirectory).catch(() => []);
return (
!files.includes(`xray_worker_${worker.id}.json`) &&
!processIsRunning(supervisorPid) &&
!processIsRunning(xrayPid)
);
},
{
timeoutMs: 15_000,
description: "Xray worker process and configuration cleanup",
},
);
await assert.rejects(readFile(runtimePath), { code: "ENOENT" });
const persistedTraffic = await app.invoke("get_profile_traffic_snapshot", {
profileId: profile.id,
});
assert.ok(
persistedTraffic.total_bytes_sent >= liveTraffic.total_bytes_sent,
);
assert.ok(
persistedTraffic.total_bytes_received >= liveTraffic.total_bytes_received,
);
} catch (error) {
await app.capture("failure");
throw error;
} finally {
activeCdp?.close();
if (app.session) {
if (apiPort) await app.invoke("stop_api_server").catch(() => {});
if (profile) {
const latest = (
await app.invoke("list_browser_profiles").catch(() => [])
).find((candidate) => candidate.id === profile.id);
if (latest?.process_id) {
await app
.invoke("kill_browser_profile", { profile: latest })
.catch(() => {});
}
}
}
await app.close();
}
});
test("visible UI creates and assigns profiles, groups, proxies, VPNs, extensions, and extension groups", async () => {
const httpSettings = proxySettings(
process.env.RESIDENTIAL_PROXY_URL_ONE_HTTP,
+446 -34
View File
@@ -1,5 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import Color from "color";
import { getDerivedThemeColors, THEMES } from "../../src/lib/themes.ts";
import { withApp } from "../lib/app.mjs";
const THEME_VARIABLES = [
@@ -31,34 +33,8 @@ const THEME_VARIABLES = [
"--chart-5",
];
const DRACULA_THEME = {
"--background": "#282a36",
"--foreground": "#f8f8f2",
"--card": "#44475a",
"--card-foreground": "#f8f8f2",
"--popover": "#44475a",
"--popover-foreground": "#f8f8f2",
"--primary": "#bd93f9",
"--primary-foreground": "#282a36",
"--secondary": "#8be9fd",
"--secondary-foreground": "#282a36",
"--muted": "#6272a4",
"--muted-foreground": "#f8f8f2",
"--accent": "#ff79c6",
"--accent-foreground": "#282a36",
"--destructive": "#ff5555",
"--destructive-foreground": "#f8f8f2",
"--success": "#50fa7b",
"--success-foreground": "#282a36",
"--warning": "#ffb86c",
"--warning-foreground": "#282a36",
"--border": "#6272a4",
"--chart-1": "#bd93f9",
"--chart-2": "#50fa7b",
"--chart-3": "#ff79c6",
"--chart-4": "#8be9fd",
"--chart-5": "#ffb86c",
};
const DRACULA_THEME = THEMES.find((theme) => theme.id === "dracula").colors;
const AYU_LIGHT_THEME = THEMES.find((theme) => theme.id === "ayu-light").colors;
async function dismissSurface(app) {
await app.pressShortcut({ key: "Escape" });
@@ -108,6 +84,90 @@ function themeVariablesEqual(actual, expected) {
);
}
async function applyThemeForContrastAudit(app, theme) {
await app.execute(
`
const [colors, derived, mode] = arguments;
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(mode);
for (const [key, value] of Object.entries({ ...colors, ...derived })) {
root.style.setProperty(key, value, "important");
}
`,
[theme.colors, getDerivedThemeColors(theme.colors), theme.mode],
);
await new Promise((resolve) => setTimeout(resolve, 200));
}
async function animatedTabContrastSnapshot(app) {
return app.execute(`
const rootStyle = getComputedStyle(document.documentElement);
const triggers = [
...document.querySelectorAll('[data-slot="animated-tabs-trigger"]'),
];
const active = triggers.find(
(trigger) => trigger.getAttribute("data-state") === "active",
);
const inactive = triggers.find(
(trigger) => trigger.getAttribute("data-state") === "inactive",
);
const content = (trigger) =>
[...(trigger?.children ?? [])].filter(
(child) =>
child.getAttribute("data-slot") !== "animated-tabs-indicator",
);
const activeContent = content(active);
const inactiveContent = content(inactive);
const indicator = active?.querySelector(
'[data-slot="animated-tabs-indicator"]',
);
return {
mode: document.documentElement.classList.contains("light")
? "light"
: "dark",
background: rootStyle.getPropertyValue("--background").trim(),
accent: rootStyle.getPropertyValue("--accent").trim(),
accentForeground: rootStyle
.getPropertyValue("--accent-foreground")
.trim(),
mutedForeground: rootStyle
.getPropertyValue("--muted-foreground")
.trim(),
activeTitle: activeContent[0]
? getComputedStyle(activeContent[0]).color
: null,
activeCount: activeContent[1]
? getComputedStyle(activeContent[1]).color
: null,
activeBackground: indicator
? getComputedStyle(indicator).backgroundColor
: null,
inactiveTitle: inactiveContent[0]
? getComputedStyle(inactiveContent[0]).color
: null,
inactiveCount: inactiveContent[1]
? getComputedStyle(inactiveContent[1]).color
: null,
};
`);
}
function assertColorEquals(actual, expected, description) {
assert.ok(actual, `${description} is missing`);
assert.equal(Color(actual).hex(), Color(expected).hex(), description);
}
function assertContrast(foreground, background, minimum, description) {
assert.ok(foreground, `${description} foreground is missing`);
assert.ok(background, `${description} background is missing`);
const ratio = Color(foreground).contrast(Color(background));
assert.ok(
ratio >= minimum,
`${description} is ${ratio.toFixed(2)}:1; expected at least ${minimum}:1`,
);
}
async function chooseSelectOption(app, triggerSelector, option) {
await app.clickSelector(triggerSelector);
await app.clickText(option, { roles: ["option"] });
@@ -204,13 +264,15 @@ async function dragBackgroundColorPicker(app) {
const pointerEvents = await app.execute(
`return window.__donutE2eThemePointerEvents ?? [];`,
);
assert.deepEqual(
pointerEvents.map((event) => event.type),
["pointermove", "pointerdown", "pointermove", "pointerup"],
);
assert.equal(pointerEvents[0]?.type, "pointermove");
assert.equal(pointerEvents[1]?.type, "pointerdown");
assert.equal(pointerEvents.at(-1)?.type, "pointerup");
const dragMoves = pointerEvents.slice(2, -1);
assert.ok(dragMoves.length >= 1);
assert.ok(dragMoves.every((event) => event.type === "pointermove"));
assert.match(pointerEvents[1].target, /cursor-pointer/);
assert.match(pointerEvents[2].target, /cursor-pointer/);
assert.equal(pointerEvents[2].buttons, 1);
assert.match(dragMoves.at(-1).target, /cursor-pointer/);
assert.equal(dragMoves.at(-1).buttons, 1);
await app.waitFor(
() =>
app.execute(
@@ -274,6 +336,325 @@ test("all primary navigation buttons and sub-page tabs render and remain interac
});
});
test("every custom theme keeps tabs, counts, group pills, and rail states readable", async () => {
await withApp("ui-theme-contrast", async (app) => {
await app.clickSelector('[aria-label="Extensions"]');
await app.waitFor(
() =>
app.execute(
`return document.querySelectorAll('[data-slot="animated-tabs-trigger"]').length === 2;`,
),
{ description: "Extension tabs" },
);
for (const theme of THEMES) {
await applyThemeForContrastAudit(app, theme);
const snapshot = await animatedTabContrastSnapshot(app);
assert.equal(snapshot.mode, theme.mode, `${theme.id} appearance mode`);
assertColorEquals(
snapshot.activeBackground,
snapshot.accent,
`${theme.id} active tab background`,
);
for (const [label, color] of [
["active tab title", snapshot.activeTitle],
["active tab count", snapshot.activeCount],
]) {
assertColorEquals(
color,
snapshot.accentForeground,
`${theme.id} ${label} token`,
);
assertContrast(
color,
snapshot.activeBackground,
4.5,
`${theme.id} ${label}`,
);
}
for (const [label, color] of [
["inactive tab title", snapshot.inactiveTitle],
["inactive tab count", snapshot.inactiveCount],
]) {
assertColorEquals(
color,
snapshot.mutedForeground,
`${theme.id} ${label} token`,
);
assertContrast(color, snapshot.background, 4.5, `${theme.id} ${label}`);
}
await app.clickSelector(
'[data-slot="animated-tabs-trigger"][data-state="inactive"]',
);
await app.waitFor(
() =>
app.execute(
`return Boolean(document.querySelector(
'[data-slot="animated-tabs-trigger"][data-state="active"] [data-slot="animated-tabs-indicator"]'
));`,
),
{ description: `${theme.id} tab indicator after switching` },
);
}
await app.clickSelector('[aria-label="Groups"]');
await app.waitFor(
() =>
app.execute(
`return Boolean(document.querySelector('[data-slot="group-summary-pill"]'));`,
),
{ description: "Profile groups summary pill" },
);
for (const theme of THEMES) {
await applyThemeForContrastAudit(app, theme);
const snapshot = await app.execute(`
const rootStyle = getComputedStyle(document.documentElement);
const pill = document.querySelector(
'[data-slot="group-summary-pill"]',
);
const count = pill?.querySelector(
'[data-slot="group-summary-count"]',
);
const title = pill?.firstElementChild;
const rail = document.querySelector('nav [aria-current="page"]');
return {
pillBackground: pill ? getComputedStyle(pill).backgroundColor : null,
pillTitle: title ? getComputedStyle(title).color : null,
pillCount: count ? getComputedStyle(count).color : null,
railBackground: rail
? getComputedStyle(rail).backgroundColor
: null,
railForeground: rail ? getComputedStyle(rail).color : null,
accent: rootStyle.getPropertyValue("--accent").trim(),
accentForeground: rootStyle
.getPropertyValue("--accent-foreground")
.trim(),
};
`);
assertColorEquals(
snapshot.pillBackground,
snapshot.accent,
`${theme.id} group pill background`,
);
for (const [label, color] of [
["group pill title", snapshot.pillTitle],
["group pill count", snapshot.pillCount],
]) {
assertColorEquals(
color,
snapshot.accentForeground,
`${theme.id} ${label} token`,
);
assertContrast(
color,
snapshot.pillBackground,
4.5,
`${theme.id} ${label}`,
);
}
assertContrast(
snapshot.railForeground,
snapshot.railBackground,
3,
`${theme.id} selected rail icon`,
);
}
});
});
test("VLESS proxy form keeps the share URI as one clear, validated input", async () => {
await withApp("ui-vless-proxy-form", async (app) => {
const uri =
"vless://6d6e21a1-4829-4d2b-bc7f-1b25707b61e4@vpn.example.com:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.example.com&fp=chrome&pbk=BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc&sid=0123456789abcdef&type=tcp#E2E";
await app.clickSelector('[aria-label="Network"]');
await app.waitForText("New proxy");
await app.clickSelector('[aria-label="New proxy"]');
await app.waitForText("Add Proxy");
await app.fillSelector("#proxy-name", "E2E VLESS");
await chooseSelectOption(app, "#proxy-type", "VLESS · Vision · REALITY");
assert.equal(
await app.execute(
`return document.querySelector("#proxy-vless-uri") instanceof HTMLTextAreaElement;`,
),
true,
);
assert.equal(
await app.execute(
`return document.querySelector("#proxy-host") === null &&
document.querySelector("#proxy-port") === null &&
document.querySelector("#proxy-username") === null &&
document.querySelector("#proxy-password") === null;`,
),
true,
);
await app.fillSelector(
"#proxy-vless-uri",
"vless://6d6e21a1-4829-4d2b-bc7f-1b25707b61e4@vpn.example.com",
);
await app.waitFor(
() =>
app.execute(
`return document.querySelector("#proxy-vless-uri")?.getAttribute("aria-invalid") === "true";`,
),
{ description: "invalid VLESS endpoint feedback" },
);
assert.equal(
await app.execute(
`return [...document.querySelectorAll("[role='dialog'] button")]
.find((button) => button.textContent?.trim() === "Add Proxy")
?.disabled === true;`,
),
true,
);
await app.fillSelector("#proxy-vless-uri", uri);
await app.waitFor(
() =>
app.execute(
`return document.querySelector("#proxy-vless-uri")?.getAttribute("aria-invalid") === "false";`,
),
{ description: "valid VLESS endpoint feedback" },
);
await app.clickTextIn('[role="dialog"]', "Add Proxy", {
roles: ["button"],
});
await app.waitForText("E2E VLESS");
const proxy = (await app.invoke("get_stored_proxies")).find(
(item) => item.name === "E2E VLESS",
);
assert.ok(proxy);
assert.equal(proxy.proxy_settings.proxy_type, "vless");
assert.equal(proxy.proxy_settings.host, "vpn.example.com");
assert.equal(proxy.proxy_settings.port, 443);
assert.equal(proxy.proxy_settings.username, null);
assert.equal(proxy.proxy_settings.password, null);
assert.match(proxy.proxy_settings.vless_uri, /^vless:\/\//);
await app.invoke("delete_stored_proxy", { proxyId: proxy.id });
});
});
test("About exposes a searchable, responsive third-party license inventory", async () => {
await withApp("ui-about-licenses", async (app) => {
await app.clickSelector('[aria-label="More"]');
await app.waitFor(
() =>
app.execute(`return Boolean(document.querySelector("[role='menu']"));`),
{ description: "More menu" },
);
await app.clickText("About Donut Browser", {
exact: false,
roles: ["menuitem"],
});
await app.waitForText("Open-source anti-detect browser.");
const aboutText = await app.execute(
`return document.querySelector("[role='dialog']")?.textContent ?? "";`,
);
assert.doesNotMatch(aboutText, /AGPL-3\.0|licensed under/i);
await app.clickText("Licenses", { roles: ["button"] });
await app.waitFor(
() =>
app.execute(
`return document.querySelector("[role='dialog'] [data-slot='dialog-title']")?.textContent === "Licenses";`,
),
{ description: "Licenses view" },
);
await app.session.command("POST", "/window/rect", {
width: 640,
height: 400,
});
const inventory = await app.execute(`
const dialog = document.querySelector("[role='dialog']");
const list = dialog?.querySelector(".scroll-fade");
const search = dialog?.querySelector('input[type="search"]');
const rows = [...(dialog?.querySelectorAll("li") ?? [])].map((row) =>
[...row.children].map((child) => (child.textContent || "").trim())
);
const rect = dialog?.getBoundingClientRect();
return {
activeSearch: document.activeElement === search,
rows,
scrollable: Boolean(list && list.scrollHeight > list.clientHeight),
bounds: rect
? {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
viewportWidth: innerWidth,
viewportHeight: innerHeight,
}
: null,
};
`);
assert.equal(inventory.activeSearch, true);
assert.equal(inventory.scrollable, true);
assert.ok(inventory.bounds);
assert.ok(inventory.bounds.left >= 0);
assert.ok(inventory.bounds.top >= 0);
assert.ok(inventory.bounds.right <= inventory.bounds.viewportWidth);
assert.ok(inventory.bounds.bottom <= inventory.bounds.viewportHeight);
assert.ok(
inventory.rows.some(
([name, license]) => name === "Xray-core" && license === "MPL-2.0",
),
);
assert.ok(
inventory.rows.some(
([name, license]) =>
name === "Donut Browser" && license === "AGPL-3.0-only",
),
);
assert.ok(
inventory.rows.some(
([name, license]) =>
name === "tauri-plugin-opener" && license === "Apache-2.0 OR MIT",
),
);
assert.ok(inventory.rows.every((row) => row.length === 2));
const search = await app.session.findCss('input[type="search"]');
await app.session.sendKeys(search, "xray");
await app.waitFor(
() =>
app.execute(
`return document.querySelectorAll("[role='dialog'] li").length === 1;`,
),
{ description: "license search result" },
);
assert.match(
await app.execute(
`return document.querySelector("[role='dialog'] li")?.textContent ?? "";`,
),
/Xray-core.*MPL-2\.0/s,
);
await app.clickSelector('button[aria-label="Back"]');
await app.waitFor(
() =>
app.execute(
`return document.querySelector("[role='dialog'] [data-slot='dialog-title']")?.textContent === "About";`,
),
{ description: "About view after returning from licenses" },
);
assert.equal(
await app.execute(
`return document.activeElement?.textContent?.trim() === "Licenses";`,
),
true,
);
});
});
test("settings tabs, command palette filtering, and responsive layout survive resize", async () => {
await withApp("ui-settings-responsive", async (app) => {
await app.clickSelector('[aria-label="Settings"]');
@@ -503,3 +884,34 @@ test("preset and manually customized themes survive navigation and restart", asy
);
});
});
test("a light custom preset keeps light component behavior after restart", async () => {
await withApp("ui-theme-custom-light", async (app) => {
await app.clickSelector('[aria-label="Settings"]');
await app.waitForText("Appearance");
await chooseSelectOption(app, "#theme-select", "Custom");
await chooseSelectOption(app, "#theme-preset-select", "Ayu Light");
await saveSettings(app);
const selected = await waitForTheme(
app,
(snapshot) =>
snapshot.mode === "light" &&
themeVariablesEqual(snapshot.inline, AYU_LIGHT_THEME) &&
themeVariablesEqual(snapshot.resolved, AYU_LIGHT_THEME),
"Ayu Light variables and light mode to render",
);
await assertThemeAcrossNavigation(app, selected);
await app.restart();
assert.deepEqual(
(await app.invoke("get_app_settings")).custom_theme,
AYU_LIGHT_THEME,
);
await app.waitFor(
async () =>
JSON.stringify(await themeSnapshot(app)) === JSON.stringify(selected),
{ description: "Ayu Light preset after restart" },
);
});
});