diff --git a/docs/UX-BENCHMARKS.md b/docs/UX-BENCHMARKS.md index 2af03f0..2caecac 100644 --- a/docs/UX-BENCHMARKS.md +++ b/docs/UX-BENCHMARKS.md @@ -78,7 +78,11 @@ Scheduled(有日期的)。 - ✅ macOS 已落地:⌘/⇧ 多选 + 批量完成/排期/移动/删除,删除走单条撤销横幅 (PRODUCT-SPEC §5b 已收录批量语义)。 -- 🔜 短期:Windows/Linux 跟进同一套多选与批量语义(见 README 平台状态)。 +- ✅ Windows 列表选择基础:WinUI Extended 模式支持 Ctrl/Shift + 多选、方向键与范围选择;Enter 打开当前选中任务的编辑对话框,Esc + 清除选择。按任务 ID 在列表刷新后恢复选择(普通列表与搜索结果)。 +- 🔜 **尚未完成**:Windows 的批量完成/排期/移动/删除、行内详情展开和撤销; + Linux 的批量操作;以及三平台按真实快捷键进行实机回归。 ### 2.5 Quick Find @@ -113,3 +117,15 @@ Scheduled(有日期的)。 https://digitoolbook.com/en/blog/things3-minimalist-task-management-gtd - 2Do 官方功能页:https://www.2doapp.com/page-section/main-features - 2Do Focus Lists:https://www.2doapp.com/docs/ios/focus-lists + +### Windows task ownership recovery + +- ✅ Task rows expose a right-click **Move to List / 移动到列表** submenu for + other lists. It calls `update_task` with the target list ID, preserving + title, due date/time, completion state and notes. +- This is the **explicit** recovery path for tasks already saved into the + wrong list by older Windows versions. Do not auto-migrate existing rows: + the original intended list cannot be inferred reliably. +- Manual acceptance: move a task from 工作 to 生活, confirm it disappears from + 工作, appears in 生活 and keeps all other fields; then restart and verify + ownership persists. Exercise both languages and all smart-list views. diff --git a/scripts/e2e/e2e-windows.ps1 b/scripts/e2e/e2e-windows.ps1 index 7d721bd..c52ddce 100644 --- a/scripts/e2e/e2e-windows.ps1 +++ b/scripts/e2e/e2e-windows.ps1 @@ -101,6 +101,119 @@ try { return $false }) + # Regression: a task entered in 生活 must stay in 生活, not the + # database's first/default list 工作. These are the shipped default + # list names, from racket/taskly/model.rkt. The disposable acceptance + # profile guarantees default-list id 1 and the new 生活 list id 2. + $createList = Wait-Until 'create-list control' { Find-ById $window 'create-list' } + $createList.GetCurrentPattern( + [System.Windows.Automation.InvokePattern]::Pattern).Invoke() + $nameInput = Wait-Until 'new-list input' { Find-ById $window 'prompt-input' } + $nameInput.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).SetValue('生活') + $confirm = Wait-Until 'new-list confirmation' { Find-ByName $window 'OK' } + $confirm.GetCurrentPattern( + [System.Windows.Automation.InvokePattern]::Pattern).Invoke() + + function Select-TaskList { + param([int]$ListId, [string]$Name) + # WinUI's inner Grid is not a ControlView automation peer (verified + # against the failing packaged Windows screenshot). Resolve the + # displayed TextBlock instead, then select its ListViewItem parent. + $label = Wait-Until ("visible list: {0}" -f $Name) { + $found = Find-ById $window ("list-name-{0}" -f $ListId) + if ($null -eq $found) { $found = Find-ByName $window $Name } + return $found + } + $item = $label + $walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker + while ($null -ne $item -and + $item.Current.ControlType -ne [System.Windows.Automation.ControlType]::ListItem) { + $item = $walker.GetParent($item) + } + if ($null -eq $item) { throw ("no selectable list item for {0}" -f $Name) } + $item.GetCurrentPattern( + [System.Windows.Automation.SelectionItemPattern]::Pattern).Select() + } + + $paneTitle = Wait-Until 'active view label' { + Find-ById $window 'current-view-title' + } + Select-TaskList 2 '生活' + [void](Wait-Until '生活 view selected' { + if ($paneTitle.Current.Name -eq '生活') { return $true } + return $false + }) + $plainTask = 'Life-only task without due date or notes' + $quickAdd.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).SetValue($plainTask) + $quickAdd.SetFocus() + # Exercise the actual Enter key, not only the plus-button click. + [System.Windows.Forms.SendKeys]::SendWait('{ENTER}') + + $titleText = Wait-Until 'task saved under 生活' { + Find-ByName $window $plainTask + } + # Layout assertions use the text and native checkbox peers: a Grid + # itself is not reliably exposed in WinUI UIA's ControlView. + $plainCheck = Wait-Until 'new task checkbox' { + Find-ById $window 'task-complete-2' + } + # Both elements share the row's first Grid row; the title must + # visually align with the native checkbox for a metadata-free task. + $checkBoxBounds = $plainCheck.Current.BoundingRectangle + $titleBounds = $titleText.Current.BoundingRectangle + $centerDiff = [Math]::Abs( + ($checkBoxBounds.Top + $checkBoxBounds.Height / 2) - + ($titleBounds.Top + $titleBounds.Height / 2)) + $limit = [Math]::Max(8.0, $checkBoxBounds.Height / 4) + if ($centerDiff -gt $limit) { + throw ("plain task title and checkbox are misaligned by {0:N1}px" -f $centerDiff) + } + + $taskList = Wait-Until 'extended task list' { + Find-ById $window 'task-list' + } + $selection = $taskList.GetCurrentPattern( + [System.Windows.Automation.SelectionPattern]::Pattern) + if (-not $selection.Current.CanSelectMultiple) { + throw 'Windows task list must support Ctrl/Shift multi-selection' + } + + Select-TaskList 1 '工作' + [void](Wait-Until '工作 view selected' { + if ($paneTitle.Current.Name -eq '工作') { return $true } + return $false + }) + [void](Wait-Until '生活 task absent from 工作' { + if ($null -eq (Find-ByName $window $plainTask)) { return $true } + return $false + }) + Select-TaskList 2 '生活' + [void](Wait-Until '生活 task remains in 生活' { + if ($paneTitle.Current.Name -eq '生活' -and + $null -ne (Find-ByName $window $plainTask)) { return $true } + return $false + }) + + # Search is global, even while 生活 is selected. A result must + # display its owning list, never appear as an unlabeled local row. + $searchBox = Wait-Until 'search box' { Find-ById $window 'task-search' } + $searchBox.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).SetValue($plainTask) + $owner = Wait-Until 'search result list owner' { + Find-ById $window 'task-owner-2' + } + if ($owner.Current.Name -ne '生活') { + throw ("search result claims wrong owner: {0}" -f $owner.Current.Name) + } + $searchBox.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).SetValue('') + [void](Wait-Until 'search cleared' { + if ($null -eq (Find-ById $window 'task-owner-2')) { return $true } + return $false + }) + $nodes = $window.FindAll( [System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition) @@ -127,12 +240,17 @@ try { [ordered]@{ platform = 'windows' artifact = $executable.FullName - actions = @('set:quick-add', 'invoke:quick-add-submit', 'toggle:task-complete-1') + actions = @('set:quick-add', 'invoke:quick-add-submit', 'toggle:task-complete-1', 'create-list:生活', 'select-list:2', 'enter:quick-add', 'select-list:1', 'select-list:2') assertions = @( 'packaged WinUI window activated', 'stable UI Automation identifiers exposed', 'RPC-created task appeared', 'changed Event removed the completed task from the active view', + 'new task submitted by Enter is owned by 生活, not 工作', + 'task checkbox and plain-text title are vertically aligned', + 'Windows task list exposes native multiple selection', + 'view switches preserve task list ownership', + 'global search shows the real owning list for every task', 'native window close stopped the process' ) } | ConvertTo-Json -Depth 5 | diff --git a/shared/spec/PRODUCT-SPEC.md b/shared/spec/PRODUCT-SPEC.md index 99dfd5c..8a41797 100644 --- a/shared/spec/PRODUCT-SPEC.md +++ b/shared/spec/PRODUCT-SPEC.md @@ -52,6 +52,13 @@ README's honest gaps). text first runs through ExtractTimeCommand (trailing `@10am`, `+1d`…), target list = selected list else first list (none → error `taskCreateListFirst`); new task lands at top; counts refresh. + The host captures the **concrete list ID at submission**, before any + asynchronous parsing or persistence. Switching lists while the request + is running must never change the destination. No list / invalid selected + list must keep the unsent text rather than silently using another list. + The Windows packaged acceptance must create a second list, submit a + task via Enter, verify that it remains in the selected list, and verify + that it never appears in the first/default list. - Empty states: not connected → 📂 + `taskListEmptyHint`; connected and empty → ✓ + `taskListEmpty`. - Row order per view (SQL in DATA-FORMAT/CLI-SPEC §list): All & list views @@ -64,6 +71,10 @@ README's honest gaps). - 20px circular checkbox: incomplete = hollow ring; complete = ring + check in accent; completed text gets strikethrough + tertiary color. + The checkbox and task title share the **same first-row vertical center** + on every platform. Due date, time and notes (only when actually present) + sit beneath the title, not beside or vertically above the checkbox. + A task with no metadata renders as one clean centered checkbox/title row. - Meta line under the text when present: the localized due date (today/tomorrow/yesterday/`Aug 31`/`8月31日`, plus the time when set) in its semantic color (overdue incomplete `#FF3B30`, due today accent, diff --git a/windows/MainWindow.xaml b/windows/MainWindow.xaml index eca5d31..f167cb4 100644 --- a/windows/MainWindow.xaml +++ b/windows/MainWindow.xaml @@ -238,6 +238,7 @@ Foreground="{ThemeResource TasklySecondaryTextBrush}" />