Improve draft saving mechanism to start timer on first edit

Modified AutoSaveViewModel.needsSaving() to not reset the timer if already
counting down. This ensures the timer starts when the user begins typing and
continues counting even if they keep typing continuously, leading to auto-save
every few seconds instead of waiting for the user to stop typing.

Added automated tests for the new behavior.

Fixes the issue where drafts would only save after user stops typing,
potentially leading to data loss if the app is closed too quickly.

Closes: https://github.com/damus-io/damus/issues/3164
Changelog-Changed: Improved draft saving feature to prevent data loss if app closes too quickly
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2025-12-10 15:41:10 -08:00
parent 674d4683c3
commit 9eda7e5886
3 changed files with 136 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
//
// AutoSaveViewModelTests.swift
// damusTests
//
// Created by Daniel D'Aquino on 2025-12-10.
//
import XCTest
@testable import damus
@MainActor
final class AutoSaveViewModelTests: XCTestCase {
func testTimerStartsOnFirstEdit() async throws {
// Given
var saveCount = 0
let viewModel = AutoSaveIndicatorView.AutoSaveViewModel(
save: { saveCount += 1 },
saveDelay: 2
)
// When - user starts typing
viewModel.needsSaving()
// Then - timer should be started
if case .needsSaving(let secondsRemaining) = viewModel.savedState {
XCTAssertEqual(secondsRemaining, 2)
} else {
XCTFail("Expected needsSaving state")
}
}
func testTimerDoesNotResetOnContinuousTyping() async throws {
// Given
var saveCount = 0
let viewModel = AutoSaveIndicatorView.AutoSaveViewModel(
save: { saveCount += 1 },
saveDelay: 3
)
// When - user starts typing
viewModel.needsSaving()
// Verify initial state
if case .needsSaving(let secondsRemaining) = viewModel.savedState {
XCTAssertEqual(secondsRemaining, 3)
} else {
XCTFail("Expected needsSaving state")
}
// Simulate timer countdown by waiting a bit
try await Task.sleep(nanoseconds: 1_500_000_000) // 1.5 seconds
// When - user continues typing (timer should be around 1-2 seconds now)
viewModel.needsSaving()
// Then - timer should NOT reset to 3 seconds
if case .needsSaving(let secondsRemaining) = viewModel.savedState {
XCTAssertLessThan(secondsRemaining, 3, "Timer should not reset on continuous typing")
} else {
XCTFail("Expected needsSaving state")
}
}
func testTimerRestartsAfterSave() async throws {
// Given
var saveCount = 0
let viewModel = AutoSaveIndicatorView.AutoSaveViewModel(
save: {
saveCount += 1
},
saveDelay: 1
)
// When - user starts typing
viewModel.needsSaving()
// Wait for save to complete
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
// Then - should have saved
XCTAssertEqual(saveCount, 1)
XCTAssertEqual(viewModel.savedState, .saved)
// When - user types again after save
viewModel.needsSaving()
// Then - timer should start again
if case .needsSaving(let secondsRemaining) = viewModel.savedState {
XCTAssertEqual(secondsRemaining, 1)
} else {
XCTFail("Expected needsSaving state after typing post-save")
}
}
func testAutoSaveEveryFewSecondsWithContinuousTyping() async throws {
// Given
var saveCount = 0
let viewModel = AutoSaveIndicatorView.AutoSaveViewModel(
save: {
saveCount += 1
},
saveDelay: 1
)
// When - user starts typing
viewModel.needsSaving()
// Simulate continuous typing every 0.5 seconds for 5 seconds
for _ in 0..<10 {
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
viewModel.needsSaving()
}
// Then - should have saved multiple times
XCTAssertGreaterThan(saveCount, 1, "Should auto-save multiple times with continuous typing")
}
}