From 492cdb531ab27e325f927267fbe433e8c68ca9b4 Mon Sep 17 00:00:00 2001 From: Mohammad Shahibzadeh Date: Sat, 31 Oct 2026 11:26:02 +0400 Subject: [PATCH] Fix download progress indicators after task replacement --- Xcodes.xcodeproj/project.pbxproj | 4 ++ .../InfoPane/InstallationStepDetailView.swift | 1 + .../RuntimeInstallationStepDetailView.swift | 1 + .../XcodeList/InstallationStepRowView.swift | 1 + .../InstallationProgressViewTests.swift | 58 +++++++++++++++++++ 5 files changed, 65 insertions(+) create mode 100644 XcodesTests/InstallationProgressViewTests.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index e8e0ad6a..52ea009a 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + AF29174092FE4E0F921AB001 /* InstallationProgressViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF29174092FE4E0F921AB002 /* InstallationProgressViewTests.swift */; }; 14d2f5a1273f6c350cad4406 /* NewVersionNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884f01aed2f43048ab4d3323 /* NewVersionNotificationTests.swift */; }; 1596C2913043765600178C86 /* FileOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1596C2903043765600178C86 /* FileOperations.swift */; }; 1596C2923043765600178C86 /* FileOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1596C2903043765600178C86 /* FileOperations.swift */; }; @@ -188,6 +189,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + AF29174092FE4E0F921AB002 /* InstallationProgressViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationProgressViewTests.swift; sourceTree = ""; }; 1596C2903043765600178C86 /* FileOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperations.swift; sourceTree = ""; }; 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; }; 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = ""; }; @@ -598,6 +600,7 @@ isa = PBXGroup; children = ( CA452BBD259FDDBF0072DFA4 /* Fixtures */, + AF29174092FE4E0F921AB002 /* InstallationProgressViewTests.swift */, CAC281E1259FA44600B8AB0B /* Bundle+XcodesTests.swift */, CAC281E6259FA45A00B8AB0B /* Environment+Mock.swift */, CAD2E7B72449575100113D76 /* AppStateTests.swift */, @@ -980,6 +983,7 @@ buildActionMask = 2147483647; files = ( CAC281E7259FA45A00B8AB0B /* Environment+Mock.swift in Sources */, + AF29174092FE4E0F921AB001 /* InstallationProgressViewTests.swift in Sources */, CAC281E2259FA44600B8AB0B /* Bundle+XcodesTests.swift in Sources */, CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */, 14d2f5a1273f6c350cad4406 /* NewVersionNotificationTests.swift in Sources */, diff --git a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift index e4bb64ff..f42a7068 100644 --- a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift +++ b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift @@ -16,6 +16,7 @@ struct InstallationStepDetailView: View { style: .bar, showsAdditionalDescription: true ) + .id(ObjectIdentifier(progress)) case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing: ProgressView() diff --git a/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift index 251c6bdb..d4c9d4c7 100644 --- a/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift +++ b/Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift @@ -24,6 +24,7 @@ struct RuntimeInstallationStepDetailView: View { style: .bar, showsAdditionalDescription: true ) + .id(ObjectIdentifier(progress)) case .installing, .trashingArchive: ObservingProgressIndicator( diff --git a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift index 1d605fa0..2ab378e3 100644 --- a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift +++ b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift @@ -18,6 +18,7 @@ struct InstallationStepRowView: View { controlSize: .small, style: .spinning ) + .id(ObjectIdentifier(progress)) case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing: ProgressView() .scaleEffect(0.5) diff --git a/XcodesTests/InstallationProgressViewTests.swift b/XcodesTests/InstallationProgressViewTests.swift new file mode 100644 index 00000000..5e1d43b2 --- /dev/null +++ b/XcodesTests/InstallationProgressViewTests.swift @@ -0,0 +1,58 @@ +import Cocoa +import SwiftUI +import XCTest + +@testable import Xcodes + +@MainActor +final class InstallationProgressViewTests: XCTestCase { + func testXcodeDetailUsesReplacementDownloadProgress() { + assertUsesReplacementProgress { progress in + InstallationStepDetailView(installationStep: .downloading(progress: progress)) + } + } + + func testXcodeRowUsesReplacementDownloadProgress() { + assertUsesReplacementProgress { progress in + InstallationStepRowView( + installationStep: .downloading(progress: progress), + highlighted: false, + cancel: {} + ) + } + } + + func testRuntimeDetailUsesReplacementDownloadProgress() { + assertUsesReplacementProgress { progress in + RuntimeInstallationStepDetailView(installationStep: .downloading(progress: progress)) + } + } + + private func assertUsesReplacementProgress( + @ViewBuilder content: (Progress) -> Content, + file: StaticString = #filePath, + line: UInt = #line + ) { + let original = Progress(totalUnitCount: 100) + original.completedUnitCount = 6 + let hostingView = NSHostingView(rootView: content(original)) + hostingView.frame = NSRect(x: 0, y: 0, width: 420, height: 180) + hostingView.layoutSubtreeIfNeeded() + XCTAssertEqual(progressIndicator(in: hostingView)?.doubleValue, 0.06, file: file, line: line) + + // A resumed download supplies a new Progress while remaining in the downloading stage. + let replacement = Progress(totalUnitCount: 100) + replacement.completedUnitCount = 75 + hostingView.rootView = content(replacement) + hostingView.layoutSubtreeIfNeeded() + + XCTAssertEqual(progressIndicator(in: hostingView)?.doubleValue, 0.75, file: file, line: line) + } + + private func progressIndicator(in view: NSView) -> NSProgressIndicator? { + if let indicator = view as? NSProgressIndicator { + return indicator + } + return view.subviews.lazy.compactMap { self.progressIndicator(in: $0) }.first + } +}