@@ -69,6 +69,25 @@ struct AppStateTests {
6969 )
7070 }
7171
72+ fileprivate static func makeAuthedState(
73+ notifications: [ GitHubNotification ] ,
74+ results: [ Result < ( Data , HTTPURLResponse ) , Error > ] = [ ]
75+ ) -> ( AppState , StubNetworkSession ) {
76+ let defaults = makeIsolatedUserDefaults ( )
77+ let session = StubNetworkSession ( results: results)
78+ let client = GitHubAPIClient ( token: " ghp_secret " , session: session, useGraphQLForSubjectMetadata: false )
79+ let state = AppState (
80+ notifications: notifications,
81+ authStatus: . signedIn( username: " octodot " ) ,
82+ apiClient: client,
83+ actionDispatchDelayNanoseconds: 0 ,
84+ backgroundRefreshEnabled: false ,
85+ sleepHandler: { _ in } ,
86+ userDefaults: defaults
87+ )
88+ return ( state, session)
89+ }
90+
7291 fileprivate static func makeAuthedState(
7392 results: [ Result < ( Data , HTTPURLResponse ) , Error > ] = [ ] ,
7493 count: Int = 5 ,
@@ -3207,6 +3226,116 @@ struct AppStateTests {
32073226 }
32083227 }
32093228
3229+ @Test func bulkDonePreservesASelectedRowThatSurvives( ) {
3230+ let notifications = ( 0 ..< 4 ) . map { Self . makeNotification ( id: $0) }
3231+ let responses = ( 0 ..< 2 ) . map { id in
3232+ Result < ( Data , HTTPURLResponse ) , Error > . success ( (
3233+ Data ( ) ,
3234+ Self . httpResponse ( url: " https://api.github.com/notifications/threads/ \( id) " , statusCode: 204 )
3235+ ) )
3236+ }
3237+ let ( state, _) = Self . makeAuthedState ( notifications: notifications, results: responses)
3238+ state. groupByRepo = false
3239+ state. selectNotification ( id: " 3 " )
3240+ state. toggleChecked ( id: " 0 " )
3241+ state. toggleChecked ( id: " 1 " )
3242+
3243+ state. done ( )
3244+
3245+ #expect( state. selectedNotificationID == " 3 " )
3246+ #expect( state. selectedIndex == 1 )
3247+ }
3248+
3249+ @Test func bulkDoneAdvancesPastAllRemovedRows( ) {
3250+ let notifications = ( 0 ..< 5 ) . map { Self . makeNotification ( id: $0) }
3251+ let responses = [ 1 , 2 ] . map { id in
3252+ Result < ( Data , HTTPURLResponse ) , Error > . success ( (
3253+ Data ( ) ,
3254+ Self . httpResponse ( url: " https://api.github.com/notifications/threads/ \( id) " , statusCode: 204 )
3255+ ) )
3256+ }
3257+ let ( state, _) = Self . makeAuthedState ( notifications: notifications, results: responses)
3258+ state. groupByRepo = false
3259+ state. selectNotification ( id: " 1 " )
3260+ state. toggleChecked ( id: " 1 " )
3261+ state. toggleChecked ( id: " 2 " )
3262+
3263+ state. done ( )
3264+
3265+ #expect( state. selectedNotificationID == " 3 " )
3266+ }
3267+
3268+ @Test func bulkDoneFallsBackToNearestPrecedingSurvivorAtEnd( ) {
3269+ let notifications = ( 0 ..< 4 ) . map { Self . makeNotification ( id: $0) }
3270+ let responses = [ 1 , 3 ] . map { id in
3271+ Result < ( Data , HTTPURLResponse ) , Error > . success ( (
3272+ Data ( ) ,
3273+ Self . httpResponse ( url: " https://api.github.com/notifications/threads/ \( id) " , statusCode: 204 )
3274+ ) )
3275+ }
3276+ let ( state, _) = Self . makeAuthedState ( notifications: notifications, results: responses)
3277+ state. groupByRepo = false
3278+ state. selectNotification ( id: " 3 " )
3279+ state. toggleChecked ( id: " 1 " )
3280+ state. toggleChecked ( id: " 3 " )
3281+
3282+ state. done ( )
3283+
3284+ #expect( state. selectedNotificationID == " 2 " )
3285+ }
3286+
3287+ @Test func bulkDoneClearsSelectionWhenAllRowsAreRemoved( ) {
3288+ let notifications = ( 0 ..< 3 ) . map { Self . makeNotification ( id: $0) }
3289+ let responses = ( 0 ..< 3 ) . map { id in
3290+ Result < ( Data , HTTPURLResponse ) , Error > . success ( (
3291+ Data ( ) ,
3292+ Self . httpResponse ( url: " https://api.github.com/notifications/threads/ \( id) " , statusCode: 204 )
3293+ ) )
3294+ }
3295+ let ( state, _) = Self . makeAuthedState ( notifications: notifications, results: responses)
3296+ state. groupByRepo = false
3297+ state. selectNotification ( id: " 1 " )
3298+ for notification in notifications {
3299+ state. toggleChecked ( id: notification. id)
3300+ }
3301+
3302+ state. done ( )
3303+
3304+ #expect( state. selectedNotificationID == nil )
3305+ #expect( state. selectedIndex == 0 )
3306+ }
3307+
3308+ @Test func groupedBulkUnsubscribeAdvancesAcrossRepositoryBoundary( ) {
3309+ let notifications = [
3310+ Self . makeNotification ( id: 0 , repo: " acme/alpha " ) ,
3311+ Self . makeNotification ( id: 1 , repo: " acme/beta " ) ,
3312+ Self . makeNotification ( id: 2 , repo: " acme/alpha " ) ,
3313+ Self . makeNotification ( id: 3 , repo: " acme/beta " ) ,
3314+ ]
3315+ let responses = [ 2 , 3 ] . flatMap { id in
3316+ [
3317+ Result< ( Data, HTTPURLResponse) , Error> . success( (
3318+ Data ( ) ,
3319+ Self . httpResponse ( url: " https://api.github.com/notifications/threads/ \( id) /subscription " , statusCode: 204 )
3320+ ) ) ,
3321+ Result < ( Data , HTTPURLResponse ) , Error > . success ( (
3322+ Data ( ) ,
3323+ Self . httpResponse ( url: " https://api.github.com/notifications/threads/ \( id) " , statusCode: 204 )
3324+ ) ) ,
3325+ ]
3326+ }
3327+ let ( state, _) = Self . makeAuthedState ( notifications: notifications, results: responses)
3328+ state. groupByRepo = true
3329+ state. selectNotification ( id: " 2 " )
3330+ state. toggleChecked ( id: " 2 " )
3331+ state. toggleChecked ( id: " 3 " )
3332+
3333+ state. unsubscribeFromThread ( )
3334+
3335+ #expect( state. selectedNotificationID == " 1 " )
3336+ #expect( state. selectedNotification? . repository == " acme/beta " )
3337+ }
3338+
32103339 @Test func bulkUnsubscribeRunsOnAllCheckedRows( ) async {
32113340 let ( state, session) = Self . makeAuthedState (
32123341 results: [
0 commit comments