feat: 支持模拟器定位与地图 fallback

This commit is contained in:
2026-07-24 10:04:55 +08:00
parent 0e0415eec4
commit f87b13cc05
24 changed files with 4040 additions and 2161 deletions

View File

@ -0,0 +1,204 @@
//
// CoreLocationProviderTests.swift
// suixinkanTests
//
import CoreLocation
import XCTest
@testable import suixinkan
/// Core Location fallback
@MainActor
final class CoreLocationProviderTests: XCTestCase {
func testPrivacyAgreementIsRequiredBeforeLocation() async {
let context = makeContext(privacyAccepted: false)
await assertLocationError(.privacyNotAccepted) {
_ = try await context.provider.requestCoordinate()
}
XCTAssertEqual(context.manager.requestLocationCallCount, 0)
}
func testDeniedAuthorizationReturnsPermissionError() async {
let context = makeContext(authorizationStatus: .denied)
await assertLocationError(.permissionDenied) {
_ = try await context.provider.requestCoordinate()
}
XCTAssertEqual(context.manager.requestLocationCallCount, 0)
}
func testNotDeterminedAuthorizationContinuesAfterGrant() async throws {
let coordinate = CLLocationCoordinate2D(latitude: 30.2741, longitude: 120.1551)
let context = makeContext(
authorizationStatus: .notDetermined,
requestedAuthorizationStatus: .authorizedWhenInUse,
locationResult: .success(CLLocation(
latitude: coordinate.latitude,
longitude: coordinate.longitude
))
)
let result = try await context.provider.requestCoordinate(
desiredAccuracy: kCLLocationAccuracyHundredMeters
)
XCTAssertEqual(context.manager.requestAuthorizationCallCount, 1)
XCTAssertEqual(context.manager.requestLocationCallCount, 1)
XCTAssertEqual(context.manager.desiredAccuracy, kCLLocationAccuracyHundredMeters)
XCTAssertEqual(result.latitude, coordinate.latitude, accuracy: 0.000_001)
XCTAssertEqual(result.longitude, coordinate.longitude, accuracy: 0.000_001)
}
func testLocationFailureUsesExistingBusinessError() async {
let context = makeContext(locationResult: .failure)
await assertLocationError(.locationFailed) {
_ = try await context.provider.requestCoordinate()
}
}
func testSnapshotContainsReverseGeocodedAddress() async throws {
let context = makeContext(
locationResult: .success(CLLocation(latitude: 31.2304, longitude: 121.4737)),
geocodedAddress: "上海市黄浦区"
)
let snapshot = try await context.provider.requestSnapshot()
XCTAssertEqual(snapshot.latitude, 31.2304, accuracy: 0.000_001)
XCTAssertEqual(snapshot.longitude, 121.4737, accuracy: 0.000_001)
XCTAssertEqual(snapshot.address, "上海市黄浦区")
XCTAssertEqual(context.geocoder.requestedCoordinates.count, 1)
}
func testSnapshotFallsBackToEmptyAddressWhenGeocoderHasNoResult() async throws {
let context = makeContext(
locationResult: .success(CLLocation(latitude: 39.9042, longitude: 116.4074)),
geocodedAddress: ""
)
let snapshot = try await context.provider.requestSnapshot()
XCTAssertEqual(snapshot.address, "")
}
private func makeContext(
privacyAccepted: Bool = true,
authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse,
requestedAuthorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse,
locationResult: FakeCoreLocationManager.LocationResult = .success(
CLLocation(latitude: 39.9042, longitude: 116.4074)
),
geocodedAddress: String = "北京市东城区"
) -> TestContext {
let suiteName = "CoreLocationProviderTests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
let appStore = AppStore(defaults: defaults)
appStore.session.privacyAgreementAccepted = privacyAccepted
let manager = FakeCoreLocationManager(
authorizationStatus: authorizationStatus,
requestedAuthorizationStatus: requestedAuthorizationStatus,
locationResult: locationResult
)
let geocoder = FakeLocationAddressGeocoder(address: geocodedAddress)
let provider = CoreLocationProvider(
appStore: appStore,
manager: manager,
geocoder: geocoder
)
return TestContext(provider: provider, manager: manager, geocoder: geocoder)
}
private func assertLocationError(
_ expected: LocationProviderError,
operation: () async throws -> Void
) async {
do {
try await operation()
XCTFail("Expected \(expected)")
} catch {
XCTAssertEqual(error as? LocationProviderError, expected)
}
}
}
/// Core Location fallback
@MainActor
private struct TestContext {
let provider: CoreLocationProvider
let manager: FakeCoreLocationManager
let geocoder: FakeLocationAddressGeocoder
}
/// `CLLocationManager`
@MainActor
private final class FakeCoreLocationManager: CoreLocationManaging {
///
enum LocationResult {
case success(CLLocation)
case failure
}
var authorizationStatus: CLAuthorizationStatus
var desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest
weak var delegate: CLLocationManagerDelegate?
private(set) var requestAuthorizationCallCount = 0
private(set) var requestLocationCallCount = 0
private let requestedAuthorizationStatus: CLAuthorizationStatus
private let locationResult: LocationResult
init(
authorizationStatus: CLAuthorizationStatus,
requestedAuthorizationStatus: CLAuthorizationStatus,
locationResult: LocationResult
) {
self.authorizationStatus = authorizationStatus
self.requestedAuthorizationStatus = requestedAuthorizationStatus
self.locationResult = locationResult
}
func requestWhenInUseAuthorization() {
requestAuthorizationCallCount += 1
authorizationStatus = requestedAuthorizationStatus
delegate?.locationManagerDidChangeAuthorization?(CLLocationManager())
}
func requestLocation() {
requestLocationCallCount += 1
let callbackManager = CLLocationManager()
switch locationResult {
case let .success(location):
delegate?.locationManager?(callbackManager, didUpdateLocations: [location])
case .failure:
delegate?.locationManager?(
callbackManager,
didFailWithError: CLError(.locationUnknown)
)
}
}
}
///
@MainActor
private final class FakeLocationAddressGeocoder: LocationAddressGeocoding {
private(set) var requestedCoordinates: [CLLocationCoordinate2D] = []
private let address: String
init(address: String) {
self.address = address
}
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
requestedCoordinates.append(CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude
))
return address
}
}

View File

@ -3,6 +3,7 @@
// suixinkanTests
//
import CoreLocation
import XCTest
@testable import suixinkan