Skip to content

Commit 5e32031

Browse files
malbertsclaude
andcommitted
Add HooksHandlerTest case pinning the foreach addModules contract
Asserts that HooksHandler::onParserAfterParse calls both addModuleStyles and addModules on the ParserOutput for each active component's modules. Previously only addModuleStyles was called, so components with a scripts entry (tooltip.fix, popover.fix, carousel.fix) had their init JS dropped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 4675834 commit 5e32031

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

‎tests/phpunit/Unit/HooksHandlerTest.php‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use MediaWiki\Extension\BootstrapComponents\ComponentLibrary;
77
use MediaWiki\Extension\BootstrapComponents\HooksHandler;
88
use MediaWiki\Extension\BootstrapComponents\NestingController;
9+
use Parser;
10+
use ParserOutput;
911
use PHPUnit\Framework\TestCase;
1012

1113
/**
@@ -72,4 +74,61 @@ public function testOnGalleryGetModes() {
7274
public function testOnOutputPageParserOutput() {
7375
$this->assertTrue( true );
7476
}
77+
78+
/**
79+
* For each active component, the foreach in onParserAfterParse must call
80+
* BOTH addModuleStyles (so the module's CSS half loads as a render-blocking
81+
* <link>) AND addModules (so the JS half is queued via mw.loader). Pins the
82+
* contract introduced when sparse per-component module loading was restored;
83+
* previously only addModuleStyles was called, so component init JS never
84+
* reached the page.
85+
*/
86+
public function testOnParserAfterParseLoadsActiveComponentScriptsAndStyles() {
87+
$loadedStyles = [];
88+
$loadedModules = [];
89+
90+
$parserOutput = $this->createMock( ParserOutput::class );
91+
$parserOutput->method( 'addModuleStyles' )
92+
->willReturnCallback( function ( $m ) use ( &$loadedStyles ) {
93+
$loadedStyles = array_merge( $loadedStyles, (array)$m );
94+
} );
95+
$parserOutput->method( 'addModules' )
96+
->willReturnCallback( function ( $m ) use ( &$loadedModules ) {
97+
$loadedModules = array_merge( $loadedModules, (array)$m );
98+
} );
99+
100+
$parser = $this->createMock( Parser::class );
101+
$parser->method( 'getOutput' )->willReturn( $parserOutput );
102+
103+
$service = $this->createMock( BootstrapComponentsService::class );
104+
$service->method( 'getNameOfActiveSkin' )->willReturn( 'vector' );
105+
$service->method( 'getActiveComponents' )->willReturn( [ 'tooltip', 'popover' ] );
106+
107+
$library = $this->createMock( ComponentLibrary::class );
108+
$library->method( 'isRegistered' )->willReturn( true );
109+
$library->method( 'getModulesFor' )
110+
->willReturnCallback( function ( $name ) {
111+
return [ 'ext.bootstrapComponents.' . $name . '.fix' ];
112+
} );
113+
114+
$handler = new HooksHandler(
115+
$service,
116+
$library,
117+
$this->createMock( NestingController::class )
118+
);
119+
120+
$text = '';
121+
$handler->onParserAfterParse( $parser, $text, null );
122+
123+
// Per-component fix modules must reach BOTH addModuleStyles and addModules
124+
// (style-only modules treat addModules as a no-op; modules with a scripts
125+
// entry get their JS init via that call).
126+
$this->assertContains( 'ext.bootstrapComponents.tooltip.fix', $loadedStyles );
127+
$this->assertContains( 'ext.bootstrapComponents.popover.fix', $loadedStyles );
128+
$this->assertContains( 'ext.bootstrapComponents.tooltip.fix', $loadedModules );
129+
$this->assertContains( 'ext.bootstrapComponents.popover.fix', $loadedModules );
130+
131+
// And the global Bootstrap-library JS module is queued as before.
132+
$this->assertContains( 'ext.bootstrap.scripts', $loadedModules );
133+
}
75134
}

0 commit comments

Comments
 (0)