mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-26 19:22:09 +02:00
refactor: harden tests
This commit is contained in:
+107
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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]",
|
||||
|
||||
Reference in New Issue
Block a user