@@ -444,6 +444,84 @@ describe('buildPivot — incomplete period columns are dropped', () => {
444444 } )
445445} )
446446
447+ describe ( 'buildPivot — hides XBRL hypercube scaffolding rows' , ( ) => {
448+ // A [Table] and its [Line Items] container sit between the section header and
449+ // the real concepts. They carry no facts and (once their role tag is stripped)
450+ // read as bare, duplicated "Statement" — pure plumbing, not headings. Elements
451+ // are keyed by qname here, mirroring the SEC route (the store route keys by
452+ // IRI); the scaffold check keys off the local name so both behave identically.
453+ const SEC_ABS = 'us-gaap:IncomeStatementAbstract'
454+ const TABLE = 'us-gaap:StatementTable'
455+ const LINEITEMS = 'us-gaap:StatementLineItems'
456+ const REV = 'us-gaap:Revenues'
457+
458+ function scaffoldReport ( ) : NormalizedReport {
459+ fid = 0
460+ return {
461+ reportId : 'r' ,
462+ reportIri : null ,
463+ entity : null ,
464+ informationBlocks : [
465+ { id : 's' , blockType : 'income_statement' , factSet : 'fs' , label : null , structureId : 's' } ,
466+ ] ,
467+ structures : [
468+ {
469+ id : 's' ,
470+ blockType : 'income_statement' ,
471+ roleUri : null ,
472+ structureName : 'Income' ,
473+ order : 0 ,
474+ } ,
475+ ] ,
476+ facts : [ fact ( REV , 'p' , 383285000000 ) ] ,
477+ elements : {
478+ [ SEC_ABS ] : el ( SEC_ABS , { abstract : true } ) ,
479+ [ TABLE ] : el ( TABLE , { abstract : true } ) ,
480+ [ LINEITEMS ] : el ( LINEITEMS , { abstract : true } ) ,
481+ [ REV ] : el ( REV , { monetary : true , periodType : 'duration' , numericKind : 'monetary' } ) ,
482+ } ,
483+ periods : {
484+ p : {
485+ id : 'p' ,
486+ type : 'duration' ,
487+ instant : null ,
488+ startDate : '2022-09-25' ,
489+ endDate : '2023-09-30' ,
490+ end : '2023-09-30' ,
491+ } ,
492+ } ,
493+ units : { 'u:usd' : { id : 'u:usd' , measure : 'iso4217:USD' , label : 'USD' , symbol : '$' } } ,
494+ calcAssociations : [ ] ,
495+ presAssociations : [
496+ { parent : SEC_ABS , child : TABLE , order : 0 , role : null , structure : 's' } ,
497+ { parent : TABLE , child : LINEITEMS , order : 0 , role : null , structure : 's' } ,
498+ { parent : LINEITEMS , child : REV , order : 0 , role : null , structure : 's' } ,
499+ ] ,
500+ }
501+ }
502+
503+ const p = buildPivot ( scaffoldReport ( ) , scaffoldReport ( ) . informationBlocks [ 0 ] )
504+
505+ it ( 'emits no header row for the [Table]/[Line Items] plumbing' , ( ) => {
506+ const scaffold = p . rows . filter (
507+ ( r ) => r . header && / (?: T a b l e | L i n e I t e m s ) $ / . test ( r . element . qname . split ( ':' ) . pop ( ) ?? '' )
508+ )
509+ expect ( scaffold ) . toHaveLength ( 0 )
510+ } )
511+
512+ it ( 'keeps the real section header and the concepts below it' , ( ) => {
513+ expect ( rowByKey ( p , SEC_ABS ) ?. header ) . toBe ( true )
514+ expect ( rowByKey ( p , REV ) ?. cells ?. [ 0 ] ?. value ) . toBe ( 383285000000 )
515+ } )
516+
517+ it ( 'promotes the concepts to the scaffold’s depth instead of over-indenting' , ( ) => {
518+ // Section header at 0; the two scaffold levels are skipped, so Revenues lands
519+ // at depth 1 — not depth 3 (header + [Table] + [Line Items]).
520+ expect ( rowByKey ( p , SEC_ABS ) ?. depth ) . toBe ( 0 )
521+ expect ( rowByKey ( p , REV ) ?. depth ) . toBe ( 1 )
522+ } )
523+ } )
524+
447525describe ( 'buildPivot — a member-only concept gets a heading row (dims as rows)' , ( ) => {
448526 // A detail disclosure: a concept reported only per-member, with no consolidated
449527 // total. Its member rows must not be orphaned "member-only" labels — a valueless
0 commit comments