Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
516 lines
22 KiB
TypeScript
516 lines
22 KiB
TypeScript
import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
||
import { createServer, type RequestListener, type Server } from "node:http";
|
||
import { tmpdir } from "node:os";
|
||
import path from "node:path";
|
||
import { type ElectronApplication, _electron as electron, expect, test } from "@playwright/test";
|
||
|
||
const APP_MARKER = "Local Rakazo stack ready";
|
||
const IMAGE_TAG = "v9.9.9";
|
||
const STACK_PROBE_PATH = "/.well-known/rakazo-desktop-stack";
|
||
const STACK_TOKEN_HEADER = "x-rakazo-desktop-stack-token";
|
||
const COMPOSE_DIR = path.resolve(import.meta.dirname, "..", "..", "..", "infra", "compose");
|
||
|
||
type FakeDockerMode =
|
||
| "ok"
|
||
| "daemon-down"
|
||
| "pull-fails"
|
||
| "pool-exhausted"
|
||
| "port-conflict"
|
||
| "ports-exhausted";
|
||
|
||
let server: Server;
|
||
let serverUrl: string;
|
||
let userData: string;
|
||
let app: ElectronApplication | undefined;
|
||
|
||
// The fake docker is a POSIX shell script; Windows desktop e2e does not run in CI.
|
||
test.skip(process.platform === "win32", "fake docker needs a POSIX shell");
|
||
|
||
test.beforeAll(async () => {
|
||
server = createServer(async (request, response) => {
|
||
if (request.url === STACK_PROBE_PATH && request.method === "GET") {
|
||
const expected = await readFile(path.join(userData, "stack", ".desktop-stack-token"), "utf8")
|
||
.then((value) => value.trim())
|
||
.catch(() => null);
|
||
if (expected === null || request.headers[STACK_TOKEN_HEADER] !== expected) {
|
||
response.writeHead(404).end("Not found");
|
||
return;
|
||
}
|
||
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
||
response.end(JSON.stringify({ ok: true, imageTag: IMAGE_TAG }));
|
||
return;
|
||
}
|
||
if (request.url === "/api/desktop-settings/rpc/integrationSetup/get") {
|
||
const expected = await readFile(path.join(userData, "stack", ".desktop-stack-token"), "utf8");
|
||
if (
|
||
request.headers["x-rakazo-local-settings-token"] !== expected.trim() ||
|
||
request.headers.cookie
|
||
) {
|
||
response.writeHead(401).end();
|
||
return;
|
||
}
|
||
response.writeHead(200, { "content-type": "application/json" });
|
||
response.end(JSON.stringify({ json: { canConfigure: true } }));
|
||
return;
|
||
}
|
||
if (request.url === "/rpc/health" && request.method === "POST") {
|
||
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
||
response.end(JSON.stringify({ json: { ok: true, version: "0.1.0" } }));
|
||
return;
|
||
}
|
||
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
||
response.end(
|
||
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Rakazo</title></head><body><main>${APP_MARKER}</main></body></html>`,
|
||
);
|
||
});
|
||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
const address = server.address();
|
||
if (address === null || typeof address === "string") throw new Error("stub server has no port");
|
||
serverUrl = `http://127.0.0.1:${address.port}`;
|
||
});
|
||
|
||
test.afterAll(async () => {
|
||
await new Promise<void>((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
});
|
||
|
||
test.beforeEach(async () => {
|
||
// The fake docker logs $PWD, which is the resolved path (macOS /var is a symlink).
|
||
userData = await realpath(await mkdtemp(path.join(tmpdir(), "rakazo-desktop-stack-")));
|
||
});
|
||
|
||
test.afterEach(async () => {
|
||
await app?.close();
|
||
app = undefined;
|
||
await rm(userData, { recursive: true, force: true });
|
||
});
|
||
|
||
function fakeDockerLog() {
|
||
return path.join(userData, "fake-docker.log");
|
||
}
|
||
|
||
/**
|
||
* Stands in for the docker CLI: records `cwd | RAKAZO_IMAGE_TAG | argv` for every call and
|
||
* answers the handful of commands the app issues. The log path and mode are baked into the
|
||
* script because the app passes docker an allowlisted environment.
|
||
*/
|
||
async function writeFakeDocker(mode: FakeDockerMode) {
|
||
const script = path.join(userData, "fake-docker.sh");
|
||
let up = ' up) echo "Container rakazo-web-1 Started"; exit 0 ;;';
|
||
if (mode === "pool-exhausted") {
|
||
up = ' up) echo "all predefined address pools have been fully subnetted" >&2; exit 1 ;;';
|
||
} else if (mode === "ports-exhausted") {
|
||
up = ' up) echo "port is already allocated" >&2; exit 1 ;;';
|
||
} else if (mode === "port-conflict") {
|
||
up = ` up) if [ ! -f '${path.join(userData, "port-conflict")}' ]; then touch '${path.join(userData, "port-conflict")}'; echo "port is already allocated" >&2; exit 1; fi; echo "Container rakazo-web-1 Started"; exit 0 ;;`;
|
||
}
|
||
const lines = [
|
||
"#!/bin/sh",
|
||
`printf '%s | %s | %s\\n' "$PWD" "$RAKAZO_IMAGE_TAG" "$*" >> '${fakeDockerLog()}'`,
|
||
'case "$1 $2" in',
|
||
' "compose version") echo "2.29.0"; exit 0 ;;',
|
||
' "info --format")',
|
||
mode === "daemon-down"
|
||
? ' echo "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?" >&2; exit 1 ;;'
|
||
: ' echo "27.1.1"; exit 0 ;;',
|
||
"esac",
|
||
"# compose --env-file .env -f docker-compose.images.yml --project-name rakazo-desktop <command> ...",
|
||
'case "$8" in',
|
||
" pull)",
|
||
mode === "pull-fails"
|
||
? ' echo "Error response from daemon: manifest unknown" >&2; exit 1 ;;'
|
||
: ' echo " a235d761c5d1 Downloading 59.47MB"; echo " a235d761c5d1 Downloading 412.3MB"; echo "app Pulled"; sleep 2; echo "computer Pulled"; exit 0 ;;',
|
||
up,
|
||
' logs) echo "web-1 | listening"; exit 0 ;;',
|
||
"esac",
|
||
"exit 0",
|
||
"",
|
||
];
|
||
await writeFile(script, lines.join("\n"), { encoding: "utf8", mode: 0o755 });
|
||
return script;
|
||
}
|
||
|
||
async function launch(mode: FakeDockerMode | "missing") {
|
||
const env: NodeJS.ProcessEnv = { ...process.env, RAKAZO_PERFORMANCE_USER_DATA: userData };
|
||
// A stale RAKAZO_WEB_URL from the developer's shell would bypass setup entirely.
|
||
delete env.RAKAZO_WEB_URL;
|
||
return electron.launch({
|
||
args: ["."],
|
||
cwd: path.resolve(import.meta.dirname, ".."),
|
||
env: {
|
||
...env,
|
||
RAKAZO_DOCKER_BINARY:
|
||
mode === "missing" ? "/nonexistent/docker" : await writeFakeDocker(mode),
|
||
RAKAZO_LOCAL_WEB_URL: serverUrl,
|
||
RAKAZO_IMAGE_TAG: IMAGE_TAG,
|
||
},
|
||
});
|
||
}
|
||
|
||
async function savedSetup() {
|
||
try {
|
||
return JSON.parse(await readFile(path.join(userData, "setup.json"), "utf8"));
|
||
} catch (error) {
|
||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function readLog() {
|
||
try {
|
||
return (await readFile(fakeDockerLog(), "utf8")).trim().split("\n");
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
test("This computer installs and starts the stack, then opens the app", async () => {
|
||
app = await launch("ok");
|
||
const setup = await app.firstWindow();
|
||
|
||
await expect(setup.getByRole("radio", { name: /This computer/ })).toBeChecked();
|
||
// The panel is empty until a start begins, so check the attribute rather than the box.
|
||
await expect(setup.locator("#panel-new")).toHaveJSProperty("hidden", false);
|
||
await expect(setup.locator("#stack")).toBeHidden();
|
||
await expect(setup.getByRole("button", { name: "Check connection" })).toBeHidden();
|
||
|
||
const appWindowPromise = app.waitForEvent("window");
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
await expect(setup.locator("#stack-phase")).toHaveText("Downloading Rakazo…");
|
||
await expect(setup.getByRole("button", { name: "Continue" })).toBeDisabled();
|
||
// Docker output stays behind the details toggle; the phase, the bar, and the size show by default.
|
||
await expect(setup.locator("#stack-detail")).toHaveText("412 MB downloaded");
|
||
await expect(setup.locator("#stack-progress")).toBeVisible();
|
||
await expect(setup.locator("#stack-output")).toBeHidden();
|
||
await setup.screenshot({
|
||
path: path.join(import.meta.dirname, "screenshots", "06-setup-installing.png"),
|
||
// Fast-forward the bar's width transition so the artifact shows the value, not a frame of it.
|
||
animations: "disabled",
|
||
});
|
||
await setup.getByRole("button", { name: "Technical details" }).click();
|
||
await expect(setup.locator("#stack-output")).toContainText("app Pulled");
|
||
|
||
const appWindow = await appWindowPromise;
|
||
await expect(appWindow.getByText(APP_MARKER)).toBeVisible();
|
||
await expect.poll(savedSetup).toEqual({ mode: "new", serverUrl });
|
||
|
||
const stackDir = path.join(userData, "stack");
|
||
const envFile = path.join(stackDir, ".env");
|
||
const tokenFile = path.join(stackDir, ".desktop-stack-token");
|
||
expect((await stat(envFile)).mode & 0o777).toBe(0o600);
|
||
expect((await stat(tokenFile)).mode & 0o777).toBe(0o600);
|
||
expect(await readFile(tokenFile, "utf8")).toMatch(/^[0-9a-f]{64}\n$/);
|
||
const env = await readFile(envFile, "utf8");
|
||
expect(env).toMatch(/^POSTGRES_PASSWORD=[0-9a-f]{32}$/m);
|
||
expect(env).toMatch(/^BETTER_AUTH_SECRET=[0-9a-f]{64}$/m);
|
||
expect(env).not.toContain("RAKAZO_IMAGE_TAG=");
|
||
expect(env).not.toContain("RAKAZO_COMPUTER_IMAGE_TAG=");
|
||
await expect(readFile(path.join(stackDir, "docker-compose.images.yml"), "utf8")).resolves.toBe(
|
||
await readFile(path.join(COMPOSE_DIR, "docker-compose.images.yml"), "utf8"),
|
||
);
|
||
|
||
const compose =
|
||
"compose --env-file .env -f docker-compose.images.yml --project-name rakazo-desktop";
|
||
expect(await readLog()).toEqual([
|
||
`${stackDir} | ${IMAGE_TAG} | compose version --short`,
|
||
`${stackDir} | ${IMAGE_TAG} | info --format {{.ServerVersion}}`,
|
||
`${stackDir} | ${IMAGE_TAG} | ${compose} pull`,
|
||
`${stackDir} | ${IMAGE_TAG} | ${compose} up -d --wait --wait-timeout 300`,
|
||
]);
|
||
});
|
||
|
||
test("without Docker the app explains how to get it and offers to check again", async () => {
|
||
app = await launch("missing");
|
||
const setup = await app.firstWindow();
|
||
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
await expect(setup.locator("#stack-phase")).toHaveText(
|
||
"Docker is not installed on this computer.",
|
||
);
|
||
await expect(setup.locator("#stack-phase")).toHaveAttribute("data-tone", "error");
|
||
await expect(setup.getByRole("button", { name: "Docker Desktop" })).toBeVisible();
|
||
await expect(setup.getByRole("button", { name: "OrbStack" })).toBeVisible();
|
||
await expect(setup.getByRole("button", { name: "Docker Engine" })).toBeVisible();
|
||
await expect(setup.getByRole("button", { name: "Check again" })).toBeEnabled();
|
||
await setup.screenshot({
|
||
path: path.join(import.meta.dirname, "screenshots", "07-setup-docker-missing.png"),
|
||
});
|
||
|
||
// Existing instance stays available without Docker.
|
||
await setup.getByRole("radio", { name: /Existing instance/ }).check();
|
||
await expect(setup.locator("#stack")).toBeHidden();
|
||
await expect(setup.getByRole("button", { name: "Continue" })).toBeEnabled();
|
||
expect(await savedSetup()).toBeNull();
|
||
});
|
||
|
||
test("switching to Existing instance while the stack starts keeps that choice", async () => {
|
||
app = await launch("ok");
|
||
const setup = await app.firstWindow();
|
||
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
await expect(setup.locator("#stack-phase")).toHaveText("Downloading Rakazo…");
|
||
|
||
// Fake docker sleeps during pull; leave This computer before ready so followStack must not save.
|
||
await setup.getByRole("radio", { name: /Existing instance/ }).check();
|
||
await expect(setup.locator("#panel-existing")).toBeVisible();
|
||
await expect(setup.getByRole("button", { name: "Continue" })).toBeEnabled();
|
||
await expect(setup.getByRole("button", { name: "Check connection" })).toBeVisible();
|
||
|
||
// Main process still finishes the install; setup.json must stay untouched.
|
||
await expect
|
||
.poll(async () => (await readLog()).some((line) => line.includes(" up -d")))
|
||
.toBe(true);
|
||
await new Promise((resolve) => setTimeout(resolve, 2500));
|
||
expect(await savedSetup()).toBeNull();
|
||
expect(app.windows()).toHaveLength(1);
|
||
|
||
// Back on This computer, Continue starts (or re-follows) and opens the app.
|
||
await setup.getByRole("radio", { name: /This computer/ }).check();
|
||
const appWindowPromise = app.waitForEvent("window");
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
await expect((await appWindowPromise).getByText(APP_MARKER)).toBeVisible();
|
||
await expect.poll(savedSetup).toEqual({ mode: "new", serverUrl });
|
||
});
|
||
|
||
test("a stopped Docker daemon is reported and Check again asks Docker once more", async () => {
|
||
app = await launch("daemon-down");
|
||
const setup = await app.firstWindow();
|
||
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
await expect(setup.locator("#stack-phase")).toHaveText(
|
||
"Docker is installed but not running. Start Docker, then check again.",
|
||
);
|
||
await expect(setup.getByRole("button", { name: "Docker Desktop" })).toBeVisible();
|
||
expect((await readLog()).filter((line) => line.includes("info --format"))).toHaveLength(1);
|
||
|
||
await setup.getByRole("button", { name: "Check again" }).click();
|
||
await expect(setup.getByRole("button", { name: "Check again" })).toBeEnabled();
|
||
expect((await readLog()).filter((line) => line.includes("info --format"))).toHaveLength(2);
|
||
expect((await readLog()).some((line) => line.includes(" pull"))).toBe(false);
|
||
});
|
||
|
||
test("unpublished images show the docker output and a Retry button", async () => {
|
||
app = await launch("pull-fails");
|
||
const setup = await app.firstWindow();
|
||
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
await expect(setup.locator("#stack-phase")).toHaveText(
|
||
`Images for ${IMAGE_TAG} are not published yet. Try again in a few minutes.`,
|
||
);
|
||
await expect(setup.locator("#stack-output")).toContainText("manifest unknown");
|
||
await expect(setup.getByRole("button", { name: "Retry" })).toBeEnabled();
|
||
await expect(setup.locator("#stack-docker-help")).toBeHidden();
|
||
expect((await readLog()).some((line) => line.endsWith(" up -d --wait --wait-timeout 300"))).toBe(
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("a saved local stack that is down is started again without asking", async () => {
|
||
// Nothing listens on the saved address, so the launch probe fails and the stack is restarted.
|
||
const probe = createServer();
|
||
await new Promise<void>((resolve) => probe.listen(0, "127.0.0.1", resolve));
|
||
const address = probe.address();
|
||
if (address === null || typeof address === "string") throw new Error("probe has no port");
|
||
await new Promise<void>((resolve, reject) => {
|
||
probe.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await writeFile(
|
||
path.join(userData, "setup.json"),
|
||
`${JSON.stringify({ mode: "new", serverUrl: `http://127.0.0.1:${address.port}` }, null, 2)}\n`,
|
||
{ encoding: "utf8", mode: 0o600 },
|
||
);
|
||
|
||
app = await launch("ok");
|
||
const setup = await app.firstWindow();
|
||
await expect(setup.getByRole("radio", { name: /This computer/ })).toBeChecked();
|
||
await expect(setup.locator("#stack")).toBeVisible();
|
||
await expect(setup.locator("#status")).toBeEmpty();
|
||
|
||
const appWindow = await app.waitForEvent("window");
|
||
await expect(appWindow.getByText(APP_MARKER)).toBeVisible();
|
||
await expect.poll(savedSetup).toEqual({ mode: "new", serverUrl });
|
||
expect((await readLog()).some((line) => line.includes(" pull"))).toBe(true);
|
||
});
|
||
|
||
test("a saved local stack is reused only after its private identity matches", async () => {
|
||
const stackDir = path.join(userData, "stack");
|
||
await mkdir(stackDir, { recursive: true });
|
||
await writeFile(path.join(stackDir, ".desktop-stack-token"), `${"ab".repeat(32)}\n`, {
|
||
encoding: "utf8",
|
||
mode: 0o600,
|
||
});
|
||
await writeFile(
|
||
path.join(userData, "setup.json"),
|
||
`${JSON.stringify({ mode: "new", serverUrl }, null, 2)}\n`,
|
||
{ encoding: "utf8", mode: 0o600 },
|
||
);
|
||
|
||
app = await launch("ok");
|
||
const appWindow = await app.firstWindow();
|
||
await expect(appWindow.getByText(APP_MARKER)).toBeVisible();
|
||
expect(await readLog()).toEqual([]);
|
||
});
|
||
|
||
test("a saved local target is the exact origin authenticated before reuse", async () => {
|
||
const uncheckedServer = createServer((request, response) => {
|
||
if (request.url === "/rpc/health" && request.method === "POST") {
|
||
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
||
response.end(JSON.stringify({ json: { ok: true, version: "0.1.0" } }));
|
||
return;
|
||
}
|
||
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
||
response.end("<!doctype html><html><body>Unchecked listener</body></html>");
|
||
});
|
||
await new Promise<void>((resolve) => uncheckedServer.listen(0, "127.0.0.1", resolve));
|
||
try {
|
||
const address = uncheckedServer.address();
|
||
if (address === null || typeof address === "string") {
|
||
throw new Error("unchecked server has no port");
|
||
}
|
||
const stackDir = path.join(userData, "stack");
|
||
await mkdir(stackDir, { recursive: true });
|
||
await writeFile(path.join(stackDir, ".desktop-stack-token"), `${"ab".repeat(32)}\n`, {
|
||
encoding: "utf8",
|
||
mode: 0o600,
|
||
});
|
||
await writeFile(
|
||
path.join(userData, "setup.json"),
|
||
`${JSON.stringify(
|
||
{ mode: "new", serverUrl: `http://127.0.0.1:${address.port}` },
|
||
null,
|
||
2,
|
||
)}\n`,
|
||
{ encoding: "utf8", mode: 0o600 },
|
||
);
|
||
|
||
app = await launch("ok");
|
||
const setup = await app.firstWindow();
|
||
await expect(setup.getByRole("radio", { name: /This computer/ })).toBeChecked();
|
||
await expect(setup.locator("#stack")).toBeVisible();
|
||
await expect(setup.getByText("Unchecked listener")).toHaveCount(0);
|
||
} finally {
|
||
await new Promise<void>((resolve, reject) => {
|
||
uncheckedServer.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("an existing stack .env is never rewritten", async () => {
|
||
const stackDir = path.join(userData, "stack");
|
||
await mkdir(stackDir, { recursive: true });
|
||
const sentinel = "POSTGRES_PASSWORD=keep-me\nSENTINEL=1\n";
|
||
await writeFile(path.join(stackDir, ".env"), sentinel, { encoding: "utf8", mode: 0o600 });
|
||
|
||
app = await launch("ok");
|
||
const setup = await app.firstWindow();
|
||
const appWindowPromise = app.waitForEvent("window");
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
const appWindow = await appWindowPromise;
|
||
await expect(appWindow.getByText(APP_MARKER)).toBeVisible();
|
||
|
||
await expect(readFile(path.join(stackDir, ".env"), "utf8")).resolves.toBe(sentinel);
|
||
});
|
||
|
||
test("exhausted address pools explain recovery", async () => {
|
||
app = await launch("pool-exhausted");
|
||
const setup = await app.firstWindow();
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
await expect(setup.locator("#stack-phase")).toHaveText(
|
||
"Docker has no free network address pools. Remove unused Docker networks or expand Docker’s address pools, then retry.",
|
||
);
|
||
await expect(setup.getByRole("button", { name: "Retry" })).toBeEnabled();
|
||
await setup.screenshot({
|
||
path: path.join(import.meta.dirname, "screenshots", "09-setup-address-pool-exhausted.png"),
|
||
});
|
||
});
|
||
|
||
test("a port conflict opens and saves the replacement managed origin", async () => {
|
||
app = await launch("port-conflict");
|
||
const setup = await app.firstWindow();
|
||
const appWindowPromise = app.waitForEvent("window");
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
const urlFile = path.join(userData, "stack", ".desktop-web-url");
|
||
let replacementUrl = "";
|
||
await expect
|
||
.poll(async () => {
|
||
replacementUrl = await readFile(urlFile, "utf8").catch(() => "");
|
||
return replacementUrl !== "" && replacementUrl !== serverUrl;
|
||
})
|
||
.toBe(true);
|
||
// Stand in for the web container Docker would bind to the selected port.
|
||
const replacement = createServer(server.listeners("request")[0] as RequestListener);
|
||
await new Promise<void>((resolve) =>
|
||
replacement.listen(Number(new URL(replacementUrl).port), "127.0.0.1", resolve),
|
||
);
|
||
try {
|
||
const appWindow = await appWindowPromise;
|
||
await expect(appWindow.getByText(APP_MARKER)).toBeVisible();
|
||
await expect.poll(savedSetup).toEqual({ mode: "new", serverUrl: replacementUrl });
|
||
expect((await readLog()).filter((line) => line.includes(" up -d"))).toHaveLength(2);
|
||
} finally {
|
||
replacement.closeAllConnections();
|
||
await new Promise<void>((resolve, reject) =>
|
||
replacement.close((error) => (error ? reject(error) : resolve())),
|
||
);
|
||
}
|
||
});
|
||
|
||
test("repeated port conflicts stop with a retry action", async () => {
|
||
app = await launch("ports-exhausted");
|
||
const setup = await app.firstWindow();
|
||
await setup.getByRole("button", { name: "Continue" }).click();
|
||
await expect(setup.locator("#stack-phase")).toHaveText(
|
||
"Could not bind a local port after retrying. Retry to choose another port.",
|
||
);
|
||
await expect(setup.getByRole("button", { name: "Retry" })).toBeEnabled();
|
||
expect((await readLog()).filter((line) => line.includes(" up -d"))).toHaveLength(3);
|
||
await setup.screenshot({
|
||
path: path.join(import.meta.dirname, "screenshots", "10-setup-ports-unavailable.png"),
|
||
});
|
||
});
|
||
|
||
test("the native settings menu opens an isolated logged-out settings capability", async () => {
|
||
app = await launch("ok");
|
||
const setup = await app.firstWindow();
|
||
const nextWindow = app.waitForEvent("window");
|
||
await setup.getByRole("button", { name: "Continue", exact: true }).click();
|
||
const main = await nextWindow;
|
||
await expect(main.getByText(APP_MARKER)).toBeVisible();
|
||
await expect.poll(savedSetup).toEqual({ mode: "new", serverUrl });
|
||
const denied = await main.evaluate(async () => {
|
||
try {
|
||
await window.rakazoDesktop?.localSettings?.request(
|
||
"/api/desktop-settings/rpc/integrationSetup/get",
|
||
"{}",
|
||
);
|
||
return false;
|
||
} catch {
|
||
return true;
|
||
}
|
||
});
|
||
expect(denied).toBe(true);
|
||
const settingsOpened = app.waitForEvent("window");
|
||
await app.evaluate(({ Menu }) => {
|
||
const item = Menu.getApplicationMenu()?.getMenuItemById("local-server-settings");
|
||
if (!item) throw new Error("Missing settings menu");
|
||
item.click();
|
||
});
|
||
const settings = await settingsOpened;
|
||
await expect(settings).toHaveURL(`${serverUrl}/desktop-settings`);
|
||
const result = await settings.evaluate(() =>
|
||
window.rakazoDesktop?.localSettings?.request(
|
||
"/api/desktop-settings/rpc/integrationSetup/get",
|
||
"{}",
|
||
),
|
||
);
|
||
expect(result?.status).toBe(200);
|
||
expect(JSON.parse(result!.body)).toEqual({ json: { canConfigure: true } });
|
||
await app.evaluate(({ Menu }) =>
|
||
Menu.getApplicationMenu()?.getMenuItemById("local-server-settings")?.click(),
|
||
);
|
||
expect(app.windows()).toHaveLength(2);
|
||
await settings.close();
|
||
await expect(main.getByText(APP_MARKER)).toBeVisible();
|
||
});
|