REST API endpoint for popups

Implement a rest API endpoint that displays the popup that is currently
only shown on a special page.

Code was revived from Idd22057a88312bf1a1cb5546d0a6edca5678d80d

Bug: T288076
Bug: T233099
Change-Id: I65fcbf25ac5818f6c649daf494c719921247e8f5
This commit is contained in:
AndreG-P 2022-07-08 14:13:08 +09:00
parent a4174e2ecd
commit 951dec1fab
No known key found for this signature in database
GPG key ID: D43E19D36B9AEB8A
13 changed files with 5458 additions and 4130 deletions

View file

@ -4,11 +4,13 @@ use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\Math\InputCheck\InputCheckFactory;
use MediaWiki\Extension\Math\Math;
use MediaWiki\Extension\Math\MathConfig;
use MediaWiki\Extension\Math\MathFormatter;
use MediaWiki\Extension\Math\MathWikibaseConnector;
use MediaWiki\Extension\Math\Render\RendererFactory;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use Wikibase\Client\WikibaseClient;
use Wikibase\Lib\Formatters\SnakFormatter;
return [
'Math.CheckerFactory' => static function ( MediaWikiServices $services ): InputCheckFactory {
@ -48,6 +50,7 @@ return [
WikibaseClient::getFallbackLabelDescriptionLookupFactory( $services ),
WikibaseClient::getSite( $services ),
WikibaseClient::getEntityIdParser( $services ),
new MathFormatter( SnakFormatter::FORMAT_HTML ),
LoggerFactory::getInstance( 'Math' )
);
}

View file

@ -20,6 +20,7 @@
},
"TestAutoloadClasses": {
"DummyPropertyDataTypeLookup": "tests/phpunit/DummyPropertyDataTypeLookup.php",
"MediaWiki\\Extension\\Math\\Tests\\MathWikibaseConnectorTestFactory": "tests/phpunit/unit/MathWikibaseConnectorTestFactory.php",
"MediaWiki\\Extension\\Math\\Tests\\MathMockHttpTrait": "tests/phpunit/MathMockHttpTrait.php"
},
"DefaultUserOptions": {
@ -351,5 +352,17 @@
"ServiceWiringFiles": [
"ServiceWiring.php"
],
"RestRoutes": [
{
"path": "/math/v0/popup/html/{qid}",
"method": "GET",
"class": "MediaWiki\\Extension\\Math\\Rest\\Popup",
"services": [
"Math.WikibaseConnector",
"LanguageFactory",
"TitleFactory"
]
}
],
"manifest_version": 2
}

