diff --git a/tests/php/Forms/GridField/GridFieldAddExistingAutocompleterTest.php b/tests/php/Forms/GridField/GridFieldAddExistingAutocompleterTest.php index c8b16e2ee43..e58d30837a5 100644 --- a/tests/php/Forms/GridField/GridFieldAddExistingAutocompleterTest.php +++ b/tests/php/Forms/GridField/GridFieldAddExistingAutocompleterTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\Forms\Tests\GridField; +use LogicException; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Convert; use SilverStripe\Dev\CSSContentParser; @@ -17,6 +18,7 @@ use SilverStripe\Forms\Tests\GridField\GridFieldTest\Stadium; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team; use SilverStripe\ORM\ArrayList; +use SilverStripe\View\ArrayData; class GridFieldAddExistingAutocompleterTest extends FunctionalTest { @@ -167,6 +169,118 @@ function ($item) { ); } + public function testGetHTMLFragmentsNeedsDataObject() + { + $component = new GridFieldAddExistingAutocompleter(); + $gridField = $this->getGridFieldForComponent($component); + $list = new ArrayList(); + $dataClass = ArrayData::class; + $list->setDataClass($dataClass); + $gridField->setList($list); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + GridFieldAddExistingAutocompleter::class + . " must be used with DataObject subclasses. Found '$dataClass'" + ); + // Calling the method will throw an exception. + $component->getHTMLFragments($gridField); + } + + public function testGetManipulatedDataNeedsDataObject() + { + $component = new GridFieldAddExistingAutocompleter(); + $gridField = $this->getGridFieldForComponent($component); + $list = new ArrayList(); + $dataClass = ArrayData::class; + $list->setDataClass($dataClass); + $gridField->setList($list); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + GridFieldAddExistingAutocompleter::class + . " must be used with DataObject subclasses. Found '$dataClass'" + ); + + // Calling the method will throw an exception. + $component->getManipulatedData($gridField, $list); + } + + public function testDoSearchNeedsDataObject() + { + $component = new GridFieldAddExistingAutocompleter(); + $gridField = $this->getGridFieldForComponent($component); + $list = new ArrayList(); + $dataClass = ArrayData::class; + $list->setDataClass($dataClass); + $gridField->setList($list); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + GridFieldAddExistingAutocompleter::class + . " must be used with DataObject subclasses. Found '$dataClass'" + ); + + // Calling the method will throw an exception. + $component->doSearch($gridField, new HTTPRequest('GET', '')); + } + + public function testScaffoldSearchFieldsNeedsDataObject() + { + $component = new GridFieldAddExistingAutocompleter(); + $gridField = $this->getGridFieldForComponent($component); + $list = new ArrayList(); + $dataClass = ArrayData::class; + $list->setDataClass($dataClass); + $gridField->setList($list); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + GridFieldAddExistingAutocompleter::class + . " must be used with DataObject subclasses. Found '$dataClass'" + ); + + // Calling the method will either throw an exception or not. + // The test pass/failure is explicitly about whether an exception is thrown. + $component->scaffoldSearchFields($dataClass); + } + + public function testGetPlaceholderTextNeedsDataObject() + { + $component = new GridFieldAddExistingAutocompleter(); + $gridField = $this->getGridFieldForComponent($component); + $list = new ArrayList(); + $dataClass = ArrayData::class; + $list->setDataClass($dataClass); + $gridField->setList($list); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + GridFieldAddExistingAutocompleter::class + . " must be used with DataObject subclasses. Found '$dataClass'" + ); + + // Calling the method will either throw an exception or not. + // The test pass/failure is explicitly about whether an exception is thrown. + $component->getPlaceholderText($dataClass); + } + + public function testSetPlaceholderTextDoesntNeedDataObject() + { + $component = new GridFieldAddExistingAutocompleter(); + $gridField = $this->getGridFieldForComponent($component); + $list = new ArrayList(); + $dataClass = ArrayData::class; + $list->setDataClass($dataClass); + $gridField->setList($list); + + // Prevent from being marked risky. + // This test passes if there's no exception thrown. + $this->expectNotToPerformAssertions(); + + $component->setPlaceholderText(''); + } + protected function getGridFieldForComponent($component) { $config = GridFieldConfig::create()->addComponents( diff --git a/tests/php/Forms/GridField/GridFieldAddNewButtonTest.php b/tests/php/Forms/GridField/GridFieldAddNewButtonTest.php index c0dd6a40c61..900c490e27e 100644 --- a/tests/php/Forms/GridField/GridFieldAddNewButtonTest.php +++ b/tests/php/Forms/GridField/GridFieldAddNewButtonTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\Forms\Tests\GridField; +use LogicException; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\FieldList; @@ -9,11 +10,14 @@ use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridFieldAddNewButton; use SilverStripe\Forms\GridField\GridFieldConfig; +use SilverStripe\Forms\GridField\GridFieldConfig_Base; use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Person; use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PeopleGroup; use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Category; use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\TestController; +use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\SS_List; +use SilverStripe\View\ArrayData; class GridFieldAddNewButtonTest extends SapphireTest { @@ -76,6 +80,24 @@ public function testButtonPassesNoParentContextToSingletonWhenNoParentRecordExis $this->mockButtonFragments($list, null); } + public function testGetHTMLFragmentsThrowsException() + { + $component = new GridFieldAddNewButton(); + $config = new GridFieldConfig_Base(); + $config->addComponent($component); + $gridField = new GridField('dummy', 'dummy', new ArrayList(), $config); + $modelClass = ArrayData::class; + $gridField->setModelClass($modelClass); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + GridFieldAddNewButton::class . ' cannot be used with models that do not implement canCreate().' + . " Remove this component from your GridField or implement canCreate() on $modelClass" + ); + + $component->getHTMLFragments($gridField); + } + protected function mockButtonFragments(SS_List $list, $parent = null) { $form = Form::create( diff --git a/tests/php/Forms/GridField/GridFieldDataColumnsTest.php b/tests/php/Forms/GridField/GridFieldDataColumnsTest.php index 5588dcf3585..b9e0c039796 100644 --- a/tests/php/Forms/GridField/GridFieldDataColumnsTest.php +++ b/tests/php/Forms/GridField/GridFieldDataColumnsTest.php @@ -3,15 +3,18 @@ namespace SilverStripe\Forms\Tests\GridField; use InvalidArgumentException; +use LogicException; use SilverStripe\Forms\GridField\GridFieldDataColumns; use SilverStripe\Security\Member; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\GridField\GridField; +use SilverStripe\Forms\GridField\GridFieldConfig_Base; +use SilverStripe\ORM\ArrayList; +use SilverStripe\View\ArrayData; use stdClass; class GridFieldDataColumnsTest extends SapphireTest { - /** * @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::getDisplayFields */ @@ -23,6 +26,19 @@ public function testGridFieldGetDefaultDisplayFields() $this->assertEquals($expected, $columns->getDisplayFields($obj)); } + /** + * @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::getDisplayFields + */ + public function testGridFieldGetDisplayFieldsWithArrayList() + { + $list = new ArrayList([new ArrayData(['Title' => 'My Item'])]); + $obj = new GridField('testfield', 'testfield', $list); + $expected = ['Title' => 'Title']; + $columns = $obj->getConfig()->getComponentByType(GridFieldDataColumns::class); + $columns->setDisplayFields($expected); + $this->assertEquals($expected, $columns->getDisplayFields($obj)); + } + /** * @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::setDisplayFields * @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::getDisplayFields @@ -76,4 +92,22 @@ public function testFieldFormatting() $columns->getFieldFormatting() ); } + + public function testGetDisplayFieldsThrowsException() + { + $component = new GridFieldDataColumns(); + $config = new GridFieldConfig_Base(); + $config->addComponent($component); + $gridField = new GridField('dummy', 'dummy', new ArrayList(), $config); + $modelClass = ArrayData::class; + $gridField->setModelClass($modelClass); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'Cannot dynamically determine columns. Pass the column names to setDisplayFields()' + . " or implement a summaryFields() method on $modelClass" + ); + + $component->getDisplayFields($gridField); + } } diff --git a/tests/php/Forms/GridField/GridFieldDeleteActionTest.php b/tests/php/Forms/GridField/GridFieldDeleteActionTest.php index 32538092ff5..8786d20bdad 100644 --- a/tests/php/Forms/GridField/GridFieldDeleteActionTest.php +++ b/tests/php/Forms/GridField/GridFieldDeleteActionTest.php @@ -2,6 +2,8 @@ namespace SilverStripe\Forms\Tests\GridField; +use LogicException; +use ReflectionMethod; use SilverStripe\Control\Controller; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse_Exception; @@ -12,6 +14,7 @@ use SilverStripe\Forms\Form; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridFieldConfig; +use SilverStripe\Forms\GridField\GridFieldConfig_Base; use SilverStripe\Forms\GridField\GridFieldDeleteAction; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader; use SilverStripe\Forms\Tests\GridField\GridFieldTest\Permissions; @@ -22,6 +25,7 @@ use SilverStripe\ORM\ValidationException; use SilverStripe\Security\Security; use SilverStripe\Security\SecurityToken; +use SilverStripe\View\ArrayData; class GridFieldDeleteActionTest extends SapphireTest { @@ -230,4 +234,70 @@ public function testMenuGroup() $group = $action->getGroup($gridField, $this->list->first(), 'dummy'); $this->assertNull($group, 'A menu group does not exist when the user cannot delete'); } + + public function provideHandleActionThrowsException() + { + return [ + 'unlinks relation' => [true], + 'deletes related record' => [false], + ]; + } + + /** + * @dataProvider provideHandleActionThrowsException + */ + public function testHandleActionThrowsException(bool $unlinkRelation) + { + $component = new GridFieldDeleteAction(); + $config = new GridFieldConfig_Base(); + $config->addComponent($component); + $gridField = new GridField('dummy', 'dummy', new ArrayList([new ArrayData(['ID' => 1])]), $config); + $modelClass = ArrayData::class; + $gridField->setModelClass($modelClass); + + $this->expectException(LogicException::class); + $permissionMethod = $unlinkRelation ? 'canEdit' : 'canDelete'; + $this->expectExceptionMessage( + GridFieldDeleteAction::class . " cannot be used with models that don't implement {$permissionMethod}()." + . " Remove this component from your GridField or implement {$permissionMethod}() on $modelClass" + ); + + // Calling the method will throw an exception. + $secondArg = $unlinkRelation ? 'unlinkrelation' : 'deleterecord'; + $component->handleAction($gridField, $secondArg, ['RecordID' => 1], []); + } + + public function provideGetRemoveActionThrowsException() + { + return [ + 'removes relation' => [true], + 'deletes related record' => [false], + ]; + } + + /** + * @dataProvider provideGetRemoveActionThrowsException + */ + public function testGetRemoveActionThrowsException(bool $removeRelation) + { + $component = new GridFieldDeleteAction(); + $component->setRemoveRelation($removeRelation); + $config = new GridFieldConfig_Base(); + $config->addComponent($component); + $gridField = new GridField('dummy', 'dummy', new ArrayList([new ArrayData(['ID' => 1])]), $config); + $modelClass = ArrayData::class; + $gridField->setModelClass($modelClass); + + $this->expectException(LogicException::class); + $permissionMethod = $removeRelation ? 'canEdit' : 'canDelete'; + $this->expectExceptionMessage( + GridFieldDeleteAction::class . " cannot be used with models that don't implement {$permissionMethod}()." + . " Remove this component from your GridField or implement {$permissionMethod}() on $modelClass" + ); + + // Calling the method will throw an exception. + $reflectionMethod = new ReflectionMethod($component, 'getRemoveAction'); + $reflectionMethod->setAccessible(true); + $reflectionMethod->invokeArgs($component, [$gridField, new ArrayData(), '']); + } } diff --git a/tests/php/Forms/GridField/GridFieldDetailFormTest.php b/tests/php/Forms/GridField/GridFieldDetailFormTest.php index 8565a6f5d29..fbc127b6a68 100644 --- a/tests/php/Forms/GridField/GridFieldDetailFormTest.php +++ b/tests/php/Forms/GridField/GridFieldDetailFormTest.php @@ -2,14 +2,20 @@ namespace SilverStripe\Forms\Tests\GridField; +use LogicException; +use ReflectionMethod; use SilverStripe\Control\Controller; +use SilverStripe\Control\HTTPRequest; use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\FunctionalTest; +use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; use SilverStripe\Forms\GridField\GridField; +use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor; use SilverStripe\Forms\GridField\GridFieldDetailForm; use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest; use SilverStripe\Forms\HiddenField; +use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\ArrayDataWithID; use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Category; use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\CategoryController; use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\GroupController; @@ -17,6 +23,8 @@ use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Person; use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PolymorphicPeopleGroup; use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\TestController; +use SilverStripe\ORM\ArrayList; +use SilverStripe\View\ArrayData; class GridFieldDetailFormTest extends FunctionalTest { @@ -460,4 +468,121 @@ public function testRedirectMissingRecords() $this->autoFollowRedirection = $origAutoFollow; } + + public function provideGetRecordFromRequestFindExisting() + { + return [ + 'No records' => [ + 'data' => [], + 'hasRecord' => false, + ], + 'Records exist but without ID field' => [ + 'data' => [new ArrayDataWithID()], + 'hasRecord' => false, + ], + 'Record exists with matching ID' => [ + 'data' => [new ArrayDataWithID(['ID' => 32])], + 'hasRecord' => true, + ], + 'Record exists, no matching ID' => [ + 'data' => [new ArrayDataWithID(['ID' => 1])], + 'hasRecord' => false, + ], + ]; + } + + /** + * @dataProvider provideGetRecordFromRequestFindExisting + */ + public function testGetRecordFromRequestFindExisting(array $data, bool $hasRecord) + { + $controller = new TestController(); + $form = $controller->Form(null, new ArrayList($data)); + $gridField = $form->Fields()->dataFieldByName('testfield'); + if (empty($data)) { + $gridField->setModelClass(ArrayDataWithID::class); + } + $component = $gridField->getConfig()->getComponentByType(GridFieldDetailForm::class); + $request = new HTTPRequest('GET', $gridField->Link('item/32')); + $request->match(Controller::join_links($gridField->Link(), 'item/$ID')); + + $reflectionMethod = new ReflectionMethod($component, 'getRecordFromRequest'); + $reflectionMethod->setAccessible(true); + $this->assertSame($hasRecord, (bool) $reflectionMethod->invoke($component, $gridField, $request)); + } + + public function provideGetRecordFromRequestCreateNew() + { + // Note that in all of these scenarios a new record gets created, so it *shouldn't* matter what's already in there. + return [ + 'No records' => [ + 'data' => [], + ], + 'Records exist but without ID field' => [ + 'data' => [new ArrayDataWithID()], + ], + 'Record exists with ID field' => [ + 'data' => [new ArrayDataWithID(['ID' => 32])], + ], + ]; + } + + /** + * @dataProvider provideGetRecordFromRequestCreateNew + */ + public function testGetRecordFromRequestCreateNew(array $data) + { + $controller = new TestController(); + $form = $controller->Form(null, new ArrayList($data)); + $gridField = $form->Fields()->dataFieldByName('testfield'); + if (empty($data)) { + $gridField->setModelClass(ArrayDataWithID::class); + } + $component = $gridField->getConfig()->getComponentByType(GridFieldDetailForm::class); + $request = new HTTPRequest('GET', $gridField->Link('item/new')); + $request->match(Controller::join_links($gridField->Link(), 'item/$ID')); + + $reflectionMethod = new ReflectionMethod($component, 'getRecordFromRequest'); + $reflectionMethod->setAccessible(true); + $this->assertEquals(new ArrayDataWithID(['ID' => 0]), $reflectionMethod->invoke($component, $gridField, $request)); + } + + public function provideGetRecordFromRequestWithoutData() + { + // Note that in all of these scenarios a new record gets created, so it *shouldn't* matter what's already in there. + return [ + 'No records' => [ + 'data' => [], + ], + 'Records exist but without ID field' => [ + 'data' => [new ArrayData()], + ], + 'Record exists with ID field' => [ + 'data' => [new ArrayData(['ID' => 32])], + ], + ]; + } + + /** + * @dataProvider provideGetRecordFromRequestWithoutData + */ + public function testGetRecordFromRequestWithoutData(array $data) + { + $controller = new TestController(); + $form = $controller->Form(null, new ArrayList($data)); + $gridField = $form->Fields()->dataFieldByName('testfield'); + if (empty($data)) { + $gridField->setModelClass(ArrayData::class); + } + $component = $gridField->getConfig()->getComponentByType(GridFieldDetailForm::class); + $request = new HTTPRequest('GET', $gridField->Link('item/new')); + $request->match(Controller::join_links($gridField->Link(), 'item/$ID')); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage(ArrayData::class . ' must have an ID field.'); + + $reflectionMethod = new ReflectionMethod($component, 'getRecordFromRequest'); + $reflectionMethod->setAccessible(true); + $reflectionMethod->invoke($component, $gridField, $request); + } } diff --git a/tests/php/Forms/GridField/GridFieldDetailFormTest/ArrayDataWithID.php b/tests/php/Forms/GridField/GridFieldDetailFormTest/ArrayDataWithID.php new file mode 100644 index 00000000000..00fed3896c1 --- /dev/null +++ b/tests/php/Forms/GridField/GridFieldDetailFormTest/ArrayDataWithID.php @@ -0,0 +1,15 @@ +filter('Name', 'My Group') - ->sort('Name') - ->First(); + if (!$list) { + $group = PeopleGroup::get() + ->filter('Name', 'My Group') + ->sort('Name') + ->First(); + $list = $group->People(); + } - $field = new GridField('testfield', 'testfield', $group->People()); + $field = new GridField('testfield', 'testfield', $list); $field->getConfig()->addComponent(new GridFieldToolbarHeader()); $field->getConfig()->addComponent(new GridFieldAddNewButton('toolbar-header-right')); $field->getConfig()->addComponent(new GridFieldViewButton()); diff --git a/tests/php/Forms/GridField/GridFieldDetailForm_ItemRequestTest.php b/tests/php/Forms/GridField/GridFieldDetailForm_ItemRequestTest.php new file mode 100644 index 00000000000..0a13a074970 --- /dev/null +++ b/tests/php/Forms/GridField/GridFieldDetailForm_ItemRequestTest.php @@ -0,0 +1,34 @@ +setModelClass($modelClass); + $itemRequest = new GridFieldDetailForm_ItemRequest($gridField, new GridFieldDetailForm(), new ArrayData(), new Controller(), ''); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'Cannot dynamically determine form fields. Pass the fields to GridFieldDetailForm::setFields()' + . " or implement a getCMSFields() method on $modelClass" + ); + + $itemRequest->ItemEditForm(); + } +} diff --git a/tests/php/Forms/GridField/GridFieldExportButtonTest.php b/tests/php/Forms/GridField/GridFieldExportButtonTest.php index 3496332b4cd..2f4c9078c59 100644 --- a/tests/php/Forms/GridField/GridFieldExportButtonTest.php +++ b/tests/php/Forms/GridField/GridFieldExportButtonTest.php @@ -3,6 +3,8 @@ namespace SilverStripe\Forms\Tests\GridField; use League\Csv\Reader; +use LogicException; +use ReflectionMethod; use SilverStripe\Forms\Tests\GridField\GridFieldExportButtonTest\NoView; use SilverStripe\Forms\Tests\GridField\GridFieldExportButtonTest\Team; use SilverStripe\ORM\DataList; @@ -12,8 +14,10 @@ use SilverStripe\Forms\GridField\GridFieldConfig; use SilverStripe\Forms\GridField\GridFieldExportButton; use SilverStripe\Forms\GridField\GridField; +use SilverStripe\Forms\GridField\GridFieldDataColumns; use SilverStripe\Forms\GridField\GridFieldPaginator; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\View\ArrayData; class GridFieldExportButtonTest extends SapphireTest { @@ -155,13 +159,16 @@ public function testNoCsvHeaders() public function testArrayListInput() { $button = new GridFieldExportButton(); + $columns = new GridFieldDataColumns(); + $columns->setDisplayFields(['ID' => 'ID']); + $this->gridField->getConfig()->addComponent($columns); $this->gridField->getConfig()->addComponent(new GridFieldPaginator()); //Create an ArrayList 1 greater the Paginator's default 15 rows $arrayList = new ArrayList(); for ($i = 1; $i <= 16; $i++) { - $dataobject = new DataObject(['ID' => $i]); - $arrayList->add($dataobject); + $datum = new ArrayData(['ID' => $i]); + $arrayList->add($datum); } $this->gridField->setList($arrayList); @@ -192,6 +199,25 @@ public function testZeroValue() ); } + public function testGetExportColumnsForGridFieldThrowsException() + { + $component = new GridFieldExportButton(); + $gridField = new GridField('dummy', 'dummy', new ArrayList()); + $gridField->getConfig()->removeComponentsByType(GridFieldDataColumns::class); + $modelClass = ArrayData::class; + $gridField->setModelClass($modelClass); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'Cannot dynamically determine columns. Add a GridFieldDataColumns component to your GridField' + . " or implement a summaryFields() method on $modelClass" + ); + + $reflectionMethod = new ReflectionMethod($component, 'getExportColumnsForGridField'); + $reflectionMethod->setAccessible(true); + $reflectionMethod->invoke($component, $gridField); + } + protected function createReader($string) { $reader = Reader::createFromString($string); diff --git a/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php b/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php index 145a87441a8..2a760c25ade 100644 --- a/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php +++ b/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\Forms\Tests\GridField; +use LogicException; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Config\Config; use SilverStripe\Dev\SapphireTest; @@ -20,6 +21,7 @@ use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; +use SilverStripe\View\ArrayData; class GridFieldFilterHeaderTest extends SapphireTest { @@ -226,4 +228,20 @@ public function testRenderHeadersNonDataObject() $this->assertNull($htmlFragment); } + + public function testGetDisplayFieldsThrowsException() + { + $component = new GridFieldFilterHeader(); + $gridField = new GridField('dummy', 'dummy', new ArrayList()); + $modelClass = ArrayData::class; + $gridField->setModelClass($modelClass); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'Cannot dynamically instantiate SearchContext. Pass the SearchContext to setSearchContext()' + . " or implement a getDefaultSearchContext() method on $modelClass" + ); + + $component->getSearchContext($gridField); + } } diff --git a/tests/php/Forms/GridField/GridFieldLevelupTest.php b/tests/php/Forms/GridField/GridFieldLevelupTest.php new file mode 100644 index 00000000000..ffa7eeccbac --- /dev/null +++ b/tests/php/Forms/GridField/GridFieldLevelupTest.php @@ -0,0 +1,29 @@ +setModelClass($modelClass); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + GridFieldLevelup::class . " must be used with DataObject subclasses. Found '$modelClass'" + ); + + $component->getHTMLFragments($gridField); + } +} diff --git a/tests/php/Forms/GridField/GridFieldPrintButtonTest.php b/tests/php/Forms/GridField/GridFieldPrintButtonTest.php index 5a8b0df4ab3..8b9b2f61d2b 100644 --- a/tests/php/Forms/GridField/GridFieldPrintButtonTest.php +++ b/tests/php/Forms/GridField/GridFieldPrintButtonTest.php @@ -2,6 +2,8 @@ namespace SilverStripe\Forms\Tests\GridField; +use LogicException; +use ReflectionMethod; use SilverStripe\Dev\SapphireTest; use SilverStripe\Control\Controller; use SilverStripe\Forms\FieldList; @@ -10,7 +12,10 @@ use SilverStripe\Forms\GridField\GridFieldConfig; use SilverStripe\Forms\GridField\GridFieldPaginator; use SilverStripe\Forms\GridField\GridField; +use SilverStripe\Forms\GridField\GridFieldDataColumns; use SilverStripe\Forms\Tests\GridField\GridFieldPrintButtonTest\TestObject; +use SilverStripe\ORM\ArrayList; +use SilverStripe\View\ArrayData; class GridFieldPrintButtonTest extends SapphireTest { @@ -33,21 +38,19 @@ protected function setUp(): void public function testLimit() { - $this->assertEquals(42, $this->getTestableRows()->count()); + $this->assertEquals(42, $this->getTestableRows(TestObject::get())->count()); } public function testCanViewIsRespected() { $orig = TestObject::$canView; TestObject::$canView = false; - $this->assertEquals(0, $this->getTestableRows()->count()); + $this->assertEquals(0, $this->getTestableRows(TestObject::get())->count()); TestObject::$canView = $orig; } - private function getTestableRows() + private function getTestableRows($list) { - $list = TestObject::get(); - $button = new GridFieldPrintButton(); $button->setPrintColumns(['Name' => 'My Name']); @@ -62,4 +65,50 @@ private function getTestableRows() $printData = $button->generatePrintData($gridField); return $printData->ItemRows; } + + public function testGeneratePrintData() + { + $names = [ + 'Bob', + 'Alice', + 'John', + 'Jane', + 'Sam', + ]; + + $list = new ArrayList(); + foreach ($names as $name) { + $list->add(new ArrayData(['Name' => $name])); + } + + $rows = $this->getTestableRows($list); + + $foundNames = []; + foreach ($rows as $row) { + foreach ($row->ItemRow as $column) { + $foundNames[] = $column->CellString; + } + } + + $this->assertSame($names, $foundNames); + } + + public function testGetPrintColumnsForGridFieldThrowsException() + { + $component = new GridFieldPrintButton(); + $gridField = new GridField('dummy', 'dummy', new ArrayList()); + $gridField->getConfig()->removeComponentsByType(GridFieldDataColumns::class); + $modelClass = ArrayData::class; + $gridField->setModelClass($modelClass); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'Cannot dynamically determine columns. Add a GridFieldDataColumns component to your GridField' + . " or implement a summaryFields() method on $modelClass" + ); + + $reflectionMethod = new ReflectionMethod($component, 'getPrintColumnsForGridField'); + $reflectionMethod->setAccessible(true); + $reflectionMethod->invoke($component, $gridField); + } } diff --git a/tests/php/ORM/Search/BasicSearchContextTest.php b/tests/php/ORM/Search/BasicSearchContextTest.php new file mode 100644 index 00000000000..58c97aacd03 --- /dev/null +++ b/tests/php/ORM/Search/BasicSearchContextTest.php @@ -0,0 +1,304 @@ + 'James', + 'Email' => 'james@example.com', + 'HairColor' => 'brown', + 'EyeColor' => 'brown', + ], + [ + 'Name' => 'John', + 'Email' => 'john@example.com', + 'HairColor' => 'blond', + 'EyeColor' => 'blue', + ], + [ + 'Name' => 'Jane', + 'Email' => 'jane@example.com', + 'HairColor' => 'brown', + 'EyeColor' => 'green', + ], + [ + 'Name' => 'Hemi', + 'Email' => 'hemi@example.com', + 'HairColor' => 'black', + 'EyeColor' => 'brown eyes', + ], + [ + 'Name' => 'Sara', + 'Email' => 'sara@example.com', + 'HairColor' => 'black', + 'EyeColor' => 'green', + ], + [ + 'Name' => 'MatchNothing', + 'Email' => 'MatchNothing', + 'HairColor' => 'MatchNothing', + 'EyeColor' => 'MatchNothing', + ], + ]; + + $list = new ArrayList(); + foreach ($data as $datum) { + $list->add(new ArrayData($datum)); + } + return $list; + } + + private function getSearchableFields(string $generalField): FieldList + { + return new FieldList([ + new HiddenField($generalField), + new TextField('Name'), + new TextField('Email'), + new TextField('HairColor'), + new TextField('EyeColor'), + ]); + } + + public function testResultSetFilterReturnsExpectedCount() + { + $context = new BasicSearchContext(ArrayData::class); + $results = $context->getQuery(['Name' => ''], existingQuery: $this->getList()); + + $this->assertEquals(6, $results->Count()); + + $results = $context->getQuery(['EyeColor' => 'green'], existingQuery: $this->getList()); + $this->assertEquals(2, $results->Count()); + + $results = $context->getQuery(['EyeColor' => 'green', 'HairColor' => 'black'], existingQuery: $this->getList()); + $this->assertEquals(1, $results->Count()); + } + + public function provideApplySearchFilters() + { + $idFilter = new ExactMatchFilter('ID'); + $idFilter->setModifiers(['nocase']); + return [ + 'defaults to PartialMatch' => [ + 'searchParams' => [ + 'q' => 'This one gets ignored', + 'ID' => 47, + 'Name' => 'some search term', + ], + 'filters' => null, + 'expected' => [ + 'q' => 'This one gets ignored', + 'ID:PartialMatch' => 47, + 'Name:PartialMatch' => 'some search term', + ], + ], + 'respects custom filters and modifiers' => [ + 'searchParams' => [ + 'q' => 'This one gets ignored', + 'ID' => 47, + 'Name' => 'some search term', + ], + 'filters' => ['ID' => $idFilter], + 'expected' => [ + 'q' => 'This one gets ignored', + 'ID:ExactMatch:nocase' => 47, + 'Name:PartialMatch' => 'some search term', + ], + ], + ]; + } + + /** + * @dataProvider provideApplySearchFilters + */ + public function testApplySearchFilters(array $searchParams, ?array $filters, array $expected) + { + $context = new BasicSearchContext(ArrayData::class); + $reflectionApplySearchFilters = new ReflectionMethod($context, 'applySearchFilters'); + $reflectionApplySearchFilters->setAccessible(true); + + if ($filters) { + $context->setFilters($filters); + } + + $this->assertSame($expected, $reflectionApplySearchFilters->invoke($context, $searchParams)); + } + + public function provideGetGeneralSearchFilterTerm() + { + return [ + 'defaults to case-insensitive partial match' => [ + 'filterType' => null, + 'fieldFilter' => null, + 'expected' => 'PartialMatch:nocase', + ], + 'uses default even when config is explicitly "null"' => [ + 'filterType' => null, + 'fieldFilter' => new StartsWithFilter('MyField'), + 'expected' => 'PartialMatch:nocase', + ], + 'uses configuration filter over field-specific filter' => [ + 'filterType' => ExactMatchFilter::class, + 'fieldFilter' => new StartsWithFilter(), + 'expected' => 'ExactMatch', + ], + 'uses field-specific filter if provided and config is empty string' => [ + 'filterType' => '', + 'fieldFilter' => new StartsWithFilter('MyField'), + 'expected' => 'StartsWith', + ], + ]; + } + + /** + * @dataProvider provideGetGeneralSearchFilterTerm + */ + public function testGetGeneralSearchFilterTerm(?string $filterType, ?SearchFilter $fieldFilter, string $expected) + { + $context = new BasicSearchContext(ArrayData::class); + $reflectionGetGeneralSearchFilterTerm = new ReflectionMethod($context, 'getGeneralSearchFilterTerm'); + $reflectionGetGeneralSearchFilterTerm->setAccessible(true); + + if ($fieldFilter) { + $context->setFilters(['MyField' => $fieldFilter]); + } + + Config::modify()->set(ArrayData::class, 'general_search_field_filter', $filterType); + + $this->assertSame($expected, $reflectionGetGeneralSearchFilterTerm->invoke($context, 'MyField')); + } + + public function provideGetQuery() + { + // Note that the search TERM is the same for both scenarios, + // but because the search FIELD is different, we get different results. + return [ + 'search against hair' => [ + 'searchParams' => [ + 'HairColor' => 'brown', + ], + 'expected' => [ + 'James', + 'Jane', + ], + ], + 'search against eyes' => [ + 'searchParams' => [ + 'EyeColor' => 'brown', + ], + 'expected' => [ + 'James', + 'Hemi', + ], + ], + 'search against all' => [ + 'searchParams' => [ + 'q' => 'brown', + ], + 'expected' => [ + 'James', + 'Jane', + 'Hemi', + ], + ], + ]; + } + + /** + * @dataProvider provideGetQuery + */ + public function testGetQuery(array $searchParams, array $expected) + { + $list = $this->getList(); + $context = new BasicSearchContext(ArrayData::class); + $context->setFields($this->getSearchableFields(BasicSearchContext::config()->get('general_search_field_name'))); + + $results = $context->getQuery($searchParams, existingQuery: $list); + $this->assertSame($expected, $results->column('Name')); + } + + public function testGeneralSearch() + { + $list = $this->getList(); + $generalField = BasicSearchContext::config()->get('general_search_field_name'); + $context = new BasicSearchContext(ArrayData::class); + $context->setFields($this->getSearchableFields($generalField)); + + $results = $context->getQuery([$generalField => 'brown'], existingQuery: $list); + $this->assertSame(['James', 'Jane', 'Hemi'], $results->column('Name')); + $results = $context->getQuery([$generalField => 'b'], existingQuery: $list); + $this->assertSame(['James', 'John', 'Jane', 'Hemi', 'Sara'], $results->column('Name')); + } + + public function testGeneralSearchSplitTerms() + { + $list = $this->getList(); + $generalField = BasicSearchContext::config()->get('general_search_field_name'); + $context = new BasicSearchContext(ArrayData::class); + $context->setFields($this->getSearchableFields($generalField)); + + // These terms don't exist in a single field in this order on any object, but they do exist in separate fields. + $results = $context->getQuery([$generalField => 'john blue'], existingQuery: $list); + $this->assertSame(['John'], $results->column('Name')); + $results = $context->getQuery([$generalField => 'eyes sara'], existingQuery: $list); + $this->assertSame(['Hemi', 'Sara'], $results->column('Name')); + } + + public function testGeneralSearchNoSplitTerms() + { + Config::modify()->set(ArrayData::class, 'general_search_split_terms', false); + $list = $this->getList(); + $generalField = BasicSearchContext::config()->get('general_search_field_name'); + $context = new BasicSearchContext(ArrayData::class); + $context->setFields($this->getSearchableFields($generalField)); + + // These terms don't exist in a single field in this order on any object + $results = $context->getQuery([$generalField => 'john blue'], existingQuery: $list); + $this->assertCount(0, $results); + + // These terms exist in a single field, but not in this order. + $results = $context->getQuery([$generalField => 'eyes brown'], existingQuery: $list); + $this->assertCount(0, $results); + + // These terms exist in a single field in this order. + $results = $context->getQuery([$generalField => 'brown eyes'], existingQuery: $list); + $this->assertSame(['Hemi'], $results->column('Name')); + } + + public function testSpecificFieldsCanBeSkipped() + { + $general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1'); + $list = new ArrayList(); + $list->merge(SearchContextTest\GeneralSearch::get()); + $generalField = BasicSearchContext::config()->get('general_search_field_name'); + $context = new BasicSearchContext(SearchContextTest\GeneralSearch::class); + + // We're searching for a value that DOES exist in a searchable field, + // but that field is set to be skipped by general search. + $results = $context->getQuery([$generalField => $general1->ExcludeThisField], existingQuery: $list); + $this->assertNotEmpty($general1->ExcludeThisField); + $this->assertCount(0, $results); + } +} diff --git a/tests/php/ORM/Search/BasicSearchContextTest.yml b/tests/php/ORM/Search/BasicSearchContextTest.yml new file mode 100644 index 00000000000..dfdd30f344f --- /dev/null +++ b/tests/php/ORM/Search/BasicSearchContextTest.yml @@ -0,0 +1,28 @@ +SilverStripe\ORM\Tests\Search\SearchContextTest\GeneralSearch: + general0: + Name: General Zero + DoNotUseThisField: omitted + HairColor: blue + ExcludeThisField: excluded + ExactMatchField: Some specific value here + PartialMatchField: A partial match is allowed for this field + MatchAny1: Some match any field + MatchAny2: Another match any field + general1: + Name: General One + DoNotUseThisField: omitted + HairColor: brown + ExcludeThisField: excluded + ExactMatchField: This requires an exact match + PartialMatchField: This explicitly allows partial matches + MatchAny1: first match + MatchAny2: second match + general2: + Name: MatchNothing + DoNotUseThisField: MatchNothing + HairColor: MatchNothing + ExcludeThisField: MatchNothing + ExactMatchField: MatchNothing + PartialMatchField: MatchNothing + MatchAny1: MatchNothing + MatchAny2: MatchNothing diff --git a/tests/php/ORM/Search/SearchContextTest.php b/tests/php/ORM/Search/SearchContextTest.php index 7236709cbd4..839de5bc96c 100644 --- a/tests/php/ORM/Search/SearchContextTest.php +++ b/tests/php/ORM/Search/SearchContextTest.php @@ -18,6 +18,7 @@ use SilverStripe\ORM\Filters\ExactMatchFilter; use SilverStripe\ORM\Filters\PartialMatchFilter; use SilverStripe\ORM\Search\SearchContext; +use SilverStripe\View\ArrayData; class SearchContextTest extends SapphireTest { @@ -527,4 +528,18 @@ public function testMatchAnySearchWithFilters() $results = $context->getResults(['PartialMatchField' => 'an']); $this->assertCount(1, $results); } + + public function testGetSearchFieldsThrowsException() + { + $modelClass = ArrayData::class; + $context = new SearchContext($modelClass); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'Cannot dynamically determine search fields. Pass the fields to setFields()' + . " or implement a scaffoldSearchFields() method on {$modelClass}" + ); + + $context->getSearchFields(); + } }