diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8b9242..923d773 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,6 +105,10 @@ jobs: with: args: --target ${{ matrix.target }} projectPath: desktop + certificatePassword: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + certificate: ${{ secrets.APPLE_CERTIFICATE }} + appleId: ${{ secrets.APPLE_ID }} + appleIdPassword: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - name: Debug - list bundle output run: | @@ -189,6 +193,12 @@ jobs: hdiutil detach aarch64-mount -quiet hdiutil detach x86_64-mount -quiet + # Re-sign the universal binary if signing identity is available + if [ -n "${{ secrets.APPLE_CERTIFICATE }}" ]; then + echo "Re-signing universal binary..." + codesign --force --deep --sign "${{ secrets.APPLE_SIGNING_IDENTITY }}" "Universal-$APP_NAME" + fi + # Create universal DMG hdiutil create -volname "PinWall" \ -srcfolder "Universal-$APP_NAME" \ diff --git a/desktop/RELEASE_QA.md b/desktop/RELEASE_QA.md index 6624fd3..fff2e06 100644 --- a/desktop/RELEASE_QA.md +++ b/desktop/RELEASE_QA.md @@ -75,8 +75,9 @@ Current local note: ## Desktop Behavior -- [ ] Clicking blank desktop/window area sends PinWall back behind normal desktop interactions. -- [ ] Floating buttons and cards remain interactive when the app is summoned. +- [ ] Default/bottom state: the main window stays on the desktop layer, and blank areas click through to desktop icons or lower windows. +- [ ] Summoned/top state: clicking a blank area is captured by PinWall and sends the main window back to the default/bottom state. +- [ ] Summoned/top state: clicking cards, floating buttons, widgets, stack panels, or modals does not send the main window back. - [ ] `Cmd+Shift+Space` toggles the main window state. - [ ] `Cmd+Shift+A` arranges cards. - [ ] `Cmd+Shift+B` opens the breathing guide. diff --git a/desktop/TEST_CASES.md b/desktop/TEST_CASES.md index d1c2e6e..bad0a43 100644 --- a/desktop/TEST_CASES.md +++ b/desktop/TEST_CASES.md @@ -9,9 +9,9 @@ | # | 测试项 | 操作步骤 | 预期结果 | 状态 | |---|--------|----------|----------|------| | 1.1 | 首次启动 | 双击 PinWall.app 启动 | 应用正常启动,主窗口最大化铺满屏幕,透明背景,无系统标题栏 | ☐ | -| 1.2 | 默认桌面底层模式 | 启动后观察主窗口行为 | 主窗口置于桌面最底层(always on bottom),鼠标可穿透空白区域点击桌面其他应用 | ☐ | +| 1.2 | 默认桌面底层模式 | 启动后观察主窗口行为 | 主窗口置于桌面最底层(always on bottom),停留在桌面上一层;点击主窗口空白区域时,鼠标事件穿透到桌面或下层应用,PinWall 不获得焦点、不拦截点击 | ☐ | | 1.3 | 系统托盘图标 | 查看 macOS 菜单栏 | 出现 PinWall 托盘图标 | ☐ | -| 1.4 | 托盘菜单 — 打开 | 点击托盘 → "打开 PinWall" | 主窗口被唤醒到前台(always on top),可交互 | ☐ | +| 1.4 | 托盘菜单 — 打开 | 点击托盘 → "打开 PinWall" | 主窗口被唤醒到前台(always on top),整窗恢复可命中,卡片和浮动按钮可交互 | ☐ | | 1.5 | 托盘菜单 — 设置 | 点击托盘 → "设置" | 设置窗口弹出,有系统标题栏 | ☐ | | 1.6 | 托盘菜单 — 退出 | 点击托盘 → "退出" | 应用完全退出 | ☐ | | 1.7 | 关闭窗口行为 | 点击主窗口的关闭按钮(如果有) | 应用退出(而非隐藏) | ☐ | @@ -22,10 +22,12 @@ | # | 测试项 | 操作步骤 | 预期结果 | 状态 | |---|--------|----------|----------|------| -| 2.1 | 唤起主窗口 | 按 `Cmd+Shift+Space` | 主窗口从底层切换到前台(可交互),可看到便签和按钮 | ☐ | -| 2.2 | 退回底层 | 再次按 `Cmd+Shift+Space` | 主窗口回到底层(鼠标穿透,不可交互) | ☐ | -| 2.3 | 空白区域点击退回 | 唤起后点击窗口空白区域(非卡片/按钮区域) | 主窗口退回底层模式 | ☐ | -| 2.4 | 重复切换稳定性 | 快速连续按 `Cmd+Shift+Space` 10 次 | 窗口状态正确切换,无崩溃或卡顿 | ☐ | +| 2.1 | 唤起主窗口 | 按 `Cmd+Shift+Space` 或托盘打开主窗口 | 主窗口从底层切换到前台(always on top),空白区域不穿透,卡片、Widget、浮动按钮、弹窗均可正常交互 | ☐ | +| 2.2 | 退回底层 | 前台状态下再次按 `Cmd+Shift+Space` | 主窗口回到底层(always on bottom),空白区域点击穿透到桌面或下层应用 | ☐ | +| 2.3 | 前台空白区域点击退回 | 主窗口被唤起到前台后,点击窗口空白区域(非卡片/按钮/Widget/弹窗区域) | 本次点击由 PinWall 接收,主窗口立即消失到桌面底层模式;不会把这次点击继续传给桌面 | ☐ | +| 2.4 | 底层空白区域点击穿透 | 主窗口处于底层模式时,点击没有卡片、按钮或 Widget 的空白区域 | 点击穿透到桌面或下层应用,可选中桌面文件、打开右键菜单或操作下层窗口;PinWall 保持底层模式 | ☐ | +| 2.5 | 交互元素不触发退回 | 主窗口被唤起到前台后,点击卡片、浮动按钮、Widget、卡片堆叠或弹窗 | 对应 PinWall 元素正常响应,主窗口保持前台,不退回底层 | ☐ | +| 2.6 | 重复切换稳定性 | 快速连续按 `Cmd+Shift+Space` 10 次 | 窗口状态正确切换,无崩溃或卡顿;最终状态下的空白点击行为符合前台/底层规则 | ☐ | --- @@ -141,7 +143,7 @@ | 10.1 | 按钮可见性 | 在唤起模式下观察右下角 | 显示 "+" 和 "⚙️" 两个浮动按钮 | ☐ | | 10.2 | 悬停效果 | 鼠标悬停在按钮上 | 按钮放大 1.15 倍,阴影增强 | ☐ | | 10.3 | 点击效果 | 点击按钮 | 按钮缩小 0.95 倍(按压反馈) | ☐ | -| 10.4 | 底层模式隐藏 | 退回桌面底层模式 | 浮动按钮不可点击(鼠标穿透) | ☐ | +| 10.4 | 底层模式穿透 | 退回桌面底层模式后点击浮动按钮所在区域 | 鼠标事件穿透到桌面或下层应用,浮动按钮不响应;再次唤起主窗口后按钮恢复可点击 | ☐ | --- diff --git a/desktop/bug.md b/desktop/bug.md index 35fbdcf..f3b5170 100644 --- a/desktop/bug.md +++ b/desktop/bug.md @@ -1 +1,224 @@ -1. 设置页面不太好看,有些地方不太合理 \ No newline at end of file +1. 设置页面不太好看,有些地方不太合理 + +版本更新规划: + +## 目标 + +- 在设置页面增加「版本更新」模块,展示当前版本、检测更新状态、最新版本信息、发布时间、更新说明。 +- 应用启动后支持静默检测;设置页支持手动检测。 +- 检测到新版本后,明确提示用户更新;用户点击后,完成下载、校验、安装,并引导重启或自动重启完成升级。 +- 更新失败时给出清晰错误提示和降级方案,例如打开 Release 下载页。 +- 尽量抽离成可复用的更新能力,后续新 Tauri App 直接接入。 + +## 方案选型 + +- 底层更新引擎采用 Tauri 官方 updater 能力,不重复造轮子。 +- 当前项目不建议一开始就自研「纯 Rust 更新插件」替代官方 updater,因为真正和系统安装、签名校验、平台差异打交道的部分本来就属于 Tauri 官方 updater 的职责。 +- 更合理的可复用方向是抽一层「更新接入套件」: + - 前端复用:更新状态管理、轮询策略、弹窗文案、下载进度、失败回退。 + - 配置复用:版本源地址、更新通道、是否启动自动检测、忽略版本策略。 + - CI 复用:发布时自动生成更新元数据、签名文件、Release 资产上传。 +- 也就是说,底层继续用官方 updater,项目内再封装成一个统一的 `updater-kit`,后续新 App 只需要配置品牌信息和发布地址即可。 + +## 用户侧完整流程 + +### 1. 应用启动 + +- 启动后延迟几秒执行一次后台检测,避免影响首屏体验。 +- 若用户关闭了自动检测,则只保留设置页手动检测。 +- 若本次没有新版本,只更新「最近检查时间」和状态文案,不打扰用户。 + +### 2. 设置页展示 + +- 在设置页基础分组下新增「版本更新」卡片。 +- 展示内容建议包含: + - 当前版本 + - 更新通道(正式版,后续可扩展 beta) + - 自动检测更新开关 + - 最近一次检测时间 + - 当前检测状态:未检测 / 检测中 / 已是最新 / 发现新版本 / 下载中 / 安装中 / 更新失败 + - 手动按钮:`检查更新` +- 当发现新版本后,卡片内出现醒目的新版本信息和 `立即更新` 按钮。 + +### 3. 发现新版本后的交互 + +- 弹出更新提示层,至少包含: + - 新版本号 + - 发布时间 + - 更新说明 + - 操作按钮:`立即更新`、`稍后再说` +- 可选增加: + - `跳过此版本` + - `查看完整发布说明` +- 如果用户选择稍后,则本次会话不再重复打扰。 +- 如果用户选择跳过此版本,则记录被忽略版本,直到下一个更高版本再重新提示。 + +### 4. 下载与安装 + +- 用户点击 `立即更新` 后,进入下载态,显示进度、下载速度或已下载大小。 +- 下载完成后进入安装态,安装期间禁用重复点击,避免并发更新。 +- 安装完成后分平台处理: + - 能自动重启则直接重启进入新版本。 + - 若平台或场景不适合自动重启,则提示用户「更新已安装,点击重启生效」。 +- 若安装失败,弹出失败原因,并提供 `打开下载页手动安装`。 + +## 工程架构规划 + +### A. 前端层 + +- 新增更新服务层,统一管理: + - `checkForUpdates` + - `downloadAndInstall` + - `dismissVersion` + - `skipVersion` + - `getUpdateState` +- 新增更新状态模型,避免把更新逻辑直接塞进 `Settings.tsx` 或 `SettingsPanel.tsx`。 +- 推荐将更新逻辑封装为: + - `src/services/updater/`:底层 API 调用 + - `src/hooks/useAppUpdater.ts`:页面状态编排 + - `src/components/update/`:更新提示卡片、更新弹窗、下载进度 UI +- 设置页只负责展示和触发,不直接持有复杂更新细节。 + +### B. 本地配置层 + +- 在现有 settings 中增加更新配置字段,建议包含: + - `autoCheckUpdates` + - `updateChannel` + - `skippedVersion` + - `lastUpdateCheckAt` + - `lastUpdatePromptedVersion` +- 默认建议: + - `autoCheckUpdates = true` + - `updateChannel = stable` +- 需要做好向后兼容:老用户升级后自动补默认值。 + +### C. Tauri 原生层 + +- 接入 updater 插件初始化。 +- 在 Tauri 配置中补齐 updater 所需配置: + - 公钥 + - 更新元数据地址 + - 平台安装策略 +- 同时补齐 capability / permission 配置,确保前端可以正常调用更新能力。 + +### D. 发布链路 + +- 这是本需求最关键的一层,因为现在项目已有 GitHub Releases,但还没有完整的「应用内更新」发布产物链路。 +- 现有发布流程目前偏向生成并上传 DMG,后续需要扩展为两类产物并存: + - 给用户手动下载的安装包 + - 给应用内更新使用的 updater 产物和更新清单 +- 发布流程需要新增: + - 生成 updater 签名密钥 + - 将私钥和密码放入 GitHub Secrets + - 构建时生成 updater 对应的签名文件和更新清单 + - 将更新清单发布到固定可访问地址 +- 建议保留现有 DMG 发布,作为失败兜底和手动安装入口。 + +## 可复用抽离方案 + +### 推荐抽法 + +- 第一阶段先在 `desktop` 内部做稳定封装,不急着拆独立仓库。 +- 等第一版在 PinWall 跑通后,再抽成一个可复用模块,例如: + - `packages/tauri-updater-kit` + - 或者项目模板中的 `shared/updater` +- 该模块应提供: + - 通用状态机 + - 通用 Hook + - 通用弹窗和进度组件 + - 通用配置类型 + - GitHub Release 工作流模板 + - 文档化接入步骤 +- 每个新 App 只需要注入: + - 应用名 + - 当前版本来源 + - 更新通道配置 + - Release 地址 + - 品牌化文案 + +### 不建议的抽法 + +- 不建议把「发布工作流 + updater 配置 + UI 提示 + 业务设置存储」一次性强耦合成一个黑盒插件。 +- 因为不同 App 的: + - 发布平台 + - 安装包格式 + - 更新文案 + - 是否强更 + - 是否支持 beta 通道 + 都会不一样。 +- 更适合做成「底层统一,接入可配置,UI 可替换」的半插件化方案。 + +## 页面交互建议 + +- 入口位置:优先放在设置页 `基础` 分组中,因为版本、启动、自启动、快捷键都属于应用级设置。 +- 卡片建议分三态: + - 默认态:显示当前版本 + 检查更新按钮 + - 有更新态:高亮显示 `发现新版本 vX.Y.Z` + - 更新中:显示下载/安装进度,按钮禁用 +- 文案建议清晰直接: + - `当前已是最新版本` + - `发现新版本,建议更新` + - `更新包下载中` + - `更新安装中,请勿关闭应用` + - `更新失败,可前往下载页手动安装` + +## 异常与边界场景 + +- 无网络:检测失败,但不影响正常使用。 +- Release 资产缺失:提示服务器配置异常,并记录日志。 +- 签名校验失败:禁止安装,提示安全校验失败。 +- 用户跳过当前版本:该版本不再弹窗,直到更高版本出现。 +- 用户正在使用关键窗口:更新提示可先提示下载,安装前再二次确认,避免突然重启影响使用。 +- 后台托盘应用场景:更新成功后要考虑主窗口、设置窗口、通知窗口和托盘状态的平滑退出与恢复。 + +## 测试与验收规划 + +### 单元与前端层 + +- 验证更新状态机切换是否正确: + - 空闲 -> 检测中 -> 已是最新 + - 空闲 -> 检测中 -> 发现更新 + - 发现更新 -> 下载中 -> 安装中 -> 待重启 + - 任意阶段 -> 失败 +- 验证忽略版本、最近检查时间、自动检测开关的持久化逻辑。 + +### 手动验收 + +- 无更新时,设置页正确显示最新状态。 +- 有更新时,设置页和弹窗都能提示。 +- 点击更新后,能完成下载、安装、重启。 +- 更新失败时,不会损坏当前版本。 +- 跳过版本后,不会重复提示同一版本。 +- 从旧版本直接升到新版本时,原有用户配置不丢失。 + +### 发布链路验收 + +- 使用测试版本 tag 跑一遍 GitHub Actions。 +- 验证更新清单地址可访问。 +- 验证签名正确。 +- 验证一台旧版本机器能真实更新到新版本。 + +## 分阶段落地计划 + +### P1. 基础接入 + +- 接入 updater 底层能力。 +- 打通 GitHub Release 到应用内更新清单的链路。 +- 设置页增加「检查更新」和「立即更新」最小可用能力。 + +### P2. 体验完善 + +- 增加自动检测、忽略版本、更新说明弹窗、下载进度、失败回退。 +- 优化更新时的窗口关闭与重启体验。 + +### P3. 可复用抽象 + +- 将 updater 服务、Hook、UI 组件、配置类型、CI 模板整理成 `updater-kit`。 +- 输出一份新项目接入文档,做到新 Tauri App 可快速复用。 + +## 我建议的最终落地原则 + +- 先以 PinWall 为首个落地样板,把完整链路跑通。 +- 底层依赖官方 updater,减少平台兼容风险。 +- 复用重点放在「接入层、状态层、UI 层、CI 模板层」,而不是重复造底层更新轮子。 +- 保留 GitHub Releases 手动下载入口,作为更新失败时的兜底方案。 diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 469bbf5..928e4c8 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -50,6 +50,7 @@ pub fn run() { commands::quit_app, window_layer::send_to_background, window_layer::summon_main, + window_layer::is_main_summoned, window_layer::set_cursor_passthrough, background::import_background_images, background::delete_background_image_file, diff --git a/desktop/src-tauri/src/window_layer.rs b/desktop/src-tauri/src/window_layer.rs index 5bbc41d..f0d97cd 100644 --- a/desktop/src-tauri/src/window_layer.rs +++ b/desktop/src-tauri/src/window_layer.rs @@ -1,4 +1,4 @@ -use tauri::Manager; +use tauri::{Emitter, Manager}; pub struct MainLayerState(pub std::sync::Mutex); @@ -9,11 +9,12 @@ pub fn set_main_default_layer(app: &tauri::AppHandle) { if let Some(window) = app.get_webview_window("main") { let _ = window.set_always_on_top(false); let _ = window.set_always_on_bottom(true); - let _ = window.set_ignore_cursor_events(false); + let _ = window.set_ignore_cursor_events(true); let _ = window.set_visible_on_all_workspaces(true); let _ = window.set_shadow(false); let _ = window.show(); } + let _ = app.emit("main-layer-changed", false); } pub fn summon_main_window(app: &tauri::AppHandle) { @@ -30,6 +31,7 @@ pub fn summon_main_window(app: &tauri::AppHandle) { let _ = window.unminimize(); let _ = window.set_focus(); } + let _ = app.emit("main-layer-changed", true); } pub fn toggle_main_layer(app: &tauri::AppHandle) { @@ -57,6 +59,15 @@ pub fn summon_main(app: tauri::AppHandle) { summon_main_window(&app); } +#[tauri::command] +pub fn is_main_summoned(app: tauri::AppHandle) -> bool { + app.state::() + .0 + .lock() + .map(|v| *v) + .unwrap_or(false) +} + #[tauri::command] pub fn set_cursor_passthrough(window: tauri::WebviewWindow, ignore: bool) { let _ = window.set_ignore_cursor_events(ignore); diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 5dd7d55..5807b76 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -68,12 +68,18 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "macOS": { + "signingIdentity": "Apple Development: YOUR_NAME (YOUR_TEAM_ID)", + "entitlements": "tauri.entitlements", + "exceptionDomain": "", + "hardenedRuntime": true + } }, "plugins": { "updater": { "endpoints": [ - "https://github.com/rain9/pinwall/releases/download/latest/{{target}}-update.json" + "https://github.com/you-want/PinWall/releases/download/latest/{{target}}-update.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IGZpbGU6c3ByaW5nLXNpZ25hdHVyZS5zaWcK" } diff --git a/desktop/src-tauri/tauri.entitlements b/desktop/src-tauri/tauri.entitlements new file mode 100644 index 0000000..194e124 --- /dev/null +++ b/desktop/src-tauri/tauri.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + \ No newline at end of file diff --git a/desktop/src/hooks/useDesktopClickThrough.ts b/desktop/src/hooks/useDesktopClickThrough.ts new file mode 100644 index 0000000..5de8276 --- /dev/null +++ b/desktop/src/hooks/useDesktopClickThrough.ts @@ -0,0 +1,48 @@ +import { useEffect } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { getCurrentWindow } from "@tauri-apps/api/window"; + +export function useDesktopClickThrough(enabled: boolean) { + useEffect(() => { + if (!enabled) return; + + const currentWindow = getCurrentWindow(); + let isSummoned = false; + let unlisten: (() => void) | null = null; + + const syncCursorPassthrough = async (summoned: boolean) => { + isSummoned = summoned; + await currentWindow.setIgnoreCursorEvents(!summoned); + }; + + invoke("is_main_summoned") + .then((summoned) => syncCursorPassthrough(summoned)) + .catch(() => syncCursorPassthrough(false)); + + listen("main-layer-changed", (event) => { + void syncCursorPassthrough(event.payload); + }).then((fn) => { + unlisten = fn; + }); + + const handleMouseDown = (event: MouseEvent) => { + if (!isSummoned || event.button !== 0) return; + + const target = event.target as HTMLElement | null; + if (target?.closest("[data-interactive]")) return; + + event.preventDefault(); + event.stopPropagation(); + void invoke("send_to_background"); + }; + + document.addEventListener("mousedown", handleMouseDown, true); + + return () => { + unlisten?.(); + document.removeEventListener("mousedown", handleMouseDown, true); + void currentWindow.setIgnoreCursorEvents(false); + }; + }, [enabled]); +} diff --git a/desktop/src/pages/Wall.tsx b/desktop/src/pages/Wall.tsx index 35d5172..fb67ec2 100644 --- a/desktop/src/pages/Wall.tsx +++ b/desktop/src/pages/Wall.tsx @@ -1,5 +1,4 @@ import { useState, useEffect, useCallback, useRef } from "react"; -import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { webviewWindow } from "@tauri-apps/api"; @@ -27,6 +26,7 @@ import { useRestReminder } from "../hooks/useRestReminder"; import { useOffWorkReminder } from "../hooks/useOffWorkReminder"; import { useEyeCareReminder } from "../hooks/useEyeCareReminder"; import { useMoodCheckin } from "../hooks/useMoodCheckin"; +import { useDesktopClickThrough } from "../hooks/useDesktopClickThrough"; import type { CardType } from "../types"; import { useWidgetStore } from "../stores/widgetStore"; @@ -37,6 +37,7 @@ type NewCardState = function Wall() { const { t } = useI18n(); const [settings, setSettings] = useState(null); + const hasSettings = !!settings; const [newCardModal, setNewCardModal] = useState({ open: false }); const [showBreathing, setShowBreathing] = useState(false); const newCardPositionRef = useRef({ x: 0, y: 0 }); @@ -70,6 +71,7 @@ function Wall() { useOffWorkReminder(); useEyeCareReminder(); useMoodCheckin(); + useDesktopClickThrough(hasSettings); const { results: quotaResults, loading: quotaLoading, refresh: quotaRefresh } = useQuotaMonitor(settings?.quotaMonitor); @@ -108,19 +110,6 @@ function Wall() { }); }, [syncWidgets]); - useEffect(() => { - const handleMouseDown = async (e: MouseEvent) => { - if (e.button !== 0) return; - const el = document.elementFromPoint(e.clientX, e.clientY); - const isBlankArea = !el || !el.closest("[data-interactive]"); - if (isBlankArea) { - await invoke('send_to_background'); - } - }; - document.addEventListener('mousedown', handleMouseDown); - return () => document.removeEventListener('mousedown', handleMouseDown); - }, []); - const openNewCardModal = useCallback(() => { const centerX = window.innerWidth / 2 - 190; const centerY = window.innerHeight / 2 - 160; @@ -175,7 +164,6 @@ function Wall() { setNewCardModal({ open: false }); }, []); - const hasSettings = !!settings; const quotaConfig = settings?.quotaMonitor; const showQuotaCard = !!(quotaConfig?.enabled && quotaConfig.models.length > 0); const showWeatherCard = !!settings?.weatherCareEnabled; diff --git a/desktop/src/test-setup.ts b/desktop/src/test-setup.ts index 9bea861..8b60255 100644 --- a/desktop/src/test-setup.ts +++ b/desktop/src/test-setup.ts @@ -14,7 +14,13 @@ vi.mock('@tauri-apps/api', () => ({ })); vi.mock('@tauri-apps/api/core', () => ({ - invoke: vi.fn(), + invoke: vi.fn(() => Promise.resolve(false)), +})); + +vi.mock('@tauri-apps/api/window', () => ({ + getCurrentWindow: vi.fn(() => ({ + setIgnoreCursorEvents: vi.fn(() => Promise.resolve()), + })), })); vi.mock('@tauri-apps/plugin-dialog', () => ({