diff --git a/tests/Column/Base/GlobalContextTest.php b/tests/Column/Base/GlobalContextTest.php new file mode 100644 index 000000000..36a82f6af --- /dev/null +++ b/tests/Column/Base/GlobalContextTest.php @@ -0,0 +1,83 @@ + 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Mary'], + ]; + + public function testConstructor(): void + { + $dataReader = new IterableDataReader($this->data); + $translator = Mock::translator('en'); + $pathArguments = ['id' => 123]; + $queryParameters = ['sort' => 'name']; + $translationCategory = 'app'; + + $context = new GlobalContext( + $dataReader, + $pathArguments, + $queryParameters, + $translator, + $translationCategory + ); + + $this->assertSame($dataReader, $context->dataReader); + $this->assertSame($pathArguments, $context->pathArguments); + $this->assertSame($queryParameters, $context->queryParameters); + } + + public function testTranslateWithString(): void + { + $translator = Mock::translator('en'); + + $context = new GlobalContext( + new IterableDataReader($this->data), + [], + [], + $translator, + 'app' + ); + + $this->assertSame('test.message', $context->translate('test.message')); + } + + public function testTranslateWithStringable(): void + { + $stringable = new class () implements Stringable { + public function __toString(): string + { + return 'test.stringable'; + } + }; + + $translator = Mock::translator('en'); + + $context = new GlobalContext( + new IterableDataReader($this->data), + [], + [], + $translator, + 'app' + ); + + $this->assertSame('test.stringable', $context->translate($stringable)); + } +} diff --git a/tests/Column/Base/RendererContainerTest.php b/tests/Column/Base/RendererContainerTest.php new file mode 100644 index 000000000..fa4116148 --- /dev/null +++ b/tests/Column/Base/RendererContainerTest.php @@ -0,0 +1,69 @@ +renderer = new TestRenderer(); + $container = $this->createMock(ContainerInterface::class); + $container + ->method('get') + ->willReturnCallback(fn(string $class) => match ($class) { + TestRenderer::class => $this->renderer, + default => throw new RuntimeException("Unexpected class: $class"), + }); + $this->rendererContainer = new RendererContainer($container); + } + + public function testGetCreatesNewInstance(): void + { + $instance = $this->rendererContainer->get(TestRenderer::class); + $this->assertInstanceOf(TestRenderer::class, $instance); + } + + public function testGetReturnsCachedInstance(): void + { + $instance1 = $this->rendererContainer->get(TestRenderer::class); + $instance2 = $this->rendererContainer->get(TestRenderer::class); + $this->assertSame($instance1, $instance2); + } + + public function testAddConfigsCreatesNewInstanceWithConfig(): void + { + $container = $this->rendererContainer->addConfigs([ + TestRenderer::class => ['value' => 'custom'], + ]); + + /** @var TestRenderer $instance */ + $instance = $container->get(TestRenderer::class); + $this->assertSame('custom', $instance->getValue()); + } + + public function testAddConfigsMergesExistingConfig(): void + { + $container = $this->rendererContainer + ->addConfigs([TestRenderer::class => ['value' => 'first']]) + ->addConfigs([TestRenderer::class => ['option' => 'second']]); + + /** @var TestRenderer $instance */ + $instance = $container->get(TestRenderer::class); + $this->assertSame('first', $instance->getValue()); + $this->assertSame('second', $instance->getOption()); + } +} diff --git a/tests/Column/CheckboxColumnRendererTest.php b/tests/Column/CheckboxColumnRendererTest.php new file mode 100644 index 000000000..2eec6e977 --- /dev/null +++ b/tests/Column/CheckboxColumnRendererTest.php @@ -0,0 +1,224 @@ +renderer = new CheckboxColumnRenderer(); + $this->cell = new Cell(); + $this->dataReader = new IterableDataReader([ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Mary'], + ]); + $this->globalContext = new GlobalContext( + dataReader: $this->dataReader, + pathArguments: [], + queryParameters: [], + translator: Mock::translator('en'), + translationCategory: 'test' + ); + } + + public function testRenderColumn(): void + { + $column = new CheckboxColumn(columnAttributes: ['class' => 'test-class']); + $result = $this->renderer->renderColumn($column, $this->cell, $this->globalContext); + $this->assertSame(['class' => 'test-class'], $result->getAttributes()); + } + + public function testRenderHeaderWithoutHeaderAndSingleSelection(): void + { + $column = new CheckboxColumn(multiple: false); + $sort = Sort::any(); + $context = new HeaderContext( + originalSort: $sort, + sort: $sort, + orderProperties: ['test' => 'test'], + sortableHeaderClass: 'sortable', + sortableHeaderPrepend: '', + sortableHeaderAppend: '', + sortableHeaderAscClass: 'asc', + sortableHeaderAscPrepend: '', + sortableHeaderAscAppend: '', + sortableHeaderDescClass: 'desc', + sortableHeaderDescPrepend: '', + sortableHeaderDescAppend: '', + sortableLinkAttributes: [], + sortableLinkAscClass: 'asc-link', + sortableLinkDescClass: 'desc-link', + pageToken: null, + pageSize: 10, + multiSort: false, + urlConfig: new UrlConfig(), + urlCreator: null, + translator: Mock::translator('en'), + translationCategory: 'test' + ); + $result = $this->renderer->renderHeader($column, $this->cell, $context); + $this->assertNull($result); + } + + public function testRenderHeaderWithoutHeaderAndMultipleSelection(): void + { + $column = new CheckboxColumn(multiple: true); + $sort = Sort::any(); + $context = new HeaderContext( + originalSort: $sort, + sort: $sort, + orderProperties: ['test' => 'test'], + sortableHeaderClass: 'sortable', + sortableHeaderPrepend: '', + sortableHeaderAppend: '', + sortableHeaderAscClass: 'asc', + sortableHeaderAscPrepend: '', + sortableHeaderAscAppend: '', + sortableHeaderDescClass: 'desc', + sortableHeaderDescPrepend: '', + sortableHeaderDescAppend: '', + sortableLinkAttributes: [], + sortableLinkAscClass: 'asc-link', + sortableLinkDescClass: 'desc-link', + pageToken: null, + pageSize: 10, + multiSort: false, + urlConfig: new UrlConfig(), + urlCreator: null, + translator: Mock::translator('en'), + translationCategory: 'test' + ); + $result = $this->renderer->renderHeader($column, $this->cell, $context); + $this->assertNotNull($result); + $this->assertStringContainsString('checkbox-selection-all', (string) $result->getContent()[0]); + } + + public function testRenderHeaderWithCustomHeader(): void + { + $column = new CheckboxColumn( + header: 'Custom Header', + headerAttributes: ['class' => 'header-class'] + ); + $sort = Sort::any(); + $context = new HeaderContext( + originalSort: $sort, + sort: $sort, + orderProperties: ['test' => 'test'], + sortableHeaderClass: 'sortable', + sortableHeaderPrepend: '', + sortableHeaderAppend: '', + sortableHeaderAscClass: 'asc', + sortableHeaderAscPrepend: '', + sortableHeaderAscAppend: '', + sortableHeaderDescClass: 'desc', + sortableHeaderDescPrepend: '', + sortableHeaderDescAppend: '', + sortableLinkAttributes: [], + sortableLinkAscClass: 'asc-link', + sortableLinkDescClass: 'desc-link', + pageToken: null, + pageSize: 10, + multiSort: false, + urlConfig: new UrlConfig(), + urlCreator: null, + translator: Mock::translator('en'), + translationCategory: 'test' + ); + $result = $this->renderer->renderHeader($column, $this->cell, $context); + $this->assertNotNull($result); + $this->assertSame('Custom Header', $result->getContent()[0]); + $this->assertSame(['class' => 'header-class'], $result->getAttributes()); + } + + public function testRenderBodyWithDefaultNameAndValue(): void + { + $column = new CheckboxColumn(); + $context = new DataContext( + preparedDataReader: $this->dataReader, + column: $column, + data: ['id' => 1, 'name' => 'John'], + key: 1, + index: 0 + ); + $result = $this->renderer->renderBody($column, $this->cell, $context); + $content = (string) $result->getContent()[0]; + $this->assertStringContainsString('name="checkbox-selection"', $content); + $this->assertStringContainsString('value="1"', $content); + $this->assertFalse($result->shouldEncode()); + } + + public function testRenderBodyWithCustomNameAndValue(): void + { + $column = new CheckboxColumn( + inputAttributes: [ + 'name' => 'custom-name', + 'value' => 'custom-value', + 'class' => 'custom-class', + ] + ); + $context = new DataContext( + preparedDataReader: $this->dataReader, + column: $column, + data: ['id' => 1, 'name' => 'John'], + key: 1, + index: 0 + ); + $result = $this->renderer->renderBody($column, $this->cell, $context); + $content = (string) $result->getContent()[0]; + $this->assertStringContainsString('name="custom-name"', $content); + $this->assertStringContainsString('value="custom-value"', $content); + $this->assertStringContainsString('class="custom-class"', $content); + } + + public function testRenderBodyWithCustomContent(): void + { + $column = new CheckboxColumn( + content: static fn(Checkbox $input, DataContext $context): string => "
{$input->render()}
" + ); + $context = new DataContext( + preparedDataReader: $this->dataReader, + column: $column, + data: ['id' => 1, 'name' => 'John'], + key: 1, + index: 0 + ); + $result = $this->renderer->renderBody($column, $this->cell, $context); + $content = (string) $result->getContent()[0]; + $this->assertStringStartsWith('
', $content); + $this->assertStringEndsWith('
', $content); + } + + public function testRenderFooterWithContent(): void + { + $column = new CheckboxColumn(footer: 'Test Footer'); + $result = $this->renderer->renderFooter($column, $this->cell, $this->globalContext); + $this->assertSame('Test Footer', $result->getContent()[0]); + } + + public function testRenderFooterWithoutContent(): void + { + $column = new CheckboxColumn(); + $result = $this->renderer->renderFooter($column, $this->cell, $this->globalContext); + $this->assertEmpty($result->getContent()); + } +} diff --git a/tests/Column/DataColumnRendererTest.php b/tests/Column/DataColumnRendererTest.php index 47709bcb0..45d54d9c2 100644 --- a/tests/Column/DataColumnRendererTest.php +++ b/tests/Column/DataColumnRendererTest.php @@ -4,22 +4,29 @@ namespace Yiisoft\Yii\DataView\Tests\Column; +use DateTime; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Yiisoft\Data\Reader\Iterable\IterableDataReader; use Yiisoft\Data\Reader\Sort; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; +use Yiisoft\Validator\Result; use Yiisoft\Validator\Validator; use Yiisoft\Yii\DataView\Column\Base\Cell; use Yiisoft\Yii\DataView\Column\Base\DataContext; use Yiisoft\Yii\DataView\Column\Base\GlobalContext; use Yiisoft\Yii\DataView\Column\Base\HeaderContext; +use Yiisoft\Yii\DataView\Column\Base\FilterContext; +use Yiisoft\Yii\DataView\Column\Base\MakeFilterContext; use Yiisoft\Yii\DataView\Column\DataColumn; use Yiisoft\Yii\DataView\Column\DataColumnRenderer; +use Yiisoft\Yii\DataView\Filter\Factory\LikeFilterFactory; use Yiisoft\Yii\DataView\Tests\Support\Mock; use Yiisoft\Yii\DataView\Tests\Support\TestTrait; use Yiisoft\Yii\DataView\UrlConfig; +use Yiisoft\Validator\Rule\Number; +use Yiisoft\Yii\DataView\UrlParameterProviderInterface; final class DataColumnRendererTest extends TestCase { @@ -49,11 +56,11 @@ public function testRenderColumn(): void $translator = Mock::translator('en'); $context = new GlobalContext( - $this->dataReader, - [], - [], - $translator, - 'test' + dataReader: $this->dataReader, + pathArguments: [], + queryParameters: [], + translator: $translator, + translationCategory: 'test' ); $renderer = new DataColumnRenderer( @@ -73,28 +80,28 @@ public function testRenderHeader(): void $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' + originalSort: $sort, + sort: $sort, + orderProperties: ['test' => 'test'], + sortableHeaderClass: 'sortable', + sortableHeaderPrepend: '', + sortableHeaderAppend: '', + sortableHeaderAscClass: 'asc', + sortableHeaderAscPrepend: '', + sortableHeaderAscAppend: '', + sortableHeaderDescClass: 'desc', + sortableHeaderDescPrepend: '', + sortableHeaderDescAppend: '', + sortableLinkAttributes: [], + sortableLinkAscClass: 'asc-link', + sortableLinkDescClass: 'desc-link', + pageToken: null, + pageSize: 10, + multiSort: false, + urlConfig: new UrlConfig(), + urlCreator: null, + translator: $translator, + translationCategory: 'test' ); $renderer = new DataColumnRenderer( @@ -114,11 +121,11 @@ public function testRenderBody(): void $data = ['id' => 1, 'name' => 'John Doe', 'age' => 20]; $context = new DataContext( - $this->dataReader, - $column, - $data, - 1, - 0 + preparedDataReader: $this->dataReader, + column: $column, + data: $data, + key: 1, + index: 0 ); $renderer = new DataColumnRenderer( @@ -144,4 +151,220 @@ public function testGetOrderProperties(): void $result = $renderer->getOrderProperties($column); $this->assertEquals(['test' => 'test'], $result); } + + public function testRenderBodyWithCustomContentCallback(): void + { + $column = new DataColumn( + 'name', + content: static fn (array $data) => strtoupper($data['name']) + ); + $cell = new Cell(); + $data = ['id' => 1, 'name' => 'John Doe', 'age' => 20]; + + $context = new DataContext( + preparedDataReader: $this->dataReader, + column: $column, + data: $data, + key: 1, + index: 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 testRenderBodyWithDateTime(): void + { + $date = new DateTime('2025-03-06 02:00:22'); + $column = new DataColumn('created_at', dateTimeFormat: 'Y-m-d'); + $cell = new Cell(); + $data = ['id' => 1, 'created_at' => $date]; + + $context = new DataContext( + preparedDataReader: $this->dataReader, + column: $column, + data: $data, + key: 1, + index: 0 + ); + + $renderer = new DataColumnRenderer( + $this->filterFactoryContainer, + new Validator() + ); + + $result = $renderer->renderBody($column, $cell, $context); + $content = $result->getContent(); + $this->assertNotEmpty($content); + $this->assertStringContainsString('2025-03-06', (string)$content[0]); + } + + public function testRenderBodyWithDynamicAttributes(): void + { + $column = new DataColumn( + 'age', + bodyAttributes: static fn (array $data) => ['class' => $data['age'] >= 21 ? 'adult' : 'minor'] + ); + $cell = new Cell(); + $data = ['id' => 2, 'name' => 'Mary', 'age' => 21]; + + $context = new DataContext( + preparedDataReader: $this->dataReader, + column: $column, + data: $data, + key: 1, + index: 0 + ); + + $renderer = new DataColumnRenderer( + $this->filterFactoryContainer, + new Validator() + ); + + $result = $renderer->renderBody($column, $cell, $context); + $this->assertStringContainsString('adult', $result->getAttributes()['class']); + } + + public function testRenderFilterWithDropdown(): void + { + $column = new DataColumn( + 'status', + filter: ['active' => 'Active', 'inactive' => 'Inactive'] + ); + $cell = new Cell(); + + $urlParameterProvider = new class () implements UrlParameterProviderInterface { + public function get(string $name, int $type): ?string + { + return 'active'; + } + }; + + $context = new FilterContext( + 'filter-form', + new Result(), + 'invalid', + ['class' => 'error-container'], + $urlParameterProvider + ); + + $renderer = new DataColumnRenderer( + $this->filterFactoryContainer, + new Validator() + ); + + $result = $renderer->renderFilter($column, $cell, $context); + $this->assertNotNull($result); + $content = $result->getContent(); + $this->assertStringContainsString('select', (string)$content[0]); + $this->assertStringContainsString('Active', (string)$content[0]); + $this->assertStringContainsString('Inactive', (string)$content[0]); + } + + public function testMakeFilterWithCustomFactory(): void + { + $column = new DataColumn( + 'name', + filterFactory: LikeFilterFactory::class + ); + + $urlParameterProvider = new class () implements UrlParameterProviderInterface { + public function get(string $name, int $type): ?string + { + return match ($name) { + 'name' => 'John', + 'age' => 'not-a-number', + 'status' => '', + default => null, + }; + } + }; + + $context = new MakeFilterContext( + new Result(), + $urlParameterProvider + ); + + $renderer = new DataColumnRenderer( + $this->filterFactoryContainer, + new Validator() + ); + + $result = $renderer->makeFilter($column, $context); + $this->assertNotNull($result); + } + + public function testMakeFilterWithValidation(): void + { + $column = new DataColumn( + 'age', + filterValidation: [new Number()] + ); + + $urlParameterProvider = new class () implements UrlParameterProviderInterface { + public function get(string $name, int $type): ?string + { + return match ($name) { + 'name' => 'John', + 'age' => 'not-a-number', + 'status' => '', + default => null, + }; + } + }; + + $context = new MakeFilterContext( + new Result(), + $urlParameterProvider + ); + + $renderer = new DataColumnRenderer( + $this->filterFactoryContainer, + new Validator() + ); + + $result = $renderer->makeFilter($column, $context); + $this->assertNull($result); + $this->assertNotEmpty($context->validationResult->getErrors()); + } + + public function testMakeFilterWithEmptyValue(): void + { + $column = new DataColumn( + 'status', + filterEmpty: true + ); + + $urlParameterProvider = new class () implements UrlParameterProviderInterface { + public function get(string $name, int $type): ?string + { + return match ($name) { + 'name' => 'John', + 'age' => 'not-a-number', + 'status' => '', + default => null, + }; + } + }; + + $context = new MakeFilterContext( + new Result(), + $urlParameterProvider + ); + + $renderer = new DataColumnRenderer( + $this->filterFactoryContainer, + new Validator() + ); + + $result = $renderer->makeFilter($column, $context); + $this->assertNull($result); + } } diff --git a/tests/Support/TestRenderer.php b/tests/Support/TestRenderer.php new file mode 100644 index 000000000..e0ed18d68 --- /dev/null +++ b/tests/Support/TestRenderer.php @@ -0,0 +1,54 @@ +value; + } + + public function getOption(): string + { + return $this->option; + } + + public function renderColumn(ColumnInterface $column, Cell $cell, GlobalContext $context): Cell + { + return $cell; + } + + public function renderHeader(ColumnInterface $column, Cell $cell, HeaderContext $context): ?Cell + { + return $cell; + } + + public function renderBody(ColumnInterface $column, Cell $cell, DataContext $context): Cell + { + return $cell; + } + + public function renderFooter(ColumnInterface $column, Cell $cell, GlobalContext $context): Cell + { + return $cell; + } +} diff --git a/tests/UrlParametersFactoryTest.php b/tests/UrlParametersFactoryTest.php index 63166e1ff..2d06b47ec 100644 --- a/tests/UrlParametersFactoryTest.php +++ b/tests/UrlParametersFactoryTest.php @@ -16,31 +16,6 @@ */ final class UrlParametersFactoryTest extends TestCase { - public function testCreateWithNullValues(): void - { - $config = new UrlConfig( - pageParameterType: UrlParameterType::QUERY, - previousPageParameterType: UrlParameterType::QUERY, - pageSizeParameterType: UrlParameterType::QUERY, - sortParameterType: UrlParameterType::QUERY - ); - - [$arguments, $queryParameters] = UrlParametersFactory::create( - null, - null, - null, - $config - ); - - $this->assertSame([], $arguments); - $this->assertSame([ - 'page' => null, - 'prev-page' => null, - 'pagesize' => null, - 'sort' => null, - ], $queryParameters); - } - public function testCreateWithNextPageTokenInQueryParameter(): void { $config = new UrlConfig( @@ -293,25 +268,59 @@ public function testCreateWithExistingArgumentsAndQueryParameters(): void ], $queryParameters); } - public function testCreateWithInstanceMethod(): void + public function testCreateWithMixedParameterTypesAndValues(): void { $config = new UrlConfig( - pageParameterType: UrlParameterType::QUERY, + pageParameterType: UrlParameterType::PATH, previousPageParameterType: UrlParameterType::QUERY, + pageSizeParameterType: UrlParameterType::PATH, + sortParameterType: UrlParameterType::QUERY, + arguments: ['id' => 123], + queryParameters: ['filter' => 'active'] + ); + + [$arguments, $queryParameters] = UrlParametersFactory::create( + PageToken::next('token123'), + 20, + 'name,-date', + $config + ); + + $this->assertSame([ + 'id' => 123, + 'page' => 'token123', + 'pagesize' => 20, + ], $arguments); + $this->assertSame([ + 'filter' => 'active', + 'prev-page' => null, + 'sort' => 'name,-date', + ], $queryParameters); + } + + public function testCreateWithNullPageToken(): void + { + $config = new UrlConfig( + pageParameterType: UrlParameterType::PATH, + previousPageParameterType: UrlParameterType::PATH, pageSizeParameterType: UrlParameterType::QUERY, sortParameterType: UrlParameterType::QUERY ); - $result = UrlParametersFactory::create(null, null, null, $config); - - [$arguments, $queryParameters] = $result; + [$arguments, $queryParameters] = UrlParametersFactory::create( + null, + 20, + 'name,-date', + $config + ); - $this->assertSame([], $arguments); $this->assertSame([ 'page' => null, 'prev-page' => null, - 'pagesize' => null, - 'sort' => null, + ], $arguments); + $this->assertSame([ + 'pagesize' => 20, + 'sort' => 'name,-date', ], $queryParameters); } }