wiki.techinc.nl/tests/phpunit/includes/htmlform/HTMLFormFieldTest.php
Amir Sarabadani f4e68e055f Reorg: Move Status to MediaWiki\Status\
This class is used heavily basically everywhere, moving it to Utils
wouldn't make much sense. Also with this change, we can move
StatusValue to MediaWiki\Status as well.

Bug: T321882
Depends-On: I5f89ecf27ce1471a74f31c6018806461781213c3
Change-Id: I04c1dcf5129df437589149f0f3e284974d7c98fa
2023-08-25 15:44:17 +02:00

564 lines
17 KiB
PHP

<?php
use MediaWiki\Request\FauxRequest;
use MediaWiki\Status\Status;
use MediaWiki\Title\Title;
use Wikimedia\TestingAccessWrapper;
/**
* @covers HTMLFormField
*/
class HTMLFormFieldTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
public function getNewForm( $descriptor, $requestData = [] ) {
$requestData += [ 'wpEditToken' => 'ABC123' ];
$request = new FauxRequest( $requestData, true );
$context = new DerivativeContext( RequestContext::getMain() );
$context->setRequest( $request );
$form = HTMLForm::factory( 'ooui', $descriptor, $context );
$form->setTitle( Title::makeTitle( NS_MAIN, 'Main Page' ) )->setSubmitCallback( static function () {
return true;
} )->prepareForm();
$status = $form->trySubmit();
$this->assertTrue( $status );
return $form;
}
/**
* @covers HTMLFormField::isHidden
* @covers HTMLFormField::isDisabled
* @covers HTMLFormField::checkStateRecurse
* @covers HTMLFormField::validateCondState
* @covers HTMLFormField::getNearestField
* @covers HTMLFormField::getNearestFieldValue
* @dataProvider provideCondState
*/
public function testCondState( $fieldInfo, $requestData, $callback, $exception = null ) {
if ( $exception ) {
$this->expectException( $exception[0] );
$this->expectExceptionMessageMatches( $exception[1] );
}
$form = $this->getNewForm( array_merge_recursive( $fieldInfo, [
'check1' => [ 'type' => 'check' ],
'check2' => [ 'type' => 'check', 'invert' => true ],
'check3' => [ 'type' => 'check', 'name' => 'foo' ],
'select1' => [ 'type' => 'select', 'options' => [ 'a' => 'a', 'b' => 'b', 'c' => 'c' ], 'default' => 'b' ],
'text1' => [ 'type' => 'text' ],
'cloner' => [
'class' => HTMLFormFieldCloner::class,
'fields' => [
'check1' => [ 'type' => 'check' ],
'check2' => [ 'type' => 'check', 'invert' => true ],
'check3' => [ 'type' => 'check', 'name' => 'foo' ],
]
]
] ), $requestData );
$callback( $form, $form->mFieldData );
}
public function provideCondState() {
yield 'Field hidden if "check" field is checked' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '===', 'check1', '1' ] ],
],
'requestData' => [
'wpcheck1' => '1',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden if "check" field is not checked' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '===', 'check1', '' ] ],
],
'requestData' => [],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field not hidden if "check" field is not checked' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '===', 'check1', '1' ] ],
],
'requestData' => [],
'callback' => function ( $form, $fieldData ) {
$this->assertFalse( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden if "check" field (invert) is checked' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '===', 'check2', '1' ] ],
],
'requestData' => [
'wpcheck2' => '1',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden if "check" field (invert) is not checked' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '!==', 'check2', '1' ] ],
],
'requestData' => [],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field not hidden if "check" field (invert) is checked' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '!==', 'check2', '1' ] ],
],
'requestData' => [
'wpcheck2' => '1',
],
'callback' => function ( $form, $fieldData ) {
$this->assertFalse( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden if "select" field has value' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '===', 'select1', 'a' ] ],
],
'requestData' => [
'wpselect1' => 'a',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden if "text" field has value' => [
'fieldInfo' => [
'select1' => [ 'hide-if' => [ '===', 'text1', 'hello' ] ],
],
'requestData' => [
'wptext1' => 'hello',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'select1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden using AND conditions' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ 'AND',
[ '===', 'check1', '1' ],
[ '===', 'select1', 'a' ]
] ],
],
'requestData' => [
'wpcheck1' => '1',
'wpselect1' => 'a',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden using OR conditions' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ 'OR',
[ '===', 'check1', '1' ],
[ '===', 'select1', 'a' ]
] ],
],
'requestData' => [
'wpcheck1' => '1',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden using NAND conditions' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ 'NAND',
[ '===', 'check1', '1' ],
[ '===', 'select1', 'a' ]
] ],
],
'requestData' => [
'wpcheck1' => '1',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden using NOR conditions' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ 'NOR',
[ '===', 'check1', '1' ],
[ '===', 'select1', 'a' ]
] ],
],
'requestData' => [],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Field hidden using complex conditions' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ 'OR',
[ 'NOT', [ 'AND',
[ '===', 'check1', '1' ],
[ '===', 'check2', '1' ]
] ],
[ '===', 'select1', 'a' ]
] ],
],
'requestData' => [],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isHidden( $fieldData ) );
}
];
yield 'Invalid conditional specification (unsupported)' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '>', 'test1', '10' ] ],
],
'requestData' => [],
'callback' => null,
'exception' => [ InvalidArgumentException::class, '/Unknown operation/' ],
];
yield 'Invalid conditional specification (NOT)' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ 'NOT', '===', 'check1', '1' ] ],
],
'requestData' => [],
'callback' => null,
'exception' => [ InvalidArgumentException::class, '/NOT takes exactly one parameter/' ],
];
yield 'Invalid conditional specification (AND/OR/NAND/NOR)' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ 'AND', '===', 'check1', '1' ] ],
],
'requestData' => [],
'callback' => null,
'exception' => [ InvalidArgumentException::class, '/Expected array, found string/' ],
];
yield 'Invalid conditional specification (===/!==) 1' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '===', 'check1' ] ],
],
'requestData' => [],
'callback' => null,
'exception' => [ InvalidArgumentException::class, '/=== takes exactly two parameters/' ],
];
yield 'Invalid conditional specification (===/!==) 2' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '===', [ '===', 'check1', '1' ], '1' ] ],
],
'requestData' => [],
'callback' => null,
'exception' => [ InvalidArgumentException::class, '/Parameters for === must be strings/' ],
];
yield 'Field disabled if "check" field is checked' => [
'fieldInfo' => [
'text1' => [ 'disable-if' => [ '===', 'check1', '1' ] ],
],
'requestData' => [
'wpcheck1' => '1',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isDisabled( $fieldData ) );
}
];
yield 'Field disabled if hidden' => [
'fieldInfo' => [
'text1' => [ 'hide-if' => [ '===', 'check1', '1' ] ],
],
'requestData' => [
'wpcheck1' => '1',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isDisabled( $fieldData ) );
}
];
yield 'Field disabled even the field it relied on is named' => [
'fieldInfo' => [
'text1' => [ 'disable-if' => [ '===', 'check3', '1' ] ],
],
'requestData' => [
'foo' => '1',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isDisabled( $fieldData ) );
}
];
yield 'Field disabled even the \'wp\' prefix is used (back-compat)' => [
'fieldInfo' => [
'text1' => [ 'disable-if' => [ '===', 'wpcheck1', '1' ] ],
],
'requestData' => [
'wpcheck1' => '1',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $form->getField( 'text1' )->isDisabled( $fieldData ) );
}
];
yield 'Field name does not exist' => [
'fieldInfo' => [
'text1' => [ 'disable-if' => [ '===', 'foo', '1' ] ],
],
'requestData' => [],
'callback' => null,
'exception' => [ DomainException::class, '/no field named foo/' ],
];
yield 'Field disabled in cloner if "check" field is checked' => [
'fieldInfo' => [
'cloner' => [ 'fields' => [
'check2' => [ 'disable-if' => [ '===', 'check1', '1' ] ],
] ]
],
'requestData' => [
'wpcloner' => [ 0 => [ 'check1' => '1' ] ],
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $this->getFieldInCloner( $form, 'cloner', 0, 'check2' )
->isDisabled( $fieldData ) );
}
];
yield 'Field disabled in cloner if "check" (invert) field is checked' => [
'fieldInfo' => [
'cloner' => [ 'fields' => [
'check1' => [ 'disable-if' => [ '===', 'check2', '1' ] ],
] ]
],
'requestData' => [
'wpcloner' => [ 0 => [ 'check2' => '1' ] ],
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $this->getFieldInCloner( $form, 'cloner', 0, 'check1' )
->isDisabled( $fieldData ) );
}
];
yield 'Field disabled in cloner if "check" (named) field is checked' => [
'fieldInfo' => [
'cloner' => [ 'fields' => [
'check1' => [ 'disable-if' => [ '===', 'check3', '1' ] ],
] ]
],
'requestData' => [
'wpcloner' => [ 0 => [ 'foo' => '1' ] ],
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $this->getFieldInCloner( $form, 'cloner', 0, 'check1' )
->isDisabled( $fieldData ) );
}
];
yield 'Field disabled in cloner if "select" (outside) field has value' => [
'fieldInfo' => [
'cloner' => [ 'fields' => [
'check1' => [ 'disable-if' => [ '===', 'select1', 'a' ] ],
] ]
],
'requestData' => [
'wpselect1' => 'a',
],
'callback' => function ( $form, $fieldData ) {
$this->assertTrue( $this->getFieldInCloner( $form, 'cloner', 0, 'check1' )
->isDisabled( $fieldData ) );
}
];
}
private function getFieldInCloner( $form, $clonerName, $index, $fieldName ) {
$cloner = TestingAccessWrapper::newFromObject( $form->getField( $clonerName ) );
return $cloner->getFieldsForKey( $index )[$fieldName];
}
/**
* @covers HTMLFormField::parseCondState
* @dataProvider provideParseCondState
*/
public function testParseCondState( $fieldName, $condState, $excepted ) {
$form = $this->getNewForm( [
'normal' => [ 'type' => 'check' ],
'named' => [ 'type' => 'check', 'name' => 'foo' ],
'test' => [ 'type' => 'text' ],
'cloner' => [
'class' => HTMLFormFieldCloner::class,
'fields' => [
'normal' => [ 'type' => 'check' ],
'named' => [ 'type' => 'check', 'name' => 'foo' ],
'test' => [ 'type' => 'text' ],
]
]
], [] );
$field = $form->getField( $fieldName ?? 'test' );
$wrapped = TestingAccessWrapper::newFromObject( $field );
if ( $field instanceof HTMLFormFieldCloner ) {
$field = $wrapped->getFieldsForKey( 0 )['test'];
$wrapped = TestingAccessWrapper::newFromObject( $field );
}
$parsed = $wrapped->parseCondState( $condState );
$this->assertSame( $excepted, $parsed );
}
public static function provideParseCondState() {
yield 'Normal' => [
null,
[ '===', 'normal', '1' ],
[ '===', 'wpnormal', '1' ],
];
yield 'With the \'wp\' prefix' => [
null,
[ '===', 'wpnormal', '1' ],
[ '===', 'wpnormal', '1' ],
];
yield 'Named' => [
null,
[ '===', 'named', '1' ],
[ '===', 'foo', '1' ],
];
yield 'Normal in cloner' => [
'cloner',
[ '===', 'normal', '1' ],
[ '===', 'wpcloner[0][normal]', '1' ],
];
yield 'Named in cloner' => [
'cloner',
[ '===', 'named', '1' ],
[ '===', 'wpcloner[0][foo]', '1' ],
];
}
public function testNoticeInfo() {
$form = $this->getNewForm( [
'withNotice' => [ 'type' => 'check', 'notices' => [ 'a notice' ] ],
'withoutNotice' => [ 'type' => 'check' ],
], [] );
$configWithNotice = $configWithoutNotice = [];
$form->getField( 'withNotice' )->getOOUI( '' )->getConfig( $configWithNotice );
$form->getField( 'withoutNotice' )->getOOUI( '' )->getConfig( $configWithoutNotice );
$this->assertArrayHasKey( 'notices', $configWithNotice );
$this->assertSame(
[ 'a notice' ],
$configWithNotice['notices']
);
$this->assertArrayNotHasKey( 'notices', $configWithoutNotice );
}
/**
* @dataProvider provideCallables
*/
public function testValidationCallbacks( callable $callable ) {
$field = new class( [
'parent' => $this->getNewForm( [] ),
'fieldname' => __FUNCTION__,
'validation-callback' => $callable
] ) extends HTMLFormField {
public function getInputHTML( $value ) {
return '';
}
};
$this->assertTrue( $field->validate( '', [] ) );
}
public static function provideCallables() {
$callable = new class() {
public function validate( $value, array $fields, HTMLForm $form ): bool {
return $value || $fields || $form->wasSubmitted();
}
public static function validateStatic( $value, array $fields, HTMLForm $form ): bool {
return $value || $fields || $form->wasSubmitted();
}
public function __invoke( ...$values ): bool {
return self::validateStatic( ...$values );
}
};
return [
'Closure (short)' => [
static fn ( $value, array $fields, HTMLForm $form ) => $value || $fields || $form->wasSubmitted()
],
'Closure (traditional)' => [
static function ( $value, array $fields, HTMLForm $form ) {
return $value || $fields || $form->wasSubmitted();
}
],
'Array' => [ [ $callable, 'validate' ] ],
'Array (static)' => [ [ get_class( $callable ), 'validateStatic' ] ],
'String' => [ get_class( $callable ) . '::validateStatic' ],
'Invokable' => [ $callable ]
];
}
/**
* @dataProvider provideValidationResults
*/
public function testValidationCallbackResults( $callbackResult, $expected ) {
$field = new class( [
'parent' => $this->getNewForm( [] ),
'fieldname' => __FUNCTION__,
'validation-callback' => static fn () => $callbackResult
] ) extends HTMLFormField {
public function getInputHTML( $value ) {
return '';
}
};
$this->assertEquals( $expected, $field->validate( '', [] ) );
}
public static function provideValidationResults() {
$ok = ( new Status() )
->warning( 'test-warning' )
->setOK( true );
return [
'Ok Status' => [ $ok, "<p>⧼test-warning⧽\n</p>" ],
'Good Status' => [ Status::newGood(), true ],
'Fatal Status' => [ Status::newFatal( 'test-fatal' ), "<p>⧼test-fatal⧽\n</p>" ],
'Good StatusValue' => [ StatusValue::newGood(), true ],
'Fatal StatusValue' => [ Status::newFatal( 'test-fatal' ), "<p>⧼test-fatal⧽\n</p>" ],
'String' => [ '<strong>Invalid input</strong>', '<strong>Invalid input</strong>' ],
'True' => [ true, true ],
'False' => [ false, false ]
];
}
public function testValidationCallbackResultMessage() {
$message = $this->createMock( Message::class );
$this->testValidationCallbackResults( $message, $message );
}
/**
* @dataProvider provideValues
*/
public function testValidateWithRequiredNotGiven( $value ) {
$field = new class( [
'parent' => $this->getNewForm( [] ),
'fieldname' => __FUNCTION__,
'required' => true
] ) extends HTMLFormField {
public function getInputHTML( $value ) {
return '';
}
};
$returnValue = $field->validate( $value, [ 'text' => $value ] );
$this->assertInstanceOf( Message::class, $returnValue );
$this->assertEquals( 'htmlform-required', $returnValue->getKey() );
}
public static function provideValues() {
return [
'Empty string' => [ '' ],
'False' => [ false ],
'Null' => [ null ]
];
}
}