diff --git a/composer.json b/composer.json index 7b59f38..37b9044 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ ], "require": { "php": ">=8.1", - "composer/installers": "^2|^1.0.1" + "composer/installers": "^2|^1.0.1", + "opis/json-schema": "^2.3.0" }, "require-dev": { "phpstan/phpstan": "^2.0.1", diff --git a/example.json b/example.json new file mode 100644 index 0000000..a7132d2 --- /dev/null +++ b/example.json @@ -0,0 +1,3 @@ +{ + "linkTargetSitelinkSiteId": "enwiki" +} diff --git a/extension.json b/extension.json index 93ec2ac..994a8fa 100644 --- a/extension.json +++ b/extension.json @@ -36,11 +36,28 @@ }, "Hooks": { + "AlternateEdit": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks::onAlternateEdit", + "BeforePageDisplay": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks::onBeforePageDisplay", + "ContentHandlerDefaultModelFor": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks::onContentHandlerDefaultModelFor", + "EditFilter": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks::onEditFilter", + "EditFormPreloadText": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks::onEditFormPreloadText", "ShowSearchHitTitle": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks::onShowSearchHitTitle", "SpecialSearchResultsAppend": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks::onSpecialSearchResultsAppend" }, "config": { + "WikibaseFacetedSearchEnableInWikiConfig": { + "description": "If it should be possible to define configuration via MediaWiki:WikibaseFacetedSearch", + "value": true + }, + "WikibaseFacetedSearch": { + "description": "Config in JSON format, following the JSON Schema at schema.json. Gets combined with config defined on MediaWiki:WikibaseFacetedSearch", + "value": "" + } + }, + + "SpecialPages": { + "WikibaseFacetedSearchConfig": "ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\SpecialWikibaseFacetedSearchConfig" }, "ResourceFileModulePaths": { diff --git a/i18n/en.json b/i18n/en.json index 10afb5d..f6a8f22 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6,5 +6,13 @@ "Morne Alberts" ] }, - "wikibasefacetedsearch-description": "Enhances [[Special:Search]] with faceted search capabilities. Filter results based on instance type or statement values." + "wikibasefacetedsearch-description": "Enhances [[Special:Search]] with faceted search capabilities. Filter results based on instance type or statement values.", + + "special-wikibase-faceted-search-config": "Faceted search config", + + "wikibase-faceted-search-config-invalid": "Your changes were not saved. They contain the following {{PLURAL:$1|error|errors}}:", + + "wikibase-faceted-search-config-help-documentation": "You can [[#Documentation|view the configuration documentation]] below the edit area.", + "wikibase-faceted-search-config-help": "Configuration documentation", + "wikibase-faceted-search-config-help-example": "Full example" } diff --git a/i18n/qqq.json b/i18n/qqq.json index fd8f1b7..6a27f9d 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -6,5 +6,10 @@ "Morne Alberts" ] }, - "wikibasefacetedsearch-description": "{{Desc|name=WikibaseFacetedSearch|url=https://github.com/ProfessionalWiki/WikibaseFacetedSearch}}" + "wikibasefacetedsearch-description": "{{Desc|name=WikibaseFacetedSearch|url=https://github.com/ProfessionalWiki/WikibaseFacetedSearch}}", + "special-wikibase-faceted-search-config": "This is the label on \"Special:WikibaseFacetedSearchConfig\" linking to \"MediaWiki:WikibaseFacetedSearch\".", + "wikibase-faceted-search-config-invalid": "Error message shown when attempting to save invalid configuration on MediaWiki:WikibaseFacetedSearch", + "wikibase-faceted-search-config-help-documentation": "Link to documentation displayed on \"MediaWiki:WikibaseFacetedSearch\"", + "wikibase-faceted-search-config-help": "Help text heading displayed on \"MediaWiki:WikibaseFacetedSearch\"", + "wikibase-faceted-search-config-help-example": "Help text heading displayed on \"MediaWiki:WikibaseFacetedSearch\"", } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4fd5cee..a437a76 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,29 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Call to method addHTML\(\) on an unknown class OutputPage\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Call to method addModuleStyles\(\) on an unknown class OutputPage\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Call to method getNamespace\(\) on an unknown class Title\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Call to static method element\(\) on an unknown class Html\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - message: '#^Constant WB_NS_ITEM not found\.$#' identifier: constant.notFound @@ -31,25 +7,19 @@ parameters: path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - message: '#^Parameter \$output of method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:onSpecialSearchResultsAppend\(\) has invalid type OutputPage\.$#' - identifier: class.notFound + message: '#^Parameter \#1 \$configArray of method ProfessionalWiki\\WikibaseFacetedSearch\\Persistence\\ConfigDeserializer\:\:newConfig\(\) expects array\, array\ given\.$#' + identifier: argument.type count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php + path: src/Persistence/ConfigDeserializer.php - - message: '#^Parameter \$specialSearch of method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:onShowSearchHitTitle\(\) has invalid type SpecialSearch\.$#' - identifier: class.notFound + message: '#^Parameter \$linkTargetSitelinkSiteId of class ProfessionalWiki\\WikibaseFacetedSearch\\Application\\Config constructor expects string\|null, mixed given\.$#' + identifier: argument.type count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php + path: src/Persistence/ConfigDeserializer.php - - message: '#^Parameter \$specialSearch of method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:onSpecialSearchResultsAppend\(\) has invalid type SpecialSearch\.$#' - identifier: class.notFound + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php - - - - message: '#^Parameter \$title of method ProfessionalWiki\\WikibaseFacetedSearch\\EntryPoints\\WikibaseFacetedSearchHooks\:\:onShowSearchHitTitle\(\) has invalid type Title\.$#' - identifier: class.notFound - count: 1 - path: src/EntryPoints/WikibaseFacetedSearchHooks.php + path: src/WikibaseFacetedSearchExtension.php diff --git a/phpstan.neon b/phpstan.neon index 18318a6..b5823ff 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,3 +10,5 @@ parameters: - ../../tests/phpunit - ../../vendor - ../../extensions/Wikibase + bootstrapFiles: + - ../../includes/AutoLoader.php diff --git a/schema.json b/schema.json new file mode 100644 index 0000000..366b9d0 --- /dev/null +++ b/schema.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "linkTargetSitelinkSiteId": { + "type": [ + "string", + "null" + ] + } + } +} diff --git a/src/Application/Config.php b/src/Application/Config.php new file mode 100644 index 0000000..09819d2 --- /dev/null +++ b/src/Application/Config.php @@ -0,0 +1,20 @@ +linkTargetSitelinkSiteId ?? $this->linkTargetSitelinkSiteId + ); + } + +} diff --git a/src/EntryPoints/SpecialWikibaseFacetedSearchConfig.php b/src/EntryPoints/SpecialWikibaseFacetedSearchConfig.php new file mode 100644 index 0000000..bb4d358 --- /dev/null +++ b/src/EntryPoints/SpecialWikibaseFacetedSearchConfig.php @@ -0,0 +1,36 @@ +getOutput()->redirect( $title->getFullURL() ); + } + } + + public function getGroupName(): string { + return 'wikibase'; + } + + public function getDescription(): Message { + return $this->msg( 'special-wikibase-faceted-search-config' ); + } + +} diff --git a/src/EntryPoints/WikibaseFacetedSearchHooks.php b/src/EntryPoints/WikibaseFacetedSearchHooks.php index 17c2b8c..b94d604 100644 --- a/src/EntryPoints/WikibaseFacetedSearchHooks.php +++ b/src/EntryPoints/WikibaseFacetedSearchHooks.php @@ -4,9 +4,16 @@ namespace ProfessionalWiki\WikibaseFacetedSearch\EntryPoints; +use EditPage; +use Html; use HtmlArmor; use OutputPage; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\ConfigJsonValidator; +use ProfessionalWiki\WikibaseFacetedSearch\Presentation\ConfigJsonErrorFormatter; +use ProfessionalWiki\WikibaseFacetedSearch\Presentation\ExportConfigEditPageTextBuilder; +use ProfessionalWiki\WikibaseFacetedSearch\WikibaseFacetedSearchExtension; use SearchResult; +use Skin; use SpecialSearch; use Title; @@ -41,8 +48,72 @@ public static function onSpecialSearchResultsAppend( // TODO: generate facets from search term $output->addModuleStyles( 'ext.wikibase.facetedsearch.styles' ); $output->addHTML( - \Html::element( 'div', [ 'class' => 'wikibase-faceted-search__facets' ] ) + Html::element( 'div', [ 'class' => 'wikibase-faceted-search__facets' ] ) ); } + public static function onContentHandlerDefaultModelFor( Title $title, ?string &$model ): void { + if ( WikibaseFacetedSearchExtension::getInstance()->isConfigTitle( $title ) ) { + $model = CONTENT_MODEL_JSON; + } + } + + public static function onEditFilter( EditPage $editPage, ?string $text, ?string $section, string &$error ): void { + $validator = ConfigJsonValidator::newInstance(); + + if ( is_string( $text ) + && WikibaseFacetedSearchExtension::getInstance()->isConfigTitle( $editPage->getTitle() ) + && !$validator->validate( $text ) + ) { + $errors = $validator->getErrors(); + $error = Html::errorBox( + wfMessage( 'wikibase-faceted-search-config-invalid', count( $errors ) )->escaped() . + ConfigJsonErrorFormatter::format( $errors ) + ); + } + } + + public static function onAlternateEdit( EditPage $editPage ): void { + if ( WikibaseFacetedSearchExtension::getInstance()->isConfigTitle( $editPage->getTitle() ) ) { + $editPage->suppressIntro = true; + + $textBuilder = new ExportConfigEditPageTextBuilder( $editPage->getContext() ); + $editPage->editFormTextTop = $textBuilder->createTopHtml(); + $editPage->editFormTextBottom = $textBuilder->createBottomHtml(); + } + } + + public static function onEditFormPreloadText( string &$text, Title &$title ): void { + if ( WikibaseFacetedSearchExtension::getInstance()->isConfigTitle( $title ) ) { + $text = trim( ' +{ + "linkTargetSitelinkSiteId": null +}' ); + } + } + + public static function onBeforePageDisplay( OutputPage $out, Skin $skin ): void { + $title = $out->getTitle(); + + if ( $title === null ) { + return; + } + + if ( WikibaseFacetedSearchExtension::getInstance()->isConfigTitle( $title ) ) { + $html = $out->getHTML(); + $out->clearHTML(); + $out->addHTML( self::getConfigPageHtml( $html ) ); + } + } + + private static function getConfigPageHtml( string $html ): string { + $jsonTablePosition = strpos( $html, '' ); + + if ( !$jsonTablePosition ) { + return $html; + } + + return substr( $html, $jsonTablePosition ); + } + } diff --git a/src/Persistence/CombiningConfigLookup.php b/src/Persistence/CombiningConfigLookup.php new file mode 100644 index 0000000..e04f2f4 --- /dev/null +++ b/src/Persistence/CombiningConfigLookup.php @@ -0,0 +1,40 @@ +createDefaultConfig()->combine( + $this->deserializer->deserialize( $this->baseConfig ) + ); + + if ( !$this->enableWikiConfig ) { + return $config; + } + + return $config->combine( $this->configLookup->getConfig() ); + } + + private function createDefaultConfig(): Config { + return new Config(); + } +} diff --git a/src/Persistence/ConfigDeserializer.php b/src/Persistence/ConfigDeserializer.php new file mode 100644 index 0000000..e6244c8 --- /dev/null +++ b/src/Persistence/ConfigDeserializer.php @@ -0,0 +1,37 @@ +validator->validate( $configJson ) ) { + $configArray = json_decode( $configJson, true ); + + if ( is_array( $configArray ) ) { + return $this->newConfig( $configArray ); + } + } + + return new Config(); + } + + /** + * @param array $configArray + */ + private function newConfig( array $configArray ): Config { + return new Config( + linkTargetSitelinkSiteId: $configArray['linkTargetSitelinkSiteId'] ?? null, + ); + } + +} diff --git a/src/Persistence/ConfigJsonValidator.php b/src/Persistence/ConfigJsonValidator.php new file mode 100644 index 0000000..928453d --- /dev/null +++ b/src/Persistence/ConfigJsonValidator.php @@ -0,0 +1,69 @@ +setMaxErrors( 10 ); + + $validationResult = $validator->validate( json_decode( $config ), $this->jsonSchema ); + + $error = $validationResult->error(); + + if ( $error !== null ) { + $this->errors = $this->formatErrors( $error ); + } + + return $error === null; + } + + /** + * @return string[] + */ + public function getErrors(): array { + return $this->errors; + } + + /** + * @return string[] + */ + private function formatErrors( ValidationError $error ): array { + return ( new ErrorFormatter() )->format( $error, false ); + } + +} diff --git a/src/Persistence/ConfigLookup.php b/src/Persistence/ConfigLookup.php new file mode 100644 index 0000000..fb7a9dc --- /dev/null +++ b/src/Persistence/ConfigLookup.php @@ -0,0 +1,13 @@ +contentFetcher->getPageContent( 'MediaWiki:' . $this->pageName ); + + if ( $content instanceof \JsonContent ) { + return $this->configFromJsonContent( $content ); + } + + return new Config(); + } + + private function configFromJsonContent( JsonContent $content ): Config { + return $this->deserializer->deserialize( $content->getText() ); + } + +} diff --git a/src/Persistence/PageContentFetcher.php b/src/Persistence/PageContentFetcher.php new file mode 100644 index 0000000..e7aaed8 --- /dev/null +++ b/src/Persistence/PageContentFetcher.php @@ -0,0 +1,33 @@ +titleParser->parseTitle( $pageTitle ); + } catch ( MalformedTitleException ) { + return null; + } + + $revision = $this->revisionLookup->getRevisionByTitle( $title ); + + return $revision?->getContent( SlotRecord::MAIN ); + } + +} diff --git a/src/Presentation/ConfigJsonErrorFormatter.php b/src/Presentation/ConfigJsonErrorFormatter.php new file mode 100644 index 0000000..9d6adcf --- /dev/null +++ b/src/Presentation/ConfigJsonErrorFormatter.php @@ -0,0 +1,25 @@ +'; + + foreach ( $errors as $key => $value ) { + $html .= ''; + $html .= ''; + } + + $html .= '
' . htmlspecialchars( $key ) . '' . htmlspecialchars( $value ) . '
'; + + return $html; + } + +} diff --git a/src/Presentation/ExportConfigEditPageTextBuilder.php b/src/Presentation/ExportConfigEditPageTextBuilder.php new file mode 100644 index 0000000..23ddc76 --- /dev/null +++ b/src/Presentation/ExportConfigEditPageTextBuilder.php @@ -0,0 +1,81 @@ +' . + $this->createDocumentationLink() . + ''; + } + + private function createDocumentationLink(): string { + return '

' + . $this->context->msg( 'wikibase-faceted-search-config-help-documentation' )->parse() + . '

'; + } + + public function createBottomHtml(): string { + return << +
+

{$this->context->msg( 'wikibase-faceted-search-config-help' )->escaped()}

+ +

+ Besides the configuration reference below, you can consult the Wikibase Faceted Search + usage documentation and + demo wiki. +

+
+ +
+

Link target sitelink site ID

+ +

+ By default search result items link to their item page (Item:Q123). +

+ +

+ You can change the link to use a sitelink of the item instead. +

+ +

+ Example configuration: +

+ +
+{
+	"linkTargetSitelinkSiteId": "enwiki"
+}
+
+ +
+

{$this->context->msg( 'wikibase-faceted-search-config-help-example' )->escaped()}

+ +
{$this->getExampleContents()}
+
+ +HTML; + } + + private function getExampleContents(): string { + $example = file_get_contents( __DIR__ . '/../../example.json' ); + + if ( !is_string( $example ) ) { + return ''; + } + + return $example; + } + +} diff --git a/src/WikibaseFacetedSearchExtension.php b/src/WikibaseFacetedSearchExtension.php index b282778..5cf93e8 100644 --- a/src/WikibaseFacetedSearchExtension.php +++ b/src/WikibaseFacetedSearchExtension.php @@ -4,8 +4,22 @@ namespace ProfessionalWiki\WikibaseFacetedSearch; +use MediaWiki\MediaWikiServices; +use ProfessionalWiki\WikibaseFacetedSearch\Application\Config; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\CombiningConfigLookup; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\ConfigDeserializer; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\ConfigJsonValidator; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\ConfigLookup; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\PageContentConfigLookup; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\PageContentFetcher; +use Title; + class WikibaseFacetedSearchExtension { + public const CONFIG_PAGE_TITLE = 'WikibaseFacetedSearch'; + + private ?Config $config; + public static function getInstance(): self { /** @var ?WikibaseFacetedSearchExtension $instance */ static $instance = null; @@ -13,4 +27,43 @@ public static function getInstance(): self { return $instance; } + public function isConfigTitle( Title $title ): bool { + return $title->getNamespace() === NS_MEDIAWIKI && $title->getText() === self::CONFIG_PAGE_TITLE; + } + + public function getConfig(): Config { + $this->config ??= $this->newConfigLookup()->getConfig(); + return $this->config; + } + + public function clearConfig(): void { + $this->config = null; + } + + private function newConfigLookup(): ConfigLookup { + return new CombiningConfigLookup( + baseConfig: (string)MediaWikiServices::getInstance()->getMainConfig()->get( 'WikibaseFacetedSearch' ), + deserializer: $this->newConfigDeserializer(), + configLookup: $this->newPageContentConfigLookup(), + enableWikiConfig: (bool)MediaWikiServices::getInstance()->getMainConfig()->get( 'WikibaseFacetedSearchEnableInWikiConfig' ) + ); + } + + public function newPageContentConfigLookup(): PageContentConfigLookup { + return new PageContentConfigLookup( + contentFetcher: new PageContentFetcher( + MediaWikiServices::getInstance()->getTitleParser(), + MediaWikiServices::getInstance()->getRevisionLookup() + ), + deserializer: $this->newConfigDeserializer(), + pageName: self::CONFIG_PAGE_TITLE + ); + } + + public function newConfigDeserializer(): ConfigDeserializer { + return new ConfigDeserializer( + ConfigJsonValidator::newInstance() + ); + } + } diff --git a/tests/Application/ConfigTest.php b/tests/Application/ConfigTest.php new file mode 100644 index 0000000..ec7fd10 --- /dev/null +++ b/tests/Application/ConfigTest.php @@ -0,0 +1,47 @@ +createOriginalConfig(); + $new = new Config(); + + $combined = $original->combine( $new ); + + $this->assertEquals( $original, $combined ); + } + + public function testOriginalValuesAreReplacedWhenCombined(): void { + $original = $this->createOriginalConfig(); + $new = $this->createNewConfig(); + + $combined = $original->combine( $new ); + + $this->assertEquals( $new, $combined ); + } + +} diff --git a/tests/Persistence/CombiningConfigLookupTest.php b/tests/Persistence/CombiningConfigLookupTest.php new file mode 100644 index 0000000..46dfc72 --- /dev/null +++ b/tests/Persistence/CombiningConfigLookupTest.php @@ -0,0 +1,68 @@ +newConfigDeserializer(), + configLookup: new StubConfigLookup( $wikiConfig ), + enableWikiConfig: $enableWikiConfig + ); + } + + public function testWikiConfigSupersedesBaseConfig(): void { + $lookup = $this->newLookup( + baseConfig: '{ "linkTargetSitelinkSiteId": "enwiki" }', + wikiConfig: new Config( linkTargetSitelinkSiteId: 'dewiki' ), + enableWikiConfig: true + ); + + $this->assertSame( + 'dewiki', + $lookup->getConfig()->linkTargetSitelinkSiteId + ); + } + + public function testUsesBaseConfigIfThereIsNoWikiConfig(): void { + $lookup = $this->newLookup( + baseConfig: '{ "linkTargetSitelinkSiteId": "enwiki" }', + wikiConfig: new Config(), + enableWikiConfig: true + ); + + $this->assertSame( + 'enwiki', + $lookup->getConfig()->linkTargetSitelinkSiteId + ); + } + + public function testOnlyUsesWikiConfigWhenEnabled(): void { + $lookup = $this->newLookup( + baseConfig: '{ "linkTargetSitelinkSiteId": "enwiki" }', + wikiConfig: new Config( linkTargetSitelinkSiteId: 'dewiki' ), + enableWikiConfig: false + ); + + $this->assertSame( + 'enwiki', + $lookup->getConfig()->linkTargetSitelinkSiteId + ); + } + +} diff --git a/tests/Persistence/ConfigDeserializerTest.php b/tests/Persistence/ConfigDeserializerTest.php new file mode 100644 index 0000000..5ca4435 --- /dev/null +++ b/tests/Persistence/ConfigDeserializerTest.php @@ -0,0 +1,41 @@ +newConfigDeserializer(); + + $config = $deserializer->deserialize( Valid::configJson() ); + + $this->assertEquals( + new Config( + linkTargetSitelinkSiteId: 'enwiki' + ), + $config + ); + } + + public function testInvalidJsonReturnsEmptyConfig(): void { + $deserializer = WikibaseFacetedSearchExtension::getInstance()->newConfigDeserializer(); + + $config = $deserializer->deserialize( '}{' ); + $emptyConfig = new Config(); + + $this->assertEquals( $emptyConfig, $config ); + } + +} diff --git a/tests/Persistence/ConfigJsonValidatorTest.php b/tests/Persistence/ConfigJsonValidatorTest.php new file mode 100644 index 0000000..cec1886 --- /dev/null +++ b/tests/Persistence/ConfigJsonValidatorTest.php @@ -0,0 +1,45 @@ +assertTrue( + ConfigJsonValidator::newInstance()->validate( '{}' ) + ); + } + + public function testValidJsonPassesValidation(): void { + $this->assertTrue( + ConfigJsonValidator::newInstance()->validate( Valid::configJson() ) + ); + } + + public function testStructurallyInvalidJsonFailsValidation(): void { + $this->assertFalse( + ConfigJsonValidator::newInstance()->validate( '}{' ) + ); + } + + public function testInvalidJsonErrorsAreAvailable(): void { + $validator = ConfigJsonValidator::newInstance(); + + $validator->validate( '{ "linkTargetSitelinkSiteId": true }' ); + + $this->assertSame( + [ '/linkTargetSitelinkSiteId' => 'The data (boolean) must match the type: string, null' ], + $validator->getErrors() + ); + } + +} diff --git a/tests/Persistence/PageContentConfigLookupTest.php b/tests/Persistence/PageContentConfigLookupTest.php new file mode 100644 index 0000000..c284909 --- /dev/null +++ b/tests/Persistence/PageContentConfigLookupTest.php @@ -0,0 +1,44 @@ +editConfigPage( '{}' ); + $lookup = WikibaseFacetedSearchExtension::getInstance()->newPageContentConfigLookup(); + + $config = $lookup->getConfig(); + $emptyConfig = new Config(); + + $this->assertEquals( $emptyConfig, $config ); + } + + public function testSavedPageConfig(): void { + $this->editConfigPage( Valid::configJson() ); + $lookup = WikibaseFacetedSearchExtension::getInstance()->newPageContentConfigLookup(); + + $config = $lookup->getConfig(); + + $this->assertEquals( + new Config( + linkTargetSitelinkSiteId: 'enwiki' + ), + $config + ); + } + +} diff --git a/tests/TestDoubles/StubConfigLookup.php b/tests/TestDoubles/StubConfigLookup.php new file mode 100644 index 0000000..89fab25 --- /dev/null +++ b/tests/TestDoubles/StubConfigLookup.php @@ -0,0 +1,21 @@ +config; + } + +} diff --git a/tests/TestDoubles/Valid.php b/tests/TestDoubles/Valid.php new file mode 100644 index 0000000..9f1fcda --- /dev/null +++ b/tests/TestDoubles/Valid.php @@ -0,0 +1,17 @@ +clearConfig(); + } + + protected function editConfigPage( string $config ): void { + $this->editPage( + 'MediaWiki:' . WikibaseFacetedSearchExtension::CONFIG_PAGE_TITLE, + $config + ); + } + + protected function getPageHtml( string $pageTitle ): string { + $title = \Title::newFromText( $pageTitle ); + + $article = new Article( $title, 0 ); + $article->getContext()->getOutput()->setTitle( $title ); + + $article->view(); + + return $article->getContext()->getOutput()->getHTML(); + } + + protected function getEditPageHtml( string $pageTitle ): string { + $title = \Title::newFromText( $pageTitle ); + + $article = new Article( $title, 0 ); + $article->getContext()->getOutput()->setTitle( $title ); + + $editPage = new \EditPage( $article ); + $editPage->setContextTitle( $title ); + $editPage->getContext()->setUser( $this->getTestSysop()->getUser() ); + $editPage->edit(); + + return $editPage->getContext()->getOutput()->getHTML(); + } + +}