diff --git a/tests/php/Forms/GridField/GridFieldExportButtonTest.php b/tests/php/Forms/GridField/GridFieldExportButtonTest.php index 3496332b4cd..fb9a739b2b2 100644 --- a/tests/php/Forms/GridField/GridFieldExportButtonTest.php +++ b/tests/php/Forms/GridField/GridFieldExportButtonTest.php @@ -12,8 +12,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 +157,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); diff --git a/tests/php/Forms/GridField/GridFieldPrintButtonTest.php b/tests/php/Forms/GridField/GridFieldPrintButtonTest.php index 5a8b0df4ab3..71ad1adea34 100644 --- a/tests/php/Forms/GridField/GridFieldPrintButtonTest.php +++ b/tests/php/Forms/GridField/GridFieldPrintButtonTest.php @@ -11,6 +11,8 @@ use SilverStripe\Forms\GridField\GridFieldPaginator; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\Tests\GridField\GridFieldPrintButtonTest\TestObject; +use SilverStripe\ORM\ArrayList; +use SilverStripe\View\ArrayData; class GridFieldPrintButtonTest extends SapphireTest { @@ -33,21 +35,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 +62,31 @@ 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); + } } 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