@@ -7,8 +7,8 @@ import ViewInspector
77@MainActor
88@Suite ( " MessageList pinned turn SwiftUI behavior " )
99struct MessageListPinnedTurnSwiftUITests {
10- @Test ( " Pinned user message releases when streaming content fills the reserved space " )
11- func pinnedUserMessageReleasesWhenStreamingContentFillsReservedSpace ( ) async throws {
10+ @Test ( " Streaming content that fills the reserved space keeps the list following the bottom " )
11+ func streamingContentFillingReservedSpaceFollowsBottom ( ) async throws {
1212 let model = MessageListPinnedTurnModel ( )
1313 let view = MessageListPinnedTurnHarness ( model: model)
1414
@@ -19,39 +19,34 @@ struct MessageListPinnedTurnSwiftUITests {
1919 )
2020 defer { ViewHosting . expel ( function: #function) }
2121
22+ // A fresh user message pins to the top with reserved space below it.
2223 model. messages = [
2324 . init( text: " user " , isUserMessage: true , height: 44 ) ,
2425 ]
2526
2627 try await Task . sleep ( for: . milliseconds( 450 ) )
27- model. shouldObserveRelease = true
28+
29+ // The streaming response grows the turn until it outgrows the viewport,
30+ // collapsing the reserved space. The pin releases and the list must keep
31+ // following the bottom — it must not be stranded above the bottom.
2832 model. messages. append ( contentsOf: [
2933 . init( text: " assistant 1 " , isUserMessage: false , height: 88 ) ,
3034 . init( text: " assistant 2 " , isUserMessage: false , height: 88 ) ,
3135 . init( text: " assistant 3 " , isUserMessage: false , height: 88 ) ,
3236 ] )
3337
34- try await waitUntil ( timeout : . seconds ( 2 ) ) {
35- model . observedBottomRelease
36- }
38+ // Let the layout settle after the turn fills the viewport, then assert the
39+ // list reports it is following the bottom rather than stranded.
40+ try await Task . sleep ( for : . milliseconds ( 600 ) )
3741
38- #expect( model. observedBottomRelease )
42+ #expect( model. isAtBottom )
3943 }
4044}
4145
4246@MainActor
4347private final class MessageListPinnedTurnModel : ObservableObject {
4448 @Published var messages : [ MessageListPinnedTurnMessage ] = [ ]
4549 @Published var isAtBottom = false
46- var shouldObserveRelease = false
47- var observedBottomRelease = false
48-
49- func updateIsAtBottom( _ value: Bool ) {
50- isAtBottom = value
51- if shouldObserveRelease, value {
52- observedBottomRelease = true
53- }
54- }
5550}
5651
5752private struct MessageListPinnedTurnHarness : View {
@@ -63,7 +58,7 @@ private struct MessageListPinnedTurnHarness: View {
6358 isStreaming: true ,
6459 isAtBottom: Binding (
6560 get: { model. isAtBottom } ,
66- set: { model. updateIsAtBottom ( $0 ) }
61+ set: { model. isAtBottom = $0 }
6762 )
6863 ) { message in
6964 Text ( message. text)
@@ -78,19 +73,4 @@ private struct MessageListPinnedTurnMessage: MessageListItem {
7873 let isUserMessage : Bool
7974 let height : CGFloat
8075}
81-
82- private func waitUntil(
83- timeout: Duration ,
84- interval: Duration = . milliseconds( 20 ) ,
85- condition: @MainActor @escaping ( ) -> Bool
86- ) async throws {
87- let start = ContinuousClock . now
88- while !( await condition ( ) ) {
89- if ContinuousClock . now - start >= timeout {
90- Issue . record ( " Timed out waiting for condition " )
91- return
92- }
93- try await Task . sleep ( for: interval)
94- }
95- }
9676#endif
0 commit comments