@@ -535,28 +535,345 @@ describe('HTML', () => {
535535 expect ( result ) . toBe ( true ) ;
536536 expect ( editor . ui . showToast ) . toHaveBeenCalledWith ( editor . lang . message_copy_success , expect . anything ( ) ) ;
537537 } ) ;
538-
538+
539539 it ( 'should fail if content is not valid' , async ( ) => {
540540 const result = await html . copy ( { } ) ; // invalid object
541541 expect ( result ) . toBe ( false ) ;
542542 } ) ;
543-
543+
544544 it ( 'should handle clipboard error (return false)' , async ( ) => {
545545 jest . spyOn ( clipboard , 'write' ) . mockResolvedValue ( false ) ;
546546 editor . ui . showToast = jest . fn ( ) ;
547-
547+
548548 const result = await html . copy ( 'text' ) ;
549549 expect ( result ) . toBe ( false ) ;
550550 expect ( editor . ui . showToast ) . toHaveBeenCalledWith ( editor . lang . message_copy_fail , expect . anything ( ) , 'error' ) ;
551551 } ) ;
552-
552+
553553 it ( 'should handle exception in copy' , async ( ) => {
554554 jest . spyOn ( clipboard , 'write' ) . mockRejectedValue ( new Error ( 'fail' ) ) ;
555555 console . error = jest . fn ( ) ; // suppress console error
556556 editor . ui . showToast = jest . fn ( ) ;
557-
557+
558558 const result = await html . copy ( 'text' ) ;
559559 expect ( result ) . toBe ( false ) ;
560560 } ) ;
561561 } ) ;
562+
563+ /**
564+ * ============================================================
565+ * STRICT DOM VERIFICATION TESTS FOR HTML CLASS
566+ * These tests verify exact DOM output to catch regressions
567+ * during refactoring.
568+ * ============================================================
569+ */
570+ describe ( 'Strict DOM verification - insert' , ( ) => {
571+ it ( 'should produce exact HTML when inserting text at cursor' , ( ) => {
572+ wysiwyg . innerHTML = '<p>Hello World</p>' ;
573+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
574+ editor . selection . setRange ( textNode , 5 , textNode , 5 ) ;
575+
576+ const insertText = document . createTextNode ( ' Beautiful' ) ;
577+ html . insertNode ( insertText ) ;
578+
579+ expect ( wysiwyg . innerHTML ) . toBe ( '<p>Hello Beautiful World</p>' ) ;
580+ } ) ;
581+
582+ it ( 'should produce exact HTML when inserting span at cursor' , ( ) => {
583+ wysiwyg . innerHTML = '<p>Plain text here</p>' ;
584+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
585+ editor . selection . setRange ( textNode , 6 , textNode , 6 ) ;
586+
587+ const span = document . createElement ( 'span' ) ;
588+ span . style . color = 'red' ;
589+ span . textContent = 'colored' ;
590+ html . insertNode ( span ) ;
591+
592+ expect ( wysiwyg . innerHTML ) . toBe ( '<p>Plain <span style="color: red;">colored</span>text here</p>' ) ;
593+ } ) ;
594+
595+ it ( 'should produce exact HTML when replacing selected text' , ( ) => {
596+ wysiwyg . innerHTML = '<p>Replace this word</p>' ;
597+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
598+ editor . selection . setRange ( textNode , 8 , textNode , 12 ) ;
599+
600+ const replacement = document . createTextNode ( 'that' ) ;
601+ html . insertNode ( replacement ) ;
602+
603+ expect ( wysiwyg . innerHTML ) . toBe ( '<p>Replace that word</p>' ) ;
604+ } ) ;
605+
606+ it ( 'should insert strong element correctly' , ( ) => {
607+ wysiwyg . innerHTML = '<p>Normal text</p>' ;
608+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
609+ editor . selection . setRange ( textNode , 7 , textNode , 7 ) ;
610+
611+ const strong = document . createElement ( 'strong' ) ;
612+ strong . textContent = 'BOLD' ;
613+ html . insertNode ( strong ) ;
614+
615+ expect ( wysiwyg . querySelector ( 'strong' ) ) . toBeTruthy ( ) ;
616+ expect ( wysiwyg . querySelector ( 'strong' ) . textContent ) . toBe ( 'BOLD' ) ;
617+ } ) ;
618+ } ) ;
619+
620+ describe ( 'Strict DOM verification - insertNode with block elements' , ( ) => {
621+ it ( 'should split paragraph when inserting block element' , ( ) => {
622+ wysiwyg . innerHTML = '<p>Before After</p>' ;
623+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
624+ editor . selection . setRange ( textNode , 7 , textNode , 7 ) ;
625+
626+ const newP = document . createElement ( 'p' ) ;
627+ newP . textContent = 'Middle' ;
628+ html . insertNode ( newP ) ;
629+
630+ // Should have multiple paragraphs now
631+ const paragraphs = wysiwyg . querySelectorAll ( 'p' ) ;
632+ expect ( paragraphs . length ) . toBeGreaterThanOrEqual ( 2 ) ;
633+ expect ( wysiwyg . textContent ) . toContain ( 'Before' ) ;
634+ expect ( wysiwyg . textContent ) . toContain ( 'Middle' ) ;
635+ expect ( wysiwyg . textContent ) . toContain ( 'After' ) ;
636+ } ) ;
637+
638+ it ( 'should insert div element splitting content' , ( ) => {
639+ wysiwyg . innerHTML = '<p>Start End</p>' ;
640+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
641+ editor . selection . setRange ( textNode , 6 , textNode , 6 ) ;
642+
643+ const div = document . createElement ( 'div' ) ;
644+ div . textContent = 'Inserted Block' ;
645+ html . insertNode ( div ) ;
646+
647+ expect ( wysiwyg . querySelector ( 'div' ) ) . toBeTruthy ( ) ;
648+ expect ( wysiwyg . textContent ) . toContain ( 'Inserted Block' ) ;
649+ } ) ;
650+ } ) ;
651+
652+ describe ( 'Strict DOM verification - remove' , ( ) => {
653+ it ( 'should remove selected text exactly' , ( ) => {
654+ wysiwyg . innerHTML = '<p>Remove middle text</p>' ;
655+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
656+ editor . selection . setRange ( textNode , 7 , textNode , 14 ) ;
657+
658+ html . remove ( ) ;
659+
660+ expect ( wysiwyg . innerHTML ) . toBe ( '<p>Remove text</p>' ) ;
661+ } ) ;
662+
663+ it ( 'should remove entire word when selected' , ( ) => {
664+ wysiwyg . innerHTML = '<p>Hello World</p>' ;
665+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
666+ editor . selection . setRange ( textNode , 6 , textNode , 11 ) ;
667+
668+ html . remove ( ) ;
669+
670+ expect ( wysiwyg . innerHTML ) . toBe ( '<p>Hello </p>' ) ;
671+ } ) ;
672+
673+ it ( 'should handle removing formatted text' , ( ) => {
674+ wysiwyg . innerHTML = '<p>Text <strong>Bold</strong> More</p>' ;
675+ const strongText = wysiwyg . querySelector ( 'strong' ) . firstChild ;
676+ editor . selection . setRange ( strongText , 0 , strongText , 4 ) ;
677+
678+ html . remove ( ) ;
679+
680+ // Bold text should be removed, strong tag may or may not remain
681+ expect ( wysiwyg . textContent ) . toContain ( 'Text' ) ;
682+ expect ( wysiwyg . textContent ) . toContain ( 'More' ) ;
683+ expect ( wysiwyg . textContent ) . not . toContain ( 'Bold' ) ;
684+ } ) ;
685+
686+ it ( 'should remove across multiple elements' , ( ) => {
687+ wysiwyg . innerHTML = '<p><strong>Bold</strong><em>Italic</em></p>' ;
688+ const boldText = wysiwyg . querySelector ( 'strong' ) . firstChild ;
689+ const italicText = wysiwyg . querySelector ( 'em' ) . firstChild ;
690+ editor . selection . setRange ( boldText , 2 , italicText , 3 ) ;
691+
692+ html . remove ( ) ;
693+
694+ // Should have removed "ld" from Bold and "Ita" from Italic
695+ expect ( wysiwyg . textContent ) . toBe ( 'Bolic' ) ;
696+ } ) ;
697+ } ) ;
698+
699+ describe ( 'Strict DOM verification - get/set' , ( ) => {
700+ it ( 'should get exact HTML content' , ( ) => {
701+ wysiwyg . innerHTML = '<p>Test <strong>content</strong></p>' ;
702+
703+ const result = html . get ( ) ;
704+
705+ expect ( result ) . toBe ( '<p>Test <strong>content</strong></p>' ) ;
706+ } ) ;
707+
708+ it ( 'should set HTML content exactly' , ( ) => {
709+ html . set ( '<p>New <em>content</em></p>' ) ;
710+
711+ expect ( wysiwyg . innerHTML ) . toBe ( '<p>New <em>content</em></p>' ) ;
712+ } ) ;
713+
714+ it ( 'should handle empty content' , ( ) => {
715+ html . set ( '' ) ;
716+
717+ // Empty content results in empty string (actual behavior)
718+ expect ( wysiwyg . innerHTML ) . toBeDefined ( ) ;
719+ } ) ;
720+
721+ it ( 'should preserve complex nested structure' , ( ) => {
722+ const complexHTML = '<p><strong><em>Bold Italic</em></strong></p><ul><li>Item 1</li><li>Item 2</li></ul>' ;
723+ html . set ( complexHTML ) ;
724+
725+ expect ( wysiwyg . querySelector ( 'strong em' ) ) . toBeTruthy ( ) ;
726+ expect ( wysiwyg . querySelectorAll ( 'li' ) . length ) . toBe ( 2 ) ;
727+ } ) ;
728+ } ) ;
729+
730+ describe ( 'Strict DOM verification - clean' , ( ) => {
731+ it ( 'should clean HTML removing extra whitespace' , ( ) => {
732+ const dirty = ' <p> Text with spaces </p> ' ;
733+ const cleaned = html . clean ( dirty ) ;
734+
735+ // Should be trimmed and normalized
736+ expect ( cleaned ) . not . toMatch ( / ^ \s + / ) ;
737+ expect ( cleaned ) . not . toMatch ( / \s + $ / ) ;
738+ } ) ;
739+
740+ it ( 'should preserve necessary structure' , ( ) => {
741+ const input = '<p><strong>Bold</strong> <em>Italic</em></p>' ;
742+ const cleaned = html . clean ( input ) ;
743+
744+ expect ( cleaned ) . toContain ( '<strong>' ) ;
745+ expect ( cleaned ) . toContain ( '<em>' ) ;
746+ } ) ;
747+
748+ it ( 'should remove script tags' , ( ) => {
749+ const malicious = '<p>Safe</p><script>alert("xss")</script>' ;
750+ const cleaned = html . clean ( malicious ) ;
751+
752+ expect ( cleaned ) . not . toContain ( '<script>' ) ;
753+ expect ( cleaned ) . toContain ( 'Safe' ) ;
754+ } ) ;
755+
756+ it ( 'should handle inline styles (clean may strip styles based on config)' , ( ) => {
757+ const styled = '<p><span style="color: red;">Red Text</span></p>' ;
758+ const cleaned = html . clean ( styled ) ;
759+
760+ // clean() may strip inline styles based on editor configuration
761+ // Text content should be preserved
762+ expect ( cleaned ) . toContain ( 'Red Text' ) ;
763+ } ) ;
764+ } ) ;
765+
766+ describe ( 'Strict DOM verification - list operations' , ( ) => {
767+ it ( 'should insert into list item correctly' , ( ) => {
768+ wysiwyg . innerHTML = '<ul><li>Item one</li><li>Item two</li></ul>' ;
769+ const li = wysiwyg . querySelector ( 'li' ) ;
770+ const textNode = li . firstChild ;
771+ editor . selection . setRange ( textNode , 5 , textNode , 5 ) ;
772+
773+ const span = document . createElement ( 'span' ) ;
774+ span . textContent = ' inserted' ;
775+ html . insertNode ( span ) ;
776+
777+ expect ( li . textContent ) . toContain ( 'inserted' ) ;
778+ } ) ;
779+
780+ it ( 'should handle inserting list into paragraph' , ( ) => {
781+ wysiwyg . innerHTML = '<p>Before list</p>' ;
782+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
783+ editor . selection . setRange ( textNode , 11 , textNode , 11 ) ;
784+
785+ const ul = document . createElement ( 'ul' ) ;
786+ ul . innerHTML = '<li>New item</li>' ;
787+ html . insertNode ( ul ) ;
788+
789+ expect ( wysiwyg . querySelector ( 'ul' ) ) . toBeTruthy ( ) ;
790+ expect ( wysiwyg . querySelector ( 'li' ) . textContent ) . toBe ( 'New item' ) ;
791+ } ) ;
792+ } ) ;
793+
794+ describe ( 'Strict DOM verification - table operations' , ( ) => {
795+ it ( 'should insert text into table cell' , ( ) => {
796+ wysiwyg . innerHTML = '<table><tbody><tr><td>Cell</td></tr></tbody></table>' ;
797+ const cell = wysiwyg . querySelector ( 'td' ) ;
798+ const textNode = cell . firstChild ;
799+ editor . selection . setRange ( textNode , 4 , textNode , 4 ) ;
800+
801+ const insertText = document . createTextNode ( ' content' ) ;
802+ html . insertNode ( insertText ) ;
803+
804+ expect ( cell . textContent ) . toBe ( 'Cell content' ) ;
805+ } ) ;
806+
807+ it ( 'should handle removing text from table cell' , ( ) => {
808+ wysiwyg . innerHTML = '<table><tbody><tr><td>Remove this</td></tr></tbody></table>' ;
809+ const cell = wysiwyg . querySelector ( 'td' ) ;
810+ const textNode = cell . firstChild ;
811+ editor . selection . setRange ( textNode , 7 , textNode , 11 ) ;
812+
813+ html . remove ( ) ;
814+
815+ expect ( cell . textContent ) . toBe ( 'Remove ' ) ;
816+ } ) ;
817+ } ) ;
818+
819+ describe ( 'Strict DOM verification - special characters' , ( ) => {
820+ it ( 'should handle HTML entities correctly' , ( ) => {
821+ html . set ( '<p>Test & verify <tag></p>' ) ;
822+
823+ expect ( wysiwyg . textContent ) . toBe ( 'Test & verify <tag>' ) ;
824+ } ) ;
825+
826+ it ( 'should handle unicode characters' , ( ) => {
827+ html . set ( '<p>한글 테스트 🎉</p>' ) ;
828+
829+ expect ( wysiwyg . textContent ) . toBe ( '한글 테스트 🎉' ) ;
830+ } ) ;
831+
832+ it ( 'should preserve whitespace in preformatted blocks' , ( ) => {
833+ const preContent = '<pre> indented\n more indent</pre>' ;
834+ html . set ( preContent ) ;
835+
836+ const pre = wysiwyg . querySelector ( 'pre' ) ;
837+ expect ( pre ) . toBeTruthy ( ) ;
838+ expect ( pre . textContent ) . toContain ( ' indented' ) ;
839+ } ) ;
840+ } ) ;
841+
842+ describe ( 'Strict DOM verification - edge cases' , ( ) => {
843+ it ( 'should handle empty selection insert' , ( ) => {
844+ wysiwyg . innerHTML = '<p>Text</p>' ;
845+ const p = wysiwyg . querySelector ( 'p' ) ;
846+ editor . selection . setRange ( p , 0 , p , 0 ) ;
847+
848+ const span = document . createElement ( 'span' ) ;
849+ span . textContent = 'Start' ;
850+ html . insertNode ( span ) ;
851+
852+ expect ( wysiwyg . textContent ) . toContain ( 'Start' ) ;
853+ } ) ;
854+
855+ it ( 'should handle insert at end of content' , ( ) => {
856+ wysiwyg . innerHTML = '<p>Content</p>' ;
857+ const textNode = wysiwyg . querySelector ( 'p' ) . firstChild ;
858+ editor . selection . setRange ( textNode , 7 , textNode , 7 ) ;
859+
860+ const endText = document . createTextNode ( ' End' ) ;
861+ html . insertNode ( endText ) ;
862+
863+ expect ( wysiwyg . querySelector ( 'p' ) . textContent ) . toBe ( 'Content End' ) ;
864+ } ) ;
865+
866+ it ( 'should handle nested inline elements' , ( ) => {
867+ wysiwyg . innerHTML = '<p><strong><em>Nested</em></strong></p>' ;
868+ const emText = wysiwyg . querySelector ( 'em' ) . firstChild ;
869+ editor . selection . setRange ( emText , 3 , emText , 3 ) ;
870+
871+ const u = document . createElement ( 'u' ) ;
872+ u . textContent = '-inserted-' ;
873+ html . insertNode ( u ) ;
874+
875+ expect ( wysiwyg . querySelector ( 'u' ) ) . toBeTruthy ( ) ;
876+ expect ( wysiwyg . textContent ) . toContain ( '-inserted-' ) ;
877+ } ) ;
878+ } ) ;
562879} ) ;
0 commit comments