diff --git a/e2e/updates.spec.ts b/e2e/updates.spec.ts index bf004fcb0..8d17e7280 100644 --- a/e2e/updates.spec.ts +++ b/e2e/updates.spec.ts @@ -1,4 +1,40 @@ -import { expect, test } from "./fixtures"; +import { expect, stubDesktop, test } from "./fixtures"; + +for (const windowKind of ["project", "welcome", "lightEdit"]) { + test(`manual update answers stay visible in ${windowKind} with saved Notifications layout and logging off`, async ({ + workbench: page, + }) => { + await stubDesktop(page); + await page.evaluate(async (kind) => { + const projectPath = "/src/platform/project.ts"; + const layoutPath = "/src/workbench/toolwindows/store.ts"; + const updatesPath = "/src/features/app/updates.tsx"; + const { useProject } = await import(/* @vite-ignore */ projectPath); + const { useToolWindowLayout } = await import(/* @vite-ignore */ layoutPath); + const { checkForUpdates } = await import(/* @vite-ignore */ updatesPath); + const settings = useProject.getState().settings; + useProject.setState({ + ...(kind === "project" ? {} : { project: null, lightEdit: kind === "lightEdit" }), + settings: { + ...settings, + notifications: { balloons: false, groups: { updates: { display: "none", log: false } } }, + }, + }); + useToolWindowLayout.setState({ + visible: { ...useToolWindowLayout.getState().visible, right: "notifications" }, + maximized: null, + }); + window.__ipc.handlers.app_check_update = () => ({ + enabled: true, + update: { version: "99.0.0", currentVersion: "0.1.40", notes: null, date: null }, + }); + window.__ipc.handlers.app_update_announcement = () => true; + await checkForUpdates(); + }, windowKind); + await expect(page.getByRole("status").filter({ hasText: "OpenIDEA 99.0.0 is available" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Install and Restart", exact: true })).toBeVisible(); + }); +} test("Updates shows check status and keeps manual checks available when automatic checks are off", async ({ workbench: page, diff --git a/src/features/app/__tests__/updates.test.ts b/src/features/app/__tests__/updates.test.ts index 5c9b83943..562f27b8b 100644 --- a/src/features/app/__tests__/updates.test.ts +++ b/src/features/app/__tests__/updates.test.ts @@ -1,4 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createElement } from "react"; +import { act, render, screen } from "@testing-library/react"; import type { Settings, UpdateCheck, UpdateInfo, UpdateProgress } from "@/bindings"; import type { NotificationService } from "@/workbench/contributions"; @@ -191,6 +193,84 @@ describe("Install and Restart", () => { }); describe("Check for Updates", () => { + describe.each(["welcome", "lightEdit", "project"])("visible answers in %s windows", (windowKind) => { + describe.each(["logged", "unlogged", "maximized"])("%s notifications", (mode) => { + it.each(["available", "none", "error"])( + "shows %s with a saved Notifications layout and popups off", + async (outcome) => { + const { notificationServiceImpl, useNotifications, dismissBalloon } = + await import("@/workbench/notificationStore"); + const { Balloons, NotificationsToolWindow } = await import("@/workbench/Notifications"); + const { registerToolWindow } = await import("@/workbench/contributions"); + const { useToolWindowLayout } = await import("@/workbench/toolwindows/store"); + const previousLayout = useToolWindowLayout.getState(); + const dispose = registerToolWindow({ + id: "notifications", + title: "Notifications", + icon: "bell", + anchor: "right", + component: NotificationsToolWindow, + }); + useProject.setState({ + project: windowKind === "project" ? ({ root: "/p", name: "p" } as never) : null, + lightEdit: windowKind === "lightEdit", + settings: { + notifications: { balloons: false, groups: { updates: { display: "none", log: mode !== "unlogged" } } }, + } as unknown as Settings, + }); + useToolWindowLayout.setState({ + visible: { ...previousLayout.visible, right: "notifications" }, + maximized: mode === "maximized" ? "left" : null, + }); + useNotifications.setState({ items: [] }); + provideNotificationService(notificationServiceImpl); + const view = render(createElement(Balloons)); + const list = + windowKind === "project" + ? render(createElement("div", { hidden: mode === "maximized" }, createElement(NotificationsToolWindow))) + : null; + try { + // A dismissed answer with the same key must not hide a later manual result. + const { notify } = await import("@/workbench/notificationStore"); + act(() => + dismissBalloon(notify({ level: "info", title: "Old update", key: UPDATE_KEY, userInitiated: true })), + ); + await act(async () => { + const done = checkForUpdates(); + check.resolve( + outcome === "error" + ? { status: "error", error: { kind: "unavailable", message: "offline" } } + : ok({ + enabled: true, + update: + outcome === "available" + ? { version: "0.1.22", currentVersion: "0.1.21", notes: null, date: null } + : null, + }), + ); + await done; + }); + const title = + outcome === "error" + ? "Update check failed" + : outcome === "available" + ? "OpenIDEA 0.1.22 is available" + : "OpenIDEA 0.1.21 is up to date"; + const answer = screen.getByRole(outcome === "error" ? "alert" : "status"); + expect(answer).toBeVisible(); + expect(answer).toHaveTextContent(title); + } finally { + view.unmount(); + list?.unmount(); + dispose(); + useNotifications.setState({ items: [] }); + useToolWindowLayout.setState(previousLayout); + } + }, + ); + }); + }); + it("shows a background task while checking and joins a running check", async () => { const first = checkForUpdates(); void checkForUpdates(); diff --git a/src/workbench/Notifications.tsx b/src/workbench/Notifications.tsx index 61003883f..8258be787 100644 --- a/src/workbench/Notifications.tsx +++ b/src/workbench/Notifications.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import { useTxTemplate, VerbatimTextContext } from "@/platform/i18n"; +import { useProject } from "@/platform/project"; import { EmptyState, IconButton, Toast, T } from "@/ui"; import { balloonTimeout, @@ -12,7 +13,7 @@ import { useNotifications, type IdeNotification, } from "./notificationStore"; -import { useToolWindowLayout } from "./toolwindows/store"; +import { useEffectiveVisible, useToolWindows } from "./toolwindows/ToolWindows"; import styles from "./Notifications.module.css"; const MAX_BALLOONS = 3; @@ -101,15 +102,15 @@ function Balloon({ n }: { n: IdeNotification }) { ); } -/** Bottom-right balloon stack (newest at the bottom); hidden while the Notifications tool window is open. */ +/** Bottom-right balloon stack (newest at the bottom); listed entries defer to the visible tool window. */ export function Balloons() { const items = useNotifications((s) => s.items); const listOpen = useListVisible(); const balloons = items - .filter((n) => n.balloon) + .filter((n) => n.balloon && (!listOpen || !n.logged)) .slice(0, MAX_BALLOONS) .reverse(); - if (listOpen || !balloons.length) return null; + if (!balloons.length) return null; return (