From a38f83fa569144c556115bd78511c751bbefc10b Mon Sep 17 00:00:00 2001 From: Morne Alberts Date: Thu, 21 May 2026 11:46:25 +0200 Subject: [PATCH] Load per-component JS init modules via the OutputPage hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-component JS init modules (tooltip.fix, popover.fix, carousel.fix) each declare a scripts entry in extension.json that calls the Bootstrap library on the trigger elements. Without these modules reaching the page, hover on a .bootstrap-tooltip and click on a data-toggle="popover" element do nothing. HooksHandler::onParserAfterParse iterates getActiveComponents() and calls addModuleStyles() per component, but getActiveComponents() is only populated by ImageModal — tooltip, popover, and carousel never register themselves as active. So the foreach never reaches their .fix modules at all; even if it did, addModuleStyles loads only CSS. Queue the per-component init modules unconditionally on OutputPage from the OutputPageParserOutput hook. addModules is the right call (loads the JS via mw.loader). Each init script is a no-op on pages without the corresponding trigger markup. Verified on a fresh MW 1.39.17 + Vector + BC stack and on a MW 1.43.8 + Chameleon BS4 + BC stack: tooltip hover and popover click both produce the expected popups. PHPUnit unit suite remains green relative to baseline (6 pre-existing failures unchanged). Co-Authored-By: Claude Opus 4.7 --- src/Hooks/OutputPageParserOutput.php | 6 ++++ .../Unit/Hooks/OutputPageParserOutputTest.php | 34 +++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Hooks/OutputPageParserOutput.php b/src/Hooks/OutputPageParserOutput.php index 7dd7350..dc4bbbc 100644 --- a/src/Hooks/OutputPageParserOutput.php +++ b/src/Hooks/OutputPageParserOutput.php @@ -82,6 +82,12 @@ public function __construct( * @return void */ public function process(): void { + $this->getOutputPage()->addModules( [ + 'ext.bootstrapComponents.tooltip.fix', + 'ext.bootstrapComponents.popover.fix', + 'ext.bootstrapComponents.carousel.fix', + ] ); + if ( $this->getBootstrapComponentsService()->vectorSkinInUse() ) { $this->getOutputPage()->addModules( [ 'ext.bootstrapComponents.vector-fix' ] ); } diff --git a/tests/phpunit/Unit/Hooks/OutputPageParserOutputTest.php b/tests/phpunit/Unit/Hooks/OutputPageParserOutputTest.php index 99f4bd6..61baa53 100644 --- a/tests/phpunit/Unit/Hooks/OutputPageParserOutputTest.php +++ b/tests/phpunit/Unit/Hooks/OutputPageParserOutputTest.php @@ -39,14 +39,16 @@ public function testCanConstruct() { ); } - public function testHookOutputPageParserOutputLoadsVectorFixUnderVector() { + public function testProcessLoadsPerComponentInitModulesAndVectorFix() { + $loadedModules = []; + $outputPage = $this->createMock( OutputPage::class ); $outputPage->expects( $this->never() )->method( 'addHTML' ); - $outputPage->expects( $this->once() ) + $outputPage->expects( $this->atLeastOnce() ) ->method( 'addModules' ) - ->with( - $this->equalTo( [ 'ext.bootstrapComponents.vector-fix' ] ) - ); + ->will( $this->returnCallback( function( $modules ) use ( &$loadedModules ) { + $loadedModules = array_merge( $loadedModules, (array)$modules ); + } ) ); $bootstrapService = $this->createMock( BootstrapComponentsService::class ); $bootstrapService->expects( $this->once() ) @@ -59,12 +61,28 @@ public function testHookOutputPageParserOutputLoadsVectorFixUnderVector() { $bootstrapService ); $instance->process(); + + $expected = [ + 'ext.bootstrapComponents.tooltip.fix', + 'ext.bootstrapComponents.popover.fix', + 'ext.bootstrapComponents.carousel.fix', + 'ext.bootstrapComponents.vector-fix', + ]; + sort( $loadedModules ); + sort( $expected ); + $this->assertEquals( $expected, $loadedModules ); } - public function testHookDoesNothingWhenNotVector() { + public function testProcessSkipsVectorFixWhenNotVector() { + $loadedModules = []; + $outputPage = $this->createMock( OutputPage::class ); $outputPage->expects( $this->never() )->method( 'addHTML' ); - $outputPage->expects( $this->never() )->method( 'addModules' ); + $outputPage->expects( $this->atLeastOnce() ) + ->method( 'addModules' ) + ->will( $this->returnCallback( function( $modules ) use ( &$loadedModules ) { + $loadedModules = array_merge( $loadedModules, (array)$modules ); + } ) ); $bootstrapService = $this->createMock( BootstrapComponentsService::class ); $bootstrapService->expects( $this->once() ) @@ -77,5 +95,7 @@ public function testHookDoesNothingWhenNotVector() { $bootstrapService ); $instance->process(); + + $this->assertNotContains( 'ext.bootstrapComponents.vector-fix', $loadedModules ); } }