diff options
Diffstat (limited to 'macos/Sources/Features/Update')
| -rw-r--r-- | macos/Sources/Features/Update/UpdateSimulator.swift | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/macos/Sources/Features/Update/UpdateSimulator.swift b/macos/Sources/Features/Update/UpdateSimulator.swift new file mode 100644 index 000000000..0cf2d221b --- /dev/null +++ b/macos/Sources/Features/Update/UpdateSimulator.swift @@ -0,0 +1,272 @@ +import Foundation +import Sparkle + +/// Simulates various update scenarios for testing the update UI. +/// +/// The expected usage is by overriding the `checkForUpdates` function in AppDelegate and +/// calling one of these instead. This will allow us to test the update flows without having to use +/// real updates. +enum UpdateSimulator { + /// Complete successful update flow: checking → available → download → extract → ready → install → idle + case happyPath + + /// No updates available: checking (2s) → "No Updates Available" (3s) → idle + case notFound + + /// Error during check: checking (2s) → error with retry callback + case error + + /// Slower download for testing progress UI: checking → available → download (20 steps, ~10s) → extract → install + case slowDownload + + /// Initial permission request flow: shows permission dialog → proceeds with happy path if accepted + case permissionRequest + + /// User cancels during download: checking → available → download (5 steps) → cancels → idle + case cancelDuringDownload + + /// User cancels while checking: checking (1s) → cancels → idle + case cancelDuringChecking + + func simulate(with viewModel: UpdateViewModel) { + switch self { + case .happyPath: + simulateHappyPath(viewModel) + case .notFound: + simulateNotFound(viewModel) + case .error: + simulateError(viewModel) + case .slowDownload: + simulateSlowDownload(viewModel) + case .permissionRequest: + simulatePermissionRequest(viewModel) + case .cancelDuringDownload: + simulateCancelDuringDownload(viewModel) + case .cancelDuringChecking: + simulateCancelDuringChecking(viewModel) + } + } + + private func simulateHappyPath(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { choice in + if choice == .install { + simulateDownload(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + } + + private func simulateNotFound(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .notFound + + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + viewModel.state = .idle + } + } + } + + private func simulateError(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .error(.init( + error: NSError(domain: "UpdateError", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Failed to check for updates" + ]), + retry: { + simulateHappyPath(viewModel) + } + )) + } + } + + private func simulateSlowDownload(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { choice in + if choice == .install { + simulateSlowDownloadProgress(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + } + + private func simulateSlowDownloadProgress(_ viewModel: UpdateViewModel) { + let download = UpdateState.Downloading( + cancel: { + viewModel.state = .idle + }, + expectedLength: nil, + progress: 0 + ) + viewModel.state = .downloading(download) + + for i in 1...20 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.5) { + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 2000, + progress: UInt64(i * 100) + ) + viewModel.state = .downloading(updatedDownload) + + if i == 20 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + simulateExtract(viewModel) + } + } + } + } + } + + private func simulatePermissionRequest(_ viewModel: UpdateViewModel) { + let request = SPUUpdatePermissionRequest(systemProfile: []) + viewModel.state = .permissionRequest(.init( + request: request, + reply: { response in + if response.automaticUpdateChecks { + simulateHappyPath(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + + private func simulateCancelDuringDownload(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { choice in + if choice == .install { + simulateDownloadThenCancel(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + } + + private func simulateDownloadThenCancel(_ viewModel: UpdateViewModel) { + let download = UpdateState.Downloading( + cancel: { + viewModel.state = .idle + }, + expectedLength: nil, + progress: 0 + ) + viewModel.state = .downloading(download) + + for i in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 1000, + progress: UInt64(i * 100) + ) + viewModel.state = .downloading(updatedDownload) + + if i == 5 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.state = .idle + } + } + } + } + } + + private func simulateCancelDuringChecking(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + viewModel.state = .idle + } + } + + private func simulateDownload(_ viewModel: UpdateViewModel) { + let download = UpdateState.Downloading( + cancel: { + viewModel.state = .idle + }, + expectedLength: nil, + progress: 0 + ) + viewModel.state = .downloading(download) + + for i in 1...10 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 1000, + progress: UInt64(i * 100) + ) + viewModel.state = .downloading(updatedDownload) + + if i == 10 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + simulateExtract(viewModel) + } + } + } + } + } + + private func simulateExtract(_ viewModel: UpdateViewModel) { + viewModel.state = .extracting(.init(progress: 0.0)) + + for j in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { + viewModel.state = .extracting(.init(progress: Double(j) / 5.0)) + + if j == 5 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.state = .readyToInstall(.init( + reply: { choice in + if choice == .install { + viewModel.state = .installing + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .idle + } + } else { + viewModel.state = .idle + } + } + )) + } + } + } + } + } +} |
