Skip to content

Commit a7a2dfd

Browse files
authored
Added tests for Checkbox list twig component (#55)
* Added tests for Checkbox list twig component * Refactor assertions in ListFieldTest and ListFieldTraitTest for improved readability
1 parent 6615f24 commit a7a2dfd

File tree

3 files changed

+369
-0
lines changed

3 files changed

+369
-0
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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(
26+
ListField::class,
27+
$component,
28+
'Component should mount as Checkbox\\ListField.'
29+
);
30+
}
31+
32+
public function testDefaultRenderProducesWrapperAndRendersItems(): void
33+
{
34+
$crawler = $this->renderTwigComponent(ListField::class, $this->baseProps())->crawler();
35+
36+
$wrapper = $this->getWrapper($crawler);
37+
$classes = $this->getClassAttr($wrapper);
38+
self::assertStringContainsString(
39+
'ids-field',
40+
$classes,
41+
'Wrapper should include "ids-field".'
42+
);
43+
self::assertStringContainsString(
44+
'ids-field--list',
45+
$classes,
46+
'Wrapper should include "ids-field--list".'
47+
);
48+
self::assertStringContainsString(
49+
'ids-choice-inputs-list',
50+
$classes,
51+
'Wrapper should include "ids-choice-inputs-list".'
52+
);
53+
self::assertStringContainsString(
54+
'ids-checkboxes-list-field',
55+
$classes,
56+
'Wrapper should include "ids-checkboxes-list-field".'
57+
);
58+
59+
$items = $crawler->filter('.ids-choice-inputs-list__items .ids-checkbox-field');
60+
self::assertSame(
61+
2,
62+
$items->count(),
63+
'Should render exactly two checkbox field items.'
64+
);
65+
66+
$firstInput = $this->getCheckboxInput($items->eq(0));
67+
$secondInput = $this->getCheckboxInput($items->eq(1));
68+
69+
self::assertSame(
70+
'checkbox',
71+
$firstInput->attr('type'),
72+
'First item should be a checkbox input.'
73+
);
74+
self::assertSame(
75+
'group',
76+
$firstInput->attr('name'),
77+
'First item "name" should be taken from the top-level group.'
78+
);
79+
self::assertSame(
80+
'checkbox',
81+
$secondInput->attr('type'),
82+
'Second item should be a checkbox input.'
83+
);
84+
self::assertSame(
85+
'group',
86+
$secondInput->attr('name'),
87+
'Second item "name" should be taken from the top-level group.'
88+
);
89+
90+
self::assertStringContainsString(
91+
'Pick A',
92+
$this->getText($items->eq(0)),
93+
'First item should render its label.'
94+
);
95+
self::assertStringContainsString(
96+
'Pick B',
97+
$this->getText($items->eq(1)),
98+
'Second item should render its label.'
99+
);
100+
}
101+
102+
public function testWrapperAttributesMergeClassAndData(): void
103+
{
104+
$crawler = $this->renderTwigComponent(
105+
ListField::class,
106+
$this->baseProps([
107+
'attributes' => ['class' => 'extra-class', 'data-custom' => 'custom'],
108+
])
109+
)->crawler();
110+
111+
$wrapper = $this->getWrapper($crawler);
112+
self::assertStringContainsString(
113+
'extra-class',
114+
$this->getClassAttr($wrapper),
115+
'Custom wrapper class should be merged.'
116+
);
117+
self::assertSame(
118+
'custom',
119+
$wrapper->attr('data-custom'),
120+
'Custom data attribute should render on the wrapper.'
121+
);
122+
}
123+
124+
public function testDirectionVariantAddsExpectedClass(): void
125+
{
126+
$crawler = $this->renderTwigComponent(
127+
ListField::class,
128+
$this->baseProps(['direction' => 'horizontal'])
129+
)->crawler();
130+
131+
$wrapper = $this->getWrapper($crawler);
132+
133+
self::assertStringContainsString(
134+
'ids-choice-inputs-list--horizontal',
135+
$this->getClassAttr($wrapper),
136+
'Direction "horizontal" should add the corresponding class.'
137+
);
138+
}
139+
140+
public function testPerItemPropsAreForwardedToNestedField(): void
141+
{
142+
$props = $this->baseProps();
143+
$props['items'][0]['disabled'] = true;
144+
$props['items'][1]['required'] = true;
145+
146+
$crawler = $this->renderTwigComponent(
147+
ListField::class,
148+
$props
149+
)->crawler();
150+
151+
$items = $crawler->filter('.ids-choice-inputs-list__items .ids-checkbox-field');
152+
$first = $this->getCheckboxInput($items->eq(0));
153+
$second = $this->getCheckboxInput($items->eq(1));
154+
155+
self::assertNotNull(
156+
$first->attr('disabled'),
157+
'Disabled=true on first item should render native "disabled".'
158+
);
159+
self::assertNull(
160+
$first->attr('required'),
161+
'First item should not be required.'
162+
);
163+
164+
self::assertNull(
165+
$second->attr('disabled'),
166+
'Second item should not be disabled.'
167+
);
168+
self::assertNotNull(
169+
$second->attr('required'),
170+
'Required=true on second item should render native "required".'
171+
);
172+
}
173+
174+
public function testInvalidItemsTypeCausesResolverErrorOnMount(): void
175+
{
176+
$this->expectException(InvalidOptionsException::class);
177+
178+
$this->mountTwigComponent(ListField::class, [
179+
'name' => 'group',
180+
'items' => 'oops',
181+
]);
182+
}
183+
184+
public function testMissingRequiredOptionsCauseResolverErrorOnMount(): void
185+
{
186+
$this->expectException(MissingOptionsException::class);
187+
188+
$this->mountTwigComponent(ListField::class, [
189+
'items' => [
190+
['id' => 'opt-a', 'label' => 'Pick A'],
191+
],
192+
]);
193+
}
194+
195+
/**
196+
* @param array<string, mixed> $overrides
197+
*
198+
* @return array<string, mixed>
199+
*/
200+
private function baseProps(array $overrides = []): array
201+
{
202+
return array_replace([
203+
'name' => 'group',
204+
'items' => [
205+
[
206+
'id' => 'opt-a',
207+
'label' => 'Pick A',
208+
'value' => 'A',
209+
],
210+
[
211+
'id' => 'opt-b',
212+
'label' => 'Pick B',
213+
'value' => 'B',
214+
],
215+
],
216+
], $overrides);
217+
}
218+
219+
private function getWrapper(Crawler $crawler): Crawler
220+
{
221+
$node = $crawler->filter('.ids-field')->first();
222+
self::assertGreaterThan(
223+
0,
224+
$node->count(),
225+
'Wrapper ".ids-field" should be present.'
226+
);
227+
228+
return $node;
229+
}
230+
231+
private function getCheckboxInput(Crawler $scope): Crawler
232+
{
233+
$node = $scope->filter('input[type="checkbox"]')->first();
234+
self::assertGreaterThan(
235+
0,
236+
$node->count(),
237+
'Checkbox input should be present.'
238+
);
239+
240+
return $node;
241+
}
242+
243+
private function getClassAttr(Crawler $node): string
244+
{
245+
return (string) $node->attr('class');
246+
}
247+
248+
private function getText(Crawler $node): string
249+
{
250+
return trim($node->text(''));
251+
}
252+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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(
28+
'items',
29+
$resolved,
30+
'Resolved options should contain "items".'
31+
);
32+
self::assertCount(
33+
2,
34+
$resolved['items'],
35+
'"items" should contain two entries.'
36+
);
37+
self::assertSame(
38+
'horizontal',
39+
$resolved['direction'] ?? null,
40+
'"direction" should resolve to HORIZONTAL.'
41+
);
42+
}
43+
44+
public function testDefaultsWhenNoOptionsProvided(): void
45+
{
46+
$resolved = $this->getComponent()->resolve([]);
47+
48+
self::assertSame(
49+
[],
50+
$resolved['items'],
51+
'"items" should default to an empty array.'
52+
);
53+
self::assertSame(
54+
'vertical',
55+
$resolved['direction'],
56+
'"direction" should default to VERTICAL.'
57+
);
58+
}
59+
60+
public function testInvalidItemsTypeCausesResolverError(): void
61+
{
62+
$this->expectException(InvalidOptionsException::class);
63+
64+
$this->getComponent()->resolve([
65+
'items' => 'not-an-array',
66+
]);
67+
}
68+
69+
public function testInvalidDirectionValueCausesResolverError(): void
70+
{
71+
$this->expectException(InvalidOptionsException::class);
72+
73+
$this->getComponent()->resolve([
74+
'items' => [['id' => 'a', 'label' => 'Alpha', 'value' => 'A']],
75+
'direction' => 'diagonal',
76+
]);
77+
}
78+
79+
private function getComponent(): DummyListFieldComponent
80+
{
81+
return new DummyListFieldComponent();
82+
}
83+
}
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)