// // CommissionRateLogTests.swift // suixinkanTests // import UIKit import XCTest @testable import suixinkan /// 获客员分成比例修改记录模型、接口与分页行为测试。 @MainActor final class CommissionRateLogTests: XCTestCase { func testModelDecodesCompleteRecordAndBuildsBusinessDisplayText() throws { let data = """ { "list": [{ "id": 1, "binding_id": 10, "sale_user_id": 456, "store_user_id": 732, "before_rate": 10, "before_rate_label": "10%(旧)", "after_rate": 20, "after_rate_label": "20%(新)", "operator_type": "store_user", "operator_id": 732, "operator_name": "李师傅", "remark": " 调整合作比例 ", "created_at": "2026-07-23 11:00:00" }], "total": 1, "page": 1, "page_size": 10 } """.data(using: .utf8)! let response = try JSONDecoder().decode(CommissionRateLogListResponse.self, from: data) let item = try XCTUnwrap(response.list.first) XCTAssertEqual(response.pageSize, 10) XCTAssertEqual(item.bindingId, 10) XCTAssertEqual(item.saleUserId, 456) XCTAssertEqual(item.storeUserId, 732) XCTAssertEqual(item.operatorType, "store_user") XCTAssertEqual(item.operatorId, 732) XCTAssertEqual(item.displayBeforeRate, "10%(旧)") XCTAssertEqual(item.displayAfterRate, "20%(新)") XCTAssertEqual(item.metadataLine, "2026-07-23 11:00") XCTAssertEqual(item.displayRemark, "调整合作比例") } func testDisplayFallsBackToNumericRateUnknownOperatorAndRawTime() throws { let data = """ { "list": [{ "id": 2, "binding_id": 0, "sale_user_id": 456, "store_user_id": 0, "before_rate": 5, "before_rate_label": "", "after_rate": 15, "after_rate_label": " ", "operator_type": "", "operator_id": 0, "operator_name": "", "remark": " ", "created_at": "服务端时间" }], "total": 1, "page": 1, "page_size": 10 } """.data(using: .utf8)! let item = try JSONDecoder().decode(CommissionRateLogListResponse.self, from: data).list[0] XCTAssertEqual(item.displayBeforeRate, "5%") XCTAssertEqual(item.displayAfterRate, "15%") XCTAssertEqual(item.metadataLine, "服务端时间") XCTAssertNil(item.displayRemark) } func testAPIBuildsPathAndClampsPaginationParameters() async throws { let session = MockURLSession(responses: [envelope(list: [], total: 0, page: 1)]) let api = OrderAPI(client: APIClient(environment: .testing, session: session)) _ = try await api.saleUserCommissionRateLogs(saleUserId: 456, page: 0, pageSize: 99) let request = try XCTUnwrap(session.requests.first) XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/sale-user/commission-rate/logs") let query = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems XCTAssertEqual(query?.first { $0.name == "sale_user_id" }?.value, "456") XCTAssertEqual(query?.first { $0.name == "page" }?.value, "1") XCTAssertEqual(query?.first { $0.name == "page_size" }?.value, "50") } func testViewModelRetriesFailedPageAndDeduplicatesItems() async throws { let page1 = envelope( list: [ record(id: 1, before: 10, after: 20), record(id: 1, before: 10, after: 20), record(id: 2, before: 20, after: 30), ], total: 3, page: 1 ) let page2 = envelope( list: [ record(id: 2, before: 20, after: 30), record(id: 3, before: 30, after: 40), ], total: 3, page: 2 ) let response = HTTPURLResponse( url: URL(string: "https://api-test.zhifly.cn/mock")!, statusCode: 200, httpVersion: nil, headerFields: nil )! let session = MockURLSession(results: [ .success((page1, response)), .failure(URLError(.timedOut)), .success((page2, response)), ]) let api = OrderAPI(client: APIClient(environment: .testing, session: session)) let viewModel = CommissionRateLogViewModel(saleUserId: 456) await viewModel.refresh(api: api) await viewModel.loadMore(api: api) XCTAssertEqual(viewModel.items.map(\.id), [1, 2]) XCTAssertTrue(viewModel.canLoadMore) XCTAssertFalse(viewModel.isLoadingMore) await viewModel.loadMore(api: api) XCTAssertEqual(viewModel.items.map(\.id), [1, 2, 3]) XCTAssertFalse(viewModel.canLoadMore) let requestedPages = session.requests.compactMap { request in URLComponents(url: request.url!, resolvingAgainstBaseURL: false)? .queryItems? .first { $0.name == "page" }? .value } XCTAssertEqual(requestedPages, ["1", "2", "2"]) } func testTimelineCellOnlyMarksLatestRecord() { let item = CommissionRateLogEntity( id: 1, bindingId: 10, saleUserId: 456, storeUserId: 732, beforeRate: 10, beforeRateLabel: "10%", afterRate: 20, afterRateLabel: "20%", operatorType: "store_user", operatorId: 732, operatorName: "李师傅", remark: "", createdAt: "2026-07-23 11:00:00" ) let cell = CommissionRateLogTimelineCell(style: .default, reuseIdentifier: nil) cell.configure(with: item, isLatest: true) XCTAssertTrue(cell.accessibilityLabel?.contains("最新记录") == true) XCTAssertFalse(cell.accessibilityLabel?.contains("李师傅") == true) cell.configure(with: item, isLatest: false) XCTAssertFalse(cell.accessibilityLabel?.contains("最新记录") == true) } func testPageTitleUsesAcquirerNameAndFallsBackWhenEmpty() { let namedController = CommissionRateLogViewController( saleUserId: 456, acquirerName: " 获客员小王 " ) namedController.setupNavigationBar() XCTAssertEqual(namedController.title, "给获客员小王的分成记录") let fallbackController = CommissionRateLogViewController( saleUserId: 456, acquirerName: " " ) fallbackController.setupNavigationBar() XCTAssertEqual(fallbackController.title, "修改记录") } func testAcquirerCardUsesUnifiedActionsAndBusinessHierarchy() throws { let cell = CooperationAcquirerCell(style: .default, reuseIdentifier: nil) cell.configure( with: CooperativeSalerEntity( saleUserId: 456, name: "获客员小王", salerPhone: "13800138000", commissionRate: 20, commissionRateLabel: "20%", bindTime: "2026-07-23 11:00:00" ) ) let fittingSize = cell.contentView.systemLayoutSizeFitting( CGSize(width: 390, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel ) cell.frame = CGRect(x: 0, y: 0, width: 390, height: fittingSize.height) cell.layoutIfNeeded() let remarkButton = try XCTUnwrap( subview(in: cell, identifier: "cooperation_acquirer_edit_remark") as? UIButton ) let logButton = try XCTUnwrap( subview(in: cell, identifier: "cooperation_acquirer_commission_logs") as? UIButton ) let commissionButton = try XCTUnwrap( subview(in: cell, identifier: "cooperation_acquirer_edit_commission") as? UIButton ) let bindTimeLabel = try XCTUnwrap( subview(in: cell, identifier: "cooperation_acquirer_bind_time") as? UILabel ) XCTAssertEqual(remarkButton.bounds.height, AppSpacing.minTouchTarget, accuracy: 0.5) XCTAssertEqual(logButton.bounds.height, remarkButton.bounds.height, accuracy: 0.5) XCTAssertEqual(commissionButton.bounds.height, remarkButton.bounds.height, accuracy: 0.5) XCTAssertEqual(logButton.bounds.width, remarkButton.bounds.width, accuracy: 0.5) XCTAssertEqual(commissionButton.bounds.width, remarkButton.bounds.width, accuracy: 0.5) XCTAssertEqual(bindTimeLabel.text, "绑定于 2026-07-23 11:00") } private func envelope(list: [[String: Any]], total: Int, page: Int) -> Data { let object: [String: Any] = [ "code": 100000, "msg": "success", "data": [ "list": list, "total": total, "page": page, "page_size": 10, ], ] return try! JSONSerialization.data(withJSONObject: object) } private func record(id: Int, before: Int, after: Int) -> [String: Any] { [ "id": id, "binding_id": 10, "sale_user_id": 456, "store_user_id": 732, "before_rate": before, "before_rate_label": "\(before)%", "after_rate": after, "after_rate_label": "\(after)%", "operator_type": "store_user", "operator_id": 732, "operator_name": "李师傅", "remark": "", "created_at": "2026-07-23 11:00:00", ] } private func subview(in root: UIView, identifier: String) -> UIView? { if root.accessibilityIdentifier == identifier { return root } for child in root.subviews { if let match = subview(in: child, identifier: identifier) { return match } } return nil } }