Files
suixinkan_ios_new/suixinkanTests/LocationReport/LocationReportTests.swift
汉秋 abcac9bfdf 新增打卡点与位置上报模块,并接入首页路由
将打卡点管理与位置上报从占位页迁移为完整 MVVM 流程,包含前台定位支持与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:08:41 +08:00

180 lines
8.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// LocationReportTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/24.
//
import XCTest
@testable import suixinkan
@MainActor
/// API
final class LocationReportAPITests: XCTestCase {
/// query
func testReportLocationUsesExpectedQuery() async throws {
let session = LocationRecordingSession(data: Self.submitResponse)
let api = LocationReportAPI(client: APIClient(session: session))
let response = try await api.reportLocation(staffId: 77, latitude: 30.1, longitude: 120.2, address: "入口", type: .immediate, scenicId: 88)
let request = try XCTUnwrap(session.requests.first)
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/loacation/report")
let query = locationQueryItems(from: request)
XCTAssertEqual(query["staff_id"], "77")
XCTAssertEqual(query["latitude"], "30.1")
XCTAssertEqual(query["longitude"], "120.2")
XCTAssertEqual(query["address"], "入口")
XCTAssertEqual(query["type"], "1")
XCTAssertEqual(query["scenic_id"], "88")
XCTAssertEqual(response.expired, 7200)
}
///
func testHistoryUsesExpectedQueryAndDecodesLossyFields() async throws {
let session = LocationRecordingSession(data: Self.historyResponse)
let api = LocationReportAPI(client: APIClient(session: session))
let payload = try await api.locationReportList(staffId: 77, page: 0, pageSize: 0, type: .marked, startDate: "2026-06-01", endDate: "2026-06-24")
let request = try XCTUnwrap(session.requests.first)
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/loacation/list")
let query = locationQueryItems(from: request)
XCTAssertEqual(query["staff_id"], "77")
XCTAssertEqual(query["page"], "1")
XCTAssertEqual(query["page_size"], "1")
XCTAssertEqual(query["type"], "2")
XCTAssertEqual(query["start_date"], "2026-06-01")
XCTAssertEqual(query["end_date"], "2026-06-24")
XCTAssertEqual(payload.total, 1)
XCTAssertEqual(payload.list.first?.staffId, 77)
XCTAssertEqual(payload.list.first?.latitude, "30.1")
}
private static let submitResponse = Data(#"{"code":100000,"msg":"ok","data":{"staff_id":77,"expired":"7200","status":"1"}}"#.utf8)
private static let historyResponse = Data(#"{"code":100000,"msg":"ok","data":{"total":"1","list":[{"id":"1","staff_id":"77","type":"2","latitude":30.1,"longitude":"120.2","address":"","ip":"127.0.0.1","remark":"ok","created_at":"2026-06-24 10:00:00"}]}}"#.utf8)
}
@MainActor
/// ViewModel
final class LocationReportViewModelTests: XCTestCase {
/// staffId scenicId
func testSubmitRequiresStaffAndScenic() async {
let api = MockLocationReportService()
let viewModel = LocationReportViewModel()
viewModel.applyCurrentLocation(latitude: 30.1, longitude: 120.2, address: "入口")
let missingStaff = await viewModel.submit(type: .immediate, staffId: nil, scenicId: 88, api: api)
let missingScenic = await viewModel.submit(type: .immediate, staffId: 77, scenicId: nil, api: api)
XCTAssertFalse(missingStaff)
XCTAssertFalse(missingScenic)
XCTAssertEqual(api.reportRequests.count, 0)
}
/// 线使
func testSubmitUsesExpectedTypes() async {
let api = MockLocationReportService()
let viewModel = LocationReportViewModel()
viewModel.applyCurrentLocation(latitude: 30.1, longitude: 120.2, address: "入口")
viewModel.applyMarkedLocation(latitude: 31.1, longitude: 121.2, address: "标记点")
_ = await viewModel.submit(type: .immediate, staffId: 77, scenicId: 88, api: api)
_ = await viewModel.submit(type: .marked, staffId: 77, scenicId: 88, api: api)
_ = await viewModel.setOnline(true, staffId: 77, scenicId: 88, api: api)
XCTAssertEqual(api.reportRequests.map(\.type), [.immediate, .marked, .onlineStatus])
XCTAssertEqual(api.reportRequests[1].latitude, 31.1)
XCTAssertEqual(viewModel.secondsUntilReport, 600)
}
///
func testSubmitFailureKeepsState() async {
let api = MockLocationReportService()
api.shouldFailReport = true
let viewModel = LocationReportViewModel()
viewModel.applyCurrentLocation(latitude: 30.1, longitude: 120.2, address: "入口")
let success = await viewModel.submit(type: .immediate, staffId: 77, scenicId: 88, api: api)
XCTAssertFalse(success)
XCTAssertEqual(viewModel.secondsUntilReport, 0)
XCTAssertNotNil(viewModel.errorMessage)
}
///
func testHistoryFilterAndPagination() async {
let api = MockLocationReportService()
api.historyPages = [
ListPayload(total: 2, list: [LocationReportHistoryItem.fixture(id: 1)]),
ListPayload(total: 2, list: [LocationReportHistoryItem.fixture(id: 2)])
]
let viewModel = LocationReportHistoryViewModel()
viewModel.selectedType = .marked
await viewModel.reload(staffId: 77, api: api)
await viewModel.loadMore(staffId: 77, api: api)
await viewModel.loadMore(staffId: 77, api: api)
XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
XCTAssertEqual(api.historyRequests.map(\.page), [1, 2])
XCTAssertEqual(api.historyRequests.first?.type, .marked)
}
}
/// API URLSession
private final class LocationRecordingSession: URLSessionProtocol {
let data: Data
private(set) var requests: [URLRequest] = []
/// Session
init(data: Data) {
self.data = data
}
///
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
requests.append(request)
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
return (data, response)
}
}
@MainActor
///
private final class MockLocationReportService: LocationReportServing {
var shouldFailReport = false
var reportRequests: [(staffId: Int, latitude: Double, longitude: Double, address: String, type: LocationReportType, scenicId: Int)] = []
var historyPages: [ListPayload<LocationReportHistoryItem>] = [ListPayload(total: 0, list: [])]
var historyRequests: [(staffId: Int, page: Int, pageSize: Int, type: LocationReportType, startDate: String?, endDate: String?)] = []
func reportLocation(staffId: Int, latitude: Double, longitude: Double, address: String, type: LocationReportType, scenicId: Int) async throws -> LocationReportSubmitResponse {
if shouldFailReport { throw NSError(domain: "location", code: 1) }
reportRequests.append((staffId, latitude, longitude, address, type, scenicId))
return LocationReportSubmitResponse(staffId: "\(staffId)", expired: 600, status: 1)
}
func locationReportList(staffId: Int, page: Int, pageSize: Int, type: LocationReportType, startDate: String?, endDate: String?) async throws -> ListPayload<LocationReportHistoryItem> {
historyRequests.append((staffId, page, pageSize, type, startDate, endDate))
return historyPages.isEmpty ? ListPayload(total: 0, list: []) : historyPages.removeFirst()
}
}
private extension LocationReportHistoryItem {
///
static func fixture(id: Int) -> LocationReportHistoryItem {
let data = Data(#"{"id":\#(id),"staff_id":77,"type":1,"latitude":"30.1","longitude":"120.2","address":"","ip":"","remark":"","created_at":"2026-06-24"}"#.utf8)
return try! JSONDecoder().decode(LocationReportHistoryItem.self, from: data)
}
}
/// query
private func locationQueryItems(from request: URLRequest) -> [String: String] {
guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return [:]
}
return Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).compactMap { item in
item.value.map { (item.name, $0) }
})
}