修复合作获客扫码绑定崩溃与 Toast 层级,并接入分成比例修改。
将扫码页提升到列表层弹出并补齐 AppDelegate.window,避免扫码时 UIKit 取主窗口崩溃;Toast 改用独立 UIWindow 显示在 sheet 之上;合作获客员支持短信验证修改分成比例,同时优化冷启动根视图创建时机并补充相关测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
133
suixinkanTests/BindAcquirerViewModelTests.swift
Normal file
133
suixinkanTests/BindAcquirerViewModelTests.swift
Normal file
@ -0,0 +1,133 @@
|
||||
//
|
||||
// BindAcquirerViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/7/3.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 绑定获客员 ViewModel 分账比例校验测试。
|
||||
@MainActor
|
||||
final class BindAcquirerViewModelTests: XCTestCase {
|
||||
/// 测试分账比例输入只保留数字且最多 3 位。
|
||||
func testUpdateCommissionRateDraftFiltersInput() {
|
||||
let viewModel = BindAcquirerViewModel()
|
||||
viewModel.updateCommissionRateDraft("12a3.4")
|
||||
XCTAssertEqual(viewModel.commissionRateDraft, "123")
|
||||
viewModel.updateCommissionRateDraft("1234")
|
||||
XCTAssertEqual(viewModel.commissionRateDraft, "123")
|
||||
}
|
||||
|
||||
/// 测试未填写分账比例时提交失败。
|
||||
func testAcceptBindRequiresCommissionRate() async {
|
||||
let viewModel = BindAcquirerViewModel()
|
||||
let api = BindAcquirerMockAPI()
|
||||
await viewModel.load(api: api, saleUserId: 101)
|
||||
|
||||
do {
|
||||
try await viewModel.acceptBind(api: api)
|
||||
XCTFail("Expected validation error")
|
||||
} catch let error as CooperationOrderCommissionRateValidationError {
|
||||
XCTAssertEqual(error.message, "请输入分账比例")
|
||||
} catch {
|
||||
XCTFail("Unexpected error: \(error)")
|
||||
}
|
||||
XCTAssertEqual(api.acceptBindCalls.count, 0)
|
||||
}
|
||||
|
||||
/// 测试分账比例超出范围时提交失败。
|
||||
func testAcceptBindRejectsOutOfRangeCommissionRate() async {
|
||||
let viewModel = BindAcquirerViewModel()
|
||||
let api = BindAcquirerMockAPI()
|
||||
await viewModel.load(api: api, saleUserId: 101)
|
||||
viewModel.updateCommissionRateDraft("101")
|
||||
|
||||
do {
|
||||
try await viewModel.acceptBind(api: api)
|
||||
XCTFail("Expected validation error")
|
||||
} catch let error as CooperationOrderCommissionRateValidationError {
|
||||
XCTAssertEqual(error.message, "分账比例需在0到100之间")
|
||||
} catch {
|
||||
XCTFail("Unexpected error: \(error)")
|
||||
}
|
||||
XCTAssertEqual(api.acceptBindCalls.count, 0)
|
||||
}
|
||||
|
||||
/// 测试合法分账比例时调用 accept-bind 接口。
|
||||
func testAcceptBindPassesCommissionRateToAPI() async throws {
|
||||
let viewModel = BindAcquirerViewModel()
|
||||
let api = BindAcquirerMockAPI()
|
||||
await viewModel.load(api: api, saleUserId: 101)
|
||||
viewModel.updateCommissionRateDraft("20")
|
||||
|
||||
try await viewModel.acceptBind(api: api)
|
||||
|
||||
XCTAssertEqual(api.acceptBindCalls.count, 1)
|
||||
XCTAssertEqual(api.acceptBindCalls.first?.saleUserId, 101)
|
||||
XCTAssertEqual(api.acceptBindCalls.first?.commissionRate, 20)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class BindAcquirerMockAPI: CooperationOrderServing {
|
||||
struct AcceptBindCall {
|
||||
let saleUserId: Int
|
||||
let commissionRate: Int
|
||||
}
|
||||
|
||||
var acceptBindCalls: [AcceptBindCall] = []
|
||||
|
||||
func cooperativeSalers(storeId: Int?, page: Int, pageSize: Int) async throws -> CooperativeSalerListPayload {
|
||||
fatalError("Not used in BindAcquirerViewModelTests")
|
||||
}
|
||||
|
||||
func saleUserInviteInfo(saleUserId: Int) async throws -> SaleUserInviteInfo {
|
||||
SaleUserInviteInfo(
|
||||
saleUserId: saleUserId,
|
||||
salerName: "测试获客员",
|
||||
salerPhone: "13800138000"
|
||||
)
|
||||
}
|
||||
|
||||
func saleUserAcceptBind(saleUserId: Int, commissionRate: Int) async throws {
|
||||
acceptBindCalls.append(AcceptBindCall(saleUserId: saleUserId, commissionRate: commissionRate))
|
||||
}
|
||||
|
||||
func saleUserReferralLeads(page: Int, pageSize: Int) async throws -> ListPayload<ReferralLead> {
|
||||
fatalError("Not used in BindAcquirerViewModelTests")
|
||||
}
|
||||
|
||||
func saleUserAcquisitionOrders(keyword: String?, page: Int, pageSize: Int) async throws -> ListPayload<AcquisitionOrder> {
|
||||
fatalError("Not used in BindAcquirerViewModelTests")
|
||||
}
|
||||
|
||||
func saleUserReferralOrders(saleUserId: Int, page: Int, pageSize: Int) async throws -> ReferralOrderListPayload {
|
||||
fatalError("Not used in BindAcquirerViewModelTests")
|
||||
}
|
||||
|
||||
func saleUserBindReferralOrder(orderNumber: String, referralOrderNo: String) async throws {
|
||||
fatalError("Not used in BindAcquirerViewModelTests")
|
||||
}
|
||||
|
||||
func saleUserUpdateReferralOrderStatus(id: Int, status: Int) async throws {
|
||||
fatalError("Not used in BindAcquirerViewModelTests")
|
||||
}
|
||||
|
||||
func saleUserUpdateRemark(saleUserId: Int, photogRemarkName: String) async throws {
|
||||
fatalError("Not used in BindAcquirerViewModelTests")
|
||||
}
|
||||
|
||||
func saleUserCommissionRateSendSmsCode(phone: String) async throws {
|
||||
fatalError("Not used in BindAcquirerViewModelTests")
|
||||
}
|
||||
|
||||
func saleUserUpdateCommissionRate(
|
||||
saleUserId: Int,
|
||||
commissionRate: Int,
|
||||
smsCode: String
|
||||
) async throws -> CooperativeSaler {
|
||||
fatalError("Not used in BindAcquirerViewModelTests")
|
||||
}
|
||||
}
|
||||
@ -28,6 +28,9 @@ final class CooperationOrderAPITests: XCTestCase {
|
||||
XCTAssertEqual(response.list.first?.displayPhone, "19281871413")
|
||||
XCTAssertEqual(response.list.first?.bindingId, 5)
|
||||
XCTAssertEqual(response.list.first?.shareCode, "INV-000101")
|
||||
XCTAssertEqual(response.list.first?.commissionRate, 15)
|
||||
XCTAssertEqual(response.list.first?.commissionRateLabel, "15%")
|
||||
XCTAssertEqual(response.list.first?.displayCommissionRate, "15%")
|
||||
|
||||
let request = try XCTUnwrap(session.requests.last)
|
||||
XCTAssertEqual(request.httpMethod, "GET")
|
||||
@ -70,6 +73,7 @@ final class CooperationOrderAPITests: XCTestCase {
|
||||
XCTAssertEqual(response.total, 1)
|
||||
XCTAssertEqual(response.list.first?.displayId, "501")
|
||||
XCTAssertEqual(response.list.first?.displayPhone, "13900000002")
|
||||
XCTAssertEqual(response.list.first?.displaySalerName, "张哥")
|
||||
XCTAssertEqual(response.list.first?.remark, "景区门口获客")
|
||||
XCTAssertEqual(response.list.first?.displayImages.first, "https://example.com/lead.jpg")
|
||||
|
||||
@ -130,12 +134,14 @@ final class CooperationOrderAPITests: XCTestCase {
|
||||
|
||||
let acceptSession = CooperationRecordingURLSession(data: emptyData)
|
||||
let acceptAPI = CooperationOrderAPI(client: APIClient(session: acceptSession))
|
||||
try await acceptAPI.saleUserAcceptBind(saleUserId: 101)
|
||||
try await acceptAPI.saleUserAcceptBind(saleUserId: 101, commissionRate: 20)
|
||||
|
||||
let acceptRequest = try XCTUnwrap(acceptSession.requests.last)
|
||||
XCTAssertEqual(acceptRequest.httpMethod, "POST")
|
||||
XCTAssertEqual(acceptRequest.url?.path, "/api/yf-handset-app/photog/sale-user/accept-bind")
|
||||
XCTAssertEqual(cooperationQueryItems(from: acceptRequest)["sale_user_id"], "101")
|
||||
let acceptQuery = cooperationQueryItems(from: acceptRequest)
|
||||
XCTAssertEqual(acceptQuery["sale_user_id"], "101")
|
||||
XCTAssertEqual(acceptQuery["commission_rate"], "20")
|
||||
XCTAssertNil(acceptRequest.httpBody)
|
||||
|
||||
let bindSession = CooperationRecordingURLSession(data: emptyData)
|
||||
@ -181,6 +187,72 @@ final class CooperationOrderAPITests: XCTestCase {
|
||||
XCTAssertEqual(json["sale_user_id"] as? Int, 123)
|
||||
XCTAssertEqual(json["photog_remark_name"] as? String, "张哥")
|
||||
}
|
||||
|
||||
/// 测试发送修改分成比例验证码请求体。
|
||||
func testSaleUserCommissionRateSendSmsCodeUsesPostBody() async throws {
|
||||
let emptyData = try TestFixture.data(named: "empty_success")
|
||||
let session = CooperationRecordingURLSession(data: emptyData)
|
||||
let api = CooperationOrderAPI(client: APIClient(session: session))
|
||||
|
||||
try await api.saleUserCommissionRateSendSmsCode(phone: "19281871413")
|
||||
|
||||
let request = try XCTUnwrap(session.requests.last)
|
||||
XCTAssertEqual(request.httpMethod, "POST")
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/sale-user/commission-rate/send-sms-code")
|
||||
let body = try XCTUnwrap(request.httpBody)
|
||||
let json = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
|
||||
XCTAssertEqual(json["phone"] as? String, "19281871413")
|
||||
}
|
||||
|
||||
/// 测试更新合作获客员分成比例请求体与响应解码。
|
||||
func testSaleUserUpdateCommissionRateUsesPostBodyAndDecodesResponse() async throws {
|
||||
let responseData = Data(
|
||||
"""
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"binding_id": 5,
|
||||
"sale_user_id": 101,
|
||||
"share_code": "INV-000101",
|
||||
"name": "管铜彬",
|
||||
"saler_name": "管铜彬",
|
||||
"phone": "19281871413",
|
||||
"phone_masked": "192****1413",
|
||||
"saler_phone": "19281871413",
|
||||
"photog_commission_rate": 20,
|
||||
"commission_rate": 20,
|
||||
"commission_rate_label": "20%",
|
||||
"bind_time": "2026-06-25 17:11:01",
|
||||
"bound_at": "2026-06-25 17:11:01",
|
||||
"store_id": 37
|
||||
}
|
||||
}
|
||||
""".utf8
|
||||
)
|
||||
let session = CooperationRecordingURLSession(data: responseData)
|
||||
let api = CooperationOrderAPI(client: APIClient(session: session))
|
||||
|
||||
let updated = try await api.saleUserUpdateCommissionRate(
|
||||
saleUserId: 101,
|
||||
commissionRate: 20,
|
||||
smsCode: "889900"
|
||||
)
|
||||
|
||||
XCTAssertEqual(updated.saleUserId, 101)
|
||||
XCTAssertEqual(updated.commissionRate, 20)
|
||||
XCTAssertEqual(updated.commissionRateLabel, "20%")
|
||||
XCTAssertEqual(updated.displayCommissionRate, "20%")
|
||||
|
||||
let request = try XCTUnwrap(session.requests.last)
|
||||
XCTAssertEqual(request.httpMethod, "POST")
|
||||
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/sale-user/commission-rate")
|
||||
let body = try XCTUnwrap(request.httpBody)
|
||||
let json = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
|
||||
XCTAssertEqual(json["sale_user_id"] as? Int, 101)
|
||||
XCTAssertEqual(json["commission_rate"] as? Int, 20)
|
||||
XCTAssertEqual(json["sms_code"] as? String, "889900")
|
||||
}
|
||||
}
|
||||
|
||||
/// URLSession 测试替身,记录合作订单 API 请求。
|
||||
|
||||
@ -18,6 +18,9 @@
|
||||
"sale_user_id": 101,
|
||||
"saler_name": "管铜彬",
|
||||
"saler_phone": "19281871413",
|
||||
"photog_commission_rate": 0,
|
||||
"commission_rate": 15,
|
||||
"commission_rate_label": "15%",
|
||||
"share_code": "INV-000101",
|
||||
"store_id": 37
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
"list": [
|
||||
{
|
||||
"lead_id": 501,
|
||||
"display_name": "张哥",
|
||||
"photog_remark_name": "张哥",
|
||||
"name": "管铜彬",
|
||||
"saler_name": "管铜彬",
|
||||
"user_phone": "13900000002",
|
||||
"remark": "景区门口获客",
|
||||
"created_at": "2026-05-21 14:00:00",
|
||||
|
||||
42
suixinkanTests/RootViewDisplayStateTests.swift
Normal file
42
suixinkanTests/RootViewDisplayStateTests.swift
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// RootViewDisplayStateTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
// Created by Codex on 2026/7/3.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 根视图展示状态测试,覆盖冷启动门禁与登录阶段的映射关系。
|
||||
final class RootViewDisplayStateTests: XCTestCase {
|
||||
/// 测试冷启动 bootstrap 未完成时不会提前创建登录页。
|
||||
func testBootstrapPendingLoggedOutUsesPlaceholder() {
|
||||
let state = RootContentDisplayState.resolve(
|
||||
isColdStartBootstrapPending: true,
|
||||
appPhase: .loggedOut
|
||||
)
|
||||
|
||||
XCTAssertEqual(state, .bootstrapPlaceholder)
|
||||
}
|
||||
|
||||
/// 测试 bootstrap 完成且未登录时展示登录页。
|
||||
func testBootstrapCompletedLoggedOutShowsLogin() {
|
||||
let state = RootContentDisplayState.resolve(
|
||||
isColdStartBootstrapPending: false,
|
||||
appPhase: .loggedOut
|
||||
)
|
||||
|
||||
XCTAssertEqual(state, .login)
|
||||
}
|
||||
|
||||
/// 测试 bootstrap 完成且已登录时展示主 Tab。
|
||||
func testBootstrapCompletedLoggedInShowsMainTabs() {
|
||||
let state = RootContentDisplayState.resolve(
|
||||
isColdStartBootstrapPending: false,
|
||||
appPhase: .loggedIn
|
||||
)
|
||||
|
||||
XCTAssertEqual(state, .mainTabs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user