feat(windowManager): enhance logging and window management for web and desktop runtimes; conditionally register lifecycle flush and handle child window creation based on runtime

This commit is contained in:
Kang committed 2026-10-04 08:54:01 +08:00
1 parent 0ce52dd085
commit 304e3b3a2d
5 files changed
+122 -16

No files matched your search

+5 -1
View File
@@ -44,7 +44,7 @@ class BrowserWindow {
position: "fixed",
inset: "0",
zIndex: "10000",
display: "flex",
display: options.visible === false ? "none" : "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(0,0,0,.5)",
@@ -96,6 +96,10 @@ class BrowserWindow {
async setFocus() {
this.frame.focus();
}
async setTitle(title: string) {
this.frame.title = title;
}
async setFocusable() {}
async isVisible() {
return this.overlay.isConnected && this.overlay.style.display !== "none";
}
+1
View File
@@ -50,6 +50,7 @@ const browserWindow = {
show: noop,
hide: noop,
setEnabled: noop,
setFocusable: noop,
setAlwaysOnTop: noop,
setShadow: noop,
setDecorations: noop,
+95
View File
@@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
vi.mock("@/i18n", () => ({ default: { t: (key: string) => key } }));
class FakeSocket {
static OPEN = 1;
static instances: FakeSocket[] = [];
@@ -149,6 +150,100 @@ describe("browser backend boundary", () => {
await child.close();
expect(document.querySelector("iframe")).toBeNull();
});
it("opens settings through the window manager, waits for readiness, reuses and closes the dialog", async () => {
vi.spyOn(window, "focus").mockImplementation(() => {});
const { openSettings } = await import("../windowManager");
const { browserEmit, browserListen } = await import("./events");
const { CHILD_WINDOW_LIFECYCLE_EVENT } =
await import("../childWindowProtocol");
const command = vi.fn();
const stop = await browserListen("settings-open-tab", command);
const opening = openSettings("appearance");
await waitFor(() => expect(document.querySelector("iframe")).toBeTruthy());
const frame = document.querySelector("iframe")!;
const url = new URL(frame.src);
expect(url.pathname).toBe("/nyaterm/index.html");
expect(url.searchParams.get("webWindowLabel")).toBe("settings");
expect(frame.parentElement?.style.display).toBe("none");
const identity = {
label: "settings",
token: url.searchParams.get("readyToken")!,
};
expect(identity.token).toBeTruthy();
await browserEmit(CHILD_WINDOW_LIFECYCLE_EVENT, {
...identity,
phase: "shell-ready",
});
const child = await opening;
expect(await child.isVisible()).toBe(true);
expect(command).not.toHaveBeenCalled();
await browserEmit(CHILD_WINDOW_LIFECYCLE_EVENT, {
...identity,
phase: "command-ready",
command: "settings-open-tab",
});
expect(command).toHaveBeenCalledWith(
expect.objectContaining({
payload: { tab: "appearance", targetWindowLabel: "main" },
}),
);
expect(await openSettings("general")).toBe(child);
expect(document.querySelectorAll("iframe")).toHaveLength(1);
await child.close();
expect(document.querySelector("iframe")).toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
stop();
});
it("removes a browser child when bootstrap fails before readiness", async () => {
vi.spyOn(window, "focus").mockImplementation(() => {});
const { openChildWindow } = await import("../windowManager");
const { browserEmit } = await import("./events");
const { CHILD_WINDOW_LIFECYCLE_EVENT } =
await import("../childWindowProtocol");
const opening = openChildWindow({
label: "new-session",
title: "New session",
url: "index.html?window=new-session&owner=main",
});
const failure = expect(opening).rejects.toThrow(
"Child window did not finish rendering",
);
await waitFor(() => expect(document.querySelector("iframe")).toBeTruthy());
const url = new URL(document.querySelector("iframe")!.src);
await browserEmit(CHILD_WINDOW_LIFECYCLE_EVENT, {
label: "new-session",
token: url.searchParams.get("readyToken"),
phase: "load-failed",
stage: "bootstrap-import",
});
await failure;
expect(document.querySelector("iframe")).toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
});
it("keeps browser logs in the console without sending unsupported persistence requests", async () => {
const info = vi.spyOn(console, "info").mockImplementation(() => {});
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const { logger } = await import("../logger");
for (let index = 0; index < 60; index += 1) {
logger.info({
domain: "app.lifecycle",
event: "test.entry",
message: "Browser log",
});
}
logger.error({
domain: "ui.error",
event: "test.error",
message: "Browser error",
});
await logger.flush();
window.dispatchEvent(new Event("pagehide"));
expect(info).toHaveBeenCalledTimes(60);
expect(error).toHaveBeenCalledTimes(1);
expect(fetchMock).not.toHaveBeenCalled();
info.mockRestore();
error.mockRestore();
});
it("shows the login gate and mounts the existing UI only after authentication", async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ error: "Sign in required" }), {
+3 -1
View File
@@ -1,4 +1,5 @@
import { invoke as tauriInvoke } from "@/lib/backend/api";
import { runtime } from "@/lib/backend/runtime";
import type { DiagnosticsLogLevel } from "@/types/global";
type LogLevel = "debug" | "info" | "warn" | "error";
@@ -339,6 +340,7 @@ function emit(level: LogLevel, payload: LogPayload): void {
const entry = normalizePayload(level, payload);
writeConsole(entry);
if (runtime === "web") return;
queue.push(entry);
enforceQueueLimit();
@@ -373,4 +375,4 @@ export const logger = {
createRequestId,
};
registerLifecycleFlush();
if (runtime === "desktop") registerLifecycleFlush();
+18 -14
View File
@@ -1,5 +1,6 @@
import { emit, listen } from "@/lib/backend/api";
import { WebviewWindow } from "@/lib/backend/platform/webviewWindow";
import { runtime } from "@/lib/backend/runtime";
import {
availableMonitors,
getCurrentWindow,
@@ -740,20 +741,23 @@ async function openChildWindowInternal(opts: ChildWindowOptions) {
);
const listenerReadyMs = Math.round(performance.now() - startedAt);
try {
await invoke("open_child_window", {
options: {
label: opts.label,
title: opts.title,
url: appendChildWindowReadyToken(opts.url, readyToken),
kind,
parentLabel: opts.parentLabel ?? ownerMainWindowLabel,
width: opts.width ?? 720,
height: opts.height ?? 560,
resizable: opts.resizable ?? true,
alwaysOnTop: needsAlwaysOnTop(opts.label),
stateKey: opts.stateKey,
},
});
const options = {
label: opts.label,
title: opts.title,
url: appendChildWindowReadyToken(opts.url, readyToken),
kind,
parentLabel: opts.parentLabel ?? ownerMainWindowLabel,
width: opts.width ?? 720,
height: opts.height ?? 560,
resizable: opts.resizable ?? true,
alwaysOnTop: needsAlwaysOnTop(opts.label),
stateKey: opts.stateKey,
};
if (runtime === "web") {
new WebviewWindow(opts.label, { ...options, visible: false });
} else {
await invoke("open_child_window", { options });
}
const invokeMs = Math.round(performance.now() - startedAt);
const win = await WebviewWindow.getByLabel(opts.label);