Skip to content

Commit f8473ae

Browse files
committed
Added tests for Checkbox list twig component
1 parent 7350356 commit f8473ae

File tree

3 files changed

+269
-0
lines changed

3 files changed

+269
-0
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Tests\Integration\DesignSystemTwig\Twig\Components\Checkbox;
10+
11+
use Ibexa\DesignSystemTwig\Twig\Components\Checkbox\ListField;
12+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
13+
use Symfony\Component\DomCrawler\Crawler;
14+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
15+
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
16+
use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents;
17+
18+
final class ListFieldTest extends KernelTestCase
19+
{
20+
use InteractsWithTwigComponents;
21+
22+
public function testMount(): void
23+
{
24+
$component = $this->mountTwigComponent(ListField::class, $this->baseProps());
25+
self::assertInstanceOf(ListField::class, $component, 'Component should mount as Checkbox\\ListField.');
26+
}
27+
28+
public function testDefaultRenderProducesWrapperAndRendersItems(): void
29+
{
30+
$crawler = $this->renderTwigComponent(ListField::class, $this->baseProps())->crawler();
31+
32+
$wrapper = $this->getWrapper($crawler);
33+
$classes = $this->getClassAttr($wrapper);
34+
self::assertStringContainsString('ids-field', $classes, 'Wrapper should include "ids-field".');
35+
self::assertStringContainsString('ids-field--list', $classes, 'Wrapper should include "ids-field--list".');
36+
self::assertStringContainsString('ids-choice-inputs-list', $classes, 'Wrapper should include "ids-choice-inputs-list".');
37+
self::assertStringContainsString('ids-checkboxes-list-field', $classes, 'Wrapper should include "ids-checkboxes-list-field".');
38+
39+
$items = $crawler->filter('.ids-choice-inputs-list__items .ids-checkbox-field');
40+
self::assertSame(2, $items->count(), 'Should render exactly two checkbox field items.');
41+
42+
$firstInput = $this->getCheckboxInput($items->eq(0));
43+
$secondInput = $this->getCheckboxInput($items->eq(1));
44+
45+
self::assertSame('checkbox', $firstInput->attr('type'), 'First item should be a checkbox input.');
46+
self::assertSame('group', $firstInput->attr('name'), 'First item "name" should be taken from the top-level group.');
47+
self::assertSame('checkbox', $secondInput->attr('type'), 'Second item should be a checkbox input.');
48+
self::assertSame('group', $secondInput->attr('name'), 'Second item "name" should be taken from the top-level group.');
49+
50+
self::assertStringContainsString('Pick A', $this->getText($items->eq(0)), 'First item should render its label.');
51+
self::assertStringContainsString('Pick B', $this->getText($items->eq(1)), 'Second item should render its label.');
52+
}
53+
54+
public function testWrapperAttributesMergeClassAndData(): void
55+
{
56+
$crawler = $this->renderTwigComponent(
57+
ListField::class,
58+
$this->baseProps([
59+
'attributes' => ['class' => 'extra-class', 'data-custom' => 'custom'],
60+
])
61+
)->crawler();
62+
63+
$wrapper = $this->getWrapper($crawler);
64+
self::assertStringContainsString('extra-class', $this->getClassAttr($wrapper), 'Custom wrapper class should be merged.');
65+
self::assertSame('custom', $wrapper->attr('data-custom'), 'Custom data attribute should render on the wrapper.');
66+
}
67+
68+
public function testDirectionVariantAddsExpectedClass(): void
69+
{
70+
$crawler = $this->renderTwigComponent(
71+
ListField::class,
72+
$this->baseProps(['direction' => 'horizontal'])
73+
)->crawler();
74+
75+
$wrapper = $this->getWrapper($crawler);
76+
77+
self::assertStringContainsString('ids-choice-inputs-list--horizontal', $this->getClassAttr($wrapper), 'Direction "horizontal" should add the corresponding class.');
78+
}
79+
80+
public function testPerItemPropsAreForwardedToNestedField(): void
81+
{
82+
$props = $this->baseProps();
83+
$props['items'][0]['disabled'] = true;
84+
$props['items'][1]['required'] = true;
85+
86+
$crawler = $this->renderTwigComponent(
87+
ListField::class,
88+
$props
89+
)->crawler();
90+
91+
$items = $crawler->filter('.ids-choice-inputs-list__items .ids-checkbox-field');
92+
$first = $this->getCheckboxInput($items->eq(0));
93+
$second = $this->getCheckboxInput($items->eq(1));
94+
95+
self::assertNotNull($first->attr('disabled'), 'Disabled=true on first item should render native "disabled".');
96+
self::assertNull($first->attr('required'), 'First item should not be required.');
97+
98+
self::assertNull($second->attr('disabled'), 'Second item should not be disabled.');
99+
self::assertNotNull($second->attr('required'), 'Required=true on second item should render native "required".');
100+
}
101+
102+
public function testInvalidItemsTypeCausesResolverErrorOnMount(): void
103+
{
104+
$this->expectException(InvalidOptionsException::class);
105+
106+
$this->mountTwigComponent(ListField::class, [
107+
'name' => 'group',
108+
'items' => 'oops',
109+
]);
110+
}
111+
112+
public function testMissingRequiredOptionsCauseResolverErrorOnMount(): void
113+
{
114+
$this->expectException(MissingOptionsException::class);
115+
116+
$this->mountTwigComponent(ListField::class, [
117+
'items' => [
118+
['id' => 'opt-a', 'label' => 'Pick A'],
119+
],
120+
]);
121+
}
122+
123+
/**
124+
* @param array<string, mixed> $overrides
125+
*
126+
* @return array<string, mixed>
127+
*/
128+
private function baseProps(array $overrides = []): array
129+
{
130+
return array_replace([
131+
'name' => 'group',
132+
'items' => [
133+
[
134+
'id' => 'opt-a',
135+
'label' => 'Pick A',
136+
'value' => 'A',
137+
],
138+
[
139+
'id' => 'opt-b',
140+
'label' => 'Pick B',
141+
'value' => 'B',
142+
],
143+
],
144+
], $overrides);
145+
}
146+
147+
private function getWrapper(Crawler $crawler): Crawler
148+
{
149+
$node = $crawler->filter('.ids-field')->first();
150+
self::assertGreaterThan(0, $node->count(), 'Wrapper ".ids-field" should be present.');
151+
152+
return $node;
153+
}
154+
155+
private function getCheckboxInput(Crawler $scope): Crawler
156+
{
157+
$node = $scope->filter('input[type="checkbox"]')->first();
158+
self::assertGreaterThan(0, $node->count(), 'Checkbox input should be present.');
159+
160+
return $node;
161+
}
162+
163+
private function getClassAttr(Crawler $node): string
164+
{
165+
return (string) $node->attr('class');
166+
}
167+
168+
private function getText(Crawler $node): string
169+
{
170+
return trim($node->text(''));
171+
}
172+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Tests\Integration\DesignSystemTwig\Twig\Components;
10+
11+
use Ibexa\Tests\Integration\DesignSystemTwig\Twig\Stub\DummyListFieldComponent;
12+
use PHPUnit\Framework\TestCase;
13+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
14+
15+
final class ListFieldTraitTest extends TestCase
16+
{
17+
public function testResolveWithValidItemsAndHorizontalDirectionSucceeds(): void
18+
{
19+
$resolved = $this->getComponent()->resolve([
20+
'items' => [
21+
['id' => 'a', 'label' => 'Alpha', 'value' => 'A'],
22+
['id' => 'b', 'label' => 'Beta', 'value' => 'B'],
23+
],
24+
'direction' => 'horizontal',
25+
]);
26+
27+
self::assertArrayHasKey('items', $resolved, 'Resolved options should contain "items".');
28+
self::assertCount(2, $resolved['items'], '"items" should contain two entries.');
29+
self::assertSame('horizontal', $resolved['direction'] ?? null, '"direction" should resolve to HORIZONTAL.');
30+
}
31+
32+
public function testDefaultsWhenNoOptionsProvided(): void
33+
{
34+
$resolved = $this->getComponent()->resolve([]);
35+
36+
self::assertSame([], $resolved['items'], '"items" should default to an empty array.');
37+
self::assertSame('vertical', $resolved['direction'], '"direction" should default to VERTICAL.');
38+
}
39+
40+
public function testInvalidItemsTypeCausesResolverError(): void
41+
{
42+
$this->expectException(InvalidOptionsException::class);
43+
44+
$this->getComponent()->resolve([
45+
'items' => 'not-an-array',
46+
]);
47+
}
48+
49+
public function testInvalidDirectionValueCausesResolverError(): void
50+
{
51+
$this->expectException(InvalidOptionsException::class);
52+
53+
$this->getComponent()->resolve([
54+
'items' => [['id' => 'a', 'label' => 'Alpha', 'value' => 'A']],
55+
'direction' => 'diagonal',
56+
]);
57+
}
58+
59+
private function getComponent(): DummyListFieldComponent
60+
{
61+
return new DummyListFieldComponent();
62+
}
63+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Tests\Integration\DesignSystemTwig\Twig\Stub;
10+
11+
use Ibexa\DesignSystemTwig\Twig\Components\ListFieldTrait;
12+
use Symfony\Component\OptionsResolver\OptionsResolver;
13+
14+
final class DummyListFieldComponent
15+
{
16+
use ListFieldTrait;
17+
18+
public string $name = 'group';
19+
20+
public bool $required = false;
21+
22+
/**
23+
* @param array<string, mixed> $options
24+
*
25+
* @return array<string, mixed>
26+
*/
27+
public function resolve(array $options): array
28+
{
29+
$resolver = new OptionsResolver();
30+
$this->validateListFieldProps($resolver);
31+
32+
return $resolver->resolve($options);
33+
}
34+
}

0 commit comments

Comments
 (0)