diff --git a/docs/后端消息推送格式.md b/docs/后端消息推送格式.md index eabc42c..7cc16b3 100644 --- a/docs/后端消息推送格式.md +++ b/docs/后端消息推送格式.md @@ -295,7 +295,7 @@ iOS 通过 APNs/极光收到 `userInfo` 字典。与 Android 相比: | --- | --- | | 标题示例 | `付款成功通知` | | 正文示例 | `您的订单 #260722128006 已支付成功,支付金额 ¥0.01` | -| 客户端点击行为 | Android、iOS 均进入收款记录页 | +| 客户端点击行为 | iOS 进入订单 Tab,恢复默认筛选并刷新;Android 行为以 Android 实现为准 | `data` 字段: @@ -410,9 +410,9 @@ iOS 通过 APNs/极光收到 `userInfo` 字典。与 Android 相比: ## 7. 客户端解析约定 1. Android、iOS 的通知点击路由只根据业务字段 `type` 判断,不读取标题、正文、`data`、`route`、`uri`、`action` 或极光内部字段决定页面。 -2. `type = 1`:进入收款记录页。 +2. iOS `type = 1`、`type = 5`:进入订单 Tab 根页面,清空搜索、日期与状态筛选并刷新列表;不进入订单详情。Android 行为以 Android 实现为准。 3. `type = 6`:进入收款详情页。 -4. 其他任意 `type`、缺少 `type`、类型无法解析或 payload 损坏:进入消息中心。 +4. iOS `type = 14`:有效 `ai_retouch_batch_id` 进入 AI 修图任务详情,否则进入任务列表。其余未单独处理的 `type`、缺少 `type` 或类型无法解析:进入消息中心。 5. Android 从 `notificationExtras` 或厂商通道包装字段中提取 `type`;iOS 优先读取 payload 顶层 `type`,并兼容常见推送包装层中的 `type`。 6. `data` 仍可按消息类型用于页面内容刷新,但不参与通知点击路由。新增字段应保持向后兼容,客户端应忽略当前版本不认识的额外字段。 7. 金额继续使用字符串;时间戳建议统一为 Unix 秒级整数,并在协议中固定单位。 diff --git a/suixinkan/Core/Push/PushModels.swift b/suixinkan/Core/Push/PushModels.swift index a0dbd0f..3d98d6c 100644 --- a/suixinkan/Core/Push/PushModels.swift +++ b/suixinkan/Core/Push/PushModels.swift @@ -2,7 +2,7 @@ import Foundation /// 推送点击后可到达的业务页面。 enum PushDestination: Sendable, Equatable { - case paymentRecord + case orders case paymentDetails case messageCenter case aiRetouchTaskList @@ -36,8 +36,8 @@ struct PushPayload: Sendable, Equatable { /// 仅根据后端业务消息类型解析目标页面。 nonisolated var destination: PushDestination { switch type { - case "1": - return .paymentRecord + case "1", "5": + return .orders case "6": return .paymentDetails case "14": diff --git a/suixinkan/Features/Orders/ViewModels/DepositOrderListViewModel.swift b/suixinkan/Features/Orders/ViewModels/DepositOrderListViewModel.swift index fbd0c82..8789fee 100644 --- a/suixinkan/Features/Orders/ViewModels/DepositOrderListViewModel.swift +++ b/suixinkan/Features/Orders/ViewModels/DepositOrderListViewModel.swift @@ -8,6 +8,14 @@ import Foundation /// 店铺管理员押金单列表 ViewModel,对齐 Android `DepositOrderListViewModel`。 final class DepositOrderListViewModel { + private let appStore: AppStore + + /// 注入账号上下文,使订单查询与测试使用各自独立的会话。 + init(appStore: AppStore = .shared) { + self.appStore = appStore + } + + private(set) var searchText = "" private(set) var filterLabel = "全部" private(set) var tabIndex = 0 @@ -22,6 +30,7 @@ final class DepositOrderListViewModel { var onStateChange: (() -> Void)? var onShowMessage: ((String) -> Void)? + private var listRequestID = UUID() private var currentPage = 1 private var totalCount = 0 private var searchDebounceTask: Task? @@ -34,6 +43,18 @@ final class DepositOrderListViewModel { return formatter }() + /// 清除通知进入前的查询条件,由页面随后发起第一页刷新。 + func resetFiltersForOrderNotification() { + searchDebounceTask?.cancel() + searchDebounceTask = nil + searchText = "" + filterLabel = "全部" + tabIndex = 0 + startTime = nil + endTime = nil + notifyStateChange() + } + func refreshOrderList(api: OrderAPI) async { currentPage = 1 isRefreshing = true @@ -102,7 +123,7 @@ final class DepositOrderListViewModel { onShowMessage?("核销码内容不完整") return } - let storeId = AppStore.shared.session.currentStoreId + let storeId = appStore.session.currentStoreId guard storeId > 0 else { onShowMessage?("店铺信息异常,请先选择店铺") return @@ -125,7 +146,7 @@ final class DepositOrderListViewModel { refundReason: String, api: OrderAPI ) async { - let storeId = AppStore.shared.session.currentStoreId + let storeId = appStore.session.currentStoreId guard storeId > 0 else { onShowMessage?("店铺信息异常") return @@ -163,10 +184,12 @@ final class DepositOrderListViewModel { } private func loadOrderList(refresh: Bool, api: OrderAPI) async { + let requestID = UUID() + listRequestID = requestID let phone = searchText.trimmingCharacters(in: .whitespacesAndNewlines) let isPhoneSearch = !phone.isEmpty && Self.isValidPhone(phone) - let storeId = AppStore.shared.session.currentStoreId + let storeId = appStore.session.currentStoreId if storeId <= 0 { finishLoading() orderList = [] @@ -201,6 +224,7 @@ final class DepositOrderListViewModel { startTime: start, endTime: end ) + guard listRequestID == requestID else { return } totalCount = response.total if refresh || currentPage == 1 { orderList = response.list @@ -213,6 +237,7 @@ final class DepositOrderListViewModel { } catch is CancellationError { return } catch { + guard listRequestID == requestID else { return } onShowMessage?(error.localizedDescription) } finishLoading() diff --git a/suixinkan/Features/Orders/ViewModels/OrderListViewModel.swift b/suixinkan/Features/Orders/ViewModels/OrderListViewModel.swift index bcc8815..2276763 100644 --- a/suixinkan/Features/Orders/ViewModels/OrderListViewModel.swift +++ b/suixinkan/Features/Orders/ViewModels/OrderListViewModel.swift @@ -83,6 +83,14 @@ struct OrderSourceSelection: Equatable { /// 摄影师/景区管理员订单列表 ViewModel,对齐 Android `OrderListViewModel`。 final class OrderListViewModel { + private let appStore: AppStore + + /// 注入账号上下文,使订单查询与测试使用各自独立的会话。 + init(appStore: AppStore = .shared) { + self.appStore = appStore + } + + private(set) var orderList: [OrderEntity] = [] private(set) var searchText = "" private(set) var filterLabel = "全部" @@ -110,6 +118,7 @@ final class OrderListViewModel { var onShowMessage: ((String) -> Void)? var onNavigateBindAcquirer: ((Int) -> Void)? + private var listRequestID = UUID() private var currentPage = 1 private var totalCount = 0 @@ -122,7 +131,7 @@ final class OrderListViewModel { }() var isScenicAdminReadOnly: Bool { - AppStore.shared.session.currentAppRole == .scenicAdmin + appStore.session.currentAppRole == .scenicAdmin } var listAPIPath: String { @@ -131,6 +140,17 @@ final class OrderListViewModel { : "/api/yf-handset-app/photog/order/listv2" } + /// 清除通知进入前的查询条件,由页面随后发起第一页刷新。 + func resetFiltersForOrderNotification() { + searchText = "" + filterLabel = "全部" + tabIndex = 0 + rTabIndex = 0 + startTime = nil + endTime = nil + notifyStateChange() + } + func refreshOrderList(api: OrderAPI) async { currentPage = 1 isRefreshing = true @@ -359,7 +379,7 @@ final class OrderListViewModel { isLoadingOrderSourcePeople = false notifyStateChange() } - let storeId = AppStore.shared.session.currentStoreId + let storeId = appStore.session.currentStoreId let storeIdParam = storeId > 0 ? String(storeId) : nil do { let response = try await api.cooperativeSalers(storeId: storeIdParam) @@ -402,7 +422,9 @@ final class OrderListViewModel { } private func loadOrderList(api: OrderAPI, append: Bool) async { - let scenicId = AppStore.shared.session.currentScenicId + let requestID = UUID() + listRequestID = requestID + let scenicId = appStore.session.currentScenicId guard scenicId > 0 else { finishLoading() onShowMessage?("当前景区信息缺失") @@ -436,6 +458,7 @@ final class OrderListViewModel { isRefined: rTabIndex ) } + guard listRequestID == requestID else { return } totalCount = response.total if append { orderList.append(contentsOf: response.list) @@ -447,6 +470,7 @@ final class OrderListViewModel { } catch is CancellationError { return } catch { + guard listRequestID == requestID else { return } onShowMessage?(error.localizedDescription) } finishLoading() @@ -479,7 +503,7 @@ final class OrderListViewModel { var json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil } - json["scenic_id"] = String(AppStore.shared.session.currentScenicId) + json["scenic_id"] = String(appStore.session.currentScenicId) guard let updated = try? JSONSerialization.data(withJSONObject: json), let string = String(data: updated, encoding: .utf8) else { return nil diff --git a/suixinkan/UI/MainTab/MainTabBarController.swift b/suixinkan/UI/MainTab/MainTabBarController.swift index 07d0aff..d35cc23 100644 --- a/suixinkan/UI/MainTab/MainTabBarController.swift +++ b/suixinkan/UI/MainTab/MainTabBarController.swift @@ -67,16 +67,20 @@ final class MainTabBarController: UITabBarController { /// 响应推送点击,切换到目标 Tab 并打开对应业务页面。 func openPushDestination(_ destination: PushDestination) { - let targetTab = AppTab.home + loadViewIfNeeded() + let targetTab: AppTab = destination == .orders ? .orders : .home + let wasAlreadyOnOrders = targetTab == .orders && selectedViewController === tabNavigationControllers[.orders] selectTab(targetTab) guard let navigationController = tabNavigationControllers[targetTab] else { return } _ = navigationController.popToRootViewController(animated: false) let controller: UIViewController? switch destination { - case .paymentRecord: - controller = PaymentCollectionRecordViewController() + case .orders: + (navigationController.viewControllers.first as? OrdersViewController)?.openFromOrderNotification() + if wasAlreadyOnOrders { badgeViewModel.refreshPendingWriteOffCount() } + controller = nil case .paymentDetails: controller = PaymentCollectionDetailsViewController() case .messageCenter: diff --git a/suixinkan/UI/Orders/OrdersViewController.swift b/suixinkan/UI/Orders/OrdersViewController.swift index 50ee754..cc8635c 100644 --- a/suixinkan/UI/Orders/OrdersViewController.swift +++ b/suixinkan/UI/Orders/OrdersViewController.swift @@ -8,17 +8,64 @@ import SnapKit import UIKit /// 订单 Tab 根页面,按角色分流摄影师/景区管理员/店铺管理员。 -final class OrdersViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate { +final class OrdersViewController: BaseViewController, UITableViewDelegate { - private let orderAPI = NetworkServices.shared.orderAPI - private let orderListViewModel = OrderListViewModel() - private let depositViewModel = DepositOrderListViewModel() + private let appStore: AppStore + private let orderAPI: OrderAPI + private let orderListViewModel: OrderListViewModel + private let depositViewModel: DepositOrderListViewModel + + /// 表格快照持有完整行数据,避免刷新清空 ViewModel 后仍按旧行号取值。 + private struct OrderRow: Hashable { + /// 同一快照内按位置区分行,兼容接口返回重复订单号。 + let position: Int + let content: Content + + /// 摄影师和店铺订单的不可变展示数据。 + enum Content: Equatable { + case photographer(OrderEntity, source: OrderSourceSelection?, readOnly: Bool, showSource: Bool) + case deposit(StoreOrderItem) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(position) + switch content { + case .photographer(let order, _, _, _): + hasher.combine(0) + hasher.combine(order.orderNumber) + case .deposit(let order): + hasher.combine(1) + hasher.combine(order.orderNumber) + } + } + } + + private var dataSource: UITableViewDiffableDataSource? + + /// 注入订单数据依赖;默认使用应用共享 API 和独立的页面 ViewModel。 + init( + orderAPI: OrderAPI? = nil, + orderListViewModel: OrderListViewModel? = nil, + depositViewModel: DepositOrderListViewModel? = nil, + appStore: AppStore = .shared + ) { + self.appStore = appStore + self.orderAPI = orderAPI ?? NetworkServices.shared.orderAPI + self.orderListViewModel = orderListViewModel ?? OrderListViewModel(appStore: appStore) + self.depositViewModel = depositViewModel ?? DepositOrderListViewModel(appStore: appStore) + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } private let filterBar = OrderFilterBarView() private let listView = OrderListView() private var hasInitialized = false - private var isStoreMode: Bool { AppStore.shared.session.currentAppRole == .storeAdmin } + private var isStoreMode: Bool { appStore.session.currentAppRole == .storeAdmin } private var pendingOrderSourceNumber: String? private weak var orderSourcePicker: OrderSourcePickerViewController? private var pendingGiftOrderNumber: String? @@ -32,10 +79,10 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT override func setupUI() { view.addSubview(filterBar) view.addSubview(listView) - listView.tableView.dataSource = self listView.tableView.delegate = self listView.tableView.register(OrderCardCell.self, forCellReuseIdentifier: OrderCardCell.reuseIdentifier) listView.tableView.register(DepositOrderCardCell.self, forCellReuseIdentifier: DepositOrderCardCell.reuseIdentifier) + configureDataSource() } override func setupConstraints() { @@ -133,6 +180,21 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT Task { await initializeList() } } + /// 从订单通知进入时恢复默认查询,并刷新已有或首次打开的列表。 + func openFromOrderNotification() { + loadViewIfNeeded() + hasInitialized = true + if isStoreMode { + depositViewModel.resetFiltersForOrderNotification() + } else { + orderListViewModel.resetFiltersForOrderNotification() + } + applyViewModel() + Task { @MainActor [weak self] in + await self?.initializeList() + } + } + private func initializeList() async { if isStoreMode { await depositViewModel.refreshOrderList(api: orderAPI) @@ -142,6 +204,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT } private func applyViewModel() { + applyOrderSnapshot() if isStoreMode { filterBar.configure( isStoreMode: true, @@ -191,7 +254,6 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT presentMultiVerifySheet() } } - listView.tableView.reloadData() } private func dateRangeText(start: Date?, end: Date?) -> String { @@ -314,31 +376,60 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT // MARK: - UITableView - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - isStoreMode ? depositViewModel.orderList.count : orderListViewModel.orderList.count + private func configureDataSource() { + dataSource = UITableViewDiffableDataSource(tableView: listView.tableView) { [weak self] tableView, _, row in + guard let self else { return nil } + switch row.content { + case .deposit(let item): + let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCardCell.reuseIdentifier) as! DepositOrderCardCell + cell.configure(item: item, actions: self.makeDepositActions(for: item)) + return cell + case .photographer(let order, let source, let readOnly, let showSource): + let cell = tableView.dequeueReusableCell(withIdentifier: OrderCardCell.reuseIdentifier) as! OrderCardCell + cell.configure( + order: order, + hideWriteActions: readOnly, + actions: self.makeOrderActions(for: order), + showOrderSource: showSource, + sourceSelection: source + ) + return cell + } + } } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + private func applyOrderSnapshot() { + let rows: [OrderRow] if isStoreMode { - let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCardCell.reuseIdentifier, for: indexPath) as! DepositOrderCardCell - let item = depositViewModel.orderList[indexPath.row] - cell.configure(item: item, actions: makeDepositActions(for: item)) - listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: depositViewModel.shouldLoadMore(at: indexPath.row)) - return cell + rows = depositViewModel.orderList.enumerated().map { + OrderRow(position: $0.offset, content: .deposit($0.element)) + } + } else { + rows = orderListViewModel.orderList.enumerated().map { index, order in + OrderRow(position: index, content: .photographer( + order, + source: orderListViewModel.orderSourceSelection(for: order), + readOnly: orderListViewModel.isScenicAdminReadOnly, + showSource: orderListViewModel.shouldShowOrderSource(for: order) + )) + } } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([0]) + snapshot.appendItems(rows) + dataSource?.apply(snapshot, animatingDifferences: true) + } - let cell = tableView.dequeueReusableCell(withIdentifier: OrderCardCell.reuseIdentifier, for: indexPath) as! OrderCardCell - let order = orderListViewModel.orderList[indexPath.row] - let source = orderListViewModel.orderSourceSelection(for: order) - cell.configure( - order: order, - hideWriteActions: orderListViewModel.isScenicAdminReadOnly, - actions: makeOrderActions(for: order), - showOrderSource: orderListViewModel.shouldShowOrderSource(for: order), - sourceSelection: source - ) - listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: orderListViewModel.shouldLoadMore(at: indexPath.row)) - return cell + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let row = dataSource?.itemIdentifier(for: indexPath) else { return } + let shouldLoad: Bool + switch row.content { + case .deposit: + shouldLoad = isStoreMode && depositViewModel.shouldLoadMore(at: row.position) + case .photographer: + shouldLoad = !isStoreMode && orderListViewModel.shouldLoadMore(at: row.position) + } + listView.notifyLoadMoreIfNeeded(at: row.position, shouldLoad: shouldLoad) } private func makeOrderActions(for order: OrderEntity) -> OrderCardActions { diff --git a/suixinkanTests/DepositOrderListViewModelTests.swift b/suixinkanTests/DepositOrderListViewModelTests.swift index f94896a..4a6b928 100644 --- a/suixinkanTests/DepositOrderListViewModelTests.swift +++ b/suixinkanTests/DepositOrderListViewModelTests.swift @@ -24,6 +24,19 @@ final class DepositOrderListViewModelTests: XCTestCase { super.tearDown() } + func testOrderNotificationResetsFilters() { + let viewModel = DepositOrderListViewModel() + viewModel.onSearchTextChange("123", api: OrderAPI(client: APIClient(environment: .testing, session: MockURLSession(results: [])))) + viewModel.onFilterChange(tab: 18) + viewModel.onDateRangeSelected(start: Date(), end: Date()) + viewModel.resetFiltersForOrderNotification() + XCTAssertEqual(viewModel.searchText, "") + XCTAssertEqual(viewModel.tabIndex, 0) + XCTAssertEqual(viewModel.filterLabel, "全部") + XCTAssertNil(viewModel.startTime) + XCTAssertNil(viewModel.endTime) + } + func testStoreFilterMapsPaidStatusToAPI20() { XCTAssertEqual(DepositOrderListViewModel.apiOrderStatus(forTabIndex: 18), 20) XCTAssertEqual(DepositOrderListViewModel.apiOrderStatus(forTabIndex: 30), 30) diff --git a/suixinkanTests/OrderListUITokensTests.swift b/suixinkanTests/OrderListUITokensTests.swift index fa2bde6..b94388c 100644 --- a/suixinkanTests/OrderListUITokensTests.swift +++ b/suixinkanTests/OrderListUITokensTests.swift @@ -72,3 +72,88 @@ private extension UIColor { return UInt(r << 16 | g << 8 | b) } } + +/// 回归通知刷新时表格仍请求旧行的场景,使用 mock 数据覆盖两种订单页面。 +@MainActor +final class OrderNotificationListSnapshotTests: XCTestCase { + private var appStore: AppStore! + private var suiteName: String! + + override func setUp() { + super.setUp() + suiteName = "OrderNotificationListSnapshotTests.\(UUID().uuidString)" + appStore = AppStore(defaults: UserDefaults(suiteName: suiteName)!) + appStore.session.configureTestAccount(userId: "push-list-test", accountType: .storeUser, scenicId: 10) + appStore.session.currentStoreId = 20 + } + + override func tearDown() { + UserDefaults.standard.removePersistentDomain(forName: suiteName) + appStore = nil + super.tearDown() + } + + func testPhotographerRowsRemainReadableWhileNotificationRefreshClearsModel() async throws { + try await verifyRefreshUsesSnapshot(storeMode: false) + } + + func testDepositRowsRemainReadableWhileNotificationRefreshClearsModel() async throws { + try await verifyRefreshUsesSnapshot(storeMode: true) + } + + private func verifyRefreshUsesSnapshot(storeMode: Bool) async throws { + appStore.session.roleCode = storeMode ? AppRoleCode.storeAdmin.rawValue : AppRoleCode.photographer.rawValue + let initial = Data(#"{"code":100000,"msg":"success","data":{"list":[{"order_number":"push-order-a","order_status":30},{"order_number":"push-order-b","order_status":30}],"total":2}}"#.utf8) + let empty = Data(#"{"code":100000,"msg":"success","data":{"list":[],"total":0}}"#.utf8) + let session = MockURLSession(responses: [initial, empty, initial]) + let api = OrderAPI(client: APIClient(environment: .testing, session: session)) + let orders = OrderListViewModel(appStore: appStore) + let deposits = DepositOrderListViewModel(appStore: appStore) + let controller = OrdersViewController(orderAPI: api, orderListViewModel: orders, depositViewModel: deposits, appStore: appStore) + controller.openFromOrderNotification() + let list = try XCTUnwrap(controller.view.subviews.first { $0 is OrderListView } as? OrderListView) + let table = list.tableView + await waitForRows(2, in: table) + let source = try XCTUnwrap(table.dataSource) + + // 保持表格的旧快照,精确模拟模型已清空、主线程 UI 回调尚未执行的窗口。 + let notify = storeMode ? deposits.onStateChange : orders.onStateChange + if storeMode { + deposits.onStateChange = nil + deposits.resetFiltersForOrderNotification() + await deposits.refreshOrderList(api: api) + XCTAssertTrue(deposits.orderList.isEmpty) + } else { + orders.onStateChange = nil + orders.resetFiltersForOrderNotification() + await orders.refreshOrderList(api: api) + XCTAssertTrue(orders.orderList.isEmpty) + } + XCTAssertEqual(source.tableView(table, numberOfRowsInSection: 0), 2) + for index in 0..<2 { + let cell = source.tableView(table, cellForRowAt: IndexPath(row: index, section: 0)) + if storeMode { + XCTAssertTrue(cell is DepositOrderCardCell) + } else { + XCTAssertTrue(cell is OrderCardCell) + } + } + if storeMode { deposits.onStateChange = notify } else { orders.onStateChange = notify } + notify?() + await waitForRows(0, in: table) + + // 已打开的订单页再次点击通知,仍能加载完整列表。 + controller.openFromOrderNotification() + await waitForRows(2, in: table) + XCTAssertEqual(session.requests.count, 3) + } + + private func waitForRows(_ count: Int, in table: UITableView) async { + let applied = expectation(for: NSPredicate { _, _ in + MainActor.assumeIsolated { + table.numberOfRows(inSection: 0) == count + } + }, evaluatedWith: nil) + await fulfillment(of: [applied], timeout: 3) + } +} diff --git a/suixinkanTests/OrderListViewModelTests.swift b/suixinkanTests/OrderListViewModelTests.swift index e2fef2e..a4a54dc 100644 --- a/suixinkanTests/OrderListViewModelTests.swift +++ b/suixinkanTests/OrderListViewModelTests.swift @@ -20,6 +20,20 @@ final class OrderListViewModelTests: XCTestCase { super.tearDown() } + func testOrderNotificationResetsFilters() { + let viewModel = OrderListViewModel() + viewModel.onSearchTextChange("13800138000") + viewModel.onFilterChange(orderStatus: 18, isRefined: 1) + viewModel.onDateRangeSelected(start: Date(), end: Date()) + viewModel.resetFiltersForOrderNotification() + XCTAssertEqual(viewModel.searchText, "") + XCTAssertEqual(viewModel.tabIndex, 0) + XCTAssertEqual(viewModel.filterLabel, "全部") + XCTAssertNil(viewModel.startTime) + XCTAssertNil(viewModel.endTime) + XCTAssertEqual(viewModel.rTabIndex, 0) + } + func testOrderStatusQueryMapping() { XCTAssertNil(OrderListQueryBuilder.orderStatusQuery(tabIndex: 0)) XCTAssertEqual(OrderListQueryBuilder.orderStatusQuery(tabIndex: 18), "18") diff --git a/suixinkanTests/PushNotificationTests.swift b/suixinkanTests/PushNotificationTests.swift index 73ae235..f603cd0 100644 --- a/suixinkanTests/PushNotificationTests.swift +++ b/suixinkanTests/PushNotificationTests.swift @@ -27,10 +27,10 @@ final class PushNotificationTests: XCTestCase { super.tearDown() } - func testPayloadRoutesPaymentTypesToTheirPages() { + func testPayloadRoutesOrderAndScanTypesToTheirPages() { XCTAssertEqual( PushPayload(userInfo: ["type": 1, "route": "message_center"]).destination, - .paymentRecord + .orders ) XCTAssertEqual( PushPayload(userInfo: ["type": "6", "action": "task_management"]).destination, @@ -38,6 +38,36 @@ final class PushNotificationTests: XCTestCase { ) } + func testOrderTypesSupportNumbersStringsAndVendorWrappers() { + for type in [1, 5] { + XCTAssertEqual(PushPayload(userInfo: ["type": type]).destination, .orders) + XCTAssertEqual(PushPayload(userInfo: ["type": "\(type)"]).destination, .orders) + XCTAssertEqual(PushPayload(userInfo: ["extras": "{\"type\":\(type)}"]).destination, .orders) + XCTAssertEqual(PushPayload(values: ["type": "\(type)"]).destination, .orders) + } + } + + func testOrderNotificationReturnsToOrdersRootAndDeduplicatesPendingRoute() { + let coordinator = PushRouteCoordinator(appStore: appStore) + let window = UIWindow(frame: UIScreen.main.bounds) + coordinator.attach(window: window) + coordinator.handle(destination: .orders, requestIdentifier: "order-request") + authenticate(userID: "100") + let mainTab = MainTabBarController() + mainTab.loadViewIfNeeded() + mainTab.selectTab(.orders) + let ordersNavigation = mainTab.selectedViewController as? TabNavigationController + ordersNavigation?.pushViewController(UIViewController(), animated: false) + mainTab.selectTab(.home) + window.rootViewController = mainTab + coordinator.routePendingIfPossible() + XCTAssertTrue(mainTab.selectedViewController === ordersNavigation) + XCTAssertEqual(ordersNavigation?.viewControllers.count, 1) + XCTAssertTrue(ordersNavigation?.topViewController is OrdersViewController) + coordinator.handle(destination: .messageCenter, requestIdentifier: "order-request") + XCTAssertTrue(mainTab.selectedViewController === ordersNavigation) + } + func testPayloadIgnoresNonTypeRoutingFieldsAndOtherTypesFallBackToMessageCenter() { let conflictingFields: [AnyHashable: Any] = [ "type": 2, @@ -46,7 +76,7 @@ final class PushNotificationTests: XCTestCase { "action": "收款", ] - [0, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 100, 999].forEach { type in + [0, 2, 3, 4, 7, 8, 9, 10, 11, 12, 100, 999].forEach { type in XCTAssertEqual(PushPayload(userInfo: ["type": type]).destination, .messageCenter) } XCTAssertEqual(PushPayload(userInfo: conflictingFields).destination, .messageCenter) @@ -220,7 +250,7 @@ final class PushNotificationTests: XCTestCase { payload: PushPayload(userInfo: ["type": 1, "route": "task_management"]), requestIdentifier: "request-1" ) - XCTAssertEqual(router.destinations, [.paymentRecord]) + XCTAssertEqual(router.destinations, [.orders]) } func testRemoteNotificationNotifiesUnreadMessageStateChange() async {