mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
fix(ui): replace the new-layout banner with a one-time dialog
The banner arrived ~0.7s after launch and pushed the content area down. Upgrading users now get a dialog over the page instead, listing what moved and linking to this version's release notes. Confirmation is stored as newLayoutNoticeConfirmed in settings.json, following the existing *_confirmed flags. The welcome dialog sets it too, so fresh installs never see it.
This commit is contained in:
1 parent
3dc46460fe
commit
d4e47d7197
12 files changed
+263
-205
No files matched your search
@@ -418,6 +418,9 @@ pub struct AppSettings {
|
||||
/// User has confirmed the first-run welcome notice
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub first_run_notice_confirmed: Option<bool>,
|
||||
/// User has confirmed the one-time "new layout" dialog shown to upgrading users
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub new_layout_notice_confirmed: Option<bool>,
|
||||
/// User has confirmed the common config first-run notice
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub common_config_confirmed: Option<bool>,
|
||||
@@ -553,6 +556,7 @@ impl Default for AppSettings {
|
||||
unify_codex_migrate_existing: None,
|
||||
failover_confirmed: None,
|
||||
first_run_notice_confirmed: None,
|
||||
new_layout_notice_confirmed: None,
|
||||
common_config_confirmed: None,
|
||||
language: None,
|
||||
visible_apps: None,
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ import {
|
||||
type View,
|
||||
} from "@/lib/navigation";
|
||||
import { Sidebar } from "@/components/shell/Sidebar";
|
||||
import { NewLayoutNotice } from "@/components/shell/NewLayoutNotice";
|
||||
import { NewLayoutDialog } from "@/components/shell/NewLayoutDialog";
|
||||
import {
|
||||
AppPageHeader,
|
||||
WindowControlsContext,
|
||||
@@ -1491,7 +1491,6 @@ function App() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NewLayoutNotice />
|
||||
<div
|
||||
key={
|
||||
currentView === "settings"
|
||||
@@ -1581,6 +1580,7 @@ function App() {
|
||||
|
||||
<DeepLinkImportDialog />
|
||||
<FirstRunNoticeDialog />
|
||||
<NewLayoutDialog />
|
||||
</WindowControlsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,12 @@ export function FirstRunNoticeDialog() {
|
||||
if (!settings) return;
|
||||
try {
|
||||
const { webdavSync: _, ...rest } = settings;
|
||||
await settingsApi.save({ ...rest, firstRunNoticeConfirmed: true });
|
||||
// 新装用户看的就是新界面,「界面改版了」弹窗一并记成已看过
|
||||
await settingsApi.save({
|
||||
...rest,
|
||||
firstRunNoticeConfirmed: true,
|
||||
newLayoutNoticeConfirmed: true,
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
} catch (error) {
|
||||
console.error("Failed to save firstRunNoticeConfirmed:", error);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { APP_IDS } from "@/config/appConfig";
|
||||
import { providersApi, settingsApi } from "@/lib/api";
|
||||
import { useSettingsQuery } from "@/lib/query";
|
||||
|
||||
const RELEASES_URL = "https://github.com/farion1231/cc-switch/releases";
|
||||
|
||||
/** 任一应用的数据库里已经有供应商(找到一家就停) */
|
||||
async function hasAnyProvider(): Promise<boolean> {
|
||||
for (const app of APP_IDS) {
|
||||
try {
|
||||
const providers = await providersApi.getAll(app);
|
||||
if (Object.keys(providers).length > 0) return true;
|
||||
} catch {
|
||||
// 某个应用读失败不影响其他应用
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「界面改版了」一次性弹窗:只给升级上来的老用户看,确认后写 newLayoutNoticeConfirmed。
|
||||
* - 必须已经确认过首次启动的欢迎弹窗。全新安装时后端会先导入当前配置、再预置官方
|
||||
* 供应商,单看「有没有供应商」分不出新老用户;新用户点欢迎弹窗的「我知道了」时
|
||||
* 会把这个字段一起写成 true,以后也不会再看到这里。
|
||||
* - 数据库里已经有供应商(任一应用)。
|
||||
* 弹窗盖在页面上,不占布局,晚一点出来也不会推动内容区。
|
||||
*/
|
||||
export function NewLayoutDialog() {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: settings } = useSettingsQuery();
|
||||
// 点了就先关,不等保存回来
|
||||
const [closed, setClosed] = useState(false);
|
||||
|
||||
const eligible =
|
||||
settings != null &&
|
||||
settings.firstRunNoticeConfirmed === true &&
|
||||
settings.newLayoutNoticeConfirmed !== true;
|
||||
|
||||
const { data: hasProviders } = useQuery({
|
||||
queryKey: ["new-layout-notice", "has-providers"],
|
||||
queryFn: hasAnyProvider,
|
||||
enabled: eligible && !closed,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const isOpen = eligible && !closed && hasProviders === true;
|
||||
|
||||
const handleAcknowledge = async () => {
|
||||
setClosed(true);
|
||||
if (!settings) return;
|
||||
try {
|
||||
const { webdavSync: _, ...rest } = settings;
|
||||
await settingsApi.save({ ...rest, newLayoutNoticeConfirmed: true });
|
||||
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
} catch (error) {
|
||||
console.error("Failed to save newLayoutNoticeConfirmed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewReleaseNotes = async () => {
|
||||
try {
|
||||
const version = await getVersion();
|
||||
await settingsApi.openExternal(
|
||||
version ? `${RELEASES_URL}/tag/v${version}` : RELEASES_URL,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to open release notes:", error);
|
||||
}
|
||||
await handleAcknowledge();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) void handleAcknowledge();
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-md" zIndex="top">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-fg-1" />
|
||||
{t("newLayoutNotice.title")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="px-6 py-5">
|
||||
<DialogDescription className="whitespace-pre-line leading-relaxed">
|
||||
{t("newLayoutNotice.body")}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleViewReleaseNotes}>
|
||||
{t("newLayoutNotice.viewReleaseNotes")}
|
||||
</Button>
|
||||
<Button onClick={handleAcknowledge}>
|
||||
{t("newLayoutNotice.confirm")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Notice } from "@/components/ui/notice";
|
||||
import { APP_IDS } from "@/config/appConfig";
|
||||
import { providersApi } from "@/lib/api";
|
||||
import { useSettingsQuery } from "@/lib/query";
|
||||
|
||||
export const NEW_LAYOUT_NOTICE_KEY = "cc-switch-new-layout-notice-dismissed";
|
||||
|
||||
function readDismissed(): boolean {
|
||||
try {
|
||||
return window.localStorage.getItem(NEW_LAYOUT_NOTICE_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function writeDismissed() {
|
||||
try {
|
||||
window.localStorage.setItem(NEW_LAYOUT_NOTICE_KEY, "1");
|
||||
} catch {
|
||||
// 存不下就只在这次会话里隐藏,下次启动可能再出现一次,不影响使用
|
||||
}
|
||||
}
|
||||
|
||||
/** 任一应用的数据库里已经有供应商(找到一家就停) */
|
||||
async function hasAnyProvider(): Promise<boolean> {
|
||||
for (const app of APP_IDS) {
|
||||
try {
|
||||
const providers = await providersApi.getAll(app);
|
||||
if (Object.keys(providers).length > 0) return true;
|
||||
} catch {
|
||||
// 某个应用读失败不影响其他应用
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「界面改版了」一次性提示(方案 B6 coach.newLayout、B7 阶段 1):主内容区顶部的
|
||||
* 中性通知条,关掉后永不再出现。只给老用户看:
|
||||
* - 数据库里已经有供应商(任一应用);
|
||||
* - 并且之前已经确认过首次启动的欢迎弹窗。全新安装时后端启动会先导入当前配置、
|
||||
* 再预置官方供应商,单看「有没有供应商」分不出新老用户;新装用户这次启动会看到
|
||||
* 欢迎弹窗(firstRunNoticeConfirmed 还不是 true),这时直接记成「已看过」,
|
||||
* 以后也不再给他看「改版」。
|
||||
*/
|
||||
export function NewLayoutNotice() {
|
||||
const { t } = useTranslation();
|
||||
const [dismissed, setDismissed] = useState(readDismissed);
|
||||
const { data: settings } = useSettingsQuery();
|
||||
const [returningUser, setReturningUser] = useState<boolean | null>(null);
|
||||
|
||||
// 只在第一次拿到设置时判断一次:欢迎弹窗确认之后字段会变 true,不能因此再弹
|
||||
useEffect(() => {
|
||||
if (dismissed || returningUser !== null || !settings) return;
|
||||
const returning = settings.firstRunNoticeConfirmed === true;
|
||||
setReturningUser(returning);
|
||||
if (!returning) {
|
||||
writeDismissed();
|
||||
setDismissed(true);
|
||||
}
|
||||
}, [dismissed, returningUser, settings]);
|
||||
|
||||
const { data: hasProviders } = useQuery({
|
||||
queryKey: ["new-layout-notice", "has-providers"],
|
||||
queryFn: hasAnyProvider,
|
||||
enabled: !dismissed && returningUser === true,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
if (dismissed || returningUser !== true || hasProviders !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shrink-0 px-6 pt-3">
|
||||
<Notice
|
||||
tone="neutral"
|
||||
title={t("coach.newLayout")}
|
||||
onDismiss={() => {
|
||||
writeDismissed();
|
||||
setDismissed(true);
|
||||
}}
|
||||
dismissLabel={t("common.close")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4476,8 +4476,11 @@
|
||||
"headerUpdatesClear": "Show all Skills",
|
||||
"updateAllCount": "Update all ({{count}})"
|
||||
},
|
||||
"coach": {
|
||||
"newLayout": "New layout: modes sit at the top of each app page; Usage and Accounts are in the sidebar."
|
||||
"newLayoutNotice": {
|
||||
"title": "CC Switch has a new look",
|
||||
"body": "This version redesigns the interface, so a few things have moved:\n• Modes now sit at the top of each app page\n• Usage and Accounts are in the sidebar\nSee the release notes for everything that changed.",
|
||||
"viewReleaseNotes": "View release notes",
|
||||
"confirm": "Got it"
|
||||
},
|
||||
"authCenter": {
|
||||
"helpTitle": "What is Accounts?",
|
||||
|
||||
@@ -4476,8 +4476,11 @@
|
||||
"headerUpdatesClear": "すべての Skill を表示",
|
||||
"updateAllCount": "すべて更新({{count}} 件)"
|
||||
},
|
||||
"coach": {
|
||||
"newLayout": "画面が新しくなりました:モードは各アプリのページ上部に、使用量とアカウントはサイドバーにあります。"
|
||||
"newLayoutNotice": {
|
||||
"title": "画面が新しくなりました",
|
||||
"body": "このバージョンでは画面を一新し、いくつかの機能の場所が変わりました:\n• モードは各アプリのページ上部に移動しました\n• 使用量とアカウントはサイドバーにあります\n変更点の詳細はリリースノートをご覧ください。",
|
||||
"viewReleaseNotes": "リリースノートを見る",
|
||||
"confirm": "OK"
|
||||
},
|
||||
"authCenter": {
|
||||
"helpTitle": "アカウントとは",
|
||||
|
||||
@@ -4477,8 +4477,11 @@
|
||||
"headerUpdatesClear": "顯示全部 Skill",
|
||||
"updateAllCount": "全部更新({{count}} 個)"
|
||||
},
|
||||
"coach": {
|
||||
"newLayout": "介面改版了:模式移到了每個應用頁的頂部,用量統計和授權中心在左側邊欄。"
|
||||
"newLayoutNotice": {
|
||||
"title": "介面改版了",
|
||||
"body": "CC Switch 這一版重新設計了介面,常用功能的位置有變化:\n• 模式移到了每個應用頁的頂部\n• 用量統計和授權中心移到了左側邊欄\n想了解全部變化,可以看看這一版的更新說明。",
|
||||
"viewReleaseNotes": "查看更新說明",
|
||||
"confirm": "我知道了"
|
||||
},
|
||||
"authCenter": {
|
||||
"helpTitle": "授權中心是什麼",
|
||||
|
||||
@@ -4476,8 +4476,11 @@
|
||||
"headerUpdatesClear": "显示全部 Skill",
|
||||
"updateAllCount": "全部更新({{count}} 个)"
|
||||
},
|
||||
"coach": {
|
||||
"newLayout": "界面改版了:模式移到了每个应用页的顶部,用量统计和授权中心在左侧边栏。"
|
||||
"newLayoutNotice": {
|
||||
"title": "界面改版了",
|
||||
"body": "CC Switch 这一版重新设计了界面,常用功能的位置有变化:\n• 模式移到了每个应用页的顶部\n• 用量统计和授权中心移到了左侧边栏\n想了解全部变化,可以看看这一版的更新说明。",
|
||||
"viewReleaseNotes": "查看更新说明",
|
||||
"confirm": "我知道了"
|
||||
},
|
||||
"authCenter": {
|
||||
"helpTitle": "授权中心是什么",
|
||||
|
||||
@@ -407,6 +407,8 @@ export interface Settings {
|
||||
failoverConfirmed?: boolean;
|
||||
// User has confirmed the first-run welcome notice
|
||||
firstRunNoticeConfirmed?: boolean;
|
||||
// User has confirmed the one-time "new layout" dialog shown to upgrading users
|
||||
newLayoutNoticeConfirmed?: boolean;
|
||||
// User has confirmed the auto-sync traffic warning
|
||||
autoSyncConfirmed?: boolean;
|
||||
// User has confirmed the common config first-run notice
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getAll: vi.fn(),
|
||||
save: vi.fn(),
|
||||
openExternal: vi.fn(),
|
||||
settings: undefined as
|
||||
| { firstRunNoticeConfirmed?: boolean; newLayoutNoticeConfirmed?: boolean }
|
||||
| undefined,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api", () => ({
|
||||
providersApi: { getAll: mocks.getAll },
|
||||
settingsApi: { save: mocks.save, openExternal: mocks.openExternal },
|
||||
}));
|
||||
vi.mock("@/lib/query", () => ({
|
||||
useSettingsQuery: () => ({ data: mocks.settings }),
|
||||
}));
|
||||
vi.mock("@tauri-apps/api/app", () => ({
|
||||
getVersion: async () => "4.0.0",
|
||||
}));
|
||||
|
||||
import { NewLayoutDialog } from "@/components/shell/NewLayoutDialog";
|
||||
|
||||
function renderDialog() {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<NewLayoutDialog />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("NewLayoutDialog", () => {
|
||||
beforeEach(() => {
|
||||
mocks.settings = { firstRunNoticeConfirmed: true };
|
||||
mocks.save.mockReset().mockResolvedValue(true);
|
||||
mocks.openExternal.mockReset().mockResolvedValue(undefined);
|
||||
mocks.getAll
|
||||
.mockReset()
|
||||
.mockImplementation(async (app: string) =>
|
||||
app === "codex" ? { p1: { id: "p1" } } : {},
|
||||
);
|
||||
});
|
||||
|
||||
it("shows to returning users and records the confirmation", async () => {
|
||||
renderDialog();
|
||||
expect(
|
||||
await screen.findByText("newLayoutNotice.title"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "newLayoutNotice.confirm" }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByText("newLayoutNotice.title"),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(mocks.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ newLayoutNoticeConfirmed: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("opens this version's release notes and closes", async () => {
|
||||
renderDialog();
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", {
|
||||
name: "newLayoutNotice.viewReleaseNotes",
|
||||
}),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(mocks.openExternal).toHaveBeenCalledWith(
|
||||
"https://github.com/farion1231/cc-switch/releases/tag/v4.0.0",
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(mocks.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ newLayoutNoticeConfirmed: true }),
|
||||
),
|
||||
);
|
||||
expect(screen.queryByText("newLayoutNotice.title")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("stays closed once confirmed", async () => {
|
||||
mocks.settings = {
|
||||
firstRunNoticeConfirmed: true,
|
||||
newLayoutNoticeConfirmed: true,
|
||||
};
|
||||
renderDialog();
|
||||
expect(mocks.getAll).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("newLayoutNotice.title")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("stays closed when no app has providers", async () => {
|
||||
mocks.getAll.mockResolvedValue({});
|
||||
renderDialog();
|
||||
await waitFor(() => expect(mocks.getAll).toHaveBeenCalledTimes(10));
|
||||
expect(screen.queryByText("newLayoutNotice.title")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("never shows to a fresh install that is seeing the welcome dialog", async () => {
|
||||
mocks.settings = { firstRunNoticeConfirmed: undefined };
|
||||
renderDialog();
|
||||
expect(mocks.getAll).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("newLayoutNotice.title")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getAll: vi.fn(),
|
||||
settings: undefined as { firstRunNoticeConfirmed?: boolean } | undefined,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api", () => ({
|
||||
providersApi: { getAll: mocks.getAll },
|
||||
}));
|
||||
vi.mock("@/lib/query", () => ({
|
||||
useSettingsQuery: () => ({ data: mocks.settings }),
|
||||
}));
|
||||
|
||||
import {
|
||||
NEW_LAYOUT_NOTICE_KEY,
|
||||
NewLayoutNotice,
|
||||
} from "@/components/shell/NewLayoutNotice";
|
||||
|
||||
function renderNotice() {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<NewLayoutNotice />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("NewLayoutNotice", () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
mocks.settings = { firstRunNoticeConfirmed: true };
|
||||
mocks.getAll
|
||||
.mockReset()
|
||||
.mockImplementation(async (app: string) =>
|
||||
app === "codex" ? { p1: { id: "p1" } } : {},
|
||||
);
|
||||
});
|
||||
|
||||
it("shows once for returning users and stays dismissed", async () => {
|
||||
const view = renderNotice();
|
||||
expect(await screen.findByText("coach.newLayout")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.close" }));
|
||||
expect(screen.queryByText("coach.newLayout")).not.toBeInTheDocument();
|
||||
expect(window.localStorage.getItem(NEW_LAYOUT_NOTICE_KEY)).toBe("1");
|
||||
|
||||
view.unmount();
|
||||
renderNotice();
|
||||
await waitFor(() => expect(mocks.getAll).toHaveBeenCalled());
|
||||
expect(screen.queryByText("coach.newLayout")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("stays hidden when no app has providers", async () => {
|
||||
mocks.getAll.mockResolvedValue({});
|
||||
renderNotice();
|
||||
await waitFor(() => expect(mocks.getAll).toHaveBeenCalledTimes(10));
|
||||
expect(screen.queryByText("coach.newLayout")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("never shows to a fresh install that is seeing the welcome dialog", async () => {
|
||||
mocks.settings = { firstRunNoticeConfirmed: undefined };
|
||||
const view = renderNotice();
|
||||
await waitFor(() =>
|
||||
expect(window.localStorage.getItem(NEW_LAYOUT_NOTICE_KEY)).toBe("1"),
|
||||
);
|
||||
expect(mocks.getAll).not.toHaveBeenCalled();
|
||||
|
||||
// 欢迎弹窗确认之后字段变成 true,也不再补弹「改版」
|
||||
mocks.settings = { firstRunNoticeConfirmed: true };
|
||||
view.rerender(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<NewLayoutNotice />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText("coach.newLayout")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("still works when localStorage throws", async () => {
|
||||
const getItem = vi
|
||||
.spyOn(Storage.prototype, "getItem")
|
||||
.mockImplementation(() => {
|
||||
throw new Error("blocked");
|
||||
});
|
||||
const setItem = vi
|
||||
.spyOn(Storage.prototype, "setItem")
|
||||
.mockImplementation(() => {
|
||||
throw new Error("blocked");
|
||||
});
|
||||
try {
|
||||
renderNotice();
|
||||
expect(await screen.findByText("coach.newLayout")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.close" }));
|
||||
expect(screen.queryByText("coach.newLayout")).not.toBeInTheDocument();
|
||||
} finally {
|
||||
getItem.mockRestore();
|
||||
setItem.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user