统一账号作用域 Key 对齐 Android,新增缓存 schema 迁移清理旧数据,并同步更新相关模块与单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
214 lines
9.1 KiB
Swift
214 lines
9.1 KiB
Swift
//
|
|
// StatisticsViewModelTests.swift
|
|
// suixinkanTests
|
|
//
|
|
|
|
import XCTest
|
|
@testable import suixinkan
|
|
|
|
@MainActor
|
|
/// 数据统计 ViewModel 测试。
|
|
final class StatisticsViewModelTests: XCTestCase {
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
AppStore.shared.logout()
|
|
AppStore.shared.session.configureTestAccount(userId: "statistics-user", scenicId: 10)
|
|
}
|
|
|
|
override func tearDown() {
|
|
AppStore.shared.logout()
|
|
super.tearDown()
|
|
}
|
|
|
|
func testStatisticsSummaryDecodesRefundAmountAsString() throws {
|
|
let json = """
|
|
{
|
|
"code": 100000,
|
|
"msg": "success",
|
|
"data": {
|
|
"order_amount_sum": "0.00",
|
|
"order_count": 0,
|
|
"order_price_avg": "0.00",
|
|
"received_amount_sum": "0.00",
|
|
"refund_amount_sum": "0.00"
|
|
}
|
|
}
|
|
""".data(using: .utf8)!
|
|
|
|
let envelope = try JSONDecoder().decode(APIEnvelope<StatisticsSummaryResponse>.self, from: json)
|
|
XCTAssertEqual(envelope.data?.refundAmountSum, "0.00")
|
|
XCTAssertEqual(envelope.data?.orderAmountSum, "0.00")
|
|
}
|
|
|
|
func testRangeValueMapping() {
|
|
let viewModel = StatisticsViewModel()
|
|
XCTAssertEqual(viewModel.rangeValue(for: "今日"), "1")
|
|
XCTAssertEqual(viewModel.rangeValue(for: "昨日"), "2")
|
|
XCTAssertEqual(viewModel.rangeValue(for: "7日"), "3")
|
|
XCTAssertEqual(viewModel.rangeValue(for: "本月"), "4")
|
|
}
|
|
|
|
func testFormattedSelectedPeriodTimeForToday() {
|
|
let viewModel = StatisticsViewModel()
|
|
let text = viewModel.formattedSelectedPeriodTime(for: "今日")
|
|
let today = formattedToday()
|
|
XCTAssertEqual(text, "\(today)至\(today)")
|
|
}
|
|
|
|
func testFormattedSelectedPeriodTimeForThisMonth() {
|
|
let viewModel = StatisticsViewModel()
|
|
let text = viewModel.formattedSelectedPeriodTime(for: "本月")
|
|
XCTAssertTrue(text.contains("至"))
|
|
XCTAssertTrue(text.hasPrefix(firstDayOfMonth()))
|
|
}
|
|
|
|
func testUpdateRangeByShortcutAllClearsTimes() async {
|
|
let summaryJSON = """
|
|
{"code":100000,"msg":"success","data":{"order_amount_sum":"0","order_count":0,"order_price_avg":"0","received_amount_sum":"0","refund_amount_sum":0}}
|
|
""".data(using: .utf8)!
|
|
let listJSON = """
|
|
{"code":100000,"msg":"success","data":{"total":0,"data":[]}}
|
|
""".data(using: .utf8)!
|
|
|
|
AppStore.shared.session.currentScenicId = 10
|
|
let session = MockURLSession(responses: [listJSON])
|
|
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = StatisticsViewModel()
|
|
|
|
await viewModel.updateRangeByShortcut("全部", api: api)
|
|
|
|
XCTAssertEqual(viewModel.statisticsRangeText, "全部")
|
|
XCTAssertEqual(viewModel.listStartTime, "")
|
|
XCTAssertEqual(viewModel.listEndTime, "")
|
|
XCTAssertEqual(session.requests.count, 1)
|
|
XCTAssertEqual(session.requests[0].url?.path, "/api/yf-handset-app/photog/analyse/user/daily")
|
|
let queryItems = URLComponents(url: session.requests[0].url!, resolvingAgainstBaseURL: false)?.queryItems ?? []
|
|
XCTAssertEqual(queryItems.first(where: { $0.name == "start_time" })?.value, "")
|
|
XCTAssertEqual(queryItems.first(where: { $0.name == "end_time" })?.value, "")
|
|
}
|
|
|
|
func testPhotographerSummaryAndDailyRequests() async throws {
|
|
let summaryJSON = """
|
|
{"code":100000,"msg":"success","data":{"order_amount_sum":"100","order_count":2,"order_price_avg":"50","received_amount_sum":"90","refund_amount_sum":10}}
|
|
""".data(using: .utf8)!
|
|
let listJSON = """
|
|
{"code":100000,"msg":"success","data":{"total":1,"data":[{"date":"2025-12-09","order_count":2,"order_price":"50","refund":"10","received":"90"}]}}
|
|
""".data(using: .utf8)!
|
|
|
|
AppStore.shared.session.currentScenicId = 10
|
|
let session = MockURLSession(responses: [summaryJSON, listJSON])
|
|
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = StatisticsViewModel()
|
|
|
|
await viewModel.loadStatistics("7日", api: api)
|
|
await viewModel.updateRangeByShortcut("今日", api: api)
|
|
|
|
XCTAssertEqual(viewModel.statistics?.orderAmountSum, "100")
|
|
XCTAssertEqual(viewModel.statisticsList.count, 1)
|
|
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
|
"/api/yf-handset-app/photog/analyse/user",
|
|
"/api/yf-handset-app/photog/analyse/user/daily",
|
|
])
|
|
XCTAssertTrue(session.requests[0].url?.query?.contains("range=3") == true)
|
|
XCTAssertTrue(session.requests[1].url?.query?.contains("paeg_size=15") == true)
|
|
XCTAssertTrue(session.requests[1].url?.query?.contains("scenic_id=10") == true)
|
|
}
|
|
|
|
func testScenicAdminUsesScenicAdminEndpoints() async {
|
|
let summaryJSON = """
|
|
{"code":100000,"msg":"success","data":{"order_amount_sum":"0","order_count":0,"order_price_avg":"0","received_amount_sum":"0","refund_amount_sum":0}}
|
|
""".data(using: .utf8)!
|
|
let listJSON = """
|
|
{"code":100000,"msg":"success","data":{"total":0,"data":[]}}
|
|
""".data(using: .utf8)!
|
|
|
|
AppStore.shared.session.roleCode = AppRoleCode.scenicAdmin.rawValue
|
|
AppStore.shared.session.currentScenicId = 20
|
|
let session = MockURLSession(responses: [summaryJSON, listJSON])
|
|
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = StatisticsViewModel()
|
|
|
|
await viewModel.loadStatistics("今日", api: api)
|
|
await viewModel.updateRangeByShortcut("今日", api: api)
|
|
|
|
XCTAssertEqual(session.requests[0].url?.path, "/api/app/scenic-admin/analyse")
|
|
XCTAssertEqual(session.requests[1].url?.path, "/api/app/scenic-admin/analyse/daily")
|
|
}
|
|
|
|
func testStoreAdminUsesStoreEndpoints() async {
|
|
let summaryJSON = """
|
|
{"code":100000,"msg":"success","data":{"order_amount_sum":"0","order_count":0,"order_price_avg":"0","received_amount_sum":"0","refund_amount_sum":0}}
|
|
""".data(using: .utf8)!
|
|
let listJSON = """
|
|
{"code":100000,"msg":"success","data":{"total":0,"data":[]}}
|
|
""".data(using: .utf8)!
|
|
|
|
AppStore.shared.session.roleCode = AppRoleCode.storeAdmin.rawValue
|
|
AppStore.shared.session.currentStoreId = 30
|
|
let session = MockURLSession(responses: [summaryJSON, listJSON])
|
|
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = StatisticsViewModel()
|
|
|
|
await viewModel.loadStatistics("今日", api: api)
|
|
await viewModel.updateRangeByShortcut("今日", api: api)
|
|
|
|
XCTAssertEqual(session.requests[0].url?.path, "/api/app/store/analyse")
|
|
XCTAssertTrue(session.requests[0].url?.query?.contains("store_id=30") == true)
|
|
XCTAssertEqual(session.requests[1].url?.path, "/api/app/store/analyse/daily")
|
|
}
|
|
|
|
func testStoreAdminWithoutStoreIdDoesNotRequest() async {
|
|
AppStore.shared.session.roleCode = AppRoleCode.storeAdmin.rawValue
|
|
AppStore.shared.session.currentStoreId = 0
|
|
let session = MockURLSession(responses: [])
|
|
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = StatisticsViewModel()
|
|
var message: String?
|
|
viewModel.onShowMessage = { message = $0 }
|
|
|
|
await viewModel.loadStatistics("今日", api: api)
|
|
|
|
XCTAssertEqual(message, "请先选择店铺")
|
|
XCTAssertEqual(session.requests.count, 0)
|
|
}
|
|
|
|
func testLoadMoreStopsWhenReachedTotal() async {
|
|
let page1JSON = """
|
|
{"code":100000,"msg":"success","data":{"total":1,"data":[{"date":"2025-12-09","order_count":1,"order_price":"10","refund":"0","received":"10"}]}}
|
|
""".data(using: .utf8)!
|
|
|
|
AppStore.shared.session.currentScenicId = 10
|
|
let session = MockURLSession(responses: [page1JSON])
|
|
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
|
let viewModel = StatisticsViewModel()
|
|
|
|
await viewModel.updateRangeByShortcut("今日", api: api)
|
|
XCTAssertEqual(viewModel.statisticsList.count, 1)
|
|
XCTAssertFalse(viewModel.canLoadMore)
|
|
|
|
await viewModel.loadMore(api: api)
|
|
XCTAssertEqual(viewModel.listCurrentPage, 1)
|
|
XCTAssertEqual(session.requests.count, 1)
|
|
}
|
|
|
|
private func formattedToday() -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.calendar = Calendar(identifier: .gregorian)
|
|
formatter.locale = Locale(identifier: "zh_CN")
|
|
formatter.dateFormat = "yyyy/MM/dd"
|
|
return formatter.string(from: Date())
|
|
}
|
|
|
|
private func firstDayOfMonth() -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.calendar = Calendar(identifier: .gregorian)
|
|
formatter.locale = Locale(identifier: "zh_CN")
|
|
formatter.dateFormat = "yyyy/MM/dd"
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
let components = calendar.dateComponents([.year, .month], from: Date())
|
|
let firstDay = calendar.date(from: components) ?? Date()
|
|
return formatter.string(from: firstDay)
|
|
}
|
|
}
|