--- /dev/null
+<?php
+namespace api\v4\SearchDisplay;
+
+use Civi\Api4\Contact;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * @group headless
+ */
+class SearchRunTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface {
+
+ public function setUpHeadless() {
+ // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+ // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+ return \Civi\Test::headless()
+ ->installMe(__DIR__)
+ ->apply();
+ }
+
+ public function setUp() {
+ parent::setUp();
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * Test running a searchDisplay with various filters.
+ */
+ public function testRunDisplay() {
+ $lastName = uniqid(__FUNCTION__);
+ $sampleData = [
+ ['first_name' => 'One', 'last_name' => $lastName],
+ ['first_name' => 'Two', 'last_name' => $lastName],
+ ['first_name' => 'Three', 'last_name' => $lastName],
+ ['first_name' => 'Four', 'last_name' => $lastName],
+ ];
+ Contact::save(FALSE)->setRecords($sampleData)->execute();
+
+ $params = [
+ 'checkPermissions' => FALSE,
+ 'return' => 'page:1',
+ 'savedSearch' => [
+ 'api_entity' => 'Contact',
+ 'api_params' => [
+ 'version' => 4,
+ 'select' => ['id', 'first_name', 'last_name'],
+ 'where' => [],
+ ],
+ ],
+ 'display' => [
+ 'type' => 'table',
+ 'label' => '',
+ 'settings' => [
+ 'limit' => 20,
+ 'pager' => TRUE,
+ 'columns' => [
+ [
+ 'key' => 'id',
+ 'label' => 'Contact ID',
+ 'dataType' => 'Integer',
+ 'type' => 'field',
+ ],
+ [
+ 'key' => 'first_name',
+ 'label' => 'First Name',
+ 'dataType' => 'String',
+ 'type' => 'field',
+ ],
+ [
+ 'key' => 'last_name',
+ 'label' => 'Last Name',
+ 'dataType' => 'String',
+ 'type' => 'field',
+ ],
+ ],
+ 'sort' => [
+ ['id', 'ASC'],
+ ],
+ ],
+ ],
+ 'filters' => ['last_name' => $lastName],
+ 'afform' => NULL,
+ ];
+
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertCount(4, $result);
+
+ $params['filters']['first_name'] = ['One', 'Two'];
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertCount(2, $result);
+ $this->assertEquals('One', $result[0]['first_name']);
+ $this->assertEquals('Two', $result[1]['first_name']);
+
+ $params['filters'] = ['id' => ['>' => $result[0]['id'], '<=' => $result[1]['id'] + 1]];
+ $params['sort'] = [['first_name', 'ASC']];
+ $result = civicrm_api4('SearchDisplay', 'run', $params);
+ $this->assertCount(2, $result);
+ $this->assertEquals('Three', $result[0]['first_name']);
+ $this->assertEquals('Two', $result[1]['first_name']);
+ }
+
+}
--- /dev/null
+<?php
+
+ini_set('memory_limit', '2G');
+ini_set('safe_mode', 0);
+// phpcs:disable
+eval(cv('php:boot --level=classloader', 'phpcode'));
+// phpcs:enable
+// Allow autoloading of PHPUnit helper classes in this extension.
+$loader = new \Composer\Autoload\ClassLoader();
+$loader->add('CRM_', __DIR__);
+$loader->add('Civi\\', __DIR__);
+$loader->add('api_', __DIR__);
+$loader->add('api\\', __DIR__);
+$loader->register();
+
+/**
+ * Call the "cv" command.
+ *
+ * @param string $cmd
+ * The rest of the command to send.
+ * @param string $decode
+ * Ex: 'json' or 'phpcode'.
+ * @return string
+ * Response output (if the command executed normally).
+ * @throws \RuntimeException
+ * If the command terminates abnormally.
+ */
+function cv($cmd, $decode = 'json') {
+ $cmd = 'cv ' . $cmd;
+ $descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR);
+ $oldOutput = getenv('CV_OUTPUT');
+ putenv("CV_OUTPUT=json");
+
+ // Execute `cv` in the original folder. This is a work-around for
+ // phpunit/codeception, which seem to manipulate PWD.
+ $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd);
+
+ $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__);
+ putenv("CV_OUTPUT=$oldOutput");
+ fclose($pipes[0]);
+ $result = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+ if (proc_close($process) !== 0) {
+ throw new RuntimeException("Command failed ($cmd):\n$result");
+ }
+ switch ($decode) {
+ case 'raw':
+ return $result;
+
+ case 'phpcode':
+ // If the last output is /*PHPCODE*/, then we managed to complete execution.
+ if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") {
+ throw new \RuntimeException("Command failed ($cmd):\n$result");
+ }
+ return $result;
+
+ case 'json':
+ return json_decode($result, 1);
+
+ default:
+ throw new RuntimeException("Bad decoder format ($decode)");
+ }
+}