Skip to content

Commit dc18c12

Browse files
committed
Cover the source filter of the tree picker
Pins the two decisions that are not obvious from reading the code: an empty list of allowed ids means "nothing matches" rather than "no filter", and gaps in the list must not survive into the filter - callers hand over whatever their own query produced. Refs #460
1 parent fd35534 commit dc18c12

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
3+
/**
4+
* This file is part of contao-community-alliance/dc-general.
5+
*
6+
* (c) 2013-2026 Contao Community Alliance.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* This project is provided in good faith and hope to be usable by anyone.
12+
*
13+
* @package contao-community-alliance/dc-general
14+
* @author Ingolf Steinhardt <info@e-spin.de>
15+
* @copyright 2013-2026 Contao Community Alliance.
16+
* @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later
17+
* @filesource
18+
*/
19+
20+
declare(strict_types=1);
21+
22+
namespace ContaoCommunityAlliance\DcGeneral\Test\Contao\View\Contao2BackendView;
23+
24+
use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\TreePicker;
25+
use ContaoCommunityAlliance\DcGeneral\DataDefinition\ContainerInterface;
26+
use ContaoCommunityAlliance\DcGeneral\DataDefinition\DefaultContainer;
27+
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\DefaultBasicDefinition;
28+
use ContaoCommunityAlliance\DcGeneral\DcGeneral;
29+
use ContaoCommunityAlliance\DcGeneral\EnvironmentInterface;
30+
use ContaoCommunityAlliance\DcGeneral\Test\TestCase;
31+
use PHPUnit\Framework\Attributes\CoversClass;
32+
use ReflectionClass;
33+
34+
/**
35+
* This tests that the picker only offers what the caller allows.
36+
*
37+
* The picker builds its own container for the source table. Without being told otherwise it
38+
* offers every record in it, even when the list beside it shows a filtered selection.
39+
*/
40+
#[CoversClass(TreePicker::class)]
41+
final class TreePickerSourceFilterTest extends TestCase
42+
{
43+
/**
44+
* The ids the caller allows have to end up as a filter on the target container.
45+
*
46+
* @return void
47+
*/
48+
public function testLimitsToTheAllowedIds(): void
49+
{
50+
$definition = $this->buildPicker(['3', '7'])->getEnvironment()->getDataDefinition();
51+
52+
self::assertInstanceOf(ContainerInterface::class, $definition);
53+
self::assertSame(
54+
[['property' => 'id', 'operation' => 'IN', 'values' => ['3', '7']]],
55+
$definition->getBasicDefinition()->getAdditionalFilter('tl_member')
56+
);
57+
}
58+
59+
/**
60+
* Without the option nothing is filtered - the picker keeps offering the whole table.
61+
*
62+
* @return void
63+
*/
64+
public function testLeavesTheContainerAloneWithoutTheOption(): void
65+
{
66+
$definition = $this->buildPicker(null)->getEnvironment()->getDataDefinition();
67+
68+
self::assertInstanceOf(ContainerInterface::class, $definition);
69+
self::assertFalse($definition->getBasicDefinition()->hasAdditionalFilter('tl_member'));
70+
}
71+
72+
/**
73+
* An empty list means "nothing matches", not "no filter at all".
74+
*
75+
* The caller asked for a filter and it came out empty. Offering the whole table instead
76+
* would be the opposite answer to the question, so the filter has to be set either way.
77+
*
78+
* @return void
79+
*/
80+
public function testAnEmptyListMatchesNothing(): void
81+
{
82+
$definition = $this->buildPicker([])->getEnvironment()->getDataDefinition();
83+
84+
self::assertInstanceOf(ContainerInterface::class, $definition);
85+
self::assertSame(
86+
[['property' => 'id', 'operation' => 'IN', 'values' => []]],
87+
$definition->getBasicDefinition()->getAdditionalFilter('tl_member')
88+
);
89+
}
90+
91+
/**
92+
* The ids are matched against whatever the field declared as its id property.
93+
*
94+
* @return void
95+
*/
96+
public function testUsesTheConfiguredIdProperty(): void
97+
{
98+
$definition = $this->buildPicker(['first'], 'alias')->getEnvironment()->getDataDefinition();
99+
100+
self::assertInstanceOf(ContainerInterface::class, $definition);
101+
self::assertSame(
102+
[['property' => 'alias', 'operation' => 'IN', 'values' => ['first']]],
103+
$definition->getBasicDefinition()->getAdditionalFilter('tl_member')
104+
);
105+
}
106+
107+
/**
108+
* Gaps in the list must not turn the values into an object when they get encoded.
109+
*
110+
* Callers hand over whatever their own query produced, and that is rarely a clean list.
111+
*
112+
* @return void
113+
*/
114+
public function testKeepsTheValuesAList(): void
115+
{
116+
$definition = $this->buildPicker([5 => '3', 9 => '7'])->getEnvironment()->getDataDefinition();
117+
118+
self::assertInstanceOf(ContainerInterface::class, $definition);
119+
self::assertSame(
120+
[['property' => 'id', 'operation' => 'IN', 'values' => ['3', '7']]],
121+
$definition->getBasicDefinition()->getAdditionalFilter('tl_member')
122+
);
123+
}
124+
125+
/**
126+
* Build a picker around a bare container and let it apply its source filter.
127+
*
128+
* The widget is created without its constructor - all it needs here is the magic setters
129+
* Contao provides and the container the filter is meant to land on.
130+
*
131+
* @param array<int, string>|null $sourceFilter The ids to hand over, null to omit the option.
132+
* @param string|null $idProperty The property the ids refer to.
133+
*
134+
* @return DcGeneral
135+
*/
136+
private function buildPicker(?array $sourceFilter, ?string $idProperty = null): DcGeneral
137+
{
138+
$container = new DefaultContainer('tl_member');
139+
$container->setBasicDefinition(new DefaultBasicDefinition());
140+
141+
$environment = $this->createMock(EnvironmentInterface::class);
142+
$environment->method('getDataDefinition')->willReturn($container);
143+
144+
$itemContainer = $this->createMock(DcGeneral::class);
145+
$itemContainer->method('getEnvironment')->willReturn($environment);
146+
147+
$reflection = new ReflectionClass(TreePicker::class);
148+
$picker = $reflection->newInstanceWithoutConstructor();
149+
150+
$picker->sourceName = 'tl_member';
151+
if (null !== $idProperty) {
152+
$picker->idProperty = $idProperty;
153+
}
154+
if (null !== $sourceFilter) {
155+
$picker->sourceFilter = $sourceFilter;
156+
}
157+
158+
$property = $reflection->getProperty('itemContainer');
159+
$property->setValue($picker, $itemContainer);
160+
161+
$method = $reflection->getMethod('applySourceFilter');
162+
$method->invoke($picker);
163+
164+
return $itemContainer;
165+
}
166+
}

0 commit comments

Comments
 (0)