8813
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,14 +4,17 @@
"scripts": {
"test": "grunt test",
"selenium-daily": "npm run selenium-test",
"selenium-test": "wdio tests/selenium/wdio.conf.js"
"selenium-test": "wdio tests/selenium/wdio.conf.js",
"api-testing": "mocha tests/api-testing"
},
"devDependencies": {
"@wdio/cli": "7.16.13",
"@wdio/dot-reporter": "7.16.13",
"@wdio/junit-reporter": "7.16.13",
"@wdio/local-runner": "7.16.13",
"@wdio/mocha-framework": "7.16.13",
"@wdio/spec-reporter": "7.16.13",
"api-testing": "^1.4.2",
"eslint-config-wikimedia": "0.22.1",
"grunt": "1.5.2",
"grunt-banana-checker": "0.9.0",

View file

@ -60,6 +60,9 @@ class MathWikibaseConnector {
/** @var FallbackLabelDescriptionLookupFactory */
private $labelDescriptionLookupFactory;
/** @var MathFormatter */
private $mathFormatter;
/** @var EntityIdParser */
private $idParser;
@ -86,6 +89,7 @@ class MathWikibaseConnector {
* @param FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory
* @param Site $site
* @param EntityIdParser $entityIdParser
* @param MathFormatter $mathFormatter
* @param LoggerInterface $logger
*/
public function __construct(
@ -96,6 +100,7 @@ class MathWikibaseConnector {
FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory,
Site $site,
EntityIdParser $entityIdParser,
MathFormatter $mathFormatter,
LoggerInterface $logger
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
@ -105,6 +110,7 @@ class MathWikibaseConnector {
$this->labelDescriptionLookupFactory = $labelDescriptionLookupFactory;
$this->site = $site;
$this->idParser = $entityIdParser;
$this->mathFormatter = $mathFormatter;
$this->logger = $logger;
$this->propertyIdHasPart = $this->loadPropertyId(
@ -160,7 +166,7 @@ class MathWikibaseConnector {
* @throws InvalidArgumentException if the language code does not exist or the given
* id does not exist
*/
public function fetchWikibaseFromId( $qid, $langCode ): MathWikibaseInfo {
public function fetchWikibaseFromId( string $qid, string $langCode ): MathWikibaseInfo {
try {
$lang = $this->languageFactory->getLanguage( $langCode );
} catch ( MWException $e ) {
@ -182,7 +188,7 @@ class MathWikibaseConnector {
}
$entity = $entityRevision->getEntity();
$output = new MathWikibaseInfo( $entityId );
$output = new MathWikibaseInfo( $entityId, $this->mathFormatter );
if ( $entity instanceof Item ) {
$this->fetchLabelDescription( $output, $langLookup );
@ -292,7 +298,7 @@ class MathWikibaseConnector {
$entityIdValue = $dataVal->getValue();
if ( $entityIdValue instanceof EntityIdValue ) {
$innerEntityId = $entityIdValue->getEntityId();
$innerInfo = new MathWikibaseInfo( $innerEntityId );
$innerInfo = new MathWikibaseInfo( $innerEntityId, $output->getFormatter() );
$this->fetchLabelDescription( $innerInfo, $langLookup );
$url = $this->fetchPageUrl( $innerEntityId );
if ( $url ) {

View file

@ -47,9 +47,13 @@ class MathWikibaseInfo {
*/
private $url;
public function __construct( EntityId $entityId ) {
/**
* @param EntityId $entityId
* @param MathFormatter|null $mathFormatter to format math equations. Default format is HTML.
*/
public function __construct( EntityId $entityId, MathFormatter $mathFormatter = null ) {
$this->id = $entityId;
$this->mathFormatter = new MathFormatter( SnakFormatter::FORMAT_HTML );
$this->mathFormatter = $mathFormatter ?: new MathFormatter( SnakFormatter::FORMAT_HTML );
}
/**
@ -77,7 +81,7 @@ class MathWikibaseInfo {
* @param MathWikibaseInfo $info
*/
public function addHasPartElement( MathWikibaseInfo $info ) {
array_push( $this->hasParts, $info );
$this->hasParts[] = $info;
}
/**
@ -147,6 +151,13 @@ class MathWikibaseInfo {
return $this->url;
}
/**
* @return MathFormatter
*/
public function getFormatter() {
return $this->mathFormatter;
}
/**
* Does this info object has elements?
* @return bool true if there are elements otherwise false

115
src/Rest/Popup.php Normal file
View file

@ -0,0 +1,115 @@
<?php
namespace MediaWiki\Extension\Math\Rest;
use Html;
use MediaWiki\Extension\Math\MathWikibaseConnector;
use MediaWiki\Extension\Math\MathWikibaseInfo;
use MediaWiki\Languages\LanguageFactory;
use MediaWiki\Rest\Response;
use MediaWiki\Rest\SimpleHandler;
use MWException;
use Title;
use TitleFactory;
use Wikimedia\ParamValidator\ParamValidator;
class Popup extends SimpleHandler {
/** @var MathWikibaseConnector */
private $wikibase;
/** @var LanguageFactory */
private $languageFactory;
/** @var Title|null */
private $specialPageTitle;
/**
* @param MathWikibaseConnector $wikibase
* @param LanguageFactory $languageFactory
* @param TitleFactory $titleFactory
*/
public function __construct(
MathWikibaseConnector $wikibase,
LanguageFactory $languageFactory,
TitleFactory $titleFactory
) {
$this->wikibase = $wikibase;
$this->languageFactory = $languageFactory;
$this->specialPageTitle = $titleFactory->newFromText( 'Special:MathWikibase' );
}
public function run( int $qid ): Response {
$uselang = $this->getRequest()->getHeaderLine( 'Accept-Language' );
if ( $uselang === '' ) {
$uselang = 'en';
}
$rf = $this->getResponseFactory();
try {
$langObj = $this->languageFactory->getLanguage( $uselang );
} catch ( MWException $e ) {
return $rf->createHttpError( 400, [ 'message' => 'Invalid language code.' ] );
}
try {
$info = $this->wikibase->fetchWikibaseFromId( "Q{$qid}", $uselang );
} catch ( \InvalidArgumentException $exception ) {
return $rf->createHttpError( 400, [ 'message' => $exception->getMessage() ] );
}
$html = $this->buildHTMLRepresentation( $info );
$response = [
'title' => $info->getLabel(),
'contentmodel' => 'html',
'pagelanguagedir' => $langObj->getDir(),
'pagelanguage' => $langObj->getCode(),
'pagelanguagehtmlcode' => $langObj->getHtmlCode(),
'extract' => $html
];
if ( $this->specialPageTitle ) {
$response = array_merge( $response, [
'canonicalurl' => $this->specialPageTitle->getLocalURL( [ 'qid' => "Q{$qid}" ] ),
'fullurl' => $this->specialPageTitle->getFullURL( [ 'qid' => "Q{$qid}" ] )
] );
}
return $rf->createJson( $response );
}
/**
* Generates an HTML string from the given data.
* @param MathWikibaseInfo $info an info object generated by fetchWikibaseFromId
* @return string an HTML representation of the given info object
*/
private function buildHTMLRepresentation( MathWikibaseInfo $info ): string {
$output =
Html::openElement( "div",
[ "style" =>
"width: 100%; display: flex; flex-direction: column;
align-items: flex-start; flex-wrap: wrap;" ] );
$output .= Html::element( "span", [
"style" => "font-weight: bold; text-transform: capitalize;"
], $info->getLabel() );
$output .= Html::element( "span", [ "style" => "font-size: small" ], " (" . $info->getDescription() . ")" );
if ( $info->hasParts() ) {
$output .= Html::rawElement( "div",
[ "style" => "width: 100%; display: flex; justify-content: left; padding-top: 5px;" ],
$info->generateSmallTableOfParts() );
}
$output .= Html::closeElement( "div" );
return $output;
}
public function getParamSettings() {
return [
'qid' => [
self::PARAM_SOURCE => 'path',
ParamValidator::PARAM_TYPE => 'integer',
ParamValidator::PARAM_REQUIRED => true,
]
];
}
}

View file

@ -178,7 +178,7 @@ class SpecialMathWikibase extends SpecialPage {
->plain();
$output->addHTML( self::createHTMLHeader( $header ) );
if ( $info->getSymbol() ) {
if ( $info->getFormattedSymbol() ) {
$math = $info->getFormattedSymbol();
$formulaInfo = new Message( 'math-wikibase-formula-header-format' );
$formulaInfo->rawParams(
@ -251,7 +251,7 @@ class SpecialMathWikibase extends SpecialPage {
* Check whether Wikibase is available or not
* @return bool
*/
public static function isWikibaseAvailable() {
public static function isWikibaseAvailable(): bool {
return ExtensionRegistry::getInstance()->isLoaded( 'WikibaseClient' );
}
}

View file

@ -0,0 +1,14 @@
{
"root": true,
"extends": [
"wikimedia/server",
"wikimedia/node",
"wikimedia/language/es2017"
],
"env": {
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2018
}
}

View file

@ -0,0 +1,21 @@
'use strict';
const { assert, REST } = require( 'api-testing' );
describe( 'Math popup endpoint test', () => {
const client = new REST( 'rest.php/math/v0' );
it( 'should get a 400 response for bad value of hash param', async () => {
const { status, body } = await client.get( '/popup/html/thebadvalue' );
assert.strictEqual( status, 400 );
assert.strictEqual( body.httpReason, 'Bad Request' );
assert.include( body.messageTranslations.en, 'thebadvalue' );
} );
it( 'should get a 400 response for a malformed item ID starting with Q', async () => {
const { status, body } = await client.get( '/popup/html/Q1' );
assert.strictEqual( status, 400 );
assert.strictEqual( body.httpReason, 'Bad Request' );
assert.include( body.messageTranslations.en, 'Q1' );
} );
} );

View file

@ -3,61 +3,25 @@
namespace MediaWiki\Extension\Math\Tests;
use DataValues\StringValue;
use Language;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\Math\MathWikibaseConnector;
use MediaWiki\Languages\LanguageFactory;
use MediaWiki\Logger\LoggerFactory;
use MediaWikiUnitTestCase;
use MWException;
use Psr\Log\LoggerInterface;
use Site;
use TestLogger;
use Wikibase\Client\RepoLinker;
use Wikibase\DataAccess\DatabaseEntitySource;
use Wikibase\DataAccess\EntitySourceDefinitions;
use Wikibase\DataModel\Entity\BasicEntityIdParser;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Entity\EntityIdParsingException;
use Wikibase\DataModel\Entity\EntityIdValue;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Entity\NumericPropertyId;
use Wikibase\DataModel\SiteLink;
use Wikibase\DataModel\Snak\PropertyValueSnak;
use Wikibase\DataModel\Snak\SnakList;
use Wikibase\DataModel\Statement\Statement;
use Wikibase\DataModel\Statement\StatementList;
use Wikibase\DataModel\Term\Term;
use Wikibase\Lib\Store\EntityRevision;
use Wikibase\Lib\Store\EntityRevisionLookup;
use Wikibase\Lib\Store\FallbackLabelDescriptionLookup;
use Wikibase\Lib\Store\FallbackLabelDescriptionLookupFactory;
use Wikibase\Lib\Store\StorageException;
use Wikibase\Lib\SubEntityTypesMapper;
/**
* @covers \MediaWiki\Extension\Math\MathWikibaseConnector
*/
class MathWikibaseConnectorTest extends MediaWikiUnitTestCase {
private const EXAMPLE_URL = 'https://example.com/';
private const TEST_ITEMS = [
'Q1' => [ 'massenergy equivalence', 'physical law relating mass to energy', 'E = mc^2' ],
'Q2' => [ 'energy', 'measure for the ability of a system to do work', 'E' ],
'Q3' => [
'speed of light',
'speed at which all massless particles and associated fields travel in vacuum',
'c'
],
'Q4' => [
'mass',
'property of matter to resist changes of the state of motion and to attract other bodies',
'm'
]
];
class MathWikibaseConnectorTest extends MathWikibaseConnectorTestFactory {
public function testGetUrl() {
$mathWikibase = $this->getWikibaseConnector();
@ -184,6 +148,7 @@ class MathWikibaseConnectorTest extends MediaWikiUnitTestCase {
$this->assertCount( 0, $wikibaseInfo->getParts() );
$this->assertFalse( $wikibaseInfo->hasParts() );
$this->assertNull( $wikibaseInfo->getSymbol() );
$this->assertNull( $wikibaseInfo->getFormattedSymbol() );
}
public function testFetchItemWithFormula() {
@ -203,6 +168,10 @@ class MathWikibaseConnectorTest extends MediaWikiUnitTestCase {
$wikibaseInfo = $wikibaseConnector->fetchWikibaseFromId( 'Q1', 'en' );
$this->assertFalse( $wikibaseInfo->hasParts() );
$this->assertEquals( $formulaValue, $wikibaseInfo->getSymbol() );
$this->assertEquals(
$this->getExpectedMathML( $formulaValue->getValue() ),
$wikibaseInfo->getFormattedSymbol()
);
}
/**
@ -215,7 +184,9 @@ class MathWikibaseConnectorTest extends MediaWikiUnitTestCase {
$this->assertEquals( $item->getId(), $wikibaseInfo->getId() );
$this->assertEquals( self::TEST_ITEMS[ 'Q1' ][0], $wikibaseInfo->getLabel() );
$this->assertEquals( self::TEST_ITEMS[ 'Q1' ][1], $wikibaseInfo->getDescription() );
$this->assertEquals( self::TEST_ITEMS[ 'Q1' ][2], $wikibaseInfo->getSymbol()->getValue() );
$mathFormula = self::TEST_ITEMS[ 'Q1' ][2];
$this->assertEquals( $mathFormula, $wikibaseInfo->getSymbol()->getValue() );
$this->assertEquals( $this->getExpectedMathML( $mathFormula ), $wikibaseInfo->getFormattedSymbol() );
$this->assertTrue( $wikibaseInfo->hasParts() );
$parts = $wikibaseInfo->getParts();
@ -224,7 +195,9 @@ class MathWikibaseConnectorTest extends MediaWikiUnitTestCase {
$key = $part->getId()->getSerialization();
$this->assertEquals( self::TEST_ITEMS[ $key ][0], $part->getLabel() );
$this->assertEquals( self::TEST_ITEMS[ $key ][1], $part->getDescription() );
$this->assertEquals( self::TEST_ITEMS[ $key ][2], $part->getSymbol()->getValue() );
$mathFormula = self::TEST_ITEMS[ $key ][2];
$this->assertEquals( $mathFormula, $part->getSymbol()->getValue() );
$this->assertEquals( $this->getExpectedMathML( $mathFormula ), $part->getFormattedSymbol() );
$this->assertEquals( self::EXAMPLE_URL, $part->getUrl() );
}
}
@ -270,174 +243,4 @@ class MathWikibaseConnectorTest extends MediaWikiUnitTestCase {
[ $this->setupMassEnergyEquivalenceItem( false ) ],
];
}
private function setupMassEnergyEquivalenceItem(
bool $hasPartMode
) {
$partPropertyId = new NumericPropertyId( $hasPartMode ? 'P1' : 'P4' );
$symbolPropertyId = new NumericPropertyId( $hasPartMode ? 'P3' : 'P5' );
$items = [];
$statements = [];
foreach ( self::TEST_ITEMS as $key => $itemInfo ) {
$itemId = new ItemId( $key );
$items[ $key ] = new Item( $itemId );
$siteLinkMock = $this->createMock( SiteLink::class );
$siteLinkMock->method( 'getSiteId' )->willReturn( '' );
$siteLinkMock->method( 'getPageName' )->willReturn( '' );
$items[ $key ]->addSiteLink( $siteLinkMock );
if ( $key === 'Q1' ) {
continue;
}
$partSnak = new PropertyValueSnak(
$partPropertyId,
$hasPartMode ? new EntityIdValue( $items[ $key ]->getId() ) : new StringValue( $itemInfo[2] )
);
$partQualifier = new PropertyValueSnak(
$symbolPropertyId,
$hasPartMode ? new StringValue( $itemInfo[2] ) : new EntityIdValue( $items[ $key ]->getId() )
);
$statement = new Statement( $partSnak );
$statement->setQualifiers( new SnakList( [ $partQualifier ] ) );
$statements[] = $statement;
}
$mainFormulaValue = new StringValue( self::TEST_ITEMS[ 'Q1' ][2] );
$definingFormulaStatement = new Statement( new PropertyValueSnak(
new NumericPropertyId( 'P2' ),
$mainFormulaValue
) );
$statementList = new StatementList( ...$statements );
$statementList->addStatement( $definingFormulaStatement );
$items[ 'Q1' ]->setStatements( $statementList );
return $items[ 'Q1' ];
}
private function newConnector(): RepoLinker {
return new RepoLinker(
new EntitySourceDefinitions(
[
new DatabaseEntitySource(
'test',
'testdb',
[ 'item' => [ 'namespaceId' => 123, 'slot' => 'main' ] ],
self::EXAMPLE_URL . 'entity',
'',
'',
''
)
],
new SubEntityTypesMapper( [] )
),
self::EXAMPLE_URL,
'/wiki/$1',
'' );
}
private function getWikibaseConnectorWithExistingItems(
EntityRevision $entityRevision,
bool $storageExceptionOnQ3 = false,
LoggerInterface $logger = null,
EntityIdParser $parser = null
): MathWikibaseConnector {
$revisionLookupMock = $this->createMock( EntityRevisionLookup::class );
$revisionLookupMock->method( 'getEntityRevision' )->willReturnCallback(
static function ( EntityId $entityId ) use ( $entityRevision, $storageExceptionOnQ3 ) {
if ( $storageExceptionOnQ3 && $entityId->getSerialization() === 'Q3' ) {
throw new StorageException( 'Test Exception' );
} else {
return $entityRevision;
}
}
);
$revisionLookupMock->expects( $this->atLeastOnce() )
->method( 'getEntityRevision' );
$fallbackLabelDescriptionLookupFactoryMock = $this->createMock( FallbackLabelDescriptionLookupFactory::class );
$languageMock = $this->createMock( Language::class );
$languageFactoryMock = $this->createMock( LanguageFactory::class );
$languageFactoryMock->method( 'getLanguage' )
->with( 'en' )
->willReturn( $languageMock );
$fallbackLabelDescriptionLookupFactoryMock->method( 'newLabelDescriptionLookup' )
->with( $languageMock )
->willReturnCallback( [ $this, 'newLabelDescriptionLookup' ] );
return $this->getWikibaseConnector(
$languageFactoryMock,
$fallbackLabelDescriptionLookupFactoryMock,
$revisionLookupMock,
$logger,
$parser
);
}
/**
* @param LanguageFactory|null $languageFactory
* @param FallbackLabelDescriptionLookupFactory|null $labelDescriptionLookupFactory
* @param EntityRevisionLookup|null $entityRevisionLookupMock
* @param LoggerInterface|null $logger
* @param EntityIdParser|null $parser
* @return MathWikibaseConnector
*/
public function getWikibaseConnector(
LanguageFactory $languageFactory = null,
FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory = null,
EntityRevisionLookup $entityRevisionLookupMock = null,
LoggerInterface $logger = null,
EntityIdParser $parser = null
): MathWikibaseConnector {
$labelDescriptionLookupFactory = $labelDescriptionLookupFactory ?:
$this->createMock( FallbackLabelDescriptionLookupFactory::class );
$entityRevisionLookup = $entityRevisionLookupMock ?:
$this->createMock( EntityRevisionLookup::class );
$languageFactory = $languageFactory ?: $this->createMock( LanguageFactory::class );
$site = $this->createMock( Site::class );
$site->method( 'getGlobalId' )->willReturn( '' );
$site->method( 'getPageUrl' )->willReturn( self::EXAMPLE_URL );
return new MathWikibaseConnector(
new ServiceOptions( MathWikibaseConnector::CONSTRUCTOR_OPTIONS, [
'MathWikibasePropertyIdHasPart' => 'P1',
'MathWikibasePropertyIdDefiningFormula' => 'P2',
'MathWikibasePropertyIdQuantitySymbol' => 'P3',
'MathWikibasePropertyIdInDefiningFormula' => 'P4',
'MathWikibasePropertyIdSymbolRepresents' => 'P5'
] ),
$this->newConnector(),
$languageFactory,
$entityRevisionLookup,
$labelDescriptionLookupFactory,
$site,
$parser ?: new BasicEntityIdParser(),
$logger ?: new TestLogger()
);
}
public function newLabelDescriptionLookup(): FallbackLabelDescriptionLookup {
$lookup = $this->createMock( FallbackLabelDescriptionLookup::class );
$lookup->method( 'getLabel' )
->willReturnCallback( static function ( EntityId $entityId ) {
if ( self::TEST_ITEMS[ $entityId->getSerialization() ] !== null ) {
return new Term( 'en', self::TEST_ITEMS[ $entityId->getSerialization() ][0] );
} else {
return null;
}
} );
$lookup->method( 'getDescription' )
->willReturnCallback( static function ( EntityId $entityId ) {
if ( self::TEST_ITEMS[ $entityId->getSerialization() ] !== null ) {
return new Term( 'en', self::TEST_ITEMS[ $entityId->getSerialization() ][1] );
} else {
return null;
}
} );
return $lookup;
}
}

View file

@ -0,0 +1,214 @@
<?php
namespace MediaWiki\Extension\Math\Tests;
use DataValues\StringValue;
use Language;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\Math\MathFormatter;
use MediaWiki\Extension\Math\MathWikibaseConnector;
use MediaWiki\Languages\LanguageFactory;
use MediaWikiUnitTestCase;
use Psr\Log\LoggerInterface;
use Site;
use TestLogger;
use Wikibase\Client\RepoLinker;
use Wikibase\DataModel\Entity\BasicEntityIdParser;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Entity\EntityIdValue;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Entity\NumericPropertyId;
use Wikibase\DataModel\SiteLink;
use Wikibase\DataModel\Snak\PropertyValueSnak;
use Wikibase\DataModel\Snak\SnakList;
use Wikibase\DataModel\Statement\Statement;
use Wikibase\DataModel\Statement\StatementList;
use Wikibase\DataModel\Term\Term;
use Wikibase\Lib\Store\EntityRevision;
use Wikibase\Lib\Store\EntityRevisionLookup;
use Wikibase\Lib\Store\FallbackLabelDescriptionLookup;
use Wikibase\Lib\Store\FallbackLabelDescriptionLookupFactory;
use Wikibase\Lib\Store\StorageException;
class MathWikibaseConnectorTestFactory extends MediaWikiUnitTestCase {
public const EXAMPLE_URL = 'https://example.com/';
public const TEST_ITEMS = [
'Q1' => [ 'massenergy equivalence', 'physical law relating mass to energy', 'E = mc^2' ],
'Q2' => [ 'energy', 'measure for the ability of a system to do work', 'E' ],
'Q3' => [
'speed of light',
'speed at which all massless particles and associated fields travel in vacuum',
'c'
],
'Q4' => [
'mass',
'property of matter to resist changes of the state of motion and to attract other bodies',
'm'
]
];
public function getWikibaseConnectorWithExistingItems(
EntityRevision $entityRevision,
bool $storageExceptionOnQ3 = false,
LoggerInterface $logger = null,
EntityIdParser $parser = null
): MathWikibaseConnector {
$revisionLookupMock = self::createMock( EntityRevisionLookup::class );
$revisionLookupMock->method( 'getEntityRevision' )->willReturnCallback(
static function ( EntityId $entityId ) use ( $entityRevision, $storageExceptionOnQ3 ) {
if ( $storageExceptionOnQ3 && $entityId->getSerialization() === 'Q3' ) {
throw new StorageException( 'Test Exception' );
} else {
return $entityRevision;
}
}
);
$revisionLookupMock->expects( self::atLeastOnce() )
->method( 'getEntityRevision' );
$fallbackLabelDescriptionLookupFactoryMock = self::createMock( FallbackLabelDescriptionLookupFactory::class );
$languageMock = self::createMock( Language::class );
$languageFactoryMock = self::createMock( LanguageFactory::class );
$languageFactoryMock->method( 'getLanguage' )
->with( 'en' )
->willReturn( $languageMock );
$fallbackLabelDescriptionLookupFactoryMock->method( 'newLabelDescriptionLookup' )
->with( $languageMock )
->willReturnCallback( [ $this, 'newLabelDescriptionLookup' ] );
return self::getWikibaseConnector(
$languageFactoryMock,
$fallbackLabelDescriptionLookupFactoryMock,
$revisionLookupMock,
$logger,
$parser
);
}
public function getWikibaseConnector(
LanguageFactory $languageFactory = null,
FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory = null,
EntityRevisionLookup $entityRevisionLookupMock = null,
LoggerInterface $logger = null,
EntityIdParser $parser = null
): MathWikibaseConnector {
$labelDescriptionLookupFactory = $labelDescriptionLookupFactory ?:
self::createMock( FallbackLabelDescriptionLookupFactory::class );
$entityRevisionLookup = $entityRevisionLookupMock ?:
self::createMock( EntityRevisionLookup::class );
$languageFactory = $languageFactory ?: self::createMock( LanguageFactory::class );
$site = self::createMock( Site::class );
$site->method( 'getGlobalId' )->willReturn( '' );
$site->method( 'getPageUrl' )->willReturn( self::EXAMPLE_URL );
$repoConnector = self::createMock( RepoLinker::class );
$repoConnector->method( 'getEntityUrl' )
->willReturnCallback( static function ( ItemId $itemId ) {
return self::EXAMPLE_URL . 'wiki/Special:EntityPage/' . $itemId->serialize();
} );
$mathFormatter = self::createMock( MathFormatter::class );
$mathFormatter->method( 'format' )
->willReturnCallback( static function ( StringValue $value ) {
return self::getExpectedMathML( $value->getValue() );
} );
return new MathWikibaseConnector(
new ServiceOptions( MathWikibaseConnector::CONSTRUCTOR_OPTIONS, [
'MathWikibasePropertyIdHasPart' => 'P1',
'MathWikibasePropertyIdDefiningFormula' => 'P2',
'MathWikibasePropertyIdQuantitySymbol' => 'P3',
'MathWikibasePropertyIdInDefiningFormula' => 'P4',
'MathWikibasePropertyIdSymbolRepresents' => 'P5'
] ),
$repoConnector,
$languageFactory,
$entityRevisionLookup,
$labelDescriptionLookupFactory,
$site,
$parser ?: new BasicEntityIdParser(),
$mathFormatter,
$logger ?: new TestLogger()
);
}
public function setupMassEnergyEquivalenceItem(
bool $hasPartMode
) {
$partPropertyId = new NumericPropertyId( $hasPartMode ? 'P1' : 'P4' );
$symbolPropertyId = new NumericPropertyId( $hasPartMode ? 'P3' : 'P5' );
$items = [];
$statements = [];
foreach ( self::TEST_ITEMS as $key => $itemInfo ) {
$itemId = new ItemId( $key );
$items[ $key ] = new Item( $itemId );
$siteLinkMock = self::createMock( SiteLink::class );
$siteLinkMock->method( 'getSiteId' )->willReturn( '' );
$siteLinkMock->method( 'getPageName' )->willReturn( '' );
$items[ $key ]->addSiteLink( $siteLinkMock );
if ( $key === 'Q1' ) {
continue;
}
$partSnak = new PropertyValueSnak(
$partPropertyId,
$hasPartMode ? new EntityIdValue( $items[ $key ]->getId() ) : new StringValue( $itemInfo[2] )
);
$partQualifier = new PropertyValueSnak(
$symbolPropertyId,
$hasPartMode ? new StringValue( $itemInfo[2] ) : new EntityIdValue( $items[ $key ]->getId() )
);
$statement = new Statement( $partSnak );
$statement->setQualifiers( new SnakList( [ $partQualifier ] ) );
$statements[] = $statement;
}
$mainFormulaValue = new StringValue( self::TEST_ITEMS[ 'Q1' ][2] );
$definingFormulaStatement = new Statement( new PropertyValueSnak(
new NumericPropertyId( 'P2' ),
$mainFormulaValue
) );
$statementList = new StatementList( ...$statements );
$statementList->addStatement( $definingFormulaStatement );
$items[ 'Q1' ]->setStatements( $statementList );
return $items[ 'Q1' ];
}
public function newLabelDescriptionLookup(): FallbackLabelDescriptionLookup {
$lookup = self::createMock( FallbackLabelDescriptionLookup::class );
$lookup->method( 'getLabel' )
->willReturnCallback( static function ( EntityId $entityId ) {
if ( self::TEST_ITEMS[ $entityId->getSerialization() ] !== null ) {
return new Term( 'en', self::TEST_ITEMS[ $entityId->getSerialization() ][0] );
} else {
return null;
}
} );
$lookup->method( 'getDescription' )
->willReturnCallback( static function ( EntityId $entityId ) {
if ( self::TEST_ITEMS[ $entityId->getSerialization() ] !== null ) {
return new Term( 'en', self::TEST_ITEMS[ $entityId->getSerialization() ][1] );
} else {
return null;
}
} );
return $lookup;
}
public static function getExpectedMathML( $str ) {
return '<math>' . $str . '</math>';
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace MediaWiki\Extension\Math\Tests;
use MediaWiki\Extension\Math\Rest\Popup;
use MediaWiki\Languages\LanguageFactory;
use MediaWiki\Rest\Handler;
use MediaWiki\Rest\HttpException;
use MediaWiki\Rest\RequestData;
use MediaWiki\Tests\Rest\Handler\HandlerTestTrait;
use Title;
use Wikibase\DataModel\Entity\Item;
use Wikibase\Lib\Store\EntityRevision;
use Wikimedia\ParamValidator\ParamValidator;
/**
* @covers \MediaWiki\Extension\Math\Rest\Popup
*/
class PopupTest extends MathWikibaseConnectorTestFactory {
use HandlerTestTrait;
public function testNonExistingId() {
$popupHandler = $this->getPopup();
$response = $this->executeHandler( $popupHandler, $this->getRequest( '1', 'en' ) );
$this->assertEquals( 400, $response->getStatusCode() );
$data = json_decode( $response->getBody(), true );
$this->assertEquals( 'Non-existing Wikibase ID.', $data[ 'message' ] );
}
public function testParameterSettingsSetup() {
$popupHandler = $this->getPopup();
$this->assertSame( [ 'qid' => [
HANDLER::PARAM_SOURCE => 'path',
ParamValidator::PARAM_TYPE => 'integer',
ParamValidator::PARAM_REQUIRED => true,
] ], $popupHandler->getParamSettings() );
}
public function testValidationExceptionForMalformedId() {
$popupHandler = $this->getPopup();
$this->initHandler( $popupHandler, $this->getRequest( 'Q1', 'en' ) );
$this->expectException( HttpException::class );
$this->validateHandler( $popupHandler );
}
public function testInvalidLanguage() {
$languageFactoryMock = $this->createMock( LanguageFactory::class );
$languageFactoryMock->expects( $this->once() )
->method( 'getLanguage' )
->with( 'tmp' )
->willThrowException( new \MWException() );
$popupHandler = $this->getPopup( $languageFactoryMock );
$response = $this->executeHandler( $popupHandler, $this->getRequest( '1', 'tmp' ) );
$this->assertEquals( 400, $response->getStatusCode() );
$data = json_decode( $response->getBody(), true );
$this->assertEquals( 'Invalid language code.', $data[ 'message' ] );
}
/**
* @dataProvider provideItemSetups
*/
public function testExistingId( Item $item ) {
$popupHandler = $this->getPopup( null, $item );
$request = $this->getRequest( '1', 'en' );
$data = $this->executeHandlerAndGetBodyData( $popupHandler, $request );
$this->assertArrayHasKey( 'title', $data );
$this->assertArrayHasKey( 'canonicalurl', $data );
$this->assertArrayHasKey( 'fullurl', $data );
$this->assertArrayHasKey( 'contentmodel', $data );
$this->assertArrayHasKey( 'pagelanguagedir', $data );
$this->assertArrayHasKey( 'pagelanguage', $data );
$this->assertArrayHasKey( 'pagelanguagehtmlcode', $data );
$this->assertArrayHasKey( 'extract', $data );
$this->assertEquals( 'massenergy equivalence', $data[ 'title' ] );
$this->assertEquals( 'special/Q1', $data[ 'canonicalurl' ] );
$this->assertEquals( 'special/Q1', $data[ 'fullurl' ] );
$this->assertEquals( 'html', $data[ 'contentmodel' ] );
$html = $data[ 'extract' ];
// popup contains formula label and description
$this->assertStringContainsString( self::TEST_ITEMS[ 'Q1' ][0], $html );
$this->assertStringContainsString( self::TEST_ITEMS[ 'Q1' ][1], $html );
// popup contains formula and labels of each element
foreach ( self::TEST_ITEMS as $key => $part ) {
if ( $key === 'Q1' ) {
// the formula itself is not shown in the popup, only its elements
continue;
}
$this->assertStringContainsString( $part[0], $html );
$this->assertStringContainsString( self::getExpectedMathML( $part[2] ), $html );
}
}
private function getRequest( $id, $lang ): RequestData {
return new RequestData( [
'pathParams' => [ 'qid' => $id ],
'headers' => [
'Accept-Language' => $lang
]
] );
}
private function getPopup(
LanguageFactory $languageFactoryMock = null,
Item $item = null
): Popup {
$languageFactoryMock = $languageFactoryMock ?: $this->createMock( LanguageFactory::class );
$mathWikibaseConnectorMock = $item ?
$this->getWikibaseConnectorWithExistingItems( new EntityRevision( $item ) ) :
$this->getWikibaseConnector( $languageFactoryMock );
$titleMock = $this->createMock( Title::class );
$titleMock->method( 'getLocalURL' )->willReturn( 'special/Q1' );
$titleMock->method( 'getFullURL' )->willReturn( 'special/Q1' );
$titleFactoryMock = $this->createMock( \TitleFactory::class );
$titleFactoryMock->expects( $this->once() )
->method( 'newFromText' )
->willReturn( $titleMock );
return new Popup( $mathWikibaseConnectorMock, $languageFactoryMock, $titleFactoryMock );
}
public function provideItemSetups(): array {
return [
[ $this->setupMassEnergyEquivalenceItem( true ) ],
[ $this->setupMassEnergyEquivalenceItem( false ) ],
];
}
}