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
+107
View File
@@ -47,6 +47,8 @@ export class AppSession {
extraEnv = {},
args = [],
seedVersionCache = true,
onboardingCompleted = true,
wayfernTermsAccepted = true,
}) {
this.name = name;
this.root = root;
@@ -57,6 +59,8 @@ export class AppSession {
this.extraEnv = extraEnv;
this.args = args;
this.seedVersionCache = seedVersionCache;
this.onboardingCompleted = onboardingCompleted;
this.wayfernTermsAccepted = wayfernTermsAccepted;
this.session = null;
}
@@ -70,6 +74,69 @@ export class AppSession {
mkdir(path.join(this.root, "tmp"), { recursive: true }),
mkdir(path.join(this.root, "artifacts"), { recursive: true }),
]);
if (this.onboardingCompleted) {
const settingsFile = path.join(
this.dataRoot,
"data",
"settings",
"app_settings.json",
);
await mkdir(path.dirname(settingsFile), { recursive: true });
await writeFile(
settingsFile,
`${JSON.stringify(
{
language: "en",
onboarding_completed: true,
commercial_trial_acknowledged: true,
window_resize_warning_dismissed: true,
disable_auto_updates: true,
},
null,
2,
)}\n`,
{ flag: "wx" },
).catch((error) => {
if (error.code !== "EEXIST") {
throw error;
}
});
}
if (this.wayfernTermsAccepted) {
const termsFile =
process.platform === "darwin"
? path.join(
this.root,
"home",
"Library",
"Application Support",
"Wayfern",
"license-accepted",
)
: process.platform === "win32"
? path.join(
this.root,
"windows",
"roaming",
"Wayfern",
"license-accepted",
)
: path.join(
this.root,
"xdg",
"config",
"Wayfern",
"license-accepted",
);
await mkdir(path.dirname(termsFile), { recursive: true });
await writeFile(termsFile, `${Math.floor(Date.now() / 1000)}\n`, {
flag: "wx",
}).catch((error) => {
if (error.code !== "EEXIST") {
throw error;
}
});
}
if (this.seedVersionCache) {
const versionCache = path.join(
this.root,
@@ -257,6 +324,44 @@ export class AppSession {
await this.session.click(element);
}
async clickTextIn(
containerSelector,
text,
{ exact = true, roles = ["button", "tab", "menuitem", "link"] } = {},
) {
const element = await this.execute(
`
const containers = [...document.querySelectorAll(arguments[0])];
const wanted = arguments[1];
const exact = arguments[2];
const roles = new Set(arguments[3]);
const visible = (node) => {
const style = getComputedStyle(node);
const rect = node.getBoundingClientRect();
return style.visibility !== "hidden" && style.display !== "none" &&
rect.width > 0 && rect.height > 0;
};
for (const container of containers.reverse()) {
if (!visible(container)) continue;
const candidates = [...container.querySelectorAll("button, a, [role], [data-slot='button']")];
const match = candidates.find((node) => {
const role = node.getAttribute("role") || (node.tagName === "A" ? "link" : "button");
const label = (node.getAttribute("aria-label") || node.innerText || node.textContent || "").trim();
return roles.has(role) && visible(node) && (exact ? label === wanted : label.includes(wanted));
});
if (match) return match;
}
return null;
`,
[containerSelector, text, exact, roles],
);
assert.ok(
element,
`No visible interactive element inside ${containerSelector} matched ${JSON.stringify(text)}`,
);
await this.session.click(element);
}
async clickSelector(selector) {
const element = await this.waitFor(
() =>
@@ -366,6 +471,8 @@ export function appFromEnvironment(name, options = {}) {
extraEnv: options.extraEnv,
args: options.args,
seedVersionCache: options.seedVersionCache,
onboardingCompleted: options.onboardingCompleted,
wayfernTermsAccepted: options.wayfernTermsAccepted,
});
}
+99
View File
@@ -0,0 +1,99 @@
import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import {
redactSensitiveText,
sensitiveVariants,
} from "../../scripts/redact-sensitive-text.mjs";
const MAX_LOG_BYTES = 512 * 1024;
async function logFiles(directory, fileNamePattern = /\.(?:log|txt)$/iu) {
const entries = await readdir(directory, { withFileTypes: true }).catch(
() => [],
);
return entries
.filter((entry) => entry.isFile() && fileNamePattern.test(entry.name))
.map((entry) => path.join(directory, entry.name))
.sort();
}
async function diagnosticSources(runRoot) {
const sources = await logFiles(path.join(runRoot, "logs"));
const sessions = await readdir(path.join(runRoot, "sessions"), {
withFileTypes: true,
}).catch(() => []);
for (const session of sessions.filter((entry) => entry.isDirectory())) {
const root = path.join(runRoot, "sessions", session.name);
sources.push(...(await logFiles(path.join(root, "donut", "logs"))));
sources.push(
...(await logFiles(path.join(root, "tmp"), /^donut-proxy-.*\.log$/iu)),
);
}
return sources;
}
export async function assertSafeDiagnostics(
diagnosticsRoot,
sensitiveValues = [],
) {
const entries = await readdir(diagnosticsRoot, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile() || !/\.(?:json|log)$/iu.test(entry.name)) {
throw new Error(`Unsafe diagnostics entry: ${entry.name}`);
}
const content = await readFile(
path.join(diagnosticsRoot, entry.name),
"utf8",
);
for (const value of sensitiveVariants(sensitiveValues)) {
if (content.includes(value)) {
throw new Error(
`Sensitive value survived diagnostics redaction in ${entry.name}`,
);
}
}
}
}
export async function createSafeDiagnostics(
runRoot,
{ suite, failed, sensitiveValues = [] },
) {
const diagnosticsRoot = path.join(runRoot, "diagnostics");
await mkdir(diagnosticsRoot, { recursive: true, mode: 0o700 });
await chmod(diagnosticsRoot, 0o700);
const sources = await diagnosticSources(runRoot);
for (const [index, source] of sources.entries()) {
const content = await readFile(source, "utf8").catch(() => "");
const tail = content.slice(-MAX_LOG_BYTES);
const destination = path.join(
diagnosticsRoot,
`${String(index + 1).padStart(3, "0")}.log`,
);
await writeFile(
destination,
redactSensitiveText(tail, { sensitiveValues }),
{ mode: 0o600 },
);
await chmod(destination, 0o600);
}
const summaryPath = path.join(diagnosticsRoot, "summary.json");
await writeFile(
summaryPath,
`${JSON.stringify(
{
suite,
status: failed ? "failed" : "passed",
sanitized_log_files: sources.length,
},
null,
2,
)}\n`,
{ mode: 0o600 },
);
await chmod(summaryPath, 0o600);
await assertSafeDiagnostics(diagnosticsRoot, sensitiveValues);
return diagnosticsRoot;
}
+44 -23
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { chmod, copyFile, mkdir, symlink, writeFile } from "node:fs/promises";
import { chmod, copyFile, cp, mkdir, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -11,17 +11,13 @@ export function defaultWayfernPath(projectRoot) {
if (process.env.DONUT_E2E_WAYFERN_PATH) {
return path.resolve(process.env.DONUT_E2E_WAYFERN_PATH);
}
const sibling = path.resolve(
projectRoot,
"../wayfern-test/test_extracted_app",
);
if (process.platform === "darwin") {
return path.join(sibling, "Wayfern.app");
}
if (process.platform === "win32") {
return path.join(sibling, "Wayfern.exe");
}
return path.join(sibling, "wayfern");
const fixtureRoot = path.join(projectRoot, ".cache", "e2e-wayfern-fixture");
return process.platform === "darwin"
? path.join(fixtureRoot, "Wayfern.app")
: path.join(
fixtureRoot,
process.platform === "win32" ? "Wayfern.exe" : "wayfern",
);
}
export function wayfernExecutable(bundlePath) {
@@ -60,18 +56,16 @@ export function inspectWayfern(bundlePath) {
return { bundlePath, executable, version: match[1], output };
}
async function linkOrCopy(source, destination) {
async function cloneAppBundle(source, destination) {
await mkdir(path.dirname(destination), { recursive: true });
try {
await symlink(
source,
destination,
process.platform === "win32" ? "junction" : undefined,
);
} catch (error) {
if (error.code !== "EEXIST") {
throw error;
}
execFileSync("/bin/cp", ["-cR", source, destination]);
} catch (_error) {
await cp(source, destination, {
recursive: true,
preserveTimestamps: true,
errorOnExist: true,
});
}
}
@@ -85,7 +79,10 @@ export async function seedWayfern(dataRoot, wayfern) {
);
await mkdir(installDir, { recursive: true });
if (process.platform === "darwin") {
await linkOrCopy(wayfern.bundlePath, path.join(installDir, "Wayfern.app"));
await cloneAppBundle(
wayfern.bundlePath,
path.join(installDir, "Wayfern.app"),
);
} else {
const name = process.platform === "win32" ? "wayfern.exe" : "wayfern";
const destination = path.join(installDir, name);
@@ -116,6 +113,30 @@ export async function seedWayfern(dataRoot, wayfern) {
return installDir;
}
export async function prepareWayfern(app, projectRoot) {
const localBundle = defaultWayfernPath(projectRoot);
if (existsSync(localBundle)) {
const wayfern = inspectWayfern(localBundle);
await seedWayfern(app.dataRoot, wayfern);
return { version: wayfern.version, source: "local fixture" };
}
if (!app.session) 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" };
}
export function wireGuardFixture() {
return [
"[Interface]",