@@ -16,7 +16,11 @@ describe('ThreadList', () => {
1616 'pageflow_scrolled.review.reply_placeholder' : 'Reply...' ,
1717 'pageflow_scrolled.review.send' : 'Send' ,
1818 'pageflow_scrolled.review.enter_for_new_line' : 'Enter for new line' ,
19- 'pageflow_scrolled.review.toggle_replies' : 'Toggle replies'
19+ 'pageflow_scrolled.review.toggle_replies' : 'Toggle replies' ,
20+ 'pageflow_scrolled.review.resolve' : 'Mark as resolved' ,
21+ 'pageflow_scrolled.review.unresolve' : 'Mark as unresolved' ,
22+ 'pageflow_scrolled.review.resolved_count.one' : '1 resolved' ,
23+ 'pageflow_scrolled.review.resolved_count.other' : '%{count} resolved'
2024 } ) ;
2125 it ( 'displays comments of threads for subject' , ( ) => {
2226 const { getByText} = renderWithReviewState (
@@ -389,6 +393,121 @@ describe('ThreadList', () => {
389393 expect ( getByRole ( 'button' , { name : 'Send' } ) ) . toBeInTheDocument ( ) ;
390394 } ) ;
391395
396+ it ( 'hides resolved threads and shows resolved count pill' , ( ) => {
397+ const { queryByText, getByText} = renderWithReviewState (
398+ < ThreadList subjectType = "ContentElement" subjectId = { 10 } /> ,
399+ {
400+ commentThreads : [
401+ { id : 1 , subjectType : 'ContentElement' , subjectId : 10 ,
402+ resolvedAt : '2026-04-09T10:00:00Z' ,
403+ comments : [ { id : 10 , body : 'Resolved thread' , creatorName : 'Bob' , creatorId : 2 } ] } ,
404+ { id : 2 , subjectType : 'ContentElement' , subjectId : 10 ,
405+ resolvedAt : null ,
406+ comments : [ { id : 20 , body : 'Active thread' , creatorName : 'Alice' , creatorId : 1 } ] }
407+ ]
408+ }
409+ ) ;
410+
411+ expect ( getByText ( 'Active thread' ) ) . toBeInTheDocument ( ) ;
412+ expect ( queryByText ( 'Resolved thread' ) ) . not . toBeInTheDocument ( ) ;
413+ expect ( getByText ( '1 resolved' ) ) . toBeInTheDocument ( ) ;
414+ } ) ;
415+
416+ it ( 'toggles resolved threads when pill is clicked' , async ( ) => {
417+ const user = userEvent . setup ( ) ;
418+
419+ const { getByText, queryByText} = renderWithReviewState (
420+ < ThreadList subjectType = "ContentElement" subjectId = { 10 } /> ,
421+ {
422+ commentThreads : [
423+ { id : 1 , subjectType : 'ContentElement' , subjectId : 10 ,
424+ resolvedAt : '2026-04-09T10:00:00Z' ,
425+ comments : [ { id : 10 , body : 'Resolved thread' , creatorName : 'Bob' , creatorId : 2 } ] } ,
426+ { id : 2 , subjectType : 'ContentElement' , subjectId : 10 ,
427+ resolvedAt : null ,
428+ comments : [ { id : 20 , body : 'Active thread' , creatorName : 'Alice' , creatorId : 1 } ] }
429+ ]
430+ }
431+ ) ;
432+
433+ await user . click ( getByText ( '1 resolved' ) ) ;
434+ expect ( getByText ( 'Resolved thread' ) ) . toBeInTheDocument ( ) ;
435+ expect ( getByText ( '1 resolved' ) ) . toBeInTheDocument ( ) ;
436+
437+ await user . click ( getByText ( '1 resolved' ) ) ;
438+ expect ( queryByText ( 'Resolved thread' ) ) . not . toBeInTheDocument ( ) ;
439+ } ) ;
440+
441+ it ( 'posts resolve message when resolve button is clicked' , async ( ) => {
442+ const user = userEvent . setup ( ) ;
443+ const postMessage = jest . spyOn ( window . top , 'postMessage' ) . mockImplementation ( ( ) => { } ) ;
444+
445+ const { getByText} = renderWithReviewState (
446+ < ThreadList subjectType = "ContentElement" subjectId = { 10 } /> ,
447+ {
448+ commentThreads : [
449+ { id : 1 , subjectType : 'ContentElement' , subjectId : 10 ,
450+ resolvedAt : null ,
451+ comments : [ { id : 10 , body : 'Open thread' , creatorName : 'Bob' , creatorId : 2 } ] }
452+ ]
453+ }
454+ ) ;
455+
456+ await user . click ( getByText ( 'Mark as resolved' ) ) ;
457+
458+ expect ( postMessage ) . toHaveBeenCalledWith (
459+ { type : 'UPDATE_THREAD' , payload : { threadId : 1 , resolved : true } } ,
460+ window . location . origin
461+ ) ;
462+
463+ postMessage . mockRestore ( ) ;
464+ } ) ;
465+
466+ it ( 'does not show resolve button on collapsed threads' , ( ) => {
467+ const { queryByText} = renderWithReviewState (
468+ < ThreadList subjectType = "ContentElement" subjectId = { 10 } /> ,
469+ {
470+ commentThreads : [
471+ { id : 1 , subjectType : 'ContentElement' , subjectId : 10 ,
472+ resolvedAt : null ,
473+ comments : [ { id : 10 , body : 'First thread' , creatorName : 'Bob' , creatorId : 2 } ] } ,
474+ { id : 2 , subjectType : 'ContentElement' , subjectId : 10 ,
475+ resolvedAt : null ,
476+ comments : [ { id : 20 , body : 'Second thread' , creatorName : 'Alice' , creatorId : 1 } ] }
477+ ]
478+ }
479+ ) ;
480+
481+ expect ( queryByText ( 'Mark as resolved' ) ) . not . toBeInTheDocument ( ) ;
482+ } ) ;
483+
484+ it ( 'does not show reply form on resolved threads' , async ( ) => {
485+ const user = userEvent . setup ( ) ;
486+
487+ const { queryByPlaceholderText, getByText} = renderWithReviewState (
488+ < ThreadList subjectType = "ContentElement" subjectId = { 10 } /> ,
489+ {
490+ commentThreads : [
491+ { id : 1 , subjectType : 'ContentElement' , subjectId : 10 ,
492+ resolvedAt : null ,
493+ comments : [ { id : 10 , body : 'Active thread' , creatorName : 'Bob' , creatorId : 2 } ] } ,
494+ { id : 2 , subjectType : 'ContentElement' , subjectId : 10 ,
495+ resolvedAt : '2026-04-09T10:00:00Z' ,
496+ comments : [ { id : 20 , body : 'Resolved thread' , creatorName : 'Alice' , creatorId : 1 } ] }
497+ ]
498+ }
499+ ) ;
500+
501+ await user . click ( getByText ( '1 resolved' ) ) ;
502+
503+ const replyFields = queryByPlaceholderText ( 'Reply...' ) ;
504+ expect ( replyFields ) . toBeInTheDocument ( ) ;
505+
506+ expect ( getByText ( 'Resolved thread' ) ) . toBeInTheDocument ( ) ;
507+ const resolvedThread = getByText ( 'Resolved thread' ) . closest ( '[class*="thread"]' ) ;
508+ expect ( resolvedThread . querySelector ( 'textarea[placeholder="Reply..."]' ) ) . toBeNull ( ) ;
509+ } ) ;
510+
392511 it ( 'shows reply form in collapsed thread without replies' , ( ) => {
393512 const { getAllByPlaceholderText} = renderWithReviewState (
394513 < ThreadList subjectType = "ContentElement" subjectId = { 10 } /> ,
0 commit comments