2023-12-14 03:28:19 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace SilverStripe\Forms\Tests;
|
|
|
|
|
|
|
|
use SilverStripe\Control\HTTPRequest;
|
2024-06-17 01:59:59 +02:00
|
|
|
use SilverStripe\Core\ClassInfo;
|
2023-12-14 03:28:19 +01:00
|
|
|
use SilverStripe\Dev\SapphireTest;
|
|
|
|
use SilverStripe\Forms\FieldList;
|
|
|
|
use SilverStripe\Forms\SearchableDropdownField;
|
|
|
|
use SilverStripe\Forms\Tests\FormTest\Team;
|
|
|
|
use SilverStripe\Forms\SearchableMultiDropdownField;
|
|
|
|
use SilverStripe\Forms\FormField;
|
|
|
|
use SilverStripe\ORM\Search\SearchContext;
|
|
|
|
use SilverStripe\Security\SecurityToken;
|
|
|
|
use SilverStripe\Forms\HiddenField;
|
|
|
|
use stdClass;
|
|
|
|
use SilverStripe\Forms\Form;
|
2024-09-18 03:53:44 +02:00
|
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
2023-12-14 03:28:19 +01:00
|
|
|
|
|
|
|
class SearchableDropdownTraitTest extends SapphireTest
|
|
|
|
{
|
|
|
|
protected static $fixture_file = 'SearchableDropdownTraitTest.yml';
|
|
|
|
|
|
|
|
protected static $extra_dataobjects = [
|
|
|
|
Team::class,
|
|
|
|
];
|
|
|
|
|
|
|
|
public function testGetSchemaDataType(): void
|
|
|
|
{
|
|
|
|
$singleField = new SearchableDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$multiField = new SearchableMultiDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$this->assertSame($singleField->getSchemaDataType(), FormField::SCHEMA_DATA_TYPE_SINGLESELECT);
|
|
|
|
$this->assertSame($multiField->getSchemaDataType(), FormField::SCHEMA_DATA_TYPE_MULTISELECT);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testSearch(): void
|
|
|
|
{
|
|
|
|
$field = new SearchableDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$request = new HTTPRequest('GET', 'someurl', ['term' => 'Team']);
|
|
|
|
$request->addHeader('X-SecurityID', SecurityToken::getSecurityID());
|
|
|
|
$response = $field->search($request);
|
|
|
|
$this->assertSame(200, $response->getStatusCode());
|
|
|
|
$actual = json_decode($response->getBody(), true);
|
|
|
|
$ids = Team::get()->column('ID');
|
|
|
|
$names = Team::get()->column('Name');
|
|
|
|
$expected = [
|
|
|
|
['value' => $ids[0], 'label' => $names[0]],
|
|
|
|
['value' => $ids[1], 'label' => $names[1]],
|
|
|
|
['value' => $ids[2], 'label' => $names[2]],
|
|
|
|
];
|
|
|
|
$this->assertSame($expected, $actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testSearchNoCsrfToken(): void
|
|
|
|
{
|
|
|
|
$field = new SearchableDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$request = new HTTPRequest('GET', 'someurl', ['term' => 'Team']);
|
|
|
|
$response = $field->search($request);
|
|
|
|
$this->assertSame(400, $response->getStatusCode());
|
|
|
|
$actual = json_decode($response->getBody(), true);
|
|
|
|
$expected = ['message' => 'Invalid CSRF token'];
|
|
|
|
$this->assertSame($expected, $actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testPlaceholder(): void
|
|
|
|
{
|
|
|
|
$field = new SearchableDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$this->assertSame('Select or type to search...', $field->getPlaceholder());
|
|
|
|
$field->setIsSearchable(false);
|
|
|
|
$this->assertSame('Select...', $field->getPlaceholder());
|
|
|
|
$field->setIsLazyLoaded(true);
|
|
|
|
$this->assertSame('Type to search...', $field->getPlaceholder());
|
|
|
|
$field->setEmptyString('My empty string');
|
|
|
|
$this->assertSame('My empty string', $field->getPlaceholder());
|
|
|
|
$field->setPlaceholder('My placeholder');
|
|
|
|
$this->assertSame('My placeholder', $field->getPlaceholder());
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testSeachContext(): void
|
|
|
|
{
|
|
|
|
$field = new SearchableDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$team = Team::get()->first();
|
|
|
|
// assert fallback is the default search context
|
|
|
|
$this->assertSame(
|
|
|
|
$team->getDefaultSearchContext()->getFields()->dataFieldNames(),
|
|
|
|
$field->getSearchContext()->getFields()->dataFieldNames()
|
|
|
|
);
|
|
|
|
// assert setting a custom search context should override the default
|
|
|
|
$searchContext = new SearchContext(Team::class, new FieldList(new HiddenField('lorem')));
|
|
|
|
$field->setSearchContext($searchContext);
|
|
|
|
$this->assertSame(
|
|
|
|
$searchContext->getFields()->dataFieldNames(),
|
|
|
|
$field->getSearchContext()->getFields()->dataFieldNames()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testLabelField(): void
|
|
|
|
{
|
|
|
|
$field = new SearchableDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
// will use the default value of 'Title' for label field
|
|
|
|
$this->assertSame('Title', $field->getLabelField());
|
|
|
|
// can override the default
|
|
|
|
$field->setLabelField('Something');
|
|
|
|
$this->assertSame('Something', $field->getLabelField());
|
|
|
|
}
|
|
|
|
|
2024-09-18 03:53:44 +02:00
|
|
|
#[DataProvider('provideGetValueArray')]
|
2023-12-14 03:28:19 +01:00
|
|
|
public function testGetValueArray(mixed $value, string|array $expected): void
|
|
|
|
{
|
|
|
|
if ($value === '<DataListValue>') {
|
|
|
|
$value = Team::get();
|
|
|
|
$ids = Team::get()->column('ID');
|
|
|
|
$expected = [$ids[0], $ids[1], $ids[2]];
|
|
|
|
} elseif ($value === '<DataObjectValue>') {
|
|
|
|
$value = Team::get()->first();
|
|
|
|
$expected = [$value->ID];
|
|
|
|
}
|
|
|
|
$field = new SearchableDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$field->setValue($value);
|
|
|
|
$this->assertSame($expected, $field->getValueArray());
|
|
|
|
}
|
|
|
|
|
2024-09-18 03:53:44 +02:00
|
|
|
public static function provideGetValueArray(): array
|
2023-12-14 03:28:19 +01:00
|
|
|
{
|
|
|
|
return [
|
|
|
|
'empty' => [
|
|
|
|
'value' => '',
|
|
|
|
'expected' => [],
|
|
|
|
],
|
|
|
|
'array single form builder' => [
|
|
|
|
'value' => ['label' => 'MyTitle15', 'value' => '10', 'selected' => false],
|
|
|
|
'expected' => [10],
|
|
|
|
],
|
|
|
|
'array multi form builder' => [
|
|
|
|
'value' => [
|
|
|
|
['label' => 'MyTitle10', 'value' => '10', 'selected' => true],
|
|
|
|
['label' => 'MyTitle15', 'value' => '15', 'selected' => false],
|
|
|
|
],
|
|
|
|
'expected' => [10, 15],
|
|
|
|
],
|
|
|
|
'string int' => [
|
|
|
|
'value' => '3',
|
|
|
|
'expected' => [3],
|
|
|
|
],
|
|
|
|
'zero string' => [
|
|
|
|
'value' => '0',
|
|
|
|
'expected' => [],
|
|
|
|
],
|
|
|
|
'datalist' => [
|
|
|
|
'value' => '<DataListValue>',
|
|
|
|
'expected' => '<DataListExpected>',
|
|
|
|
],
|
|
|
|
'dataobject' => [
|
|
|
|
'value' => '<DataObjectValue>',
|
|
|
|
'expected' => '<DataObjectExpected>',
|
|
|
|
],
|
|
|
|
'something else' => [
|
|
|
|
'value' => new stdClass(),
|
|
|
|
'expected' => [],
|
|
|
|
],
|
|
|
|
'negative int' => [
|
|
|
|
'value' => -1,
|
|
|
|
'expected' => [],
|
|
|
|
],
|
|
|
|
'negative string int' => [
|
|
|
|
'value' => '-1',
|
|
|
|
'expected' => [],
|
|
|
|
],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testGetSchemaDataDefaults(): void
|
|
|
|
{
|
|
|
|
// setting a form is required for Link() which is called for 'optionUrl'
|
|
|
|
$form = new Form();
|
|
|
|
$field = new SearchableDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$field->setHasEmptyDefault(false);
|
|
|
|
$field->setForm($form);
|
|
|
|
$team = Team::get()->first();
|
|
|
|
$schema = $field->getSchemaDataDefaults();
|
|
|
|
$this->assertSame('MyField', $schema['name']);
|
|
|
|
$this->assertSame(['value' => $team->ID, 'label' => $team->Name, 'selected' => false], $schema['value']);
|
|
|
|
$this->assertFalse($schema['multi']);
|
|
|
|
$this->assertTrue(is_array($schema['options']));
|
|
|
|
$this->assertFalse(array_key_exists('optionUrl', $schema));
|
|
|
|
$this->assertFalse($schema['disabled']);
|
|
|
|
// lazyload changes options/optionUrl
|
|
|
|
$field->setIsLazyLoaded(true);
|
|
|
|
$schema = $field->getSchemaDataDefaults();
|
|
|
|
$this->assertFalse(array_key_exists('options', $schema));
|
|
|
|
$this->assertSame('field/MyField/search', $schema['optionUrl']);
|
|
|
|
// disabled
|
|
|
|
$field->setReadonly(true);
|
|
|
|
$schema = $field->getSchemaDataDefaults();
|
|
|
|
$this->assertTrue($schema['disabled']);
|
|
|
|
// multi field name
|
|
|
|
$field = new SearchableMultiDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$field->setForm($form);
|
|
|
|
$schema = $field->getSchemaDataDefaults();
|
|
|
|
$this->assertSame('MyField[]', $schema['name']);
|
|
|
|
$this->assertTrue($schema['multi']);
|
|
|
|
// accessors
|
|
|
|
$field = new SearchableDropdownField('MyField', 'MyField', Team::get());
|
|
|
|
$field->setForm($form);
|
|
|
|
$schema = $field->getSchemaDataDefaults();
|
|
|
|
$this->assertFalse($schema['lazyLoad']);
|
2024-08-06 05:35:36 +02:00
|
|
|
$this->assertTrue($schema['clearable']);
|
2023-12-14 03:28:19 +01:00
|
|
|
$this->assertSame('Select or type to search...', $schema['placeholder']);
|
|
|
|
$this->assertTrue($schema['searchable']);
|
|
|
|
$field->setIsLazyLoaded(true);
|
2024-08-06 05:35:36 +02:00
|
|
|
$field->setIsClearable(false);
|
2023-12-14 03:28:19 +01:00
|
|
|
$field->setPlaceholder('My placeholder');
|
|
|
|
$field->setIsSearchable(false);
|
|
|
|
$schema = $field->getSchemaDataDefaults();
|
|
|
|
$this->assertTrue($schema['lazyLoad']);
|
2024-08-06 05:35:36 +02:00
|
|
|
$this->assertFalse($schema['clearable']);
|
2023-12-14 03:28:19 +01:00
|
|
|
$this->assertSame('My placeholder', $schema['placeholder']);
|
|
|
|
$this->assertFalse($schema['searchable']);
|
|
|
|
}
|
2024-06-17 01:59:59 +02:00
|
|
|
|
2024-09-18 03:53:44 +02:00
|
|
|
public static function provideLazyLoadedDoesntCallGetSource()
|
2024-06-17 01:59:59 +02:00
|
|
|
{
|
|
|
|
$methodsToCall = [
|
|
|
|
'Field',
|
|
|
|
'getSchemaStateDefaults',
|
|
|
|
'getSchemaState',
|
|
|
|
'getSchemaDataDefaults',
|
|
|
|
'getSchemaData',
|
|
|
|
];
|
|
|
|
$classes = [
|
|
|
|
SearchableMultiDropdownField::class,
|
|
|
|
SearchableDropdownField::class,
|
|
|
|
];
|
|
|
|
$scenarios = [];
|
|
|
|
foreach ($classes as $class) {
|
|
|
|
foreach ($methodsToCall as $method) {
|
|
|
|
$scenarios[] = [
|
|
|
|
'fieldClass' => $class,
|
|
|
|
'methodToCall' => $method,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $scenarios;
|
|
|
|
}
|
|
|
|
|
2024-09-18 03:53:44 +02:00
|
|
|
#[DataProvider('provideLazyLoadedDoesntCallGetSource')]
|
2024-06-17 01:59:59 +02:00
|
|
|
public function testLazyLoadedDoesntCallGetSource(string $fieldClass, string $methodToCall)
|
|
|
|
{
|
|
|
|
// Some methods aren't shared between the two form fields.
|
|
|
|
if (!ClassInfo::hasMethod($fieldClass, $methodToCall)) {
|
|
|
|
$this->markTestSkipped("$fieldClass doesn't have method $methodToCall - skipping");
|
|
|
|
}
|
|
|
|
// We have to disable the constructor because it ends up calling a static method, and we can't call static methods on mocks.
|
|
|
|
$mockField = $this->getMockBuilder($fieldClass)->onlyMethods(['getSource'])->disableOriginalConstructor()->getMock();
|
|
|
|
$mockField->expects($this->never())->method('getSource');
|
|
|
|
$mockField->setIsLazyLoaded(true);
|
|
|
|
$mockField->setSource(Team::get());
|
|
|
|
$mockField->setForm(new Form());
|
|
|
|
$mockField->$methodToCall();
|
|
|
|
}
|
2023-12-14 03:28:19 +01:00
|
|
|
}
|