@@ -558,6 +558,291 @@ describe('section-header-quality', () => {
558558 ) ;
559559 } ) ;
560560
561+ // Callout/admonition heading exclusion tests (issue #51)
562+ it ( 'excludes callout headings inside <aside> elements' , async ( ) => {
563+ const result = await check . run (
564+ makeCtx ( {
565+ status : 'pass' ,
566+ tabbedPages : [
567+ {
568+ url : 'http://test.local/page' ,
569+ tabGroups : [
570+ {
571+ framework : 'generic-aria' ,
572+ tabCount : 2 ,
573+ htmlSlice : '<div></div>' ,
574+ panels : [
575+ {
576+ label : 'Python' ,
577+ html : '<div><aside><h2>Warning</h2><p>Be careful</p></aside></div>' ,
578+ } ,
579+ {
580+ label : 'Node' ,
581+ html : '<div><aside><h2>Warning</h2><p>Be careful</p></aside></div>' ,
582+ } ,
583+ ] ,
584+ } ,
585+ ] ,
586+ totalTabbedChars : 100 ,
587+ status : 'pass' ,
588+ } ,
589+ ] ,
590+ } ) ,
591+ ) ;
592+ // No section headers remain after excluding callout headings
593+ expect ( result . status ) . toBe ( 'skip' ) ;
594+ expect ( result . message ) . toContain ( 'no section headers inside tab panels' ) ;
595+ } ) ;
596+
597+ it ( 'excludes callout headings inside elements with ARIA role="note"' , async ( ) => {
598+ const result = await check . run (
599+ makeCtx ( {
600+ status : 'pass' ,
601+ tabbedPages : [
602+ {
603+ url : 'http://test.local/page' ,
604+ tabGroups : [
605+ {
606+ framework : 'generic-aria' ,
607+ tabCount : 2 ,
608+ htmlSlice : '<div></div>' ,
609+ panels : [
610+ {
611+ label : 'Python' ,
612+ html : '<div><div role="note"><h3>Note</h3><p>Info here</p></div></div>' ,
613+ } ,
614+ {
615+ label : 'Node' ,
616+ html : '<div><div role="note"><h3>Note</h3><p>Info here</p></div></div>' ,
617+ } ,
618+ ] ,
619+ } ,
620+ ] ,
621+ totalTabbedChars : 100 ,
622+ status : 'pass' ,
623+ } ,
624+ ] ,
625+ } ) ,
626+ ) ;
627+ expect ( result . status ) . toBe ( 'skip' ) ;
628+ expect ( result . message ) . toContain ( 'no section headers inside tab panels' ) ;
629+ } ) ;
630+
631+ it ( 'excludes callout headings inside elements with admonition class' , async ( ) => {
632+ const result = await check . run (
633+ makeCtx ( {
634+ status : 'pass' ,
635+ tabbedPages : [
636+ {
637+ url : 'http://test.local/page' ,
638+ tabGroups : [
639+ {
640+ framework : 'mkdocs' ,
641+ tabCount : 2 ,
642+ htmlSlice : '<div></div>' ,
643+ panels : [
644+ {
645+ label : 'Python' ,
646+ html : '<div><div class="admonition warning"><h3>Warning</h3><p>Careful</p></div></div>' ,
647+ } ,
648+ {
649+ label : 'Node' ,
650+ html : '<div><div class="admonition warning"><h3>Warning</h3><p>Careful</p></div></div>' ,
651+ } ,
652+ ] ,
653+ } ,
654+ ] ,
655+ totalTabbedChars : 100 ,
656+ status : 'pass' ,
657+ } ,
658+ ] ,
659+ } ) ,
660+ ) ;
661+ expect ( result . status ) . toBe ( 'skip' ) ;
662+ expect ( result . message ) . toContain ( 'no section headers inside tab panels' ) ;
663+ } ) ;
664+
665+ it ( 'excludes callout headings inside elements with data-* callout attributes' , async ( ) => {
666+ // Twilio Paste pattern: data-paste-element="CALLOUT" on ancestor
667+ const result = await check . run (
668+ makeCtx ( {
669+ status : 'pass' ,
670+ tabbedPages : [
671+ {
672+ url : 'http://test.local/page' ,
673+ tabGroups : [
674+ {
675+ framework : 'generic-aria' ,
676+ tabCount : 2 ,
677+ htmlSlice : '<div></div>' ,
678+ panels : [
679+ {
680+ label : 'Python' ,
681+ html : '<div><div data-paste-element="CALLOUT"><div><h2 data-paste-element="CALLOUT_HEADING">Warning</h2></div></div></div>' ,
682+ } ,
683+ {
684+ label : 'Node' ,
685+ html : '<div><div data-paste-element="CALLOUT"><div><h2 data-paste-element="CALLOUT_HEADING">Warning</h2></div></div></div>' ,
686+ } ,
687+ ] ,
688+ } ,
689+ ] ,
690+ totalTabbedChars : 100 ,
691+ status : 'pass' ,
692+ } ,
693+ ] ,
694+ } ) ,
695+ ) ;
696+ expect ( result . status ) . toBe ( 'skip' ) ;
697+ expect ( result . message ) . toContain ( 'no section headers inside tab panels' ) ;
698+ } ) ;
699+
700+ it ( 'counts section headers but ignores callout headings in the same panel' , async ( ) => {
701+ // Panels have both a real section header and a callout heading.
702+ // Only the section header should be counted; the callout should be ignored.
703+ const result = await check . run (
704+ makeCtx ( {
705+ status : 'pass' ,
706+ tabbedPages : [
707+ {
708+ url : 'http://test.local/page' ,
709+ tabGroups : [
710+ {
711+ framework : 'generic-aria' ,
712+ tabCount : 2 ,
713+ htmlSlice : '<div></div>' ,
714+ panels : [
715+ {
716+ label : 'Python' ,
717+ html : '<div><h2>Python Setup</h2><aside><h3>Warning</h3><p>Be careful</p></aside></div>' ,
718+ } ,
719+ {
720+ label : 'Node' ,
721+ html : '<div><h2>Node Setup</h2><aside><h3>Warning</h3><p>Be careful</p></aside></div>' ,
722+ } ,
723+ ] ,
724+ } ,
725+ ] ,
726+ totalTabbedChars : 100 ,
727+ status : 'pass' ,
728+ } ,
729+ ] ,
730+ } ) ,
731+ ) ;
732+ // "Python Setup" and "Node Setup" include variant context → pass
733+ // "Warning" in <aside> is excluded from analysis entirely
734+ expect ( result . status ) . toBe ( 'pass' ) ;
735+ expect ( result . details ?. groupsWithGenericMajority ) . toBe ( 0 ) ;
736+ } ) ;
737+
738+ // Framework-specific callout pattern tests
739+ it ( 'excludes Bootstrap alert headings (role="alert" + alert-heading)' , async ( ) => {
740+ const result = await check . run (
741+ makeCtx ( {
742+ status : 'pass' ,
743+ tabbedPages : [
744+ {
745+ url : 'http://test.local/page' ,
746+ tabGroups : [
747+ {
748+ framework : 'generic-aria' ,
749+ tabCount : 2 ,
750+ htmlSlice : '<div></div>' ,
751+ panels : [
752+ {
753+ label : 'Python' ,
754+ html : '<div><div class="alert alert-warning" role="alert"><h4 class="alert-heading">Warning!</h4><p>Check your configuration.</p></div></div>' ,
755+ } ,
756+ {
757+ label : 'Node' ,
758+ html : '<div><div class="alert alert-warning" role="alert"><h4 class="alert-heading">Warning!</h4><p>Check your configuration.</p></div></div>' ,
759+ } ,
760+ ] ,
761+ } ,
762+ ] ,
763+ totalTabbedChars : 100 ,
764+ status : 'pass' ,
765+ } ,
766+ ] ,
767+ } ) ,
768+ ) ;
769+ expect ( result . status ) . toBe ( 'skip' ) ;
770+ expect ( result . message ) . toContain ( 'no section headers inside tab panels' ) ;
771+ } ) ;
772+
773+ it ( 'excludes headings inside Docusaurus admonition containers' , async ( ) => {
774+ // Docusaurus uses class names containing "admonition" and "alert"
775+ const result = await check . run (
776+ makeCtx ( {
777+ status : 'pass' ,
778+ tabbedPages : [
779+ {
780+ url : 'http://test.local/page' ,
781+ tabGroups : [
782+ {
783+ framework : 'docusaurus' ,
784+ tabCount : 2 ,
785+ htmlSlice : '<div></div>' ,
786+ panels : [
787+ {
788+ label : 'Python' ,
789+ html : '<div><div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><h5>Deprecation Notice</h5><p>This API will be removed.</p></div></div>' ,
790+ } ,
791+ {
792+ label : 'Node' ,
793+ html : '<div><div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><h5>Deprecation Notice</h5><p>This API will be removed.</p></div></div>' ,
794+ } ,
795+ ] ,
796+ } ,
797+ ] ,
798+ totalTabbedChars : 100 ,
799+ status : 'pass' ,
800+ } ,
801+ ] ,
802+ } ) ,
803+ ) ;
804+ expect ( result . status ) . toBe ( 'skip' ) ;
805+ expect ( result . message ) . toContain ( 'no section headers inside tab panels' ) ;
806+ } ) ;
807+
808+ it ( 'excludes headings inside Sphinx/MkDocs admonition with nested content' , async ( ) => {
809+ // Sphinx/MkDocs admonition titles use <p>, but user content inside
810+ // the admonition could contain headings (e.g., a long note with sections)
811+ const result = await check . run (
812+ makeCtx ( {
813+ status : 'pass' ,
814+ tabbedPages : [
815+ {
816+ url : 'http://test.local/page' ,
817+ tabGroups : [
818+ {
819+ framework : 'sphinx' ,
820+ tabCount : 2 ,
821+ htmlSlice : '<div></div>' ,
822+ panels : [
823+ {
824+ label : 'Python' ,
825+ html : '<div><h2>Python Setup</h2><div class="admonition note"><p class="admonition-title">Note</p><h4>Prerequisites</h4><p>You need Python 3.8+</p></div></div>' ,
826+ } ,
827+ {
828+ label : 'Node' ,
829+ html : '<div><h2>Node Setup</h2><div class="admonition note"><p class="admonition-title">Note</p><h4>Prerequisites</h4><p>You need Node 18+</p></div></div>' ,
830+ } ,
831+ ] ,
832+ } ,
833+ ] ,
834+ totalTabbedChars : 200 ,
835+ status : 'pass' ,
836+ } ,
837+ ] ,
838+ } ) ,
839+ ) ;
840+ // "Python Setup" / "Node Setup" are section headers → counted, contextual → pass
841+ // "Prerequisites" inside .admonition → excluded
842+ expect ( result . status ) . toBe ( 'pass' ) ;
843+ expect ( result . details ?. groupsWithGenericMajority ) . toBe ( 0 ) ;
844+ } ) ;
845+
561846 it ( 'detects contextual markdown headers in MDX panels' , async ( ) => {
562847 const result = await check . run (
563848 makeCtx ( {
0 commit comments