diff --git a/tests/Fixture/PackagesFixture.php b/tests/Fixture/PackagesFixture.php
index f720d2d3..dda787d9 100644
--- a/tests/Fixture/PackagesFixture.php
+++ b/tests/Fixture/PackagesFixture.php
@@ -28,6 +28,26 @@ public function init(): void
'latest_stable_version' => '5.0.0',
'latest_stable_release_date' => '2025-01-15',
],
+ [
+ 'id' => 25,
+ 'package' => 'cakedc/users',
+ 'description' => 'Users plugin for CakePHP.',
+ 'repo_url' => 'https://github.com/cakedc/users',
+ 'downloads' => 9000,
+ 'stars' => 500,
+ 'latest_stable_version' => '12.0.0',
+ 'latest_stable_release_date' => '2025-03-01',
+ ],
+ [
+ 'id' => 26,
+ 'package' => 'dereuromark/cakephp-tools',
+ 'description' => 'A CakePHP tools plugin containing lots of useful helpers.',
+ 'repo_url' => 'https://github.com/dereuromark/cakephp-tools',
+ 'downloads' => 20000,
+ 'stars' => 300,
+ 'latest_stable_version' => '3.0.0',
+ 'latest_stable_release_date' => '2025-04-01',
+ ],
];
for ($i = 2; $i <= 24; $i++) {
diff --git a/tests/TestCase/Controller/PackagesControllerTest.php b/tests/TestCase/Controller/PackagesControllerTest.php
index 49c7fa31..d2eaf3ac 100644
--- a/tests/TestCase/Controller/PackagesControllerTest.php
+++ b/tests/TestCase/Controller/PackagesControllerTest.php
@@ -23,6 +23,8 @@ class PackagesControllerTest extends TestCase
*/
protected array $fixtures = [
'app.Packages',
+ 'plugin.Tags.Tags',
+ 'plugin.Tags.Tagged',
];
/**
@@ -70,4 +72,96 @@ public function testIndexHidesFeaturedSliderWhenSearching(): void
$this->assertResponseNotContains('data-featured-packages-slider');
$this->assertResponseContains('vendor/package-02');
}
+
+ /**
+ * @return void
+ */
+ public function testAutocompleteReturnsJsonResults(): void
+ {
+ $this->configRequest(['headers' => ['Accept' => 'application/json']]);
+ $this->get('/autocomplete?q=asset');
+
+ $this->assertResponseOk();
+ $this->assertContentType('application/json');
+
+ $body = (string)$this->_response->getBody();
+ $results = json_decode($body, true);
+
+ $this->assertNotEmpty($results);
+ $this->assertSame('markstory/asset_compress', $results[0]['package']);
+ $this->assertArrayHasKey('description', $results[0]);
+ $this->assertArrayHasKey('repo_url', $results[0]);
+ $this->assertArrayHasKey('downloads', $results[0]);
+ $this->assertArrayHasKey('stars', $results[0]);
+ $this->assertArrayHasKey('latest_version', $results[0]);
+ $this->assertArrayHasKey('cakephp_versions', $results[0]);
+ $this->assertArrayHasKey('php_versions', $results[0]);
+ }
+
+ /**
+ * @return void
+ */
+ public function testAutocompleteReturnsEmptyForShortQuery(): void
+ {
+ $this->configRequest(['headers' => ['Accept' => 'application/json']]);
+ $this->get('/autocomplete?q=a');
+
+ $this->assertResponseOk();
+ $this->assertContentType('application/json');
+
+ $body = (string)$this->_response->getBody();
+ $results = json_decode($body, true);
+
+ $this->assertEmpty($results);
+ }
+
+ /**
+ * @return void
+ */
+ public function testAutocompleteReturnsEmptyForMissingQuery(): void
+ {
+ $this->configRequest(['headers' => ['Accept' => 'application/json']]);
+ $this->get('/autocomplete');
+
+ $this->assertResponseOk();
+ $this->assertContentType('application/json');
+
+ $body = (string)$this->_response->getBody();
+ $results = json_decode($body, true);
+
+ $this->assertEmpty($results);
+ }
+
+ /**
+ * @return void
+ */
+ public function testAutocompleteReturnsEmptyForNoMatch(): void
+ {
+ $this->configRequest(['headers' => ['Accept' => 'application/json']]);
+ $this->get('/autocomplete?q=zzzznonexistent');
+
+ $this->assertResponseOk();
+ $this->assertContentType('application/json');
+
+ $body = (string)$this->_response->getBody();
+ $results = json_decode($body, true);
+
+ $this->assertEmpty($results);
+ }
+
+ /**
+ * @return void
+ */
+ public function testAutocompleteLimitsResults(): void
+ {
+ $this->configRequest(['headers' => ['Accept' => 'application/json']]);
+ $this->get('/autocomplete?q=package');
+
+ $this->assertResponseOk();
+
+ $body = (string)$this->_response->getBody();
+ $results = json_decode($body, true);
+
+ $this->assertLessThanOrEqual(8, count($results));
+ }
}
diff --git a/tests/TestCase/Model/Table/PackagesTableTest.php b/tests/TestCase/Model/Table/PackagesTableTest.php
index eb3d8d1d..90630896 100644
--- a/tests/TestCase/Model/Table/PackagesTableTest.php
+++ b/tests/TestCase/Model/Table/PackagesTableTest.php
@@ -25,6 +25,8 @@ class PackagesTableTest extends TestCase
*/
protected array $fixtures = [
'app.Packages',
+ 'plugin.Tags.Tags',
+ 'plugin.Tags.Tagged',
];
/**
@@ -61,4 +63,117 @@ public function testValidationDefault(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
+
+ /**
+ * Test findAutocomplete returns results matching the package name.
+ *
+ * @return void
+ * @link \App\Model\Table\PackagesTable::findAutocomplete()
+ */
+ public function testFindAutocompleteMatchesPackageName(): void
+ {
+ $results = $this->Packages->find('autocomplete', search: 'asset_compress')->toArray();
+
+ $this->assertNotEmpty($results);
+ $this->assertSame('markstory/asset_compress', $results[0]->package);
+ }
+
+ /**
+ * Test findAutocomplete respects the limit option.
+ *
+ * @return void
+ */
+ public function testFindAutocompleteLimit(): void
+ {
+ $results = $this->Packages->find('autocomplete', search: 'package', maxResults: 3)->toArray();
+
+ $this->assertCount(3, $results);
+ }
+
+ /**
+ * Test findAutocomplete default limit is 8.
+ *
+ * @return void
+ */
+ public function testFindAutocompleteDefaultLimit(): void
+ {
+ $results = $this->Packages->find('autocomplete', search: 'package')->toArray();
+
+ $this->assertLessThanOrEqual(8, count($results));
+ }
+
+ /**
+ * Test findAutocomplete returns empty results for non-matching search.
+ *
+ * @return void
+ */
+ public function testFindAutocompleteNoResults(): void
+ {
+ $results = $this->Packages->find('autocomplete', search: 'zzzznonexistent')->toArray();
+
+ $this->assertEmpty($results);
+ }
+
+ /**
+ * Test findAutocomplete prioritizes name matches over description-only matches.
+ *
+ * "users" appears in cakedc/users package name and also in the description
+ * of dereuromark/cakephp-tools ("useful helpers"). The package with "users"
+ * in its name should rank first even if the other has more downloads.
+ *
+ * @return void
+ */
+ public function testFindAutocompletePrioritizesNameMatch(): void
+ {
+ $results = $this->Packages->find('autocomplete', search: 'users')->toArray();
+
+ $this->assertNotEmpty($results);
+ $this->assertSame('cakedc/users', $results[0]->package);
+ }
+
+ /**
+ * Test findAutocomplete orders by downloads within the same match type.
+ *
+ * @return void
+ */
+ public function testFindAutocompleteOrdersByDownloads(): void
+ {
+ $results = $this->Packages->find('autocomplete', search: 'package')->toArray();
+
+ $count = count($results);
+ $this->assertGreaterThanOrEqual(2, $count);
+ for ($i = 1; $i < $count; $i++) {
+ $this->assertGreaterThanOrEqual(
+ $results[$i]->downloads,
+ $results[$i - 1]->downloads,
+ 'Results should be ordered by downloads descending',
+ );
+ }
+ }
+
+ /**
+ * Test findAutocomplete matches against description field.
+ *
+ * @return void
+ */
+ public function testFindAutocompleteMatchesDescription(): void
+ {
+ $results = $this->Packages->find('autocomplete', search: 'slider')->toArray();
+
+ $this->assertNotEmpty($results);
+ $this->assertSame('markstory/asset_compress', $results[0]->package);
+ }
+
+ /**
+ * Test findAutocomplete contains tags.
+ *
+ * @return void
+ */
+ public function testFindAutocompleteContainsTags(): void
+ {
+ $results = $this->Packages->find('autocomplete', search: 'asset_compress')->toArray();
+
+ $this->assertNotEmpty($results);
+ $this->assertArrayHasKey('tags', $results[0]->toArray());
+ }
}
diff --git a/tests/TestCase/View/Helper/UserHelperTest.php b/tests/TestCase/View/Helper/UserHelperTest.php
new file mode 100644
index 00000000..5e28a9ee
--- /dev/null
+++ b/tests/TestCase/View/Helper/UserHelperTest.php
@@ -0,0 +1,125 @@
+withAttribute('identity', $identity);
+ }
+ $view = new View($request);
+
+ return new UserHelper($view);
+ }
+
+ public function testUsernameLoggedIn(): void
+ {
+ $helper = $this->createHelper(['username' => 'markstory']);
+ $this->assertSame('markstory', $helper->username());
+ }
+
+ public function testUsernameNotLoggedIn(): void
+ {
+ $helper = $this->createHelper();
+ $this->assertNull($helper->username());
+ }
+
+ public function testDisplayNameFullName(): void
+ {
+ $helper = $this->createHelper([
+ 'username' => 'markstory',
+ 'first_name' => 'Mark',
+ 'last_name' => 'Story',
+ ]);
+ $this->assertSame('Mark Story', $helper->displayName());
+ }
+
+ public function testDisplayNameFirstNameOnly(): void
+ {
+ $helper = $this->createHelper([
+ 'username' => 'markstory',
+ 'first_name' => 'Mark',
+ 'last_name' => '',
+ ]);
+ $this->assertSame('Mark', $helper->displayName());
+ }
+
+ public function testDisplayNameFallsBackToUsername(): void
+ {
+ $helper = $this->createHelper([
+ 'username' => 'markstory',
+ 'first_name' => '',
+ 'last_name' => '',
+ ]);
+ $this->assertSame('markstory', $helper->displayName());
+ }
+
+ public function testDisplayNameNotLoggedIn(): void
+ {
+ $helper = $this->createHelper();
+ $this->assertNull($helper->displayName());
+ }
+
+ public function testAvatarUrl(): void
+ {
+ $helper = $this->createHelper(['username' => 'markstory']);
+ $this->assertSame(
+ 'https://github.com/markstory.png?size=80',
+ $helper->avatarUrl(),
+ );
+ }
+
+ public function testAvatarUrlCustomSize(): void
+ {
+ $helper = $this->createHelper(['username' => 'markstory']);
+ $this->assertSame(
+ 'https://github.com/markstory.png?size=200',
+ $helper->avatarUrl(200),
+ );
+ }
+
+ public function testAvatarUrlNotLoggedIn(): void
+ {
+ $helper = $this->createHelper();
+ $this->assertNull($helper->avatarUrl());
+ }
+
+ public function testAvatar(): void
+ {
+ $helper = $this->createHelper(['username' => 'markstory']);
+ $result = $helper->avatar();
+
+ $this->assertStringContainsString('
![]()
assertStringContainsString('https://github.com/markstory.png?size=80', $result);
+ $this->assertStringContainsString('alt="markstory"', $result);
+ $this->assertStringContainsString('loading="lazy"', $result);
+ $this->assertStringContainsString('referrerpolicy="no-referrer"', $result);
+ }
+
+ public function testAvatarCustomAttrs(): void
+ {
+ $helper = $this->createHelper(['username' => 'markstory']);
+ $result = $helper->avatar(80, ['class' => 'w-full', 'alt' => '']);
+
+ $this->assertStringContainsString('class="w-full"', $result);
+ $this->assertStringContainsString('alt=""', $result);
+ }
+
+ public function testAvatarNotLoggedIn(): void
+ {
+ $helper = $this->createHelper();
+ $this->assertNull($helper->avatar());
+ }
+}