refactor: harden tests

This commit is contained in:
zhom
2026-07-21 23:13:17 +04:00
parent f7daf68b52
commit 9624ec846d
81 changed files with 11771 additions and 983 deletions
+6 -30
View File
@@ -7,11 +7,7 @@ import path from "node:path";
import test from "node:test";
import { appFromEnvironment } from "../lib/app.mjs";
import { CdpClient } from "../lib/cdp.mjs";
import {
defaultWayfernPath,
inspectWayfern,
seedWayfern,
} from "../lib/fixtures.mjs";
import { defaultWayfernPath, prepareWayfern } from "../lib/fixtures.mjs";
const fixtureUrl = process.env.DONUT_E2E_FIXTURE_URL;
@@ -36,30 +32,6 @@ async function request(url, { method = "GET", token, body } = {}) {
return { response, value };
}
async function prepareWayfern(app) {
const localBundle = defaultWayfernPath(process.env.DONUT_E2E_PROJECT_ROOT);
if (existsSync(localBundle)) {
const wayfern = inspectWayfern(localBundle);
await seedWayfern(app.dataRoot, wayfern);
return { version: wayfern.version, source: "local fixture" };
}
await app.start();
const current = await app.invoke("fetch_browser_versions_with_count", {
browserStr: "wayfern",
});
assert.ok(
current.versions.length > 0,
"No Wayfern build is published for this platform",
);
const version = current.versions[0];
await app.invoke("download_browser", {
browserStr: "wayfern",
version,
});
return { version, source: "published download" };
}
function processExists(pid) {
if (!pid) return false;
try {
@@ -166,11 +138,15 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
);
const app = appFromEnvironment("browser-wayfern", {
seedVersionCache: hasLocalWayfern,
wayfernTermsAccepted: false,
});
let cdp;
let browserPid;
try {
const prepared = await prepareWayfern(app);
const prepared = await prepareWayfern(
app,
process.env.DONUT_E2E_PROJECT_ROOT,
);
if (!app.session) await app.start();
assert.equal(await app.invoke("check_wayfern_downloaded"), true);
+53 -4
View File
@@ -1,9 +1,18 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import {
lstat,
mkdir,
mkdtemp,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { allCoveredCommands, commandCoverage } from "../coverage-map.mjs";
import { seedWayfern } from "../lib/fixtures.mjs";
import { WebDriverClient } from "../lib/webdriver.mjs";
function registeredCommands(source) {
@@ -58,10 +67,15 @@ test("every Tauri command has exactly one E2E owner and evidence level", async (
continue;
}
const suiteSource = await readFile(
const evidenceFiles = [
path.join(root, "e2e", "tests", `${entry.suite}.test.mjs`),
"utf8",
);
...(entry.suite === "browser"
? [path.join(root, "e2e", "lib", "fixtures.mjs")]
: []),
];
const suiteSource = (
await Promise.all(evidenceFiles.map((file) => readFile(file, "utf8")))
).join("\n");
for (const command of entry.commands) {
assert.equal(
commandHasExecutableEvidence(suiteSource, command),
@@ -94,3 +108,38 @@ test("WebDriver client preserves application values that contain an error field"
await new Promise((resolve) => server.close(resolve));
}
});
test("Wayfern fixtures are copied into the isolated data root, never linked", async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), "donut-wayfern-copy-"));
t.after(() => rm(root, { recursive: true, force: true }));
const source =
process.platform === "darwin"
? path.join(root, "source", "Wayfern.app", "Contents", "MacOS", "Wayfern")
: path.join(
root,
"source",
process.platform === "win32" ? "Wayfern.exe" : "wayfern",
);
await mkdir(path.dirname(source), { recursive: true });
await writeFile(source, "source-fixture");
const bundlePath =
process.platform === "darwin"
? path.join(root, "source", "Wayfern.app")
: source;
const installDir = await seedWayfern(path.join(root, "isolated"), {
bundlePath,
executable: source,
version: "1.2.3.4",
});
const destination =
process.platform === "darwin"
? path.join(installDir, "Wayfern.app", "Contents", "MacOS", "Wayfern")
: path.join(
installDir,
process.platform === "win32" ? "wayfern.exe" : "wayfern",
);
assert.equal((await lstat(destination)).isSymbolicLink(), false);
await writeFile(destination, "isolated-mutation");
assert.equal(await readFile(source, "utf8"), "source-fixture");
});
+116
View File
@@ -0,0 +1,116 @@
import assert from "node:assert/strict";
import {
mkdir,
mkdtemp,
readdir,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { after, test } from "node:test";
import { redactIssueBody } from "../../scripts/redact-sensitive-text.mjs";
import { createSafeDiagnostics } from "../lib/diagnostics.mjs";
const roots = [];
after(async () => {
await Promise.all(
roots.map((root) => rm(root, { recursive: true, force: true })),
);
});
test("shared E2E diagnostics contain only redacted text logs", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "donut-diagnostics-test-"));
roots.push(root);
const secretUrl = "http://real-user:real-password@proxy.example:8080";
const token = ["github", "pat", "example", "token", "0123456789"].join("_");
const logText = [
`proxy=${secretUrl}`,
`Authorization: Bearer ${token}`,
"visited https://example.com/callback?code=private-code",
"exit IP 203.0.113.42",
"home /Users/private-person/Library/Application Support",
"email private.person@example.com",
"PrivateKey = wireguard-private-key",
].join("\n");
await Promise.all([
mkdir(path.join(root, "logs"), { recursive: true }),
mkdir(path.join(root, "sessions", "network", "donut", "logs"), {
recursive: true,
}),
mkdir(path.join(root, "sessions", "network", "donut", "data", "proxies"), {
recursive: true,
}),
mkdir(path.join(root, "sessions", "network", "artifacts"), {
recursive: true,
}),
]);
await Promise.all([
writeFile(path.join(root, "logs", "driver.log"), logText),
writeFile(
path.join(root, "sessions", "network", "donut", "logs", "app.log"),
logText,
),
writeFile(
path.join(
root,
"sessions",
"network",
"donut",
"data",
"proxies",
"real.json",
),
JSON.stringify({ upstream_url: secretUrl, token }),
),
writeFile(
path.join(root, "sessions", "network", "artifacts", "page.html"),
`<html>${secretUrl}</html>`,
),
]);
const diagnostics = await createSafeDiagnostics(root, {
suite: "network",
failed: true,
sensitiveValues: [secretUrl, token],
});
const files = await readdir(diagnostics);
assert.deepEqual(files.sort(), ["001.log", "002.log", "summary.json"]);
const combined = (
await Promise.all(
files.map((file) => readFile(path.join(diagnostics, file), "utf8")),
)
).join("\n");
for (const value of [
secretUrl,
"real-user",
"real-password",
"proxy.example",
token,
"private-code",
"203.0.113.42",
"private-person",
"private.person@example.com",
"wireguard-private-key",
]) {
assert.ok(!combined.includes(value), `diagnostics leaked ${value}`);
}
assert.ok(
!files.some(
(file) => /\.(?:html|json)$/u.test(file) && file !== "summary.json",
),
);
});
test("automated issue processing omits the complete log field", () => {
const safe = redactIssueBody(
`### What happened?\nA failure at user@example.com\n\n### Error logs or screenshots\nARBITRARY_PRIVATE_LOG_CONTENT\npassword=hunter2\n\n### Operating System\nLinux`,
);
assert.ok(!safe.includes("ARBITRARY_PRIVATE_LOG_CONTENT"));
assert.ok(!safe.includes("hunter2"));
assert.ok(!safe.includes("user@example.com"));
assert.match(safe, /omitted from automated processing/u);
assert.match(safe, /Operating System\nLinux/u);
});
-1
View File
@@ -25,7 +25,6 @@ async function createProfile(app, name = "Entity Profile") {
test("profile, group, proxy, tag, metadata, clone, and bulk-delete lifecycle", async () => {
await withApp("entities-core", async (app) => {
await app.invoke("complete_onboarding");
const group = await app.invoke("create_profile_group", {
name: "Research",
});
+605
View File
@@ -0,0 +1,605 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readdir, readFile, writeFile } from "node:fs/promises";
import { isIP } from "node:net";
import path from "node:path";
import test from "node:test";
import { appFromEnvironment } from "../lib/app.mjs";
import { CdpClient } from "../lib/cdp.mjs";
import {
extensionZipBase64,
prepareWayfern,
wireGuardFixture,
} from "../lib/fixtures.mjs";
function proxySettings(raw, expectedKind) {
assert.ok(raw, `${expectedKind} residential proxy URL is required`);
const url = new URL(raw);
const rawType = url.protocol.slice(0, -1).toLowerCase();
const proxyType =
rawType === "socks" || rawType === "socks5h" ? "socks5" : rawType;
if (expectedKind === "HTTP") {
assert.ok(
proxyType === "http" || proxyType === "https",
`Expected an HTTP proxy URL, got ${rawType}`,
);
} else {
assert.equal(proxyType, "socks5");
}
const port = Number(url.port);
assert.ok(url.hostname && port > 0 && port <= 65535);
return {
proxy_type: proxyType,
host: url.hostname,
port,
username: url.username ? decodeURIComponent(url.username) : null,
password: url.password ? decodeURIComponent(url.password) : null,
};
}
function wireGuardFields(config) {
let section = "";
const fields = new Map();
for (const rawLine of config.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;
if (line === "[Interface]") {
section = "interface";
continue;
}
if (line === "[Peer]") {
section = "peer";
continue;
}
const separator = line.indexOf("=");
if (separator === -1) continue;
fields.set(
`${section}.${line.slice(0, separator).trim()}`,
line.slice(separator + 1).trim(),
);
}
return {
privateKey: fields.get("interface.PrivateKey"),
address: fields.get("interface.Address"),
dns: fields.get("interface.DNS") ?? "",
peerPublicKey: fields.get("peer.PublicKey"),
peerEndpoint: fields.get("peer.Endpoint"),
allowedIps: fields.get("peer.AllowedIPs") ?? "0.0.0.0/0",
persistentKeepalive: fields.get("peer.PersistentKeepalive") ?? "",
presharedKey: fields.get("peer.PresharedKey") ?? "",
};
}
async function request(url, { method = "GET", token, body } = {}) {
const response = await fetch(url, {
method,
headers: {
...(token ? { authorization: `Bearer ${token}` } : {}),
...(body === undefined ? {} : { "content-type": "application/json" }),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
let value = text;
if (text) {
try {
value = JSON.parse(text);
} catch {
// Plain-text responses are intentional for some endpoints.
}
}
return { response, value };
}
async function createGroupThroughUi(app) {
await app.clickSelector('[aria-label="Groups"]');
await app.waitForText("Profile groups");
await app.clickSelector('[aria-label="Create"]');
await app.waitForText("Create New Group");
await app.fillSelector("#group-name", "Visible UI Group");
await app.clickTextIn('[role="dialog"]', "Create", { roles: ["button"] });
await app.waitForText("Visible UI Group");
const groups = await app.invoke("get_profile_groups");
return groups.find((group) => group.name === "Visible UI Group");
}
async function createProxyThroughUi(app, settings) {
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", "Visible Residential HTTP");
await app.fillSelector("#proxy-host", settings.host);
await app.fillSelector("#proxy-port", String(settings.port));
if (settings.username)
await app.fillSelector("#proxy-username", settings.username);
if (settings.password)
await app.fillSelector("#proxy-password", settings.password);
await app.clickTextIn('[role="dialog"]', "Add Proxy", {
roles: ["button"],
});
await app.waitForText("Visible Residential HTTP");
const proxies = await app.invoke("get_stored_proxies");
return proxies.find((proxy) => proxy.name === "Visible Residential HTTP");
}
async function createVpnThroughUi(app, config) {
const fields = wireGuardFields(config);
assert.ok(
fields.privateKey &&
fields.address &&
fields.peerPublicKey &&
fields.peerEndpoint,
"WireGuard fixture is missing required fields",
);
await app.clickText("VPNs", { exact: false, roles: ["tab"] });
await app.clickSelector('[aria-label="New VPN"]');
await app.waitForText("Create WireGuard VPN");
await app.fillSelector("#wg-name", "Visible Local WireGuard");
await app.fillSelector("#wg-private-key", fields.privateKey);
await app.fillSelector("#wg-address", fields.address);
if (fields.dns) await app.fillSelector("#wg-dns", fields.dns);
await app.fillSelector("#wg-peer-public-key", fields.peerPublicKey);
await app.fillSelector("#wg-peer-endpoint", fields.peerEndpoint);
await app.fillSelector("#wg-allowed-ips", fields.allowedIps);
if (fields.persistentKeepalive) {
await app.fillSelector("#wg-keepalive", fields.persistentKeepalive);
}
if (fields.presharedKey) {
await app.fillSelector("#wg-preshared-key", fields.presharedKey);
}
await app.clickTextIn('[role="dialog"]', "Create VPN", {
roles: ["button"],
});
await app.waitForText("Visible Local WireGuard");
const vpns = await app.invoke("list_vpn_configs");
return vpns.find((vpn) => vpn.name === "Visible Local WireGuard");
}
async function createExtensionsThroughUi(app) {
const extensionFile = path.join(app.root, "visible-extension.zip");
await writeFile(extensionFile, Buffer.from(extensionZipBase64(), "base64"));
await app.clickSelector('[aria-label="Extensions"]');
await app.waitForText("Upload");
await app.execute(`
const input = document.querySelector("#ext-file-input");
input.classList.remove("hidden");
input.style.position = "fixed";
input.style.left = "12px";
input.style.bottom = "12px";
`);
const input = await app.session.findCss("#ext-file-input");
await app.session.sendKeys(input, extensionFile);
await app.waitForText("visible-extension.zip");
await app.fillSelector(
'input[placeholder="Extension name"]',
"Visible UI Extension",
);
await app.clickText("Add", { roles: ["button"] });
await app.waitForText("Donut E2E Fixture");
await app.clickText("Groups", { exact: false, roles: ["tab"] });
await app.clickSelector('[aria-label="New group"]');
await app.fillSelector(
'input[placeholder="Group name"]',
"Visible Extension Group",
);
await app.clickText("Create", { roles: ["button"] });
await app.waitForText("Visible Extension Group");
let [extensions, groups] = await Promise.all([
app.invoke("list_extensions"),
app.invoke("list_extension_groups"),
]);
const extension = extensions.find(
(item) => item.name === "Donut E2E Fixture",
);
let group = groups.find((item) => item.name === "Visible Extension Group");
assert.ok(extension && group);
const editButton = await app.execute(
`
const row = [...document.querySelectorAll("tr")].find((candidate) =>
(candidate.innerText || "").includes(arguments[0])
);
return row?.querySelector("td:last-child button") ?? null;
`,
[group.name],
);
assert.ok(editButton, "Extension group edit control was not visible");
await app.session.click(editButton);
await app.waitForText("Edit Group");
const extensionPicker = await app.execute(`
const dialogs = [...document.querySelectorAll('[role="dialog"]')];
return dialogs.reverse().find(
(dialog) => (dialog.innerText || "").includes("Edit Group")
)?.querySelector('[role="combobox"]') ?? null;
`);
assert.ok(extensionPicker, "Extension picker was not visible");
await app.session.click(extensionPicker);
await app.clickText(extension.name, { roles: ["option"] });
await app.clickTextIn('[role="dialog"]', "Save", { roles: ["button"] });
await app.waitFor(
async () => {
groups = await app.invoke("list_extension_groups");
group = groups.find((item) => item.name === "Visible Extension Group");
return group?.extension_ids.includes(extension.id);
},
{ description: "uploaded extension added to visible extension group" },
);
return {
extension,
group,
};
}
async function createProfileThroughUi(app, groupName) {
await app.clickSelector('[aria-label="Profiles"]');
await app.clickText(groupName, { exact: false, roles: ["button"] });
await app.clickText("New", { roles: ["button"] });
await app.waitFor(
async () => {
const text = await app.bodyText();
return (
text.includes("Create New Profile") ||
text.includes("Create New Chromium Profile")
);
},
{ description: "profile creation dialog" },
);
if (!(await app.visibleTextIncludes("Create New Chromium Profile"))) {
await app.clickText("Chromium", { exact: false, roles: ["button"] });
await app.waitForText("Create New Chromium Profile");
}
await app.fillSelector("#profile-name", "Visible Network Profile");
await app.clickTextIn('[role="dialog"]', "Create", { roles: ["button"] });
await app.waitForText("Visible Network Profile", 60_000);
await app.waitFor(
async () =>
!(await app.execute(`
return [...document.querySelectorAll('[role="dialog"]')].some(
(dialog) =>
(dialog.innerText || "").includes("Create New Chromium Profile")
);
`)),
{ description: "profile creation dialog to unmount" },
);
const profiles = await app.invoke("list_browser_profiles");
return profiles.find((profile) => profile.name === "Visible Network Profile");
}
async function assignNetworkThroughUi(app, profileName, currentName, newName) {
const trigger = await app.execute(
`
const row = [...document.querySelectorAll("tr")].find((candidate) =>
(candidate.innerText || "").includes(arguments[0])
);
const expected = arguments[1].toLocaleLowerCase();
return [...(row?.querySelectorAll('[aria-haspopup="dialog"]') ?? [])].find(
(trigger) => (trigger.innerText || trigger.textContent || "")
.toLocaleLowerCase()
.includes(expected)
) ?? null;
`,
[profileName, currentName],
);
assert.ok(trigger, `Network selector for ${profileName} was not visible`);
await app.session.click(trigger);
await app.clickText(newName, { exact: false, roles: ["option"] });
await app.waitFor(
() =>
app.execute(
`
return ![...document.querySelectorAll('[data-slot="popover-content"]')]
.some((content) => (content.innerText || "").includes(arguments[0]));
`,
[newName],
),
{ description: `${newName} network picker to unmount` },
);
}
async function assignExtensionGroupThroughUi(
app,
profileName,
currentName,
newName,
) {
const trigger = await app.execute(
`
const row = [...document.querySelectorAll("tr")].find((candidate) =>
(candidate.innerText || "").includes(arguments[0])
);
return [...(row?.querySelectorAll("button") ?? [])].find(
(button) => (button.innerText || button.textContent || "")
.trim()
.includes(arguments[1])
) ?? null;
`,
[profileName, currentName],
);
assert.ok(trigger, `Extension selector for ${profileName} was not visible`);
await app.session.click(trigger);
await app.clickText(newName, { exact: false, roles: ["option"] });
await app.waitFor(
() =>
app.execute(
`
return ![...document.querySelectorAll('[data-slot="popover-content"]')]
.some((content) => (content.innerText || "").includes(arguments[0]));
`,
[newName],
),
{ description: `${newName} extension picker to unmount` },
);
}
async function runProfile(_app, base, token, profileId, url) {
const launched = await request(`${base}/v1/profiles/${profileId}/run`, {
method: "POST",
token,
body: { url, headless: true },
});
assert.equal(launched.response.status, 200, JSON.stringify(launched.value));
const cdp = await CdpClient.connect(launched.value.remote_debugging_port);
return { launched: launched.value, cdp };
}
async function stopProfile(app, base, token, profileId, cdp) {
cdp.close();
const stopped = await request(`${base}/v1/profiles/${profileId}/kill`, {
method: "POST",
token,
});
assert.equal(stopped.response.status, 204);
await app.waitFor(
async () => {
const profile = (await app.invoke("list_browser_profiles")).find(
(item) => item.id === profileId,
);
return !profile?.process_id;
},
{ timeoutMs: 20_000, description: "network profile process cleanup" },
);
}
async function assertProxyWorkerLogsRedacted(app, settings) {
const files = (await readdir(path.join(app.root, "tmp"))).filter(
(file) => file.startsWith("donut-proxy-") && file.endsWith(".log"),
);
assert.ok(files.length > 0, "No proxy worker diagnostic logs were created");
const contents = (
await Promise.all(
files.map((file) => readFile(path.join(app.root, "tmp", file), "utf8")),
)
).join("\n");
for (const item of settings) {
if (!item.username) continue;
const rawAuth = `${item.username}:${item.password ?? ""}@`;
const encodedAuth = `${encodeURIComponent(item.username)}:${encodeURIComponent(item.password ?? "")}@`;
assert.equal(
contents.includes(rawAuth) || contents.includes(encodedAuth),
false,
"Proxy worker logs exposed upstream credentials",
);
}
}
function wireGuardTargetWasReached() {
const container = process.env.DONUT_E2E_WIREGUARD_CONTAINER;
assert.ok(container, "WireGuard fixture container name is required");
return (
spawnSync(
"docker",
[
"exec",
container,
"grep",
"-q",
"GET /donut-e2e-wireguard ",
"/tmp/donut-e2e-target-requests",
],
{ stdio: "ignore", timeout: 2_000 },
).status === 0
);
}
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,
"HTTP",
);
const socksSettings = proxySettings(
process.env.RESIDENTIAL_PROXY_URL_ONE_SOCKS,
"SOCKS",
);
const realWireGuardConfig = process.env.DONUT_E2E_WIREGUARD_CONFIG_BASE64
? Buffer.from(
process.env.DONUT_E2E_WIREGUARD_CONFIG_BASE64,
"base64",
).toString("utf8")
: null;
const app = appFromEnvironment("network-visible-ui", {
wayfernTermsAccepted: false,
});
let apiPort;
let activeCdp;
let activeVpnId;
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();
}
assert.equal(
await app.visibleTextIncludes("Welcome to Donut Browser"),
false,
"completed test sessions must not leave the Welcome dialog over the UI",
);
const group = await createGroupThroughUi(app);
assert.ok(group);
await app.capture("01-profile-group-created");
const httpProxy = await createProxyThroughUi(app, httpSettings);
assert.ok(httpProxy);
assert.equal(
httpProxy.proxy_settings.proxy_type === httpSettings.proxy_type &&
httpProxy.proxy_settings.host === httpSettings.host &&
httpProxy.proxy_settings.port === httpSettings.port &&
httpProxy.proxy_settings.username === httpSettings.username &&
httpProxy.proxy_settings.password === httpSettings.password,
true,
"The HTTP proxy created through the UI did not preserve its settings",
);
const vpn = await createVpnThroughUi(
app,
realWireGuardConfig ?? wireGuardFixture(),
);
assert.ok(vpn);
activeVpnId = vpn.id;
await app.capture("02-proxy-and-vpn-created");
const extensionEntities = await createExtensionsThroughUi(app);
assert.ok(extensionEntities.extension);
assert.ok(extensionEntities.group);
await app.capture("03-extension-and-group-created");
const profile = await createProfileThroughUi(app, group.name);
assert.ok(profile);
assert.equal(profile.version, prepared.version);
assert.equal(profile.group_id, group.id);
await assignExtensionGroupThroughUi(
app,
profile.name,
"Default",
extensionEntities.group.name,
);
await app.waitFor(
async () =>
(await app.invoke("list_browser_profiles")).find(
(item) => item.id === profile.id,
)?.extension_group_id === extensionEntities.group.id,
{ description: "extension group assignment persisted" },
);
await app.capture("04-profile-created");
const socksProxy = await app.invoke("create_stored_proxy", {
name: "Residential SOCKS5",
proxySettings: socksSettings,
});
const [httpCheck, socksCheck] = await Promise.all([
app.invoke("check_proxy_validity", {
proxyId: httpProxy.id,
proxySettings: null,
}),
app.invoke("check_proxy_validity", {
proxyId: socksProxy.id,
proxySettings: null,
}),
]);
assert.equal(httpCheck.is_valid, true);
assert.equal(socksCheck.is_valid, true);
assert.ok(isIP(httpCheck.ip));
assert.ok(isIP(socksCheck.ip));
await assignNetworkThroughUi(
app,
profile.name,
"Not selected",
httpProxy.name,
);
await app.waitFor(
async () =>
(await app.invoke("list_browser_profiles")).find(
(item) => item.id === profile.id,
)?.proxy_id === httpProxy.id,
{ description: "HTTP proxy assignment persisted" },
);
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 proxied = await runProfile(
app,
base,
saved.api_token,
profile.id,
"https://api.ipify.org/",
);
activeCdp = proxied.cdp;
const browserExitIp = await activeCdp.waitFor(
`(() => {
const value = document.body?.innerText?.trim() ?? "";
return /^[0-9a-f:.]+$/i.test(value) ? value : false;
})()`,
{ timeoutMs: 30_000, description: "Wayfern residential proxy exit IP" },
);
assert.ok(isIP(browserExitIp));
await stopProfile(app, base, saved.api_token, profile.id, activeCdp);
activeCdp = null;
await assertProxyWorkerLogsRedacted(app, [httpSettings, socksSettings]);
await assignNetworkThroughUi(app, profile.name, httpProxy.name, vpn.name);
await app.waitFor(
async () =>
(await app.invoke("list_browser_profiles")).find(
(item) => item.id === profile.id,
)?.vpn_id === vpn.id,
{ description: "WireGuard assignment persisted" },
);
await app.capture("05-proxy-and-vpn-assigned");
if (realWireGuardConfig) {
const tunneled = await runProfile(
app,
base,
saved.api_token,
profile.id,
process.env.DONUT_E2E_WIREGUARD_TARGET_URL,
);
activeCdp = tunneled.cdp;
await app.waitFor(wireGuardTargetWasReached, {
timeoutMs: 30_000,
description: "Wayfern GET through local WireGuard peer",
});
await stopProfile(app, base, saved.api_token, profile.id, activeCdp);
activeCdp = null;
}
} catch (error) {
await app.capture("failure");
throw error;
} finally {
activeCdp?.close();
if (app.session) {
if (apiPort) await app.invoke("stop_api_server").catch(() => {});
for (const profile of await app
.invoke("list_browser_profiles")
.catch(() => [])) {
if (profile.process_id) {
await app.invoke("kill_browser_profile", { profile }).catch(() => {});
}
}
if (activeVpnId) {
await app
.invoke("disconnect_vpn", { vpnId: activeVpnId })
.catch(() => {});
}
}
await app.close();
}
});
+60 -53
View File
@@ -5,59 +5,66 @@ import test from "node:test";
import { appFromEnvironment, withApp } from "../lib/app.mjs";
test("fresh app renders, completes onboarding, persists settings, and never touches real app roots", async () => {
await withApp("smoke-fresh", async (app) => {
assert.equal(typeof (await app.session.title()), "string");
assert.match(await app.bodyText(), /New/);
await app.waitForText("No profiles yet");
await withApp(
"smoke-fresh",
async (app) => {
assert.equal(typeof (await app.session.title()), "string");
assert.match(await app.bodyText(), /New/);
await app.waitForText("No profiles yet");
const initial = await app.invoke("get_app_settings");
assert.equal(typeof initial.onboarding_completed, "boolean");
await app.invoke("complete_onboarding");
assert.equal(await app.invoke("get_onboarding_completed"), true);
await app.invoke("dismiss_window_resize_warning");
assert.equal(await app.invoke("get_window_resize_warning_dismissed"), true);
const initial = await app.invoke("get_app_settings");
assert.equal(typeof initial.onboarding_completed, "boolean");
await app.invoke("complete_onboarding");
assert.equal(await app.invoke("get_onboarding_completed"), true);
await app.invoke("dismiss_window_resize_warning");
assert.equal(
await app.invoke("get_window_resize_warning_dismissed"),
true,
);
const saved = await app.invoke("save_app_settings", {
settings: {
...initial,
theme: "dark",
language: "en",
onboarding_completed: true,
disable_auto_updates: true,
},
});
assert.equal(saved.theme, "dark");
assert.equal(saved.language, "en");
const saved = await app.invoke("save_app_settings", {
settings: {
...initial,
theme: "dark",
language: "en",
onboarding_completed: true,
disable_auto_updates: true,
},
});
assert.equal(saved.theme, "dark");
assert.equal(saved.language, "en");
await app.invoke("save_table_sorting_settings", {
sorting: { column: "browser", direction: "desc" },
});
assert.deepEqual(await app.invoke("get_table_sorting_settings"), {
column: "browser",
direction: "desc",
});
assert.ok((await app.invoke("get_system_language")).length >= 2);
const system = await app.invoke("get_system_info");
assert.ok(system && typeof system === "object");
assert.equal(typeof (await app.invoke("read_log_files")), "string");
await app.invoke("save_table_sorting_settings", {
sorting: { column: "browser", direction: "desc" },
});
assert.deepEqual(await app.invoke("get_table_sorting_settings"), {
column: "browser",
direction: "desc",
});
assert.ok((await app.invoke("get_system_language")).length >= 2);
const system = await app.invoke("get_system_info");
assert.ok(system && typeof system === "object");
assert.equal(typeof (await app.invoke("read_log_files")), "string");
await app.restart();
const afterRestart = await app.invoke("get_app_settings");
assert.equal(afterRestart.theme, "dark");
assert.equal(afterRestart.language, "en");
assert.equal(afterRestart.onboarding_completed, true);
await app.restart();
const afterRestart = await app.invoke("get_app_settings");
assert.equal(afterRestart.theme, "dark");
assert.equal(afterRestart.language, "en");
assert.equal(afterRestart.onboarding_completed, true);
const settingsFile = path.join(
app.dataRoot,
"data",
"settings",
"app_settings.json",
);
await access(settingsFile);
const persisted = JSON.parse(await readFile(settingsFile, "utf8"));
assert.equal(persisted.api_token, null);
assert.equal(persisted.mcp_token, null);
});
const settingsFile = path.join(
app.dataRoot,
"data",
"settings",
"app_settings.json",
);
await access(settingsFile);
const persisted = JSON.parse(await readFile(settingsFile, "utf8"));
assert.equal(persisted.api_token, null);
assert.equal(persisted.mcp_token, null);
},
{ onboardingCompleted: false },
);
});
test("two isolated sessions run concurrently and do not share frontend or backend state", async () => {
@@ -89,15 +96,15 @@ test("two isolated sessions run concurrently and do not share frontend or backen
test("keyboard command palette and major navigation surfaces are operable through native WebDriver", async () => {
await withApp("smoke-ui", async (app) => {
await app.invoke("complete_onboarding");
const modifier =
process.platform === "darwin" ? { meta: true } : { ctrl: true };
await app.pressShortcut({ key: "k", ...modifier });
await app.waitFor(
() =>
app.execute(
async () => {
await app.pressShortcut({ key: "k", ...modifier });
return app.execute(
`return Boolean(document.querySelector("[cmdk-input][placeholder='Type a command or search...']"));`,
),
);
},
{ description: "open command palette" },
);
+336 -4
View File
@@ -2,15 +2,236 @@ import assert from "node:assert/strict";
import test from "node:test";
import { withApp } from "../lib/app.mjs";
const THEME_VARIABLES = [
"--background",
"--foreground",
"--card",
"--card-foreground",
"--popover",
"--popover-foreground",
"--primary",
"--primary-foreground",
"--secondary",
"--secondary-foreground",
"--muted",
"--muted-foreground",
"--accent",
"--accent-foreground",
"--destructive",
"--destructive-foreground",
"--success",
"--success-foreground",
"--warning",
"--warning-foreground",
"--border",
"--chart-1",
"--chart-2",
"--chart-3",
"--chart-4",
"--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",
};
async function dismissSurface(app) {
await app.pressShortcut({ key: "Escape" });
await new Promise((resolve) => setTimeout(resolve, 100));
}
async function themeSnapshot(app) {
return app.execute(
`
const root = document.documentElement;
const rootStyle = getComputedStyle(root);
const bodyStyle = getComputedStyle(document.body);
const variables = arguments[0];
return {
mode: root.classList.contains("light")
? "light"
: root.classList.contains("dark")
? "dark"
: "unset",
inline: Object.fromEntries(
variables.map((key) => [key, root.style.getPropertyValue(key).trim()])
),
resolved: Object.fromEntries(
variables.map((key) => [key, rootStyle.getPropertyValue(key).trim()])
),
bodyBackground: bodyStyle.backgroundColor,
bodyForeground: bodyStyle.color,
};
`,
[THEME_VARIABLES],
);
}
async function waitForTheme(app, predicate, description) {
return app.waitFor(
async () => {
const snapshot = await themeSnapshot(app);
return predicate(snapshot) ? snapshot : false;
},
{ description },
);
}
function themeVariablesEqual(actual, expected) {
return THEME_VARIABLES.every(
(key) => actual[key]?.toLowerCase() === expected[key]?.toLowerCase(),
);
}
async function chooseSelectOption(app, triggerSelector, option) {
await app.clickSelector(triggerSelector);
await app.clickText(option, { roles: ["option"] });
}
async function saveSettings(app) {
await app.clickText("Save Settings", { roles: ["button"] });
await app.waitFor(
() =>
app.execute(`return document.querySelector("#theme-select") === null;`),
{ description: "Settings to close after saving" },
);
}
async function assertThemeAcrossNavigation(app, expected) {
for (const surface of ["Network", "Extensions", "Profiles"]) {
await app.clickSelector(`[aria-label="${surface}"]`);
await app.waitFor(
async () =>
JSON.stringify(await themeSnapshot(app)) === JSON.stringify(expected),
{ description: `theme to remain unchanged on ${surface}` },
);
}
}
async function dragBackgroundColorPicker(app) {
await app.clickSelector('[aria-label="Background"]');
const drag = await app.waitFor(
() =>
app.execute(`
const popover = document.querySelector('[data-slot="popover-content"]');
const selection = [...(popover?.querySelectorAll("div") ?? [])].find(
(node) => node.style.background.includes("linear-gradient")
);
if (!selection) return null;
const rect = selection.getBoundingClientRect();
const points = [];
for (const yf of [0.2, 0.4, 0.6, 0.8]) {
for (const xf of [0.2, 0.4, 0.6, 0.8]) {
const point = {
x: Math.round(rect.left + rect.width * xf),
y: Math.round(rect.top + rect.height * yf),
};
const hit = document.elementFromPoint(point.x, point.y);
if (hit === selection || selection.contains(hit)) points.push(point);
}
}
return points.length >= 2
? { start: points[0], end: points[points.length - 1] }
: null;
`),
{ description: "two pointer-interactive background color picker points" },
);
await app.execute(`
window.__donutE2eThemePointerEvents = [];
for (const type of ["pointermove", "pointerdown", "pointerup"]) {
window.addEventListener(type, (event) => {
window.__donutE2eThemePointerEvents.push({
type,
x: event.clientX,
y: event.clientY,
buttons: event.buttons,
target: event.target?.className ?? event.target?.tagName ?? "",
});
}, true);
}
`);
await app.session.command("POST", "/actions", {
actions: [
{
type: "pointer",
id: "theme-color-pointer",
actions: [
{
type: "pointerMove",
x: drag.start.x,
y: drag.start.y,
origin: "viewport",
},
{ type: "pointerDown", button: 0 },
{ type: "pause", duration: 150 },
{
type: "pointerMove",
x: drag.end.x,
y: drag.end.y,
duration: 100,
origin: "viewport",
},
{ type: "pointerUp", button: 0 },
],
},
],
});
const pointerEvents = await app.execute(
`return window.__donutE2eThemePointerEvents ?? [];`,
);
assert.deepEqual(
pointerEvents.map((event) => event.type),
["pointermove", "pointerdown", "pointermove", "pointerup"],
);
assert.match(pointerEvents[1].target, /cursor-pointer/);
assert.match(pointerEvents[2].target, /cursor-pointer/);
assert.equal(pointerEvents[2].buttons, 1);
await app.waitFor(
() =>
app.execute(
`return document.querySelector("#theme-preset-select")?.textContent?.includes("Your Own") === true;`,
),
{
description: `customized theme to be marked as Your Own after ${JSON.stringify(pointerEvents)}`,
},
);
await app.clickSelector('[aria-label="Background"]');
await app.waitFor(
() =>
app.execute(
`return document.querySelector('[data-slot="popover-content"]') === null;`,
),
{ description: "color picker to close" },
);
}
test("all primary navigation buttons and sub-page tabs render and remain interactive", async () => {
await withApp("ui-navigation", async (app) => {
await app.invoke("complete_onboarding");
await app.restart();
const surfaces = [
["Settings", /General|Appearance|Sync/i],
["Network", /Proxies|VPNs|DNS/i],
@@ -55,8 +276,6 @@ test("all primary navigation buttons and sub-page tabs render and remain interac
test("settings tabs, command palette filtering, and responsive layout survive resize", async () => {
await withApp("ui-settings-responsive", async (app) => {
await app.invoke("complete_onboarding");
await app.restart();
await app.clickSelector('[aria-label="Settings"]');
await app.waitForText("Appearance");
for (const tab of ["Appearance", "Sync", "Encryption"]) {
@@ -115,3 +334,116 @@ test("settings tabs, command palette filtering, and responsive layout survive re
);
});
});
test("predefined theme remains rendered across navigation and restart", async () => {
await withApp("ui-theme-predefined", async (app) => {
await app.clickSelector('[aria-label="Settings"]');
await app.waitForText("Appearance");
await chooseSelectOption(app, "#theme-select", "Light");
await saveSettings(app);
const persisted = await app.invoke("get_app_settings");
assert.equal(persisted.theme, "light");
const selected = await waitForTheme(
app,
(snapshot) =>
snapshot.mode === "light" &&
Object.values(snapshot.inline).every((value) => value === ""),
"predefined light theme to render without custom variables",
);
assert.notEqual(selected.bodyBackground, "");
assert.notEqual(selected.bodyForeground, "");
await assertThemeAcrossNavigation(app, selected);
await app.restart();
assert.equal((await app.invoke("get_app_settings")).theme, "light");
await app.waitFor(
async () =>
JSON.stringify(await themeSnapshot(app)) === JSON.stringify(selected),
{ description: "predefined light theme after restart" },
);
});
});
test("preset and manually customized themes survive navigation and restart", async () => {
await withApp("ui-theme-custom", async (app) => {
await app.clickSelector('[aria-label="Settings"]');
await app.waitForText("Appearance");
await chooseSelectOption(app, "#theme-select", "Custom");
await chooseSelectOption(app, "#theme-preset-select", "Dracula");
await saveSettings(app);
const presetSettings = await app.invoke("get_app_settings");
assert.equal(presetSettings.theme, "custom");
assert.deepEqual(presetSettings.custom_theme, DRACULA_THEME);
const preset = await waitForTheme(
app,
(snapshot) =>
snapshot.mode === "dark" &&
themeVariablesEqual(snapshot.inline, DRACULA_THEME) &&
themeVariablesEqual(snapshot.resolved, DRACULA_THEME),
"Dracula preset variables to render",
);
await assertThemeAcrossNavigation(app, preset);
await app.restart();
assert.deepEqual(
(await app.invoke("get_app_settings")).custom_theme,
DRACULA_THEME,
);
await app.waitFor(
async () =>
JSON.stringify(await themeSnapshot(app)) === JSON.stringify(preset),
{ description: "Dracula preset after restart" },
);
await app.clickSelector('[aria-label="Settings"]');
await app.waitForText("Appearance");
assert.equal(
await app.execute(
`return document.querySelector("#theme-select")?.textContent?.trim();`,
),
"Custom",
);
assert.equal(
await app.execute(
`return document.querySelector("#theme-preset-select")?.textContent?.trim();`,
),
"Dracula",
);
await dragBackgroundColorPicker(app);
await saveSettings(app);
const customizedSettings = await app.invoke("get_app_settings");
assert.equal(customizedSettings.theme, "custom");
assert.notEqual(
customizedSettings.custom_theme["--background"].toLowerCase(),
DRACULA_THEME["--background"],
);
assert.deepEqual(
Object.keys(customizedSettings.custom_theme).sort(),
[...THEME_VARIABLES].sort(),
);
const customized = await waitForTheme(
app,
(snapshot) =>
snapshot.mode === "dark" &&
themeVariablesEqual(snapshot.inline, customizedSettings.custom_theme) &&
themeVariablesEqual(snapshot.resolved, customizedSettings.custom_theme),
"manually customized variables to render",
);
assert.notEqual(customized.bodyBackground, preset.bodyBackground);
await assertThemeAcrossNavigation(app, customized);
await app.restart();
assert.deepEqual(
(await app.invoke("get_app_settings")).custom_theme,
customizedSettings.custom_theme,
);
await app.waitFor(
async () =>
JSON.stringify(await themeSnapshot(app)) === JSON.stringify(customized),
{ description: "manually customized theme after restart" },
);
});
});