diff options
Diffstat (limited to 'macos/Sources')
| -rw-r--r-- | macos/Sources/App/macOS/AppDelegate.swift | 76 | ||||
| -rw-r--r-- | macos/Sources/Features/Update/UpdateSimulator.swift | 272 |
2 files changed, 273 insertions, 75 deletions
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 4fd6dfb3f..a9f72b58b 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -1008,82 +1008,8 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - // Demo mode: simulate update check with new UpdateState - updateViewModel.state = .checking(.init(cancel: { [weak self] in - self?.updateViewModel.state = .idle - })) - - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in - guard let self else { return } - - self.updateViewModel.state = .updateAvailable(.init( - appcastItem: SUAppcastItem.empty(), - reply: { [weak self] choice in - if choice == .install { - self?.simulateDownload() - } else { - self?.updateViewModel.state = .idle - } - } - )) - } - } - - private func simulateDownload() { - let download = UpdateState.Downloading( - cancel: { [weak self] in - self?.updateViewModel.state = .idle - }, - expectedLength: nil, - progress: 0, - ) - updateViewModel.state = .downloading(download) - - for i in 1...10 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { [weak self] in - let updatedDownload = UpdateState.Downloading( - cancel: download.cancel, - expectedLength: 1000, - progress: UInt64(i * 100) - ) - self?.updateViewModel.state = .downloading(updatedDownload) - - if i == 10 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.simulateExtract() - } - } - } - } - } - - private func simulateExtract() { - updateViewModel.state = .extracting(.init(progress: 0.0)) - - for j in 1...5 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { [weak self] in - self?.updateViewModel.state = .extracting(.init(progress: Double(j) / 5.0)) - - if j == 5 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.updateViewModel.state = .readyToInstall(.init( - reply: { [weak self] choice in - if choice == .install { - self?.updateViewModel.state = .installing - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in - self?.updateViewModel.state = .idle - } - } else { - self?.updateViewModel.state = .idle - } - } - )) - } - } - } - } + UpdateSimulator.notFound.simulate(with: updateViewModel) } - @IBAction func newWindow(_ sender: Any?) { 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 + } + } + )) + } + } + } + } + } +} |
