Add notification badge count and MainTabView with notification tab FRE-4740 FRE-4739

- Add getUnreadCount() endpoint to NotificationsServiceProtocol
- Add NotificationUnreadCountResponse model
- Add badgeCount and fetchUnreadCount() to NotificationsViewModel
- Update markAsRead/markAllAsRead to decrement badge count
- Create MainTabView with Home, Challenges, Clubs, Notifications tabs
- Add unread badge on notification tab using .badge() modifier
- Support injected ViewModel in NotificationsView for shared state
- Add badge count tests to NotificationServiceTests
- Fetch unread count on app launch and tab switch

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-03 20:16:05 -04:00
parent 88d57a3389
commit cb55ad95e2
9 changed files with 279 additions and 1 deletions

View File

@@ -10,6 +10,8 @@ final class MockNotificationsService: NotificationsServiceProtocol {
var markAllCalled = false
var listCallCount = 0
var listError: Error?
var mockUnreadCount: Int = 0
var getUnreadCountCallCount = 0
func list(params: NotificationListParams = NotificationListParams()) async throws -> [NotificationItem] {
listCallCount += 1
@@ -26,6 +28,11 @@ final class MockNotificationsService: NotificationsServiceProtocol {
func markAllAsRead() async throws {
markAllCalled = true
}
func getUnreadCount() async throws -> Int {
getUnreadCountCallCount += 1
return mockUnreadCount
}
}
// MARK: - Helper: Sample Notifications
@@ -216,6 +223,91 @@ final class NotificationServiceTests: XCTestCase {
XCTAssertEqual(mock.listCallCount, 1)
XCTAssertEqual(viewModel.notifications.count, 1)
}
// MARK: - Badge Count
@MainActor
func testFetchUnreadCountSetsBadgeCount() async {
let mock = MockNotificationsService()
mock.mockUnreadCount = 5
let viewModel = NotificationsViewModel(notificationsService: mock)
await viewModel.fetchUnreadCount()
XCTAssertEqual(viewModel.badgeCount, 5)
XCTAssertEqual(mock.getUnreadCountCallCount, 1)
}
@MainActor
func testFetchUnreadCountZero() async {
let mock = MockNotificationsService()
mock.mockUnreadCount = 0
let viewModel = NotificationsViewModel(notificationsService: mock)
await viewModel.fetchUnreadCount()
XCTAssertEqual(viewModel.badgeCount, 0)
}
@MainActor
func testMarkAsReadDecrementsBadgeCount() async {
let mock = MockNotificationsService()
let unread = NotificationItem.sample(id: "1", isRead: false)
let viewModel = NotificationsViewModel(notificationsService: mock)
viewModel.notifications = [unread]
viewModel.badgeCount = 3
await viewModel.markAsRead(id: "1")
XCTAssertEqual(viewModel.badgeCount, 2)
XCTAssertTrue(viewModel.notifications.first?.isRead == true)
}
@MainActor
func testMarkAsReadBadgeCountDoesNotGoBelowZero() async {
let mock = MockNotificationsService()
let unread = NotificationItem.sample(id: "1", isRead: false)
let viewModel = NotificationsViewModel(notificationsService: mock)
viewModel.notifications = [unread]
viewModel.badgeCount = 0
await viewModel.markAsRead(id: "1")
XCTAssertEqual(viewModel.badgeCount, 0)
}
@MainActor
func testMarkAllAsReadResetsBadgeCount() async {
let mock = MockNotificationsService()
let unread1 = NotificationItem.sample(id: "1", isRead: false)
let unread2 = NotificationItem.sample(id: "2", isRead: false)
let viewModel = NotificationsViewModel(notificationsService: mock)
viewModel.notifications = [unread1, unread2]
viewModel.badgeCount = 7
await viewModel.markAllAsRead()
XCTAssertEqual(viewModel.badgeCount, 0)
}
@MainActor
func testFetchNotificationsUpdatesBadgeCount() async {
let mock = MockNotificationsService()
mock.notifications = [
NotificationItem.sample(id: "1", isRead: false),
NotificationItem.sample(id: "2", isRead: true),
NotificationItem.sample(id: "3", isRead: false),
]
let viewModel = NotificationsViewModel(notificationsService: mock)
viewModel.badgeCount = 0
await viewModel.fetchNotifications()
XCTAssertEqual(viewModel.badgeCount, 2)
}
}
// MARK: - NotificationModelTests