From 304e3b3a2df7c7b8aeb09f3f9c1f58ae5ec2ca8c Mon Sep 17 00:00:00 2001 From: Kang Date: Sun, 4 Oct 2026 08:54:01 +0800 Subject: [PATCH] feat(windowManager): enhance logging and window management for web and desktop runtimes; conditionally register lifecycle flush and handle child window creation based on runtime --- src/lib/backend/platform/webviewWindow.ts | 6 +- src/lib/backend/platform/window.ts | 1 + src/lib/backend/web.test.tsx | 95 +++++++++++++++++++++++ src/lib/logger.ts | 4 +- src/lib/windowManager.ts | 32 ++++---- 5 files changed, 122 insertions(+), 16 deletions(-) diff --git a/src/lib/backend/platform/webviewWindow.ts b/src/lib/backend/platform/webviewWindow.ts index 684cdff6..7f1af141 100644 --- a/src/lib/backend/platform/webviewWindow.ts +++ b/src/lib/backend/platform/webviewWindow.ts @@ -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"; } diff --git a/src/lib/backend/platform/window.ts b/src/lib/backend/platform/window.ts index d50feebf..6d7cd74f 100644 --- a/src/lib/backend/platform/window.ts +++ b/src/lib/backend/platform/window.ts @@ -50,6 +50,7 @@ const browserWindow = { show: noop, hide: noop, setEnabled: noop, + setFocusable: noop, setAlwaysOnTop: noop, setShadow: noop, setDecorations: noop, diff --git a/src/lib/backend/web.test.tsx b/src/lib/backend/web.test.tsx index 7903fae1..b1be330a 100644 --- a/src/lib/backend/web.test.tsx +++ b/src/lib/backend/web.test.tsx @@ -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" }), { diff --git a/src/lib/logger.ts b/src/lib/logger.ts index afecdd2f..746df60c 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -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(); diff --git a/src/lib/windowManager.ts b/src/lib/windowManager.ts index 6007dad8..3fe5372a 100644 --- a/src/lib/windowManager.ts +++ b/src/lib/windowManager.ts @@ -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);