test: assert note focus before opening creation form
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s

Check detail navigation focus before the auto-focused creation input takes over. Preserve return-to-list and reading-position assertions, and include the reviewed sharing first-read test and independent validation record. Validation runs in CI per user request.
This commit is contained in:
mesalogo
2026-10-01 09:16:15 +08:00
parent 2c5300d031
commit 016f987026
4 changed files with 76 additions and 4 deletions
@@ -58,3 +58,13 @@ Agent 实现和锁不变,仍不升版。
扩展源码类型检查和 lint 已通过。用户随后明确要求停止本地测试、直接推送等 CI, 扩展源码类型检查和 lint 已通过。用户随后明确要求停止本地测试、直接推送等 CI,
进行中的定向测试已中断,不计为通过,也不沿用旧候选的全量结果。 进行中的定向测试已中断,不计为通过,也不沿用旧候选的全量结果。
最终发布取决于包含新增提交和文档的精确候选 CI 与生产构建。 最终发布取决于包含新增提交和文档的精确候选 CI 与生产构建。
## 扩展候选焦点回归
`2c5300d` 的 Desktop CI `36753498640`:5580 通过、58 跳过、1 失败,
生产构建未执行;Agent CI `36753498643` 通过。
笔记导航用例在打开并取消带 `autoFocus` 标题输入框的新建表单后,
才断言此前打开详情时的返回按钮焦点,混合了两个不同交互阶段。
将原焦点断言移至打开新建表单之前,并断言新建标题输入框取得焦点;
保留返回列表、滚动位置、搜索内容和原卡片焦点断言,不改产品代码或延长超时。
按用户要求不运行本地测试,由新候选 CI 验证。
+59 -1
View File
@@ -11,4 +11,62 @@
`GOODBUDDY_SHARING_SERVER_ROOT` 的真实 ShareServer/Electron 联调用例。 `GOODBUDDY_SHARING_SERVER_ROOT` 的真实 ShareServer/Electron 联调用例。
本轮发布准备中的定向测试被用户中断,并要求直接推送交 CI 验证; 本轮发布准备中的定向测试被用户中断,并要求直接推送交 CI 验证;
不据此声明测试或真实服务联调通过。发布结果以最终候选 CI 为准, 不据此声明测试或真实服务联调通过。发布结果以最终候选 CI 为准,
条件跳过的真实服务用例不能计入已验证能力。 条件跳过的真实服务用例不能计入已验证能力。
## 2026-10-01:桌面实现轮次的独立验证
以下记录来自设备共享实现会话,独立于上文发布准备轮次;已实际运行,不作为发布 CI 或安装包
验收的替代。对应 [FR-M1 至 FR-M5](./prd.md#当前首期本地元数据)。
| 检查 | 结果与覆盖范围 |
| --- | --- |
| 定向 7 文件 | 114 项通过:Main HTTP 契约、稳定 ID、设置重读、可信 IPC、超时与错误、页面发布/撤销、输入保留、旧设置迁移与应用排序 |
| 完整 `App.test.tsx` | 320 项通过,含新增设备共享入口与既有设置 Modal 回归 |
| `goodbuddy-config-contracts.test.ts` | 8 项通过;修正旧四项写入样例,保留重复、未知及缺失项拒绝断言 |
| 真实 Electron/ShareServer | 1 项通过,使用相邻后端源码、独立端口和临时数据目录,真实窗口运行生产页面、preload、可信 IPC 和 Main 服务 |
| 类型与静态检查 | `npm run typecheck`、`npm run lint`、`git diff --check` 通过 |
Electron 联调从应用中心打开页面,注册本机,发布知识元数据,服务返回确认
`sourceMode=server`、`search=true/read=false/download=true`,随后撤销并刷新至
`status=revoked`。注册、发布、撤销确认和刷新使用原生鼠标输入;重读磁盘保持设备 ID。
浅深主题的 480px 视口均无横向溢出。首次窄窗口检查因系统缩放与预期 CSS 宽度不一致失败,
沿用现有 Electron 测试的固定设备缩放设置后通过。该测试是组件组合,未启动完整生产 App,
也未验证安装包、截图视觉效果或 macOS/Linux 窗口。
全仓 `npm test` 用时 760.82 秒:468 文件通过、13 文件跳过、2 文件失败;5,549 项通过、88 项
跳过、2 项失败。设备共享真实 Electron 联调在这次全仓运行中也通过。两项失败分别为:
- 配置工具样例仍提交四项顺序。运行期间修正后独立复跑 8 项通过;随后与服务测试合跑
11 项也通过,服务测试包含对尚不存在设置文件的并发首次读取。
- `inline-help.electron.test.ts` 的 `Hover Escape closed modal: dark 360x320` 断言失败。
未修改该文件或帮助组件,独立复跑 1 项通过,原因未确定;不把复跑结果写成全仓一次全绿。
完整日志位于本次工作机临时目录 `opencode/goodbuddy-sharing-full-tests.log`。没有再次执行整套
全仓测试。本功能测试没有模型调用。中文设备共享文档经 `deai-writing` 扫描无阻断项,复核项
中的权限边界、真实字段及测试列表按事实保留。
### 复现命令
在 GoodBuddy 仓库执行:
```powershell
npx vitest run src/main/device-sharing-service.test.ts src/main/device-sharing-ipc.test.ts src/main/application-settings-store.test.ts src/renderer/src/DeviceSharingPage.test.tsx src/renderer/src/ApplicationCenter.test.tsx src/renderer/src/ApplicationMenu.test.tsx src/preload/preload-sandbox.test.ts
npx vitest run src/renderer/src/App.test.tsx
npx vitest run src/shared/goodbuddy-config-contracts.test.ts
$env:GOODBUDDY_SHARING_SERVER_ROOT = (Resolve-Path ../goodbuddy-shareserver).Path
npx vitest run tests/device-sharing.electron.test.ts
npx vitest run tests/inline-help.electron.test.ts
npm run typecheck
npm run lint
npm test
```
跨仓 Electron 测试需要后端已安装依赖;未设置 `GOODBUDDY_SHARING_SERVER_ROOT` 时明确跳过。
测试会关闭自己启动的服务并清理临时数据,不写后端源码。
### 手工体验
在 ShareServer 仓库运行 `npm run dev:api`,在 GoodBuddy 仓库运行 `npm run dev`。从“应用”
打开“设备共享”,默认地址为 `http://127.0.0.1:8787`。先保存名称或地址修改,再注册本机、
发布条目、检查目录、撤销并刷新。知识类型提供三个独立权限框;重启后可比较同一设备 ID。
服务关闭时刷新应显示错误并保留旧列表,不应显示为空目录或推断设备离线。
+5 -2
View File
@@ -50,11 +50,14 @@ async function setup(timeout = 1000) {
} }
it('persists one identity across concurrent first reads, saves and restarts without accepting renderer identity', async () => { it('persists one identity across concurrent first reads, saves and restarts without accepting renderer identity', async () => {
const { service, file, url } = await setup() const setupResult = await setup()
const file = `${setupResult.file}.first-read`
const url = setupResult.url
const service = new DeviceSharingService(file, 'test-version')
const [first, second] = await Promise.all([service.getSettings(), service.getSettings()]) const [first, second] = await Promise.all([service.getSettings(), service.getSettings()])
expect(first.deviceId).toBe(second.deviceId) expect(first.deviceId).toBe(second.deviceId)
await service.saveSettings({ name: 'Renamed', serverUrl: `${url}/` }) await service.saveSettings({ name: 'Renamed', serverUrl: `${url}/` })
expect(await new DeviceSharingService(file, 'v2').getSettings()).toEqual({ ...first, name: 'Renamed' }) expect(await new DeviceSharingService(file, 'v2').getSettings()).toEqual({ ...first, name: 'Renamed', serverUrl: url })
await expect(service.saveSettings({ name: 'Bad', serverUrl: url, deviceId: 'forged' })).rejects.toThrow() await expect(service.saveSettings({ name: 'Bad', serverUrl: url, deviceId: 'forged' })).rejects.toThrow()
expect(JSON.parse(await readFile(file, 'utf8')).deviceId).toBe(first.deviceId) expect(JSON.parse(await readFile(file, 'utf8')).deviceId).toBe(first.deviceId)
await writeFile(file, '{broken') await writeFile(file, '{broken')
@@ -560,8 +560,10 @@ describe('MagicNotesWorkspace overview navigation', () => {
// Back to overview lives in the page header's top-right actions while a note is open. // Back to overview lives in the page header's top-right actions while a note is open.
expect(screen.getByRole('button', { name: '返回总览' }).closest('.page-header__actions')).not.toBeNull() expect(screen.getByRole('button', { name: '返回总览' }).closest('.page-header__actions')).not.toBeNull()
expect(screen.getByRole('button', { name: '返回总览' }).nextElementSibling).toBe(screen.getByRole('button', { name: '新建笔记' })) expect(screen.getByRole('button', { name: '返回总览' }).nextElementSibling).toBe(screen.getByRole('button', { name: '新建笔记' }))
await waitFor(() => expect(screen.getByRole('button', { name: '返回总览' })).toHaveFocus())
fireEvent.click(screen.getByRole('button', { name: '新建笔记' })) fireEvent.click(screen.getByRole('button', { name: '新建笔记' }))
expect(within(overview).getByLabelText('笔记标题')).toBeVisible() expect(within(overview).getByLabelText('笔记标题')).toBeVisible()
expect(within(overview).getByLabelText('笔记标题')).toHaveFocus()
fireEvent.click(within(overview).getByRole('button', { name: '取消' })) fireEvent.click(within(overview).getByRole('button', { name: '取消' }))
expect(screen.queryByRole('button', { name: '切换到待办' })).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: '切换到待办' })).not.toBeInTheDocument()
expect(screen.getByRole('searchbox')).toBeVisible() expect(screen.getByRole('searchbox')).toBeVisible()
@@ -571,7 +573,6 @@ describe('MagicNotesWorkspace overview navigation', () => {
const css = readFileSync('src/renderer/src/styles.css', 'utf8') const css = readFileSync('src/renderer/src/styles.css', 'utf8')
expect(css).toMatch(/\.magic-notes-layout--detail\s*\{\s*border: 0;\s*border-radius: 0;/) expect(css).toMatch(/\.magic-notes-layout--detail\s*\{\s*border: 0;\s*border-radius: 0;/)
expect(css).toMatch(/\.magic-notes-stream-pane\s*\{[^}]*padding: 0 var\(--magic-note-stream-gutter\)/) expect(css).toMatch(/\.magic-notes-stream-pane\s*\{[^}]*padding: 0 var\(--magic-note-stream-gutter\)/)
await waitFor(() => expect(screen.getByRole('button', { name: '返回总览' })).toHaveFocus())
back() back()
expect(overview).toBeVisible() expect(overview).toBeVisible()
expect(screen.getByRole('heading', { level: 1, name: '魔法笔记' })).toBeVisible() expect(screen.getByRole('heading', { level: 1, name: '魔法笔记' })).toBeVisible()