|
6 | 6 | use MediaWiki\Extension\BootstrapComponents\ComponentLibrary; |
7 | 7 | use MediaWiki\Extension\BootstrapComponents\HooksHandler; |
8 | 8 | use MediaWiki\Extension\BootstrapComponents\NestingController; |
| 9 | +use Parser; |
| 10 | +use ParserOutput; |
9 | 11 | use PHPUnit\Framework\TestCase; |
10 | 12 |
|
11 | 13 | /** |
@@ -72,4 +74,61 @@ public function testOnGalleryGetModes() { |
72 | 74 | public function testOnOutputPageParserOutput() { |
73 | 75 | $this->assertTrue( true ); |
74 | 76 | } |
| 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 | + } |
75 | 134 | } |
0 commit comments