Integrate Amap SDK to replace CoreLocation and MapKit for all location features.
Unify GPS, reverse geocoding, and map display behind LocationProvider with privacy-gated bootstrap after login, aligned with Android. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
51
suixinkanTests/AMapBootstrapTests.swift
Normal file
51
suixinkanTests/AMapBootstrapTests.swift
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// AMapBootstrapTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 高德 SDK 初始化 gate 测试。
|
||||
final class AMapBootstrapTests: XCTestCase {
|
||||
|
||||
private var defaults: UserDefaults!
|
||||
private var appStore: AppStore!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
AMapBootstrap.resetForTesting()
|
||||
defaults = UserDefaults(suiteName: "AMapBootstrapTests")!
|
||||
defaults.removePersistentDomain(forName: "AMapBootstrapTests")
|
||||
appStore = AppStore(defaults: defaults)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
AMapBootstrap.resetForTesting()
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testConfigureReturnsFalseWhenPrivacyNotAccepted() {
|
||||
appStore.privacyAgreementAccepted = false
|
||||
|
||||
XCTAssertFalse(AMapBootstrap.configureIfNeeded(appStore: appStore))
|
||||
XCTAssertFalse(AMapBootstrap.isConfigured)
|
||||
}
|
||||
|
||||
func testConfigureIsIdempotentWhenPrivacyAccepted() {
|
||||
appStore.privacyAgreementAccepted = true
|
||||
|
||||
XCTAssertTrue(AMapBootstrap.configureIfNeeded(appStore: appStore))
|
||||
XCTAssertTrue(AMapBootstrap.isConfigured)
|
||||
XCTAssertTrue(AMapBootstrap.configureIfNeeded(appStore: appStore))
|
||||
XCTAssertTrue(AMapBootstrap.isConfigured)
|
||||
}
|
||||
|
||||
func testRequireConfiguredThrowsWhenPrivacyNotAccepted() {
|
||||
appStore.privacyAgreementAccepted = false
|
||||
|
||||
XCTAssertThrowsError(try AMapBootstrap.requireConfigured(appStore: appStore)) { error in
|
||||
XCTAssertEqual(error as? LocationProviderError, .privacyNotAccepted)
|
||||
}
|
||||
}
|
||||
}
|
||||
60
suixinkanTests/LocationProviderTests.swift
Normal file
60
suixinkanTests/LocationProviderTests.swift
Normal file
@ -0,0 +1,60 @@
|
||||
//
|
||||
// LocationProviderTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 定位协议与 ViewModel 注入测试。
|
||||
final class LocationProviderTests: XCTestCase {
|
||||
|
||||
private var defaults: UserDefaults!
|
||||
private var appStore: AppStore!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
defaults = UserDefaults(suiteName: "LocationProviderTests")!
|
||||
defaults.removePersistentDomain(forName: "LocationProviderTests")
|
||||
appStore = AppStore(defaults: defaults)
|
||||
}
|
||||
|
||||
func testScenicSelectionUsesInjectedProviderSnapshot() async {
|
||||
let mock = MockLocationProvider()
|
||||
mock.snapshot = HomeLocationSnapshot(latitude: 30.2, longitude: 120.1, address: "杭州")
|
||||
appStore.saveRoleScenicList([
|
||||
ScenicInfo(id: 1, name: "西湖", status: 1, location: ScenicLocationInfo(lng: 120.1, lat: 30.2, address: "杭州"), coverImg: nil),
|
||||
])
|
||||
|
||||
let viewModel = ScenicSelectionViewModel(appStore: appStore, locationProvider: mock)
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.startLocation()
|
||||
|
||||
XCTAssertEqual(viewModel.currentLocationText, "杭州")
|
||||
XCTAssertNil(message)
|
||||
}
|
||||
|
||||
func testLocationReportViewModelUsesInjectedReverseGeocode() async {
|
||||
let mock = MockLocationProvider()
|
||||
mock.reverseGeocodeResult = "标记地址"
|
||||
let viewModel = LocationReportViewModel(locationProvider: mock)
|
||||
|
||||
viewModel.toggleMarking()
|
||||
viewModel.setMarkerLocation(latitude: 30.1, longitude: 120.2)
|
||||
try? await Task.sleep(nanoseconds: 200_000_000)
|
||||
|
||||
XCTAssertEqual(viewModel.markerAddress, "标记地址")
|
||||
}
|
||||
|
||||
func testMockLocationProviderCoordinate() async throws {
|
||||
let mock = MockLocationProvider()
|
||||
mock.coordinate = CLLocationCoordinate2D(latitude: 31.0, longitude: 121.0)
|
||||
|
||||
let coordinate = try await mock.requestCoordinate()
|
||||
XCTAssertEqual(coordinate.latitude, 31.0, accuracy: 0.0001)
|
||||
XCTAssertEqual(coordinate.longitude, 121.0, accuracy: 0.0001)
|
||||
}
|
||||
}
|
||||
30
suixinkanTests/MockLocationProvider.swift
Normal file
30
suixinkanTests/MockLocationProvider.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// MockLocationProvider.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
@testable import suixinkan
|
||||
|
||||
/// 测试用定位替身。
|
||||
final class MockLocationProvider: LocationProviding, @unchecked Sendable {
|
||||
var snapshot = HomeLocationSnapshot(latitude: 39.9087, longitude: 116.3975, address: "北京市东城区")
|
||||
var coordinate = CLLocationCoordinate2D(latitude: 39.9087, longitude: 116.3975)
|
||||
var reverseGeocodeResult = "北京市东城区"
|
||||
var requestSnapshotError: Error?
|
||||
var requestCoordinateError: Error?
|
||||
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||
if let requestSnapshotError { throw requestSnapshotError }
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
if let requestCoordinateError { throw requestCoordinateError }
|
||||
return coordinate
|
||||
}
|
||||
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||
reverseGeocodeResult
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user