mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-24 20:02:58 +02:00
feat: xray support
This commit is contained in:
+56
-27
@@ -159,6 +159,10 @@ export class AppSession {
|
||||
});
|
||||
}
|
||||
if (this.seedVersionCache) {
|
||||
const seededVersion =
|
||||
typeof this.seedVersionCache === "string"
|
||||
? this.seedVersionCache
|
||||
: "150.0.7871.100";
|
||||
const versionCache = path.join(
|
||||
this.root,
|
||||
"donut",
|
||||
@@ -170,7 +174,7 @@ export class AppSession {
|
||||
await writeFile(
|
||||
versionCache,
|
||||
`${JSON.stringify({
|
||||
releases: [{ version: "150.0.7871.100", date: "2026-07-01" }],
|
||||
releases: [{ version: seededVersion, date: "2026-07-01" }],
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
})}\n`,
|
||||
{ flag: "wx" },
|
||||
@@ -228,7 +232,7 @@ export class AppSession {
|
||||
return this.session.execute(script, args);
|
||||
}
|
||||
|
||||
async invoke(command, args = {}) {
|
||||
async invoke(command, args = {}, timeoutMs = 330_000) {
|
||||
assert.ok(this.session, `${this.name} is not started`);
|
||||
const result = await this.session.executeAsync(
|
||||
`
|
||||
@@ -243,6 +247,7 @@ export class AppSession {
|
||||
}));
|
||||
`,
|
||||
[command, args],
|
||||
timeoutMs,
|
||||
);
|
||||
if (!result?.ok) {
|
||||
throw new Error(
|
||||
@@ -314,6 +319,27 @@ export class AppSession {
|
||||
});
|
||||
}
|
||||
|
||||
async clickElement(element, description = "element") {
|
||||
await this.waitFor(
|
||||
() =>
|
||||
this.execute(
|
||||
`
|
||||
const node = arguments[0];
|
||||
if (!(node instanceof Element) || !node.isConnected) return false;
|
||||
node.scrollIntoView({ block: "center", inline: "center" });
|
||||
const rect = node.getBoundingClientRect();
|
||||
const x = Math.floor(rect.left + rect.width / 2);
|
||||
const y = Math.floor(rect.top + rect.height / 2);
|
||||
const hit = document.elementFromPoint(x, y);
|
||||
return Boolean(hit && (hit === node || node.contains(hit)));
|
||||
`,
|
||||
[element],
|
||||
),
|
||||
{ description: `pointer-interactable ${description}` },
|
||||
);
|
||||
await this.session.click(element);
|
||||
}
|
||||
|
||||
async clickText(
|
||||
text,
|
||||
{ exact = true, roles = ["button", "tab", "menuitem", "link"] } = {},
|
||||
@@ -342,7 +368,7 @@ export class AppSession {
|
||||
element,
|
||||
`No visible interactive element matched ${JSON.stringify(text)}`,
|
||||
);
|
||||
await this.session.click(element);
|
||||
await this.clickElement(element, JSON.stringify(text));
|
||||
}
|
||||
|
||||
async clickTextIn(
|
||||
@@ -380,7 +406,10 @@ export class AppSession {
|
||||
element,
|
||||
`No visible interactive element inside ${containerSelector} matched ${JSON.stringify(text)}`,
|
||||
);
|
||||
await this.session.click(element);
|
||||
await this.clickElement(
|
||||
element,
|
||||
`${JSON.stringify(text)} inside ${containerSelector}`,
|
||||
);
|
||||
}
|
||||
|
||||
async clickSelector(selector) {
|
||||
@@ -399,7 +428,7 @@ export class AppSession {
|
||||
),
|
||||
{ description: `visible selector ${selector}` },
|
||||
);
|
||||
await this.session.click(element);
|
||||
await this.clickElement(element, selector);
|
||||
}
|
||||
|
||||
async fillSelector(selector, value) {
|
||||
@@ -421,28 +450,28 @@ export class AppSession {
|
||||
alt = false,
|
||||
shift = false,
|
||||
}) {
|
||||
await this.execute(
|
||||
`
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", {
|
||||
key: arguments[0],
|
||||
code: arguments[1],
|
||||
metaKey: arguments[2],
|
||||
ctrlKey: arguments[3],
|
||||
altKey: arguments[4],
|
||||
shiftKey: arguments[5],
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}));
|
||||
`,
|
||||
[
|
||||
key,
|
||||
key.length === 1 ? `Key${key.toUpperCase()}` : key,
|
||||
meta,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
],
|
||||
);
|
||||
const modifiers = [
|
||||
...(meta ? ["\uE03D"] : []),
|
||||
...(ctrl ? ["\uE009"] : []),
|
||||
...(alt ? ["\uE00A"] : []),
|
||||
...(shift ? ["\uE008"] : []),
|
||||
];
|
||||
const value = key === "Escape" ? "\uE00C" : key;
|
||||
const actions = [
|
||||
...modifiers.map((modifier) => ({ type: "keyDown", value: modifier })),
|
||||
{ type: "keyDown", value },
|
||||
{ type: "keyUp", value },
|
||||
...modifiers
|
||||
.toReversed()
|
||||
.map((modifier) => ({ type: "keyUp", value: modifier })),
|
||||
];
|
||||
try {
|
||||
await this.session.command("POST", "/actions", {
|
||||
actions: [{ type: "key", id: "keyboard", actions }],
|
||||
});
|
||||
} finally {
|
||||
await this.session.command("DELETE", "/actions");
|
||||
}
|
||||
}
|
||||
|
||||
async capture(label) {
|
||||
|
||||
+59
-5
@@ -1,7 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { chmod, copyFile, cp, mkdir, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
chmod,
|
||||
copyFile,
|
||||
cp,
|
||||
mkdir,
|
||||
rename,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -69,6 +77,42 @@ async function cloneAppBundle(source, destination) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cacheDownloadedWayfern(app, projectRoot, version) {
|
||||
if (process.env.DONUT_E2E_WAYFERN_PATH) return;
|
||||
const destination = defaultWayfernPath(projectRoot);
|
||||
if (existsSync(destination)) return;
|
||||
|
||||
const installDir = path.join(
|
||||
app.dataRoot,
|
||||
"data",
|
||||
"binaries",
|
||||
"wayfern",
|
||||
version,
|
||||
);
|
||||
const source =
|
||||
process.platform === "darwin"
|
||||
? path.join(installDir, "Wayfern.app")
|
||||
: path.join(
|
||||
installDir,
|
||||
process.platform === "win32" ? "wayfern.exe" : "wayfern",
|
||||
);
|
||||
const staging = `${destination}.tmp-${process.pid}`;
|
||||
await rm(staging, { recursive: true, force: true });
|
||||
try {
|
||||
if (process.platform === "darwin") {
|
||||
await cloneAppBundle(source, staging);
|
||||
} else {
|
||||
await mkdir(path.dirname(staging), { recursive: true });
|
||||
await copyFile(source, staging);
|
||||
if (process.platform !== "win32") await chmod(staging, 0o755);
|
||||
}
|
||||
await rename(staging, destination);
|
||||
} catch (error) {
|
||||
await rm(staging, { recursive: true, force: true });
|
||||
if (!existsSync(destination)) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function seedWayfern(dataRoot, wayfern) {
|
||||
const installDir = path.join(
|
||||
dataRoot,
|
||||
@@ -130,10 +174,20 @@ export async function prepareWayfern(app, projectRoot) {
|
||||
"No Wayfern build is published for this platform",
|
||||
);
|
||||
const version = current.versions[0];
|
||||
await app.invoke("download_browser", {
|
||||
browserStr: "wayfern",
|
||||
version,
|
||||
});
|
||||
await app.session.setTimeouts({ script: 600_000 });
|
||||
try {
|
||||
await app.invoke(
|
||||
"download_browser",
|
||||
{
|
||||
browserStr: "wayfern",
|
||||
version,
|
||||
},
|
||||
620_000,
|
||||
);
|
||||
} finally {
|
||||
await app.session.setTimeouts();
|
||||
}
|
||||
await cacheDownloadedWayfern(app, projectRoot, version);
|
||||
return { version, source: "published download" };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user