@@ -40,13 +40,18 @@ describe('updateToolbarState', () => {
4040 let mockGetQuickFormatList ;
4141 let mockCollectTrackedChanges ;
4242 let mockIsTrackedChangeActionAllowed ;
43+ let mockFindParentNode ;
44+ let mockCalculateResolvedParagraphProperties ;
4345
4446 beforeEach ( async ( ) => {
4547 vi . clearAllMocks ( ) ;
4648
4749 mockEditor = {
4850 state : {
49- selection : { from : 1 , to : 1 } ,
51+ selection : { from : 1 , to : 1 , empty : true } ,
52+ doc : {
53+ resolve : vi . fn ( ) . mockReturnValue ( { } ) ,
54+ } ,
5055 } ,
5156 commands : {
5257 setFieldAnnotationsFontSize : vi . fn ( ) ,
@@ -60,6 +65,7 @@ describe('updateToolbarState', () => {
6065 getDocumentDefaultStyles : vi . fn ( ( ) => ( { typeface : 'Arial' , fontSizePt : 12 } ) ) ,
6166 linkedStyles : [ ] ,
6267 docHiglightColors : new Set ( [ '#ff0000' , '#00ff00' ] ) ,
68+ convertedXml : { } ,
6369 } ,
6470 options : {
6571 mode : 'docx' ,
@@ -79,6 +85,13 @@ describe('updateToolbarState', () => {
7985 const { collectTrackedChanges, isTrackedChangeActionAllowed } = await import (
8086 '@extensions/track-changes/permission-helpers.js'
8187 ) ;
88+ const helpersModule = await import ( '@helpers/index.js' ) ;
89+ mockFindParentNode = helpersModule . findParentNode ;
90+ mockFindParentNode . mockImplementation ( ( ) => vi . fn ( ) . mockReturnValue ( null ) ) ;
91+ const resolvedPropsModule = await import ( '@extensions/paragraph/resolvedPropertiesCache.js' ) ;
92+ mockCalculateResolvedParagraphProperties = vi
93+ . spyOn ( resolvedPropsModule , 'calculateResolvedParagraphProperties' )
94+ . mockReturnValue ( { } ) ;
8295
8396 getActiveFormatting . mockImplementation ( mockGetActiveFormatting ) ;
8497 isInTable . mockImplementation ( mockIsInTable ) ;
@@ -154,6 +167,7 @@ describe('updateToolbarState', () => {
154167 setDisabled : vi . fn ( ) ,
155168 defaultLabel : { value : '' } ,
156169 allowWithoutEditor : { value : false } ,
170+ active : { value : false } ,
157171 } ,
158172 {
159173 name : { value : 'lineHeight' } ,
@@ -195,6 +209,10 @@ describe('updateToolbarState', () => {
195209 toolbar . documentMode = 'editing' ;
196210 } ) ;
197211
212+ afterEach ( ( ) => {
213+ mockCalculateResolvedParagraphProperties ?. mockRestore ?. ( ) ;
214+ } ) ;
215+
198216 describe ( 'document mode dropdown sync' , ( ) => {
199217 let documentModeItem ;
200218
@@ -508,6 +526,79 @@ describe('updateToolbarState', () => {
508526 expect ( fontFamilyItem . activate ) . not . toHaveBeenCalledWith ( { fontFamily : 'Arial' } ) ;
509527 } ) ;
510528
529+ it ( 'falls back to paragraph runProperties font family for empty paragraph with collapsed selection' , ( ) => {
530+ const paragraphParent = {
531+ node : {
532+ content : { size : 0 } ,
533+ attrs : { paragraphProperties : { } } ,
534+ } ,
535+ pos : 5 ,
536+ } ;
537+
538+ mockFindParentNode . mockImplementation ( ( ) => ( ) => paragraphParent ) ;
539+ const paragraphFontFamily = 'Fancy Font, serif' ;
540+ mockCalculateResolvedParagraphProperties . mockReturnValue ( {
541+ runProperties : { fontFamily : { 'w:ascii' : paragraphFontFamily } } ,
542+ } ) ;
543+ mockGetActiveFormatting . mockReturnValue ( [ ] ) ;
544+
545+ toolbar . updateToolbarState ( ) ;
546+
547+ const fontFamilyItem = toolbar . toolbarItems . find ( ( item ) => item . name . value === 'fontFamily' ) ;
548+ expect ( mockCalculateResolvedParagraphProperties ) . toHaveBeenCalled ( ) ;
549+ expect ( fontFamilyItem . activate ) . toHaveBeenCalledWith ( { fontFamily : paragraphFontFamily } ) ;
550+ } ) ;
551+
552+ it ( 'does not fallback to paragraph font when paragraph already contains text' , ( ) => {
553+ const paragraphParent = {
554+ node : {
555+ content : { size : 1 } ,
556+ attrs : { paragraphProperties : { } } ,
557+ } ,
558+ pos : 5 ,
559+ } ;
560+
561+ mockFindParentNode . mockImplementation ( ( ) => ( ) => paragraphParent ) ;
562+ mockCalculateResolvedParagraphProperties . mockReturnValue ( {
563+ runProperties : { fontFamily : { 'w:ascii' : 'Never Used' } } ,
564+ } ) ;
565+ mockGetActiveFormatting . mockReturnValue ( [ ] ) ;
566+
567+ toolbar . updateToolbarState ( ) ;
568+
569+ const fontFamilyItem = toolbar . toolbarItems . find ( ( item ) => item . name . value === 'fontFamily' ) ;
570+ expect ( fontFamilyItem . activate ) . not . toHaveBeenCalled ( ) ;
571+ } ) ;
572+
573+ it ( 'keeps linked style font family over paragraph fallback in empty paragraphs' , ( ) => {
574+ const paragraphParent = {
575+ node : {
576+ content : { size : 0 } ,
577+ attrs : { paragraphProperties : { } } ,
578+ } ,
579+ pos : 5 ,
580+ } ;
581+
582+ mockFindParentNode . mockImplementation ( ( ) => ( ) => paragraphParent ) ;
583+ mockCalculateResolvedParagraphProperties . mockReturnValue ( {
584+ styleId : 'test-style' ,
585+ runProperties : { fontFamily : { 'w:ascii' : 'Paragraph Font, serif' } } ,
586+ } ) ;
587+ mockEditor . converter . linkedStyles = [
588+ {
589+ id : 'test-style' ,
590+ definition : { styles : { 'font-family' : 'Linked Style Font' } } ,
591+ } ,
592+ ] ;
593+ mockGetActiveFormatting . mockReturnValue ( [ ] ) ;
594+
595+ toolbar . updateToolbarState ( ) ;
596+
597+ const fontFamilyItem = toolbar . toolbarItems . find ( ( item ) => item . name . value === 'fontFamily' ) ;
598+ expect ( fontFamilyItem . activate ) . toHaveBeenCalledWith ( { fontFamily : 'Linked Style Font' } ) ;
599+ expect ( fontFamilyItem . activate ) . not . toHaveBeenCalledWith ( { fontFamily : 'Paragraph Font, serif' } ) ;
600+ } ) ;
601+
511602 it ( 'should prioritize active mark over linked styles (font size)' , ( ) => {
512603 mockGetActiveFormatting . mockReturnValue ( [
513604 { name : 'fontSize' , attrs : { fontSize : '20pt' } } ,
0 commit comments