Commit graph

17 commits

Author SHA1 Message Date
Johannes Millan
b6f3aead1f test(migration): gate on work-context switch to de-flake legacy migration e2e
page.goto only changes the URL hash, so the SPA switches work-context
client-side while the previous view's task-list lingers in the DOM. The
task-list/count checks were satisfiable by the stale render, leaving the
final task-title assertion to carry the wait for the switch on a 10s budget
that occasionally outlasted under heavy parallel load. Gate on the project
title rendering in <main> first, mirroring navigateToProjectByName.
2026-06-08 19:48:15 +02:00
Johannes Millan
34d4aa0bbf
feat(tasks): allow dragging tasks into subtask lists (#7962)
* feat(tasks): allow dragging tasks into subtask lists

* fix(tasks): support childless subtask drop targets

* fix(tasks): support dragging subtasks back to main list

* fix(tasks): preserve nested subtask sorting

* fix(tasks): address subtask drag review feedback

* fix(tasks): address drag conversion review

* test: skip onboarding in migration fresh-start e2e

* fix(tasks): harden convertToSubTask guards and dedupe tag cleanup

Address review findings on the drag-to-subtask feature (#7905):

- Keep the section and crud meta-reducer guards in lock-step via a shared
  canApplyConvertToSubTask(). Previously the section reducer stripped a task
  from its section even when the crud reducer rejected the convert (missing
  target parent or self-target), leaving the task top-level yet dropped from
  section ordering on a replayed/concurrent op.
- Reject nesting under a target that is itself a subtask. The UI renders only
  two levels, so deeper nesting would orphan the task and leave the
  grandparent's time aggregation stale.
- Tighten op-log payload validation to require string taskId/targetParentId.
- Dedupe the "remove task ids from all tags" logic shared by
  convertToSubTask/deleteTask/deleteTasks into removeTasksFromAllTags().
- Collapse the tri-state afterTaskId positioning in handleConvertToMainTask
  (undefined and null both prepend via moveItemAfterAnchor).
- Name the DragPointer type in DropListService.

Adds reducer specs for the rejected-target-parent cases.

* fix(tasks): remove empty subtask drop-target for childless parents

The dashed empty sub-task drop zone that appeared under childless parent
tasks during a drag was unwanted UI. Remove it along with its supporting
machinery (the subTaskDropCandidate signal in ScheduleExternalDragService,
the pointerdown candidate-arming in TaskListComponent, the
isEmptySubTaskDropTargetMounted computed, and the related template/SCSS).

Consequence: a task can now be nested by dragging only onto a parent that
already has a subtask list. Dragging a subtask back out to a main list
(convertToMainTask) and nesting into an existing subtask list
(convertToSubTask) are unchanged.

* fix(tasks): reliably convert subtasks dragged to the top-level list

Two issues prevented dragging a subtask out to the top-level list to
convert it into a main task:

- CDK only caches a sibling drop-list's geometry when its enterPredicate
  passes at drag start (_startReceiving). The pointer is always over the
  source subtask list then, so the top-level list was never cached and
  conversion silently failed until an unrelated parent drag warmed it.
  Open a one-microtask accept window at subtask drag start so CDK caches
  the top-level lists' geometry; the pointer guard resumes afterwards.

- An expanded neighbour's subtask list "caught" the drag in the dead-band
  just above the next parent (sibling order resolves subtask lists before
  the top-level list), silently re-parenting the subtask instead of
  converting it, and growing/sticking once entered. Treat only an actual
  subtask row as "inside" a foreign subtask list; its trailing padding now
  falls through to the top-level list for conversion. The source list
  still blocks anywhere, so in-list sorting is unaffected.

* fix(tasks): use midpoint-crossing sort for drag preview placement

CDK's SingleAxisSortStrategy swaps siblings as soon as the cursor enters
any part of their clientRect. For task lists whose item rects span the
parent's full element (header + its expanded subtask list), that swap
displaces the drop preview far below the cursor — landing it past the
target's subtasks instead of where the user is pointing.

Monkey-patch the strategy's `_getItemIndexFromPointerPosition` with a
relative-position midpoint rule (the pattern used by dnd-kit and
react-beautiful-dnd): a sibling only swaps with the dragged item once
the cursor crosses into the half of the sibling that's on the dragged
item's approach side. `enter()` keeps CDK's first-inside semantics so
initial placement still lands somewhere sensible.

Also extend `_pointerSubTaskList()` with a `.sub-tasks` wrapper fallback
so the leading strip between a parent's header and its first subtask
routes the drag into that SUB list at index 0 instead of falling through
to the top-level list and converting at the parent's slot.

* test(tasks): align drag predicate spec data

* test(tasks): reconcile createMockDrop calls after combining drag PRs

Combining the two #7905 PR lineages crossed a stale signature:
the midpoint-sort commit added enterPredicate cases calling the old
3-arg createMockDrop(modelId, filteredTasks, listId), while keilogic's
spec-alignment commit narrowed it to (modelId, listId) since
enterPredicate never reads filteredTasks. Drop the vestigial arrays
from the three affected cases so they match the simplified helper.

* fix(tasks): widen first/last subtask drop targets

Dragging a task to the first/last position of a subtask list was hard:
the midpoint sort patch gives each slot a half-row trigger, and the
two end slots have no neighbouring row to borrow the other half from,
so they were only half of the edge row. The existing "easier dragging"
padding lived on :host, outside the `.task-list-inner` cdkDropList, so
it never grew the CDK hit-rect.

Move that padding inside the sub-task drop list so the strip above the
first and below the last subtask is part of the drop rect. CDK's
enter() then drops a task arriving there as the first/last child
(SingleAxisSortStrategy._shouldEnterAsFirstChild). The matching :host
padding is dropped for sub-lists so the visible gap stays compact.

* refactor(tasks): apply multi-review follow-ups to subtask drag

Hardening and fixes surfaced by the multi-agent review of the
drag-into-subtask feature:

- Scope the CDK midpoint sort patch to VERTICAL lists. It mutates the
  shared SingleAxisSortStrategy prototype app-wide, so horizontal lists
  (boards, issue panel) were silently swept into the new hit-test;
  gating to vertical keeps them on CDK's stock behaviour and sidesteps
  the right-to-left index-inversion question. Update the spec to assert
  the horizontal fall-back.
- Keep first-child re-parent in a sub-list's leading pad. The drop-target
  CSS fix moved the SUB list's top padding inside `.task-list-inner`, so
  part of the header→first-row strip now lives in the drop rect; report
  the leading pad as a row (re-parent) while the trailing pad stays a
  convert-to-main dead-band. Doc comment updated to match.
- Memoise the per-pointer subtask-list hit-test in DropListService so the
  several enterPredicate calls CDK fires per pointer move reuse one
  document.elementFromPoint.
- Tighten convertToSubTask op-log validation to isValidEntityId (rejects
  '' / 'undefined' / 'null') instead of a bare typeof string check.
- Log ids, not full task objects, in the drop handler (titles/notes must
  not reach the exportable log).
- Drop the dead _resetMidpointSortPatchForTests export; note why the
  seeded drag pointer is inert after a plain tap.
- Add direct unit tests for the canConvert/canApply guard pair.

* test(tasks): cover drop()-to-convert dispatch and the drag e2e

Close the two coverage gaps flagged in review:

- Unit: drive the public drop() handler with CdkDragDrop events and
  assert the resulting convertToSubTask / convertToMainTask dispatch,
  including the newIds→afterTaskId placement math (after-anchor,
  first-slot null anchor) and the isDone flag for the DONE list. This
  was previously untested — only _move() and the reducer were.
- E2E: a real CDK drag of a top-level task onto a subtask row, asserting
  it converts to a subtask. Uses the manual stepped-mouse gesture (CDK
  ignores HTML5 dragTo), matching work-view/sections.spec.ts. Verified
  green 3× in a row.

* fix(tasks): cut drop snap-back flicker by emitting default list sync

On drop, CDK tears down its preview/placeholder but never moves the real
DOM node (the list is NgRx-driven), so the un-moved task is briefly
revealed at its old slot until the store re-render lands. customizeUndone
Tasks() pushed *every* work-view list emission through
observeOn(animationFrameScheduler), adding a guaranteed extra frame to
that re-render and widening the visible snap-back.

Keep the frame-defer only for the customized (sort/group/filter) path —
it does heavier work and is driven by CD-bound signals, where a sync emit
can re-enter change detection. The default path is store-driven only, so
emit it on the same tick: the list re-renders without the extra frame and
the dropped task lands in place with far less (ideally no) snap-back.

task-view-customizer spec (43) green; drag-into-subtask e2e green.

* fix(tasks): scope CDK midpoint sort patch to task-list instances

Patch the dragged list's own SingleAxisSortStrategy instance instead of
the shared prototype. registerDropList is only called by task lists, so
the prototype mutation silently changed the hit-test of every other
vertical CDK list (planner, notes, boards, tree-dnd). Shadowing the
method per instance keeps the midpoint-crossing rule scoped to task
lists and leaves all other lists on CDK's stock behaviour.

The CDK-rename guard and 'this' binding are unchanged; the global
isPatched flag is dropped since each instance now patches independently.

* fix(sync): validate afterTaskId in convertToSubTask payload

The convertToSubTask op-log validation branch accepted any payload with
valid taskId/targetParentId, ignoring afterTaskId. Require it to be a
string or null (the action type's contract) so a crafted/malformed sync
payload is rejected at the validation boundary rather than treated as a
not-found anchor downstream.

* docs(tasks): clarify customizer frame-defer and drag-to-done intent

Comment-only. Correct the task-view-customizer note: the customized
path stays on the animation-frame scheduler to batch the signal-driven
emission burst on context switch (commit fddedf3fa6), not because a sync
emit re-enters change detection (the consumer is toSignal). Document
that dragging a subtask onto the DONE list intentionally converts it to
a main task done today, even outside the Today context.

* fix(tasks): keep midpoint sort patch alive across CDK strategy recreation

The previous commit scoped the patch by shadowing the method on the
strategy *instance*. That regressed subtask drag 100% of the time: CDK
rebuilds the SingleAxisSortStrategy on every drag start
(DropListRef.withOrientation runs in its beforeStarted hook), so the
instance shadow was discarded before the first pointer move and task
lists fell back to CDK's stock first-inside hit-test — the exact tall-
item preview misplacement the patch exists to prevent.

Patch the shared prototype instead (survives recreation) but apply the
midpoint rule only when the strategy's container is a registered task
list, delegating to CDK's original otherwise. This keeps the scoping
goal (planner/notes/boards/tree-dnd stay on stock behaviour) without the
recreation fragility. Extract the scoping dispatcher as a pure function
and unit-test it.

---------

Co-authored-by: kei <keletrh@gmail.com>
2026-06-02 19:43:41 +02:00
Johannes Millan
558e6a7664 test(e2e): drop dead onboarding preset-click fallback in fresh-start test
The 'app should handle fresh start correctly' test already suppresses the
first-run onboarding takeover via context.addInitScript(skipOnboardingForE2E),
which seeds SUP_ONBOARDING_PRESET_DONE so isShowOnboardingPresets stays false
and the side-nav is never hidden. The follow-up preset-card click was dead:
its isVisible({ timeout: 5000 }) guard is a no-op (Playwright ignores the
timeout on isVisible and returns immediately), and its comment wrongly claimed
the context skips the init script. Remove the fallback and the now-orphaned
[attr.e2e] preset-card hook (no other consumers). Verified test still passes.
2026-06-01 18:58:02 +02:00
Johannes Millan
cc63a566da test(e2e): handle onboarding takeover in fresh-start migration test
The 'app should handle fresh start correctly' test creates a genuinely
fresh context, so it skips the skipOnboardingForE2E init script the shared
page fixture injects and lands on the first-run onboarding preset screen.
Since the onboarding takeover hides magic-side-nav (visibility:hidden),
the side-nav visibility wait timed out. Dismiss onboarding by selecting a
preset before asserting the side-nav.

Add a stable [e2e] hook to the preset cards for the selector.
2026-06-01 18:24:58 +02:00
Johannes Millan
11e2eafcf3 test(e2e): skip onboarding in fresh-start migration test
The fresh-start test boots an unseeded browser context, so the first-run
onboarding preset screen took over. Since #7885 hides the side-nav during
onboarding (visibility:hidden), the test's wait for a visible magic-side-nav
timed out. Seed the onboarding-skip flag before boot like the shared fixture
does, keeping the fresh-empty-storage coverage while reaching the work view.
2026-06-01 18:24:58 +02:00
Johannes Millan
508998c6a1
Improve on sync (#7736)
* fix(android): restore share title derivation and dedupe shared tasks

Commit d32f7037a3 accidentally reverted the EXTRA_SUBJECT handling from
edb102534e, so the share handler stopped sending the page subject and
defaulted the title to the literal "Shared Content". The frontend's
subject -> title -> derived title chain then always fell through to that
placeholder, producing blank-looking shared tasks.

- Restore EXTRA_SUBJECT extraction; leave title/subject empty when absent
  so the frontend can derive a meaningful title from the URL or note.
- Ignore empty/blank shared text instead of creating a useless task.
- Skip handleIntent() on Activity recreation (config change) so the same
  share Intent isn't re-processed into a duplicate task.
- Extract buildTaskTitle/readableUrl as pure functions with unit tests
  and guard the effect against empty payloads.

* fix(snack): scale error/warning snack duration with message length

Long error messages (e.g. multi-sentence sync errors) were auto-dismissed
after a fixed 8s, too short to read. Error/warning snacks now stay visible
proportional to message length (~90ms/char), clamped to 10-30s.

* feat(tasks): skip undo snack when deleting a blank task

A sub task or parent task with an empty title and no data (notes, time,
estimate, attachments, issue link, reminder, repeat, scheduling,
deadline, non-blank sub tasks) no longer shows the undo-delete snack.

* fix(sync): include archive data in REPAIR operations

validateAndRepairCurrentState built the REPAIR op from the synchronous
getStateSnapshot(), which hardcodes empty archiveYoung/archiveOld
(archives live in IndexedDB, not NgRx state). The resulting REPAIR op
carried empty archives, so every other client that applied it
overwrote its archive with nothing — wiping archived tasks on all
devices except the one that ran the repair.

- Use getStateSnapshotAsync() so the REPAIR op carries real archives.
- Extend the empty-archive overwrite guard in
  ArchiveOperationHandler._handleLoadAllData() to also cover OpType.Repair
  (previously only SYNC_IMPORT/BACKUP_IMPORT), as defense in depth.

* test(sync): add archive REPAIR round-trip integration test

Wires the real StateSnapshotService, ArchiveDbAdapter, ArchiveStoreService
and ArchiveOperationHandler against real IndexedDB to verify archive data
survives the REPAIR-op round-trip:

- getStateSnapshotAsync() loads IndexedDB archives; getStateSnapshot() does not
- archive round-trips from client A's IndexedDB through a REPAIR op into a
  fresh client B's IndexedDB
- a REPAIR op carrying empty archives no longer wipes a client that has
  archive data (empty-archive guard regression)

* perf(sync): skip archive IndexedDB reads when post-sync state is valid

validateAndRepairCurrentState validated the full async snapshot (two
IndexedDB archive reads + structured-clone deserialization) on every
Checkpoint D, even when state was valid and no repair was needed. It now
validates the cheap synchronous snapshot first and only loads the async
snapshot (with archives) when a repair is actually required — the rare
path. The REPAIR op still carries archive data.

Also addresses multi-review follow-ups:
- archive-operation-handler: reword the empty-archive guard comment so it
  no longer over-promises reconciliation for REPAIR ops.
- archive-repair-roundtrip test: add isPersistent to the applied-op meta
  to match the real applier; scope the file docstring accurately.

* fix(task-repeat-cfg): schedule inbox task for today when made recurring

When an Inbox task (no dueDay) was made repeatable via the dialog with a
recurrence starting today, it stayed unscheduled. The TODAY-first-occurrence
branch of updateTaskAfterMakingItRepeatable$ derived currentDueDay from
task.created as a fallback, so a task created today looked already scheduled
and dueDay was never set.

Key the decision on task.dueDay directly. Skip timed tasks and tasks that
already have dueWithTime, since dueDay/dueWithTime are mutually exclusive and
timed scheduling is handled by addRepeatCfgToTaskUpdateTask$.

Closes #7725

* fix(tasks): correct monthly first/last-day recurrence anchoring

The "Every month on the first day" and "Every month on the last day"
quick settings scheduled the first task instance in the past. Both
presets produced a backdated startDate (1st of the current month;
hardcoded January 31), which getFirstRepeatOccurrence returns verbatim
for monthly recurrences.

- MONTHLY_FIRST_DAY now anchors startDate to the next 1st-of-month
  that is today or later.
- MONTHLY_LAST_DAY anchors startDate to the current month's last day
  and sets a new monthlyLastDay flag, so the occurrence engine clamps
  to month-end every month regardless of startDate's day-of-month.
- _normalizeMonthlyAnchor strips a stale monthlyLastDay flag when a
  config leaves the preset (CUSTOM mode has no control for it).

Closes #7726

* fix(task-repeat-cfg): re-anchor start date after instance deleted

When the user moved a repeat config's startDate earlier after deleting
its only live task instance, the stale lastTaskCreationDay anchor kept
suppressing every projected/created instance between the new startDate
and the old anchor.

rescheduleTaskOnRepeatCfgUpdate$ only re-anchored lastTaskCreationDay
when a live task instance existed (the #7423 fix) — it returned early
before the re-anchoring when there was none. Hoist the
isStartDateMovedEarlier detection above that early return: when no live
instance exists but startDate moved earlier, re-anchor to the day
before the new first occurrence so it and every following day is
created and projected fresh.

Closes #7724

* test(task-repeat-cfg): cover startDate re-anchor with no live instance

Add coverage for the #7724 fix beyond the effect unit test:

- Selector integration tests: feed a config re-anchored to the day
  before the new startDate through selectTaskRepeatCfgsForExactDay and
  assert it projects the new startDate and every following day, while
  still excluding the anchor day and earlier. Also documents that the
  stale anchor suppresses the gap days.
- E2E reproduction (recurring-move-start-date-earlier-no-instance):
  create a recurring task, delete its live instance, move startDate
  earlier via a transparent projection, and assert the new days appear
  in the planner. Verified to fail on pre-fix code.

* feat(sync): move clientId from pf into SUP_OPS for atomic rotation

Migrate the sync clientId out of the legacy `pf` IndexedDB database into
`SUP_OPS` (new `client_id` store, schema v6). The clientId write now joins
the atomic transaction in `runDestructiveStateReplacement`, so destructive
flows (clean-slate, backup-restore) rotate it atomically with
OPS/STATE_CACHE/VECTOR_CLOCK instead of a hand-rolled cross-database
two-phase commit.

- ClientIdService rewritten: SUP_OPS-backed via an independent connection,
  inline one-time pf->SUP_OPS migration, error-aware resolver. Read
  failures propagate (getOrGenerateClientId never mints a fresh id over a
  transient error — that would orphan the device's non-regenerable
  identity); loadClientId never throws.
- Delete withRotation, generateNewClientId and the CAS/rollback machinery.
- Extract pure generateClientId() + isValidClientIdFormat() into
  core/util/generate-client-id.ts.
- pf becomes a read-only, one-time migration source (never written/deleted).

Closes #7732

* fix(sync): dedup SUP_OPS connection open in ClientIdService

Address multi-agent review findings on the clientId migration:

- _getSupOpsDb() shares a single in-flight open via _supOpsDbPromise;
  concurrent cold-start callers previously each opened their own
  SUP_OPS connection, leaking all but the last.
- _putClientIdIfAbsent() collapsed to a single tx.done / exit point.
- db-upgrade.spec.ts: cover the v6 client_id store; the createObjectStore
  count assertions were stale and failing after the schema bump.
- operation-log-migration.service.ts: correct a misleading comment about
  the genesis-op clientId fallback.

* refactor(sync): align ClientIdService SUP_OPS open with in-house idiom

Re-review of the connection-leak fix recommended matching
OperationLogStoreService._ensureInit's pattern:

- _getSupOpsDb() clears the in-flight promise in .catch (failure only)
  instead of an unconditional finally — the resolved handle lives in
  _supOpsDb, so the promise field is pure in-flight coordination state.
- close/versionchange handlers now also null _supOpsDbPromise, so a
  stale (closed) connection is never re-handed-out.
- Add a regression test asserting _openSupOpsDb runs exactly once for
  concurrent cold-start callers.

* docs(sync): link the single-connection follow-up to #7735

Reference the tracked follow-up issue from the ClientIdService JSDoc
and the plan's out-of-scope section, so the deliberate trade-off (one
extra SUP_OPS connection) is traceable rather than forgotten.

* test(sync): update ClientIdService spies for getOrGenerateClientId

The op-log capture effect now resolves the clientId via
getOrGenerateClientId() (was loadClientId() ?? generateNewClientId()).
These two specs still mocked only loadClientId, so the effect called an
undefined method, captured no op, and 4 tests failed in the full suite.

- task-done-replay.integration.spec.ts
- operation-log-lock-reentry.regression.spec.ts

* test(sync): open SUP_OPS versionless in e2e read helpers

DB_VERSION was bumped 5->6; five e2e helpers still opened SUP_OPS at
the hardcoded old version 5 to read state after the app had already
upgraded it to v6, throwing VersionError. Open versionless instead —
matches the ~10 other e2e files that already do, and is future-proof
against the next schema bump.

- migration/legacy-data-migration.spec.ts (x2)
- sync/supersync-legacy-migration-sync.spec.ts
- sync/webdav-legacy-migration-sync.spec.ts
- recurring/invalid-clock-string-bug-7067.spec.ts
2026-05-22 17:49:25 +02:00
Johannes Millan
dbafa8d0a0 test(e2e): stabilize task visibility specs 2026-05-13 16:27:31 +02:00
Johannes Millan
99b7dee74a fix(backup): move shared timestamp util into electron/ for snap packaging
electron/backup.ts imported getBackupTimestamp from src/app/util/, which
tsc compiled alongside the source. electron-builder only packages
electron/** and the Angular renderer bundle, so the compiled util was
missing from app.asar and the main process crashed on launch with
"Cannot find module '../src/app/util/get-backup-timestamp'".

Moved the util to electron/shared-with-frontend/ (existing convention
for code shared between main and renderer), updated all importers.

Fixes #7270
Refs #7315, #7323, #7265, #7320
2026-04-22 16:14:56 +02:00
novikov1337danil
1abb15b804
refactor(backup): unify backup filename format (#7141)
* refactor(backup): implement consistent timestamp for backup filenames

* refactor(i18n): rename PRIVACY_EXPORT key to PRIVACY_EXPORT_DESCRIPTION

* refactor(i18n): add PRIVACY_EXPORT key for data export in multiple languages

* refactor(backup): standardize backup filename prefix

* refactor(backup): update backup filename format to use underscore separator

* refactor(i18n): update translations

* refactor(backup): move getBackupTimestamp utility to a shared location for consistency

* test(backup): add unit tests for getBackupTimestamp utility

* fix(e2e): move MIGRATION_BACKUP_PREFIX to migration.const

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor(tests): move jasmine.clock().install() to beforeEach for consistency

* refactor(backup): consolidate backup filename constants and update imports

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 14:09:09 +02:00
Johannes Millan
2f8d871042 feat(shepherd): remove all tours except keyboard navigation
Remove the full welcome tour (Welcome, AddTask, DeleteTask, Projects,
Sync, IssueProviders, ProductivityHelper, FinalCongrats, StartTourAgain)
and keep only the KeyboardNav tour, triggered from the Help menu.

- Delete ShepherdComponent (auto-start on load)
- Strip shepherd-steps.const.ts to KeyboardNav steps only
- Simplify ShepherdService with _initPromise guard and fresh tour on re-open
- Remove 3 tour menu items from Help menu, keep keyboard tour
- Remove waitForEl/waitForElObs$ helpers (unused by KeyboardNav)
- Delete e2e/utils/tour-helpers.ts and all tour dismissal calls
- Clean up stale tour comments across e2e tests
- Fix pre-existing build error: add isFinishDayEnabled to AppFeaturesConfig
  type, defaults, form toggle, component signal, and translations
2026-03-22 17:00:42 +01:00
overcuriousity
dc7742d354
fix: profile system bugs (#6614)
* fixes to the profile system storage which worked unreliable across multiple storage backends

* fix tests

* fix issues with tests

* address copilot reviews
2026-02-23 17:43:04 +01:00
Johannes Millan
b834bbedb1 test(e2e): fix 12 flaky tests by removing networkidle waits and fixing plugin setup
Replace unreliable waitForLoadState('networkidle') with element-based waits
in planner page and tests. Fix plugin helper timeout math bugs and eliminate
duplicate settings navigation in plugin tests. Improve menu overlay cleanup
and increase tight timeouts in migration and work-view tests.
2026-02-09 17:55:12 +01:00
Johannes Millan
851055b4a3 test(e2e): fix flaky tests and improve timeout handling
Fixed 3 failing focus mode break tests and 1 data persistence test by
addressing timeout and wait logic issues under parallel test execution.

Changes:
- Focus mode break tests: Increased timeouts for countdown (10s→15s) and
  session start (10s→20s) to handle load during full test suite execution
- Pre-migration dialog: Unskipped data persistence test and adopted working
  pattern from work-view.spec.ts with proper stale element handling
- WebDAV archive sync: Updated investigation notes with additional timeout
  attempts (still requires deep profiling for main thread blocking issue)

Test results:
- Standard tests: 197 passed (was 193), 0 failed (was 3), 2 skipped
- WebDAV tests: 35 passed, 1 skipped (92% pass rate)
- SuperSync tests: 125 passed, 1 skipped, 1 flaky

All tests now pass with 99% overall pass rate across all suites.
2026-01-22 21:29:25 +01:00
Johannes Millan
eeb19a351f test(e2e): add legacy data migration E2E test
Add comprehensive E2E test that verifies legacy data (pre-operation-log
format) migrates correctly when the app starts. The test:

- Seeds legacy 'pf' IndexedDB database with test data before app loads
- Verifies backup file is downloaded during migration
- Validates all data migrates correctly including:
  - Tasks with subtasks (parent-child relationships)
  - Projects and tags
  - Sync provider settings (WebDAV/SuperSync configs)
  - Archive data (archiveYoung and archiveOld)
  - Time tracking data and notes

Includes comprehensive fixture file with realistic legacy state format.
2026-01-09 12:58:58 +01:00
Johannes Millan
6a26d914b5 test: skip tmp 2025-12-24 16:27:12 +01:00
Johannes Millan
72608a4aaa feat(migration): implement pre-migration dialog and backup functionality 2025-12-24 13:49:37 +01:00
Johannes Millan
1bd96a9182 fix(sync): refresh storage cache after cleanup and add serverTime to response
Address two issues identified in code review:

1. Storage cache not refreshed after cleanup:
   - Modified deleteOldSyncedOpsForAllUsers() to return { totalDeleted, affectedUserIds }
   - Cleanup job now calls updateStorageUsage() for each affected user
   - Prevents stale quota checks after nightly cleanup

2. serverTime missing from download response:
   - Added serverTime: Date.now() to DownloadOpsResponse
   - Enables client-side clock drift detection

Tests:
- Added serverTime response test in sync-fixes.spec.ts
- Added deleteOldSyncedOpsForAllUsers return structure tests
- Updated legacy SQLite tests for new return type (excluded from CI)
2025-12-23 18:31:57 +01:00