diff --git a/tests/Column/Base/HeaderContextTest.php b/tests/Column/Base/HeaderContextTest.php
new file mode 100644
index 000000000..d5dbaaa06
--- /dev/null
+++ b/tests/Column/Base/HeaderContextTest.php
@@ -0,0 +1,237 @@
+createHeaderContext(translator: $translator);
+
+ $result = $headerContext->translate('test.message');
+
+ $this->assertSame('test.message', $result);
+ }
+
+ public function testTranslateWithStringable(): void
+ {
+ $translator = Mock::translator('en');
+
+ $headerContext = $this->createHeaderContext(translator: $translator);
+
+ $stringable = new class () {
+ public function __toString(): string
+ {
+ return 'Stringable Message';
+ }
+ };
+
+ $result = $headerContext->translate($stringable);
+
+ $this->assertSame('Stringable Message', $result);
+ }
+
+ public function testPrepareSortableWithEmptyProperty(): void
+ {
+ $cell = new Cell();
+ $headerContext = $this->createHeaderContext();
+
+ $result = $headerContext->prepareSortable($cell, 'nonexistent');
+
+ $this->assertSame($cell, $result[0]);
+ $this->assertNull($result[1]);
+ $this->assertSame('', $result[2]);
+ $this->assertSame('', $result[3]);
+ }
+
+ public function testPrepareSortableWithNullSort(): void
+ {
+ $cell = new Cell();
+ $headerContext = $this->createHeaderContext(
+ sort: null,
+ originalSort: null
+ );
+
+ $result = $headerContext->prepareSortable($cell, 'name');
+
+ $this->assertSame($cell, $result[0]);
+ $this->assertNull($result[1]);
+ $this->assertSame('', $result[2]);
+ $this->assertSame('', $result[3]);
+ }
+
+ public function testPrepareSortableWithPropertyNotInConfig(): void
+ {
+ $sort = Sort::any();
+ $cell = new Cell();
+ $headerContext = $this->createHeaderContext(
+ sort: $sort,
+ originalSort: $sort,
+ orderProperties: ['name' => 'name']
+ );
+
+ $result = $headerContext->prepareSortable($cell, 'age');
+
+ $this->assertSame($cell, $result[0]);
+ $this->assertNull($result[1]);
+ $this->assertSame('', $result[2]);
+ $this->assertSame('', $result[3]);
+ }
+
+ public function testPrepareSortableWithNoOrder(): void
+ {
+ $sort = Sort::any(['name' => []]);
+ $cell = new Cell();
+ $headerContext = $this->createHeaderContext(
+ sort: $sort,
+ originalSort: $sort,
+ orderProperties: ['name' => 'name'],
+ sortableHeaderClass: 'sortable',
+ sortableHeaderPrepend: '↕',
+ sortableHeaderAppend: '!'
+ );
+
+ $result = $headerContext->prepareSortable($cell, 'name');
+
+ $this->assertInstanceOf(Cell::class, $result[0]);
+ $this->assertStringContainsString('sortable', $result[0]->getAttributes()['class']);
+ $this->assertInstanceOf(A::class, $result[1]);
+ $this->assertSame('↕', $result[2]);
+ $this->assertSame('!', $result[3]);
+ }
+
+ public function testPrepareSortableWithAscOrder(): void
+ {
+ $sort = Sort::any(['name' => []])->withOrder(['name' => 'asc']);
+ $cell = new Cell();
+ $headerContext = $this->createHeaderContext(
+ sort: $sort,
+ originalSort: $sort,
+ orderProperties: ['name' => 'name'],
+ sortableHeaderAscClass: 'asc',
+ sortableHeaderAscPrepend: '↑',
+ sortableHeaderAscAppend: '!',
+ sortableLinkAscClass: 'link-asc'
+ );
+
+ $result = $headerContext->prepareSortable($cell, 'name');
+
+ $this->assertInstanceOf(Cell::class, $result[0]);
+ $this->assertStringContainsString('asc', $result[0]->getAttributes()['class']);
+ $this->assertInstanceOf(A::class, $result[1]);
+ $this->assertSame('↑', $result[2]);
+ $this->assertSame('!', $result[3]);
+ }
+
+ public function testPrepareSortableWithDescOrder(): void
+ {
+ $sort = Sort::any(['name' => []])->withOrder(['name' => 'desc']);
+ $cell = new Cell();
+ $headerContext = $this->createHeaderContext(
+ sort: $sort,
+ originalSort: $sort,
+ orderProperties: ['name' => 'name'],
+ sortableHeaderDescClass: 'desc',
+ sortableHeaderDescPrepend: '↓',
+ sortableHeaderDescAppend: '!',
+ sortableLinkDescClass: 'link-desc'
+ );
+
+ $result = $headerContext->prepareSortable($cell, 'name');
+
+ $this->assertInstanceOf(Cell::class, $result[0]);
+ $this->assertStringContainsString('desc', $result[0]->getAttributes()['class']);
+ $this->assertInstanceOf(A::class, $result[1]);
+ $this->assertSame('↓', $result[2]);
+ $this->assertSame('!', $result[3]);
+ }
+
+ private function createHeaderContext(
+ ?Sort $sort = null,
+ ?Sort $originalSort = null,
+ array $orderProperties = ['name' => 'name'],
+ ?string $sortableHeaderClass = null,
+ string|Stringable $sortableHeaderPrepend = '',
+ string|Stringable $sortableHeaderAppend = '',
+ ?string $sortableHeaderAscClass = null,
+ string|Stringable $sortableHeaderAscPrepend = '',
+ string|Stringable $sortableHeaderAscAppend = '',
+ ?string $sortableHeaderDescClass = null,
+ string|Stringable $sortableHeaderDescPrepend = '',
+ string|Stringable $sortableHeaderDescAppend = '',
+ array $sortableLinkAttributes = [],
+ ?string $sortableLinkAscClass = null,
+ ?string $sortableLinkDescClass = null,
+ ?PageToken $pageToken = null,
+ ?int $pageSize = null,
+ bool $multiSort = false,
+ ?TranslatorInterface $translator = null
+ ): HeaderContext {
+ if ($sort === null) {
+ $sort = Sort::any();
+ }
+
+ if ($originalSort === null) {
+ $originalSort = Sort::any();
+ }
+
+ if ($translator === null) {
+ $translator = Mock::translator('en');
+ }
+
+ $urlConfig = new UrlConfig(
+ pageParameterName: 'page',
+ previousPageParameterName: 'prev',
+ pageSizeParameterName: 'per-page',
+ sortParameterName: 'sort'
+ );
+
+ $urlCreator = fn(): string => '#';
+
+ return new HeaderContext(
+ originalSort: $originalSort,
+ sort: $sort,
+ orderProperties: $orderProperties,
+ sortableHeaderClass: $sortableHeaderClass,
+ sortableHeaderPrepend: $sortableHeaderPrepend,
+ sortableHeaderAppend: $sortableHeaderAppend,
+ sortableHeaderAscClass: $sortableHeaderAscClass,
+ sortableHeaderAscPrepend: $sortableHeaderAscPrepend,
+ sortableHeaderAscAppend: $sortableHeaderAscAppend,
+ sortableHeaderDescClass: $sortableHeaderDescClass,
+ sortableHeaderDescPrepend: $sortableHeaderDescPrepend,
+ sortableHeaderDescAppend: $sortableHeaderDescAppend,
+ sortableLinkAttributes: $sortableLinkAttributes,
+ sortableLinkAscClass: $sortableLinkAscClass,
+ sortableLinkDescClass: $sortableLinkDescClass,
+ pageToken: $pageToken,
+ pageSize: $pageSize,
+ multiSort: $multiSort,
+ urlConfig: $urlConfig,
+ urlCreator: $urlCreator,
+ translator: $translator,
+ translationCategory: 'grid'
+ );
+ }
+}
diff --git a/tests/Column/DataColumnRendererTest.php b/tests/Column/DataColumnRendererTest.php
new file mode 100644
index 000000000..47709bcb0
--- /dev/null
+++ b/tests/Column/DataColumnRendererTest.php
@@ -0,0 +1,147 @@
+filterFactoryContainer = new Container(ContainerConfig::create());
+
+ $this->dataReader = new IterableDataReader([
+ ['id' => 1, 'name' => 'John', 'age' => 20],
+ ['id' => 2, 'name' => 'Mary', 'age' => 21],
+ ]);
+ }
+
+ public function testRenderColumn(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ $column = new DataColumn('test');
+ $cell = new Cell();
+ $translator = Mock::translator('en');
+
+ $context = new GlobalContext(
+ $this->dataReader,
+ [],
+ [],
+ $translator,
+ 'test'
+ );
+
+ $renderer = new DataColumnRenderer(
+ $this->filterFactoryContainer,
+ new Validator()
+ );
+
+ $renderer->renderColumn($column, $cell, $context);
+ }
+
+ public function testRenderHeader(): void
+ {
+ $column = new DataColumn('test', 'Test Header');
+ $cell = new Cell();
+ $translator = Mock::translator('en');
+
+ $sort = Sort::any();
+
+ $context = new HeaderContext(
+ $sort,
+ $sort,
+ ['test' => 'test'],
+ 'sortable',
+ '',
+ '',
+ 'asc',
+ '',
+ '',
+ 'desc',
+ '',
+ '',
+ [],
+ 'asc-link',
+ 'desc-link',
+ null,
+ 10,
+ false,
+ new UrlConfig(),
+ null,
+ $translator,
+ 'test'
+ );
+
+ $renderer = new DataColumnRenderer(
+ $this->filterFactoryContainer,
+ new Validator()
+ );
+
+ $result = $renderer->renderHeader($column, $cell, $context);
+
+ $this->assertNotEmpty($result->getContent());
+ }
+
+ public function testRenderBody(): void
+ {
+ $column = new DataColumn('name');
+ $cell = new Cell();
+ $data = ['id' => 1, 'name' => 'John Doe', 'age' => 20];
+
+ $context = new DataContext(
+ $this->dataReader,
+ $column,
+ $data,
+ 1,
+ 0
+ );
+
+ $renderer = new DataColumnRenderer(
+ $this->filterFactoryContainer,
+ new Validator()
+ );
+
+ $result = $renderer->renderBody($column, $cell, $context);
+
+ $content = $result->getContent();
+ $this->assertNotEmpty($content);
+ $this->assertStringContainsString('John Doe', (string)$content[0]);
+ }
+
+ public function testGetOrderProperties(): void
+ {
+ $column = new DataColumn('test');
+ $renderer = new DataColumnRenderer(
+ $this->filterFactoryContainer,
+ new Validator()
+ );
+
+ $result = $renderer->getOrderProperties($column);
+ $this->assertEquals(['test' => 'test'], $result);
+ }
+}
diff --git a/tests/Filter/Factory/EqualsFilterFactoryTest.php b/tests/Filter/Factory/EqualsFilterFactoryTest.php
new file mode 100644
index 000000000..8f6f3ddc1
--- /dev/null
+++ b/tests/Filter/Factory/EqualsFilterFactoryTest.php
@@ -0,0 +1,57 @@
+create('name', 'John');
+
+ $this->assertInstanceOf(Equals::class, $filter);
+
+ $this->assertSame('name', $filter->getField());
+ $this->assertSame('John', $filter->getValue());
+ }
+
+ public function testCreateWithEmptyValue(): void
+ {
+ $factory = new EqualsFilterFactory();
+ $filter = $factory->create('name', '');
+
+ $this->assertNull($filter);
+ }
+
+ public function testCreateWithZeroValue(): void
+ {
+ $factory = new EqualsFilterFactory();
+ $filter = $factory->create('age', '0');
+
+ $this->assertNull($filter);
+ }
+
+ public function testCreateWithNonEmptyNumericValue(): void
+ {
+ $factory = new EqualsFilterFactory();
+
+ /** @var Equals $filter */
+ $filter = $factory->create('quantity', '42');
+
+ $this->assertInstanceOf(Equals::class, $filter);
+
+ $this->assertSame('quantity', $filter->getField());
+ $this->assertSame('42', $filter->getValue());
+ }
+}
diff --git a/tests/Filter/Factory/LikeFilterFactoryTest.php b/tests/Filter/Factory/LikeFilterFactoryTest.php
index 3bb936ca7..55e161f83 100644
--- a/tests/Filter/Factory/LikeFilterFactoryTest.php
+++ b/tests/Filter/Factory/LikeFilterFactoryTest.php
@@ -8,6 +8,9 @@
use PHPUnit\Framework\TestCase;
use Yiisoft\Yii\DataView\Filter\Factory\LikeFilterFactory;
+/**
+ * @covers \Yiisoft\Yii\DataView\Filter\Factory\LikeFilterFactory
+ */
final class LikeFilterFactoryTest extends TestCase
{
public function testBase(): void
@@ -21,6 +24,22 @@ public function testBase(): void
$this->assertNull($filter->isCaseSensitive());
}
+ public function testCreateWithEmptyValue(): void
+ {
+ $factory = new LikeFilterFactory();
+ $filter = $factory->create('name', '');
+
+ $this->assertNull($filter);
+ }
+
+ public function testCreateWithZeroValue(): void
+ {
+ $factory = new LikeFilterFactory();
+ $filter = $factory->create('name', '0');
+
+ $this->assertNull($filter);
+ }
+
public static function dataCaseSensitive(): iterable
{
yield [null];
diff --git a/tests/Filter/Widget/DropdownFilterTest.php b/tests/Filter/Widget/DropdownFilterTest.php
new file mode 100644
index 000000000..be531cdca
--- /dev/null
+++ b/tests/Filter/Widget/DropdownFilterTest.php
@@ -0,0 +1,91 @@
+optionsData([
+ 'active' => 'Active',
+ 'inactive' => 'Inactive',
+ ]);
+ $context = new Context('status', 'active', 'filter-form');
+
+ $html = $filter->renderFilter($context);
+
+ $this->assertStringContainsString('name="status"', $html);
+ $this->assertStringContainsString('form="filter-form"', $html);
+ $this->assertStringContainsString('onChange="this.form.submit()"', $html);
+ $this->assertStringContainsString('value="active"', $html);
+ $this->assertStringContainsString('selected', $html);
+ }
+
+ public function testRenderFilterWithoutValue(): void
+ {
+ $filter = new DropdownFilter();
+ $context = new Context('status', null, 'filter-form');
+
+ $html = $filter->renderFilter($context);
+
+ $this->assertStringContainsString('name="status"', $html);
+ $this->assertStringContainsString('form="filter-form"', $html);
+ $this->assertStringContainsString('onChange="this.form.submit()"', $html);
+ $this->assertStringNotContainsString('value="', $html);
+ }
+
+ public function testOptionsData(): void
+ {
+ $filter = new DropdownFilter();
+ $options = [
+ 'active' => 'Active',
+ 'inactive' => 'Inactive',
+ ];
+
+ $filter = $filter->optionsData($options);
+ $context = new Context('status', null, 'filter-form');
+
+ $html = $filter->renderFilter($context);
+
+ $this->assertStringContainsString('>Active<', $html);
+ $this->assertStringContainsString('>Inactive<', $html);
+ $this->assertStringContainsString('value="active"', $html);
+ $this->assertStringContainsString('value="inactive"', $html);
+ }
+
+ public function testAddAttributes(): void
+ {
+ $filter = new DropdownFilter();
+ $filter = $filter->addAttributes(['class' => 'custom-select', 'data-test' => 'value']);
+
+ $context = new Context('status', null, 'filter-form');
+ $html = $filter->renderFilter($context);
+
+ $this->assertStringContainsString('class="custom-select"', $html);
+ $this->assertStringContainsString('data-test="value"', $html);
+ }
+
+ public function testAttributes(): void
+ {
+ $filter = new DropdownFilter();
+ $filter = $filter->attributes(['class' => 'new-select', 'required' => true]);
+
+ $context = new Context('status', null, 'filter-form');
+ $html = $filter->renderFilter($context);
+
+ $this->assertStringContainsString('class="new-select"', $html);
+ $this->assertStringContainsString('required', $html);
+ }
+}
diff --git a/tests/Filter/Widget/TextInputFilterTest.php b/tests/Filter/Widget/TextInputFilterTest.php
new file mode 100644
index 000000000..ccbbdf40f
--- /dev/null
+++ b/tests/Filter/Widget/TextInputFilterTest.php
@@ -0,0 +1,85 @@
+renderFilter($context);
+
+ $this->assertStringContainsString('name="username"', $result);
+ $this->assertStringContainsString('value="john"', $result);
+ $this->assertStringContainsString('form="filter-form"', $result);
+ $this->assertStringContainsString('type="text"', $result);
+ }
+
+ public function testRenderWithNullValue(): void
+ {
+ $filter = new TextInputFilter();
+ $context = new Context('username', null, 'filter-form');
+
+ $result = $filter->renderFilter($context);
+
+ $this->assertStringContainsString('name="username"', $result);
+ $this->assertStringNotContainsString('value=', $result);
+ $this->assertStringContainsString('form="filter-form"', $result);
+ }
+
+ public function testAddAttributes(): void
+ {
+ $filter = new TextInputFilter();
+ $newFilter = $filter->addAttributes(['class' => 'form-control', 'placeholder' => 'Enter username']);
+
+ $this->assertNotSame($filter, $newFilter);
+
+ $context = new Context('username', 'john', 'filter-form');
+ $result = $newFilter->renderFilter($context);
+
+ $this->assertStringContainsString('class="form-control"', $result);
+ $this->assertStringContainsString('placeholder="Enter username"', $result);
+ }
+
+ public function testAttributes(): void
+ {
+ $filter = new TextInputFilter();
+ $filter = $filter->addAttributes(['data-test' => 'original']);
+
+ $newFilter = $filter->attributes(['class' => 'form-control', 'id' => 'username-filter']);
+
+ $this->assertNotSame($filter, $newFilter);
+
+ $context = new Context('username', 'john', 'filter-form');
+ $result = $newFilter->renderFilter($context);
+
+ $this->assertStringContainsString('class="form-control"', $result);
+ $this->assertStringContainsString('id="username-filter"', $result);
+ $this->assertStringNotContainsString('data-test="original"', $result);
+ }
+
+ public function testRender(): void
+ {
+ $context = new Context('username', 'john', 'filter-form');
+ $result = (new TextInputFilter())
+ ->withContext($context)
+ ->render();
+
+ $this->assertStringContainsString('name="username"', $result);
+ $this->assertStringContainsString('value="john"', $result);
+ $this->assertStringContainsString('form="filter-form"', $result);
+ }
+}
diff --git a/tests/PageSize/InputPageSizeTest.php b/tests/PageSize/InputPageSizeTest.php
new file mode 100644
index 000000000..efd4bc8b9
--- /dev/null
+++ b/tests/PageSize/InputPageSizeTest.php
@@ -0,0 +1,86 @@
+withContext($context);
+
+ $html = $widget->render();
+
+ $this->assertStringContainsString('value="10"', $html);
+ $this->assertStringContainsString('data-default-page-size="20"', $html);
+ $this->assertStringContainsString('data-url-pattern="/test?pagesize=YII-DATAVIEW-PAGE-SIZE-PLACEHOLDER"', $html);
+ $this->assertStringContainsString('data-default-url="/test"', $html);
+ $this->assertStringContainsString('onchange=', $html);
+ }
+
+ public function testAddAttributes(): void
+ {
+ $context = new PageSizeContext(
+ currentValue: 10,
+ defaultValue: 20,
+ constraint: false,
+ urlPattern: '/test?pagesize=YII-DATAVIEW-PAGE-SIZE-PLACEHOLDER',
+ defaultUrl: '/test'
+ );
+
+ $widget = new InputPageSize();
+ $widget = $widget->withContext($context);
+ $widget = $widget->addAttributes(['class' => 'form-control', 'id' => 'page-size-input']);
+
+ $html = $widget->render();
+
+ $this->assertStringContainsString('class="form-control"', $html);
+ $this->assertStringContainsString('id="page-size-input"', $html);
+ }
+
+ public function testAttributes(): void
+ {
+ $context = new PageSizeContext(
+ currentValue: 10,
+ defaultValue: 20,
+ constraint: false,
+ urlPattern: '/test?pagesize=YII-DATAVIEW-PAGE-SIZE-PLACEHOLDER',
+ defaultUrl: '/test'
+ );
+
+ $widget = new InputPageSize();
+ $widget = $widget->withContext($context);
+ $widget = $widget->attributes(['class' => 'custom-input', 'data-test' => 'value']);
+
+ $html = $widget->render();
+
+ $this->assertStringContainsString('class="custom-input"', $html);
+ $this->assertStringContainsString('data-test="value"', $html);
+ }
+
+ public function testGetContextWithoutSettingContext(): void
+ {
+ $widget = new InputPageSize();
+
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Context is not set.');
+
+ // This will trigger the getContext() method internally
+ $widget->render();
+ }
+}
diff --git a/tests/PageSize/SelectPageSizeTest.php b/tests/PageSize/SelectPageSizeTest.php
new file mode 100644
index 000000000..7b2e74bfb
--- /dev/null
+++ b/tests/PageSize/SelectPageSizeTest.php
@@ -0,0 +1,108 @@
+withContext($context);
+
+ $html = $widget->render();
+
+ $this->assertStringContainsString('