getPageConfigMock(); $pageBundleMock = $this->getPageBundleMock( $pageBundleLanguageCode ); $languageVariantConverter = $this->getLanguageVariantConverter( $shouldParsoidBeUsed, $shouldPageConfigFactoryBeUsed, $isLanguageConversionEnabled, $pageBundleLanguageCode, null, $titleLanguageCode, $targetLanguageCode, $sourceLanguageCode ); if ( !$shouldPageConfigFactoryBeUsed ) { $languageVariantConverter->setPageConfig( $pageConfigMock ); } $languageFactoryMock = $this->getLanguageFactoryMock(); $languageVariantConverter->convertPageBundleVariant( $pageBundleMock, new Bcp47CodeValue( $targetLanguageCode ) ); } public static function provideSetConfig() { yield 'PageConfigFactory should not be used if PageConfig is set' => [ false ]; yield 'PageConfigFactory should be used if PageConfig is not set' => [ true ]; } /** @dataProvider provideSourceLanguage */ public function testSourceLanguage( ?string $pageBundleLanguageCode, ?string $titleLanguageCode, ?string $contentLanguageOverride, ?string $targetLanguageCode, ?string $sourceLanguageCode, ?string $expectedSourceCode ) { // Decide what should be called and what should not be $shouldParsoidBeUsed = true; $shouldPageConfigFactoryBeUsed = true; $isLanguageConversionEnabled = true; // Set expected language codes $titleLanguageCode ??= 'en'; $targetLanguageCode ??= $titleLanguageCode; // Create mocks if ( $pageBundleLanguageCode ) { $pageBundleMock = $this->getPageBundleMock( $pageBundleLanguageCode ); } else { $pageBundleMock = $this->getPageBundleMockWithoutLanguage(); } $languageVariantConverter = $this->getLanguageVariantConverter( $shouldParsoidBeUsed, $shouldPageConfigFactoryBeUsed, $isLanguageConversionEnabled, $pageBundleLanguageCode, $contentLanguageOverride, $titleLanguageCode, $targetLanguageCode, // expected target language $expectedSourceCode ); $targetLanguage = new Bcp47CodeValue( $targetLanguageCode ); $sourceLanguage = $sourceLanguageCode ? new Bcp47CodeValue( $sourceLanguageCode ) : null; $languageVariantConverter->convertPageBundleVariant( $pageBundleMock, $targetLanguage, $sourceLanguage ); } public static function provideSourceLanguage() { yield 'content-language in PageBundle' => [ 'sr', // PageBundle content-language null, // Title PageLanguage null, // PageLanguage override 'sr-Cyrl', // target 'sr-Cyrl', // explicit source 'sr-Cyrl' // expected source ]; yield 'content-language but no source language' => [ 'en', // PageBundle content-language null, // Title PageLanguage null, // PageLanguage override 'en', // target null, // explicit source null // expected source ]; yield 'content-language is variant' => [ 'en-ca', // PageBundle content-language null, // Title PageLanguage null, // PageLanguage override 'en', // target null, // explicit source 'en-ca' // expected source ]; yield 'Source variant is given' => [ null, // PageBundle content-language null, // Title PageLanguage null, // PageLanguage override 'en', // target 'en-ca', // explicit source 'en-ca' // expected source ]; yield 'Source variant is a base language' => [ null, // PageBundle content-language null, // Title PageLanguage null, // PageLanguage override 'en', // target 'en', // explicit source null // expected source ]; yield 'Page language override is variant' => [ null, // PageBundle content-language null, // PageBundle content-language 'en-ca', // PageLanguage override 'en', // target 'en-ca', // explicit source 'en-ca' // expected source ]; } /** @dataProvider provideSiteConfiguration */ public function testSiteConfiguration( bool $isLanguageConversionEnabled, bool $shouldParsoidBeUsed, bool $shouldPageConfigFactoryBeUsed ) { // Set expected language codes $pageBundleLanguageCode = 'zh'; $titleLanguageCode = 'zh-hans'; $targetLanguageCode = 'zh-hans'; $sourceLanguageCode = null; // Create mocks $pageBundleMock = $this->getPageBundleMock( $pageBundleLanguageCode ); $languageVariantConverter = $this->getLanguageVariantConverter( $shouldParsoidBeUsed, $shouldPageConfigFactoryBeUsed, $isLanguageConversionEnabled, $pageBundleLanguageCode, null, $titleLanguageCode, $targetLanguageCode, $sourceLanguageCode ); $languageFactoryMock = $this->getLanguageFactoryMock(); $targetLanguage = $languageFactoryMock->getLanguage( $targetLanguageCode ); $languageVariantConverter->convertPageBundleVariant( $pageBundleMock, $targetLanguage ); } public static function provideSiteConfiguration() { $isLanguageConversionEnabled = false; $shouldParsoidBeUsed = false; $shouldPageConfigFactoryBeUsed = false; yield 'If language conversion is disabled, parsoid and page config factory should not be used' => [ $isLanguageConversionEnabled, $shouldParsoidBeUsed, $shouldPageConfigFactoryBeUsed ]; $isLanguageConversionEnabled = true; $shouldParsoidBeUsed = true; $shouldPageConfigFactoryBeUsed = true; yield 'If language conversion is enabled, parsoid and page config factory should be used' => [ $isLanguageConversionEnabled, $shouldParsoidBeUsed, $shouldPageConfigFactoryBeUsed ]; } /** * @param bool $shouldParsoidBeUsed * @param bool $shouldPageConfigFactoryBeUsed * @param bool $isLanguageConversionEnabled * @param string|null $pageBundleLanguageCode * @param string|null $contentLanguageOverride * @param string $titleLanguageCode * @param string $targetLanguageCode * @param string|null $sourceLanguageCode * * @return LanguageVariantConverter */ private function getLanguageVariantConverter( bool $shouldParsoidBeUsed, bool $shouldPageConfigFactoryBeUsed, bool $isLanguageConversionEnabled, ?string $pageBundleLanguageCode, ?string $contentLanguageOverride, string $titleLanguageCode, string $targetLanguageCode, ?string $sourceLanguageCode ): LanguageVariantConverter { // If Content language is set, use language from there, // If PageBundle language code is set, use that // Else, fallback to title page language $pageLanguageCode = $contentLanguageOverride ?? $pageBundleLanguageCode ?? $titleLanguageCode; // The page language code should not be a variant $pageLanguageCode = preg_replace( '/-.*$/', '', $pageLanguageCode ); $shouldSiteConfigBeUsed = true; $pageIdentityValue = new PageIdentityValue( 1, NS_MAIN, 'hello_world', PageIdentity::LOCAL ); // Create the necessary mocks $languageFactoryMock = $this->getLanguageFactoryMock(); $pageLanguage = new Bcp47CodeValue( $pageLanguageCode ); $sourceLanguage = $sourceLanguageCode ? new Bcp47CodeValue( $sourceLanguageCode ) : null; $targetLanguage = new Bcp47CodeValue( $targetLanguageCode ); $pageConfigMock = $this->getPageConfigMock(); $pageConfigFactoryMock = $this->getPageConfigFactoryMock( $shouldPageConfigFactoryBeUsed, // Expected arguments to PageConfigFactory mock [ $pageIdentityValue, null, null, null, $this->constraintEquals( $pageLanguage ) ], $pageConfigMock ); $pageBundleMock = $this->getPageBundleMock( $pageBundleLanguageCode ); $siteConfigMock = $this->getSiteConfigMock( $shouldSiteConfigBeUsed, $pageLanguage, $isLanguageConversionEnabled ); $titleFactoryMock = $this->getTitleFactoryMock( $pageIdentityValue, $titleLanguageCode ); $parsoidMock = $this->getParsoidMock( $shouldParsoidBeUsed, [ $pageConfigMock, 'variant', $pageBundleMock, $this->constraintEquals( [ 'variant' => [ 'source' => $sourceLanguage, 'target' => $targetLanguage, ] ] ) ] ); $languageVariantConverter = new LanguageVariantConverter( $pageIdentityValue, $pageConfigFactoryMock, $parsoidMock, $siteConfigMock, $titleFactoryMock, $this->getLanguageConverterFactoryMock(), $languageFactoryMock ); if ( $contentLanguageOverride ) { $languageVariantConverter->setPageLanguageOverride( $languageFactoryMock->getLanguage( $contentLanguageOverride ) ); } return $languageVariantConverter; } // Mock methods follow /** * @param bool $shouldBeCalled * @param array $arguments * @param PageConfig $pageConfig * * @return MockObject|PageConfigFactory */ private function getPageConfigFactoryMock( bool $shouldBeCalled, array $arguments, PageConfig $pageConfig ) { $mock = $this->createMock( PageConfigFactory::class ); if ( $shouldBeCalled ) { $mock->expects( $this->once() ) ->method( 'create' ) ->with( ...$arguments ) ->willReturn( $pageConfig ); } else { $mock->expects( $this->never() ) ->method( 'create' ); } return $mock; } /** * @param bool $shouldBeCalled * @param array $arguments * * @return MockObject|Parsoid */ private function getParsoidMock( bool $shouldBeCalled, array $arguments ) { $mock = $this->createMock( Parsoid::class ); if ( $shouldBeCalled ) { $mock->expects( $this->once() ) ->method( 'pb2pb' ) ->with( ...$arguments ); } else { $mock->expects( $this->never() ) ->method( 'pb2pb' ); } $mock->method( 'implementsLanguageConversionBcp47' ) ->willReturn( true ); return $mock; } /** * Mock constraint helper to compare equality when there are * Bcp47Code instances involved. * @param mixed $expected The expected value, with embedded Bcp47Codes * @return Constraint a PHPUnit equality constrait */ private function constraintEquals( $expected ): Constraint { return $this->callback( static function ( $actual ) use ( $expected ) { return self::arrayWithCodeEquals( $expected, $actual ); } ); } /** * Compare two values for equality, using case-insensitive BCP-47 code * comparisons for Bcp47Code instances. * @param mixed $expected * @param mixed $actual * @return bool True if the objects should be considered equal, false otherwise. */ private static function arrayWithCodeEquals( $expected, $actual ) { if ( $actual === $expected ) { return true; } if ( is_array( $actual ) && is_array( $expected ) ) { if ( count( $actual ) !== count( $expected ) ) { return false; } foreach ( $expected as $key => $value ) { if ( !array_key_exists( $key, $actual ) ) { return false; } if ( !self::arrayWithCodeEquals( $value, $actual[$key] ) ) { return false; } } return true; } if ( $actual instanceof Bcp47Code && $expected instanceof Bcp47Code ) { # BCP-47 codes are case insensitive. return strcasecmp( $actual->toBcp47Code(), $expected->toBcp47Code() ) == 0; } return false; } /** * @param bool $shouldBeCalled * @param Bcp47Code $baseLanguage * @param bool $isLanguageConversionEnabled * * @return MockObject|SiteConfig */ private function getSiteConfigMock( bool $shouldBeCalled, Bcp47Code $baseLanguage, bool $isLanguageConversionEnabled ) { $mock = $this->createMock( SiteConfig::class ); if ( $shouldBeCalled ) { $mock->expects( $this->once() ) ->method( 'langConverterEnabledBcp47' ) ->with( $this->constraintEquals( $baseLanguage ) ) ->willReturn( $isLanguageConversionEnabled ); } else { $mock->expects( $this->never() ) ->method( 'langConverterEnabledBcp47' ); } return $mock; } /** * @param PageIdentity $pageIdentity * @param string $languageCode * * @return MockObject|TitleFactory */ private function getTitleFactoryMock( PageIdentity $pageIdentity, string $languageCode ) { $languageMock = $this->getLanguageMock( $languageCode ); $titleMock = $this->createMock( Title::class ); $titleMock->method( 'getPageLanguage' ) ->willReturn( $languageMock ); $mock = $this->createMock( TitleFactory::class ); $mock->expects( $this->once() ) ->method( 'newFromPageIdentity' ) ->willReturn( $titleMock ) ->with( $pageIdentity ); return $mock; } /** * @return MockObject|LanguageFactory */ private function getLanguageFactoryMock() { $mock = $this->createMock( LanguageFactory::class ); $mock->method( 'getLanguage' ) ->willReturnCallback( function ( $code ) { static $seen = []; if ( $code instanceof Bcp47Code ) { $code = LanguageCode::bcp47ToInternal( $code ); } if ( !isset( $seen[$code] ) ) { $seen[$code] = $this->getLanguageMock( $code ); } return $seen[$code]; } ); $mock->method( 'getParentLanguage' ) ->willReturnCallback( static function ( $code ) use ( $mock ) { if ( $code instanceof Bcp47Code ) { $code = LanguageCode::bcp47ToInternal( $code ); } $code = preg_replace( '/-.*$/', '', $code ); return $mock->getLanguage( $code ); } ); return $mock; } /** * @return MockObject|PageBundle */ private function getPageBundleMockWithoutLanguage() { return $this->getPageBundleMock( null ); } /** * @param string|null $languageCode * * @return MockObject|PageBundle */ private function getPageBundleMock( ?string $languageCode ) { $mock = $this->createMock( PageBundle::class ); $mock->headers = [ # T320662: this should probably be a BCP-47 code, not internal 'content-language' => $languageCode ]; $mock->html = 'test message'; return $mock; } /** * @return MockObject|PageConfig */ private function getPageConfigMock() { $mock = $this->createNoOpMock( PageConfig::class, [ 'setVariantBcp47' ] ); return $mock; } /** * @param string $languageCode * * @return MockObject|Language */ private function getLanguageMock( $languageCode ): Language { $languageMock = $this->createMock( Language::class ); $languageMock->method( 'getCode' ) ->willReturn( $languageCode ); $languageMock->method( 'toBcp47Code' ) ->willReturn( LanguageCode::bcp47( $languageCode ) ); return $languageMock; } private function getLanguageConverterFactoryMock() { $languageConverterFactoryMock = $this->createMock( LanguageConverterFactory::class ); $languageConverter = $this->createMock( LanguageConverter::class ); $languageConverter->method( 'convertTo' ) ->willReturnCallback( static function ( $text, $code ) { return $text; } ); $languageConverter->method( 'hasVariant' ) ->willReturnCallback( static function ( $code ) { return true; } ); $languageConverterFactoryMock->method( 'getLanguageConverter' ) ->willReturn( $languageConverter ); return $languageConverterFactoryMock; } }