Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/UX-BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,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.
91 changes: 90 additions & 1 deletion scripts/e2e/e2e-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,92 @@ 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)
}

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
})

$nodes = $window.FindAll(
[System.Windows.Automation.TreeScope]::Descendants,
[System.Windows.Automation.Condition]::TrueCondition)
Expand All @@ -127,12 +213,15 @@ 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',
'view switches preserve task list ownership',
'native window close stopped the process'
)
} | ConvertTo-Json -Depth 5 |
Expand Down
11 changes: 11 additions & 0 deletions shared/spec/PRODUCT-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions windows/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@
Foreground="{ThemeResource TasklySecondaryTextBrush}" />
<Button
x:Name="NewListButton"
AutomationProperties.AutomationId="create-list"
Grid.Column="1"
Padding="6,2"
Background="Transparent"
Expand Down Expand Up @@ -303,6 +304,7 @@
Margin="10,0,12,0">
<TextBlock
x:Name="PaneTitle"
AutomationProperties.AutomationId="current-view-title"
FontSize="22"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
Expand Down
Loading
Loading