diff --git a/tests/framework/data/ActiveDataFilterTest.php b/tests/framework/data/ActiveDataFilterTest.php
index 598fcca8547..d4e2feb1c45 100644
--- a/tests/framework/data/ActiveDataFilterTest.php
+++ b/tests/framework/data/ActiveDataFilterTest.php
@@ -183,6 +183,45 @@ public function testBuild($filter, $expectedResult): void
$this->assertEquals($expectedResult, $builder->build());
}
+ public function testBuildWithQueryOperatorMap(): void
+ {
+ $builder = new ActiveDataFilter();
+ $builder->queryOperatorMap = [
+ 'AND' => 'MY_AND',
+ 'NOT' => 'MY_NOT',
+ '>' => 'MY_GT',
+ ];
+ $searchModel = (new DynamicModel(['name' => null, 'number' => null]))
+ ->addRule('name', 'string')
+ ->addRule('number', 'integer');
+ $builder->setSearchModel($searchModel);
+
+ $builder->filter = ['and' => [['name' => 'a'], ['name' => 'b']]];
+ $result = $builder->build();
+ $this->assertSame(['MY_AND', ['name' => 'a'], ['name' => 'b']], $result);
+
+ $builder->filter = ['not' => ['name' => 'a']];
+ $result = $builder->build();
+ $this->assertSame(['MY_NOT', ['name' => 'a']], $result);
+
+ $builder->filter = ['number' => ['gt' => 5]];
+ $result = $builder->build();
+ $this->assertSame(['MY_GT', 'number', 5], $result);
+ }
+
+ public function testBuildAttributeConditionWithoutConditionBuilder(): void
+ {
+ $builder = new ActiveDataFilter();
+ $searchModel = (new DynamicModel(['number' => null]))
+ ->addRule('number', 'integer');
+ $builder->setSearchModel($searchModel);
+
+ unset($builder->conditionBuilders['>']);
+ $builder->filter = ['number' => ['gt' => 5]];
+ $result = $builder->build();
+ $this->assertSame(['>', 'number', 5], $result);
+ }
+
/**
* @depends testBuild
*/
diff --git a/tests/framework/data/ArrayDataProviderTest.php b/tests/framework/data/ArrayDataProviderTest.php
index 3c747a54864..0e291db27fd 100644
--- a/tests/framework/data/ArrayDataProviderTest.php
+++ b/tests/framework/data/ArrayDataProviderTest.php
@@ -205,6 +205,13 @@ public function testGetKeys(): void
$this->assertEquals(['key1', 'key2'], $dataProvider->getKeys());
}
+ public function testPrepareModelsWithNullAllModels(): void
+ {
+ $dataProvider = new ArrayDataProvider();
+ $this->assertSame([], $dataProvider->getModels());
+ $this->assertSame(0, $dataProvider->getTotalCount());
+ }
+
public function testSortFlags(): void
{
$simpleArray = [['sortField' => 1], ['sortField' => 2], ['sortField' => 11]];
diff --git a/tests/framework/data/BaseDataProviderTest.php b/tests/framework/data/BaseDataProviderTest.php
index c7468e9db35..d089317a235 100644
--- a/tests/framework/data/BaseDataProviderTest.php
+++ b/tests/framework/data/BaseDataProviderTest.php
@@ -9,7 +9,10 @@
namespace yiiunit\framework\data;
use ReflectionClass;
+use yii\base\InvalidArgumentException;
use yii\data\BaseDataProvider;
+use yii\data\Pagination;
+use yii\data\Sort;
use yiiunit\TestCase;
/**
@@ -17,6 +20,12 @@
*/
class BaseDataProviderTest extends TestCase
{
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->mockApplication();
+ }
+
public function testGenerateId(): void
{
$rc = new ReflectionClass(BaseDataProvider::class);
@@ -33,6 +42,143 @@ public function testGenerateId(): void
$this->assertNull((new ConcreteDataProvider())->id);
$this->assertNotNull((new ConcreteDataProvider())->id);
}
+
+ public function testPrepareAndGetModels(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $this->assertSame([], $provider->getModels());
+ $this->assertSame([], $provider->getKeys());
+ }
+
+ public function testSetModels(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $provider->setModels(['a', 'b']);
+ $this->assertSame(['a', 'b'], $provider->getModels());
+ }
+
+ public function testSetKeys(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $provider->setKeys([1, 2]);
+ $this->assertSame([1, 2], $provider->getKeys());
+ }
+
+ public function testGetCount(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $provider->setModels(['a', 'b', 'c']);
+ $this->assertSame(3, $provider->getCount());
+ }
+
+ public function testGetTotalCountWithoutPagination(): void
+ {
+ $provider = new ConcreteDataProvider(['pagination' => false]);
+ $provider->setModels(['a', 'b']);
+ $this->assertSame(2, $provider->getTotalCount());
+ }
+
+ public function testGetTotalCountWithPagination(): void
+ {
+ $provider = new CountableDataProvider();
+ $this->assertSame(42, $provider->getTotalCount());
+ }
+
+ public function testSetTotalCount(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $provider->setTotalCount(42);
+ $this->assertSame(42, $provider->getTotalCount());
+ }
+
+ public function testGetPaginationDefault(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $this->assertInstanceOf(Pagination::class, $provider->getPagination());
+ }
+
+ public function testSetPaginationWithId(): void
+ {
+ $provider = new ConcreteDataProvider(['id' => 'test']);
+ $pagination = $provider->getPagination();
+ $this->assertSame('test-page', $pagination->pageParam);
+ $this->assertSame('test-per-page', $pagination->pageSizeParam);
+ }
+
+ public function testSetPaginationInstance(): void
+ {
+ $pagination = new Pagination();
+ $provider = new ConcreteDataProvider();
+ $provider->setPagination($pagination);
+ $this->assertSame($pagination, $provider->getPagination());
+ }
+
+ public function testSetPaginationFalse(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $provider->setPagination(false);
+ $this->assertFalse($provider->getPagination());
+ }
+
+ public function testSetPaginationInvalid(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $this->expectException(InvalidArgumentException::class);
+ $provider->setPagination('invalid');
+ }
+
+ public function testGetSortDefault(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $this->assertInstanceOf(Sort::class, $provider->getSort());
+ }
+
+ public function testSetSortWithId(): void
+ {
+ $provider = new ConcreteDataProvider(['id' => 'test']);
+ $sort = $provider->getSort();
+ $this->assertSame('test-sort', $sort->sortParam);
+ }
+
+ public function testSetSortInstance(): void
+ {
+ $sort = new Sort();
+ $provider = new ConcreteDataProvider();
+ $provider->setSort($sort);
+ $this->assertSame($sort, $provider->getSort());
+ }
+
+ public function testSetSortFalse(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $provider->setSort(false);
+ $this->assertFalse($provider->getSort());
+ }
+
+ public function testSetSortInvalid(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $this->expectException(InvalidArgumentException::class);
+ $provider->setSort('invalid');
+ }
+
+ public function testRefresh(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $provider->getModels();
+ $provider->setTotalCount(42);
+ $provider->refresh();
+ $this->assertSame(0, $provider->getTotalCount());
+ }
+
+ public function testForcePrepare(): void
+ {
+ $provider = new ConcreteDataProvider();
+ $provider->prepare();
+ $provider->setModels(['overridden']);
+ $provider->prepare(true);
+ $this->assertSame([], $provider->getModels());
+ }
}
/**
@@ -64,3 +210,21 @@ protected function prepareTotalCount()
return 0;
}
}
+
+class CountableDataProvider extends BaseDataProvider
+{
+ protected function prepareModels()
+ {
+ return ['prepared-model'];
+ }
+
+ protected function prepareKeys($models)
+ {
+ return array_keys($models);
+ }
+
+ protected function prepareTotalCount()
+ {
+ return 42;
+ }
+}
diff --git a/tests/framework/data/DataFilterTest.php b/tests/framework/data/DataFilterTest.php
index cd75719124e..aa7cb9de2e1 100644
--- a/tests/framework/data/DataFilterTest.php
+++ b/tests/framework/data/DataFilterTest.php
@@ -459,6 +459,263 @@ public function testNormalizeNonDefaultNull(): void
$this->assertEquals(['name' => null], $builder->normalize(false));
}
+ public function testSetSearchAttributeTypes(): void
+ {
+ $builder = new DataFilter();
+ $types = ['name' => DataFilter::TYPE_STRING, 'number' => DataFilter::TYPE_INTEGER];
+ $builder->setSearchAttributeTypes($types);
+ $this->assertSame($types, $builder->getSearchAttributeTypes());
+ }
+
+ public function testDetectSearchAttributeTypesBoolean(): void
+ {
+ $builder = new DataFilter();
+ $model = (new DynamicModel(['active' => null]))
+ ->addRule('active', 'boolean');
+ $builder->setSearchModel($model);
+ $types = $builder->getSearchAttributeTypes();
+ $this->assertSame(DataFilter::TYPE_BOOLEAN, $types['active']);
+ }
+
+ public function testDetectSearchAttributeTypeFallsBackToString(): void
+ {
+ $builder = new DataFilter();
+ $model = (new DynamicModel(['name' => null]))
+ ->addRule('name', 'required');
+ $builder->setSearchModel($model);
+ $types = $builder->getSearchAttributeTypes();
+ $this->assertSame(DataFilter::TYPE_STRING, $types['name']);
+ }
+
+ public function testGetSearchModelThrowsExceptionForNonModel(): void
+ {
+ $builder = new DataFilter();
+ $builder->setSearchModel(function () {
+ return new stdClass();
+ });
+ $this->expectException('yii\base\InvalidConfigException');
+ $builder->getSearchModel();
+ }
+
+ public function testParseErrorMessageFallback(): void
+ {
+ $builder = new DataFilter();
+ $builder->setSearchModel((new DynamicModel(['name' => null]))->addRule('name', 'string'));
+ $builder->setErrorMessages([]);
+ $builder->filter = 'invalid';
+ $builder->validate();
+ $this->assertSame('The format of Filter is invalid.', $builder->getFirstError('filter'));
+ }
+
+ public function testValidateMixedOperatorAndValue(): void
+ {
+ $builder = new DataFilter();
+ $builder->setSearchModel(
+ (new DynamicModel(['number' => null]))
+ ->addRule('number', 'integer')
+ );
+ $builder->filter = [
+ 'number' => [
+ 'gt' => 10,
+ 'invalid_key' => 20,
+ ],
+ ];
+ $this->assertFalse($builder->validate());
+ $this->assertSame(
+ ['Condition for "number" should be either a value or valid operator specification.'],
+ $builder->getErrors('filter')
+ );
+ }
+
+ public function testValidateUnsupportedOperatorType(): void
+ {
+ $builder = new DataFilter();
+ $builder->setSearchModel(
+ (new DynamicModel(['name' => null]))
+ ->addRule('name', 'string')
+ );
+ $builder->filter = [
+ 'name' => [
+ 'gt' => 'foo',
+ ],
+ ];
+ $this->assertFalse($builder->validate());
+ $this->assertSame(
+ ['"name" does not support operator "gt".'],
+ $builder->getErrors('filter')
+ );
+ }
+
+ public function testValidateMultiValueOperatorRequiresArray(): void
+ {
+ $builder = new DataFilter();
+ $builder->setSearchModel(
+ (new DynamicModel(['number' => null]))
+ ->addRule('number', 'integer')
+ );
+ $builder->filter = [
+ 'number' => [
+ 'in' => 'not-array',
+ ],
+ ];
+ $this->assertFalse($builder->validate());
+ $this->assertSame(
+ ['Operator "in" requires multiple operands.'],
+ $builder->getErrors('filter')
+ );
+ }
+
+ public function testBuildWithValidation(): void
+ {
+ $builder = new DataFilter();
+ $builder->setSearchModel(
+ (new DynamicModel(['name' => null]))
+ ->addRule('name', 'string')
+ );
+ $builder->filter = 'invalid';
+ $this->assertFalse($builder->build());
+ }
+
+ public function testBuildSuccess(): void
+ {
+ $builder = new DataFilter();
+ $builder->setSearchModel(
+ (new DynamicModel(['name' => null]))
+ ->addRule('name', 'string')
+ );
+ $builder->filter = ['name' => 'test'];
+ $result = $builder->build();
+ $this->assertSame(['name' => 'test'], $result);
+ }
+
+ public function testNormalizeWithValidationFailure(): void
+ {
+ $builder = new DataFilter();
+ $builder->setSearchModel(
+ (new DynamicModel(['name' => null]))
+ ->addRule('name', 'string')
+ );
+ $builder->filter = 'invalid';
+ $this->assertFalse($builder->normalize());
+ }
+
+ public function testMagicPropertyAccess(): void
+ {
+ $builder = new DataFilter();
+ $this->assertTrue($builder->canGetProperty('filter'));
+ $this->assertTrue($builder->canSetProperty('filter'));
+ $builder->filter = ['name' => 'test'];
+ $this->assertSame(['name' => 'test'], $builder->filter);
+ $this->assertTrue(isset($builder->filter));
+ unset($builder->filter);
+ $this->assertFalse(isset($builder->filter));
+ }
+
+ public function testMagicPropertyAccessNonFilter(): void
+ {
+ $builder = new DataFilter();
+ $this->assertTrue($builder->canGetProperty('filterAttributeName'));
+ $this->assertTrue($builder->canSetProperty('filterAttributeName'));
+ }
+
+ public function testMagicGetNonFilterThrows(): void
+ {
+ $builder = new DataFilter();
+ $this->expectException('yii\base\UnknownPropertyException');
+ $builder->__get('nonExistent');
+ }
+
+ public function testMagicSetNonFilterThrows(): void
+ {
+ $builder = new DataFilter();
+ $this->expectException('yii\base\UnknownPropertyException');
+ $builder->__set('nonExistent', 'value');
+ }
+
+ public function testMagicIssetNonFilter(): void
+ {
+ $builder = new DataFilter();
+ $this->assertFalse(isset($builder->nonExistent));
+ }
+
+ public function testMagicUnsetNonFilterThrows(): void
+ {
+ $builder = new DataFilter();
+ $this->expectException('yii\base\InvalidCallException');
+ unset($builder->nonExistent);
+ }
+
+ public function testDetectSearchAttributeTypeDateAndTime(): void
+ {
+ $builder = new DataFilter();
+ $model = (new DynamicModel(['date' => null, 'time' => null]))
+ ->addRule('date', 'date', ['format' => 'php:Y-m-d'])
+ ->addRule('time', 'time', ['format' => 'php:H:i:s']);
+ $builder->setSearchModel($model);
+ $types = $builder->getSearchAttributeTypes();
+ $this->assertSame(DataFilter::TYPE_DATE, $types['date']);
+ $this->assertSame(DataFilter::TYPE_TIME, $types['time']);
+ }
+
+ public function testParseErrorMessageUnknownKey(): void
+ {
+ $builder = new DataFilter();
+ $builder->setSearchModel(
+ (new DynamicModel(['name' => null]))
+ ->addRule('name', 'string')
+ );
+ $ref = new \ReflectionProperty($builder, '_errorMessages');
+ if (PHP_VERSION_ID < 80100) {
+ $ref->setAccessible(true);
+ }
+ $ref->setValue($builder, ['someKey' => 'some message']);
+ $builder->filter = 'invalid';
+ $builder->validate();
+ $this->assertSame('The format of Filter is invalid.', $builder->getFirstError('filter'));
+ }
+
+ public function testGetErrorMessagesCallback(): void
+ {
+ $builder = new DataFilter();
+ $ref = new \ReflectionProperty($builder, '_errorMessages');
+ if (PHP_VERSION_ID < 80100) {
+ $ref->setAccessible(true);
+ }
+ $ref->setValue($builder, function () {
+ return ['unsupportedOperatorType' => 'Callback message'];
+ });
+ $messages = $builder->getErrorMessages();
+ $this->assertSame('Callback message', $messages['unsupportedOperatorType']);
+ $this->assertArrayHasKey('unknownAttribute', $messages);
+ }
+
+ public function testValidateAttributeValueUnsafe(): void
+ {
+ $builder = new DataFilter();
+ $model = (new DynamicModel(['name' => null]))
+ ->addRule('name', 'string');
+ $builder->setSearchModel($model);
+ $builder->setSearchAttributeTypes(['name' => DataFilter::TYPE_STRING, 'fake' => DataFilter::TYPE_STRING]);
+ $builder->filter = ['fake' => 'value'];
+ $this->assertFalse($builder->validate());
+ $this->assertStringContainsString('fake', (string) $builder->getFirstError('filter'));
+ }
+
+ public function testAttributes(): void
+ {
+ $builder = new DataFilter();
+ $this->assertSame(['filter'], $builder->attributes());
+
+ $builder->filterAttributeName = 'search';
+ $this->assertSame(['search'], $builder->attributes());
+ }
+
+ public function testFormName(): void
+ {
+ $builder = new DataFilter();
+ $this->assertSame('', $builder->formName());
+ }
+
public function testSetupErrorMessages(): void
{
$builder = new DataFilter();
diff --git a/tests/framework/data/SortTest.php b/tests/framework/data/SortTest.php
index e57f9d4c308..0a7ade9f8e3 100644
--- a/tests/framework/data/SortTest.php
+++ b/tests/framework/data/SortTest.php
@@ -336,6 +336,144 @@ public function testLinkWithoutParams($enableMultiSort, $defaultOrder, $link): v
$this->assertEquals($link, $sort->link('age'));
}
+ public function testNormalizeAttributesWithPartialConfig(): void
+ {
+ $sort = new Sort([
+ 'attributes' => [
+ 'age' => [
+ 'label' => 'Age',
+ ],
+ ],
+ ]);
+
+ $this->assertSame(SORT_ASC, $sort->attributes['age']['asc']['age']);
+ $this->assertSame(SORT_DESC, $sort->attributes['age']['desc']['age']);
+ $this->assertSame('Age', $sort->attributes['age']['label']);
+ }
+
+ public function testLinkWithExistingCssClass(): void
+ {
+ $manager = new UrlManager([
+ 'baseUrl' => '/',
+ 'scriptUrl' => '/index.php',
+ 'cache' => null,
+ ]);
+
+ $sort = new Sort([
+ 'attributes' => ['age'],
+ 'params' => ['sort' => 'age'],
+ 'urlManager' => $manager,
+ 'route' => 'site/index',
+ ]);
+
+ $link = $sort->link('age', ['class' => 'custom']);
+ $this->assertSame('Age', $link);
+ }
+
+ public function testLinkWithLabelOption(): void
+ {
+ $manager = new UrlManager([
+ 'baseUrl' => '/',
+ 'scriptUrl' => '/index.php',
+ 'cache' => null,
+ ]);
+
+ $sort = new Sort([
+ 'attributes' => ['age'],
+ 'params' => ['sort' => 'age'],
+ 'urlManager' => $manager,
+ 'route' => 'site/index',
+ ]);
+
+ $link = $sort->link('age', ['label' => 'Custom Label']);
+ $this->assertSame('Custom Label', $link);
+ }
+
+ public function testLinkWithAttributeLabel(): void
+ {
+ $manager = new UrlManager([
+ 'baseUrl' => '/',
+ 'scriptUrl' => '/index.php',
+ 'cache' => null,
+ ]);
+
+ $sort = new Sort([
+ 'attributes' => [
+ 'age' => [
+ 'asc' => ['age' => SORT_ASC],
+ 'desc' => ['age' => SORT_DESC],
+ 'label' => 'Custom Age',
+ ],
+ ],
+ 'urlManager' => $manager,
+ 'route' => 'site/index',
+ ]);
+
+ $link = $sort->link('age');
+ $this->assertSame('Custom Age', $link);
+ }
+
+ public function testLinkWithModelClass(): void
+ {
+ $manager = new UrlManager([
+ 'baseUrl' => '/',
+ 'scriptUrl' => '/index.php',
+ 'cache' => null,
+ ]);
+
+ $sort = new Sort([
+ 'attributes' => ['name'],
+ 'modelClass' => SortTestModel::class,
+ 'urlManager' => $manager,
+ 'route' => 'site/index',
+ ]);
+
+ $link = $sort->link('name');
+ $this->assertSame('Test Name', $link);
+ }
+
+ public function testCreateAbsoluteUrl(): void
+ {
+ $manager = new UrlManager([
+ 'baseUrl' => '/',
+ 'scriptUrl' => '/index.php',
+ 'cache' => null,
+ 'hostInfo' => 'http://example.com',
+ ]);
+
+ $sort = new Sort([
+ 'attributes' => ['age'],
+ 'params' => ['sort' => 'age'],
+ 'urlManager' => $manager,
+ 'route' => 'site/index',
+ ]);
+
+ $url = $sort->createUrl('age', true);
+ $this->assertSame('http://example.com/index.php?r=site%2Findex&sort=-age', $url);
+ }
+
+ public function testCreateSortParamWithUnknownAttribute(): void
+ {
+ $sort = new Sort([
+ 'attributes' => ['age'],
+ 'route' => 'site/index',
+ ]);
+
+ $this->expectException(\yii\base\InvalidConfigException::class);
+ $sort->createSortParam('unknown');
+ }
+
+ public function testHasAttribute(): void
+ {
+ $sort = new Sort([
+ 'attributes' => ['age', 'name'],
+ ]);
+
+ $this->assertTrue($sort->hasAttribute('age'));
+ $this->assertTrue($sort->hasAttribute('name'));
+ $this->assertFalse($sort->hasAttribute('unknown'));
+ }
+
public function testParseSortParam(): void
{
$sort = new CustomSort([
@@ -382,6 +520,16 @@ public function testGetExpressionOrders(): void
}
}
+class SortTestModel extends \yii\base\Model
+{
+ public $name;
+
+ public function attributeLabels()
+ {
+ return ['name' => 'Test Name'];
+ }
+}
+
class CustomSort extends Sort
{
/**