Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Xcodes/Backend/AppState+Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ extension AppState {
let (xcode, url) = try await getXcodeArchiveAsync(installationType, downloader: downloader)
try Task.checkCancellation()
let installedXcode = try await installArchivedXcodeAsync(xcode, at: url)
// Refreshed before the row is patched: `updateAllXcodes` rebuilds every row from this cache
// on the next selection or update, carrying over only rows that are still `.installing`.
await updateInstalledXcodesAsync(recomposeAllXcodes: false)

guard let index = allXcodes.firstIndex(where: { $0.version.isEquivalent(to: installedXcode.version) }) else {
return installedXcode
Expand Down
3 changes: 2 additions & 1 deletion Xcodes/Backend/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -727,8 +727,8 @@ class AppState: ObservableObject {
try Task.checkCancellation()
try await uninstallXcodeAsync(path: installedXcodePath)
try Task.checkCancellation()
await updateSelectedXcodePathAsync()
await updateInstalledXcodesAsync()
await updateSelectedXcodePathAsync()
} catch is CancellationError {
} catch {
if let index = allXcodes.firstIndex(where: { $0.id == xcode.id }) {
Expand Down Expand Up @@ -798,6 +798,7 @@ class AppState: ObservableObject {

try await Current.helper.switchXcodePathAsync(installedXcodePath.string)
try Task.checkCancellation()
await updateInstalledXcodesAsync()
await updateSelectedXcodePathAsync()
if createSymLinkOnSelect && onSelectActionType != .rename {
await createSymbolicLink(to: installedXcodePath)
Expand Down
54 changes: 54 additions & 0 deletions XcodesTests/AppStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,29 @@ class AppStateTests: XCTestCase {
XCTAssertEqual(subject.selectedXcodePath, secondPath.string)
}

func test_Select_MarksXcodeInstalledSinceLastScanAsSelected() async throws {
subject.helperInstallState = .installed
let installedPath = try XCTUnwrap(Path("/Applications/Xcode-27.2.0.app"))
let version = try XCTUnwrap(Version("27.2.0"))
let installedXcode = InstalledXcode(path: installedPath, version: version)
let developerPath = (installedPath/"Contents/Developer").string
subject.availableXcodes = [
AvailableXcode(version: version, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil)
]
subject.allXcodes[0].installState = .installed(installedPath)
Current.files.installedXcodes = { _ in [installedXcode] }
Current.shell.xcodeSelectPrintPath = {
ProcessOutput(status: 0, out: developerPath, err: "")
}

subject.select(xcode: subject.allXcodes[0], shouldPrepareUserForHelperInstallation: false)
let selectTask = try XCTUnwrap(subject.selectTask)
await selectTask.value

XCTAssertEqual(subject.allXcodes[0].installState, .installed(installedPath))
XCTAssertTrue(subject.allXcodes[0].selected)
}

func test_Uninstall_MissingXcodePresentsFileNotFoundError() async throws {
let missingPath = try XCTUnwrap(Path("/Applications/Xcode-Missing.app"))
let xcode = Xcode(version: Version("15.0.0")!, installState: .installed(missingPath), selected: false, icon: nil)
Expand Down Expand Up @@ -411,6 +434,37 @@ class AppStateTests: XCTestCase {
XCTAssertEqual(subject.allXcodes[0].installState, .notInstalled)
}

func test_Uninstall_DoesNotShowRemovedXcodeAsInstalledAgain() async throws {
let installedPath = try XCTUnwrap(Path("/Applications/Xcode-0.0.0.app"))
let version = try XCTUnwrap(Version("0.0.0"))
let installedXcode = InstalledXcode(path: installedPath, version: version)
subject.availableXcodes = [
AvailableXcode(version: version, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil)
]
Current.files.installedXcodes = { _ in [installedXcode] }
await subject.updateInstalledXcodesAsync()
Current.files.installedXcodes = { _ in [] }
Current.shell.xcodeSelectPrintPath = {
ProcessOutput(status: 0, out: "", err: "")
}

let allXcodeInstallStates = try await recordAllXcodeInstallStates {
subject.uninstall(xcode: subject.allXcodes[0])
let uninstallTask = try XCTUnwrap(subject.uninstallTask)
await uninstallTask.value
}

XCTAssertEqual(
allXcodeInstallStates,
[
[XcodeInstallState.installed(installedPath)],
[.uninstalling(installedPath)],
[.notInstalled],
[.notInstalled]
]
)
}

func test_Signout_RemovesCookiesFromDownloadSession() throws {
let session = URLSession(configuration: .ephemeral)
Current.network.session = session
Expand Down
Loading