mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
perf(admin): infinite-scroll the contenus langues/mots-clés tables
The contenus page loaded both lookup tables in full via HTMX on page load: 760 tag rows + 216 language rows, ~2.4MB of HTML including ~1960 inline SVG icons and per-row CSRF forms. The DB queries were already fast (~6ms); the cost was pure client-side payload and DOM. - Add paged lookups: getTagsPage/getLanguagesPage + countTagsWithCount/ countLanguagesWithCount, and optional limit/offset on the existing unpaged variants (backward compatible). - Fragments serve 25 rows per request. A sentinel <tr> with hx-trigger="intersect once root:#<wrap>" appends the next page when it scrolls into the table's own scroll container (htmx 'revealed' only checks the window viewport, not nested scrollers). - Search forms still swap the whole wrapper and reset to offset 0. - Style the load-more row; add PagedLanguagesTagsTest coverage. Initial DOM for the page drops from ~2.4MB to ~160KB; scrolling reaches the full totals (736 tags, 216 languages) and stops cleanly.
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* PagedLanguagesTagsTest — covers the paged lookups that back the admin
|
||||
* contenus page "load more" infinite scroll (getTagsPage / getLanguagesPage
|
||||
* plus their count helpers and the optional limit/offset on the unpaged
|
||||
* variants).
|
||||
*/
|
||||
class PagedLanguagesTagsTest extends TestCase
|
||||
{
|
||||
private Database $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
TestDatabase::resetData();
|
||||
$this->db = TestDatabase::getInstance();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Languages are seeded by the schema and not cleared by resetData(),
|
||||
// so remove the test-specific rows we inserted.
|
||||
TestDatabase::getPDO()
|
||||
->exec("DELETE FROM languages WHERE name LIKE 'PagLang%'");
|
||||
}
|
||||
|
||||
private function seedTags(array $names): void
|
||||
{
|
||||
$pdo = TestDatabase::getPDO();
|
||||
$stmt = $pdo->prepare('INSERT INTO tags (name) VALUES (?)');
|
||||
foreach ($names as $n) {
|
||||
$stmt->execute([$n]);
|
||||
}
|
||||
}
|
||||
|
||||
private function seedLanguages(array $names): void
|
||||
{
|
||||
$pdo = TestDatabase::getPDO();
|
||||
$stmt = $pdo->prepare('INSERT INTO languages (name) VALUES (?)');
|
||||
foreach ($names as $n) {
|
||||
$stmt->execute([$n]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tags ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testGetTagsPageRespectsLimitAndOffset(): void
|
||||
{
|
||||
$this->seedTags(['alpha', 'bravo', 'charlie', 'delta', 'echo']);
|
||||
|
||||
$page1 = $this->db->getTagsPage('', 2, 0);
|
||||
$page2 = $this->db->getTagsPage('', 2, 2);
|
||||
$page3 = $this->db->getTagsPage('', 2, 4);
|
||||
|
||||
$this->assertSame(['alpha', 'bravo'], array_column($page1, 'name'));
|
||||
$this->assertSame(['charlie', 'delta'], array_column($page2, 'name'));
|
||||
$this->assertSame(['echo'], array_column($page3, 'name'));
|
||||
}
|
||||
|
||||
public function testGetTagsPageFiltersByPrefix(): void
|
||||
{
|
||||
$this->seedTags(['alpha', 'alpine', 'bravo']);
|
||||
|
||||
$rows = $this->db->getTagsPage('al', 100, 0);
|
||||
|
||||
$this->assertSame(['alpha', 'alpine'], array_column($rows, 'name'));
|
||||
}
|
||||
|
||||
public function testCountTagsWithCount(): void
|
||||
{
|
||||
$this->seedTags(['alpha', 'alpine', 'bravo']);
|
||||
|
||||
$this->assertSame(3, $this->db->countTagsWithCount(''));
|
||||
$this->assertSame(2, $this->db->countTagsWithCount('al'));
|
||||
$this->assertSame(0, $this->db->countTagsWithCount('zzz'));
|
||||
}
|
||||
|
||||
public function testCountTagsIgnoresSoftDeleted(): void
|
||||
{
|
||||
$this->seedTags(['alpha', 'bravo']);
|
||||
$id = (int) TestDatabase::getPDO()
|
||||
->query("SELECT id FROM tags WHERE name = 'bravo'")->fetchColumn();
|
||||
$this->db->deleteTag($id);
|
||||
|
||||
$this->assertSame(1, $this->db->countTagsWithCount(''));
|
||||
$this->assertSame(['alpha'], array_column($this->db->getTagsPage('', 100, 0), 'name'));
|
||||
}
|
||||
|
||||
public function testGetAllTagsWithCountOptionalPaging(): void
|
||||
{
|
||||
$this->seedTags(['alpha', 'bravo', 'charlie']);
|
||||
|
||||
$this->assertCount(3, $this->db->getAllTagsWithCount());
|
||||
$this->assertSame(
|
||||
['alpha', 'bravo'],
|
||||
array_column($this->db->getAllTagsWithCount(2, 0), 'name')
|
||||
);
|
||||
$this->assertSame(
|
||||
['charlie'],
|
||||
array_column($this->db->getAllTagsWithCount(2, 2), 'name')
|
||||
);
|
||||
}
|
||||
|
||||
// ── Languages ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The schema seeds a few languages, so language tests must not assume an
|
||||
* empty table. Query a unique prefix ("PagLang") and account for the
|
||||
* seeded rows in the unpaged assertions.
|
||||
*/
|
||||
public function testGetLanguagesPageRespectsLimitAndOffset(): void
|
||||
{
|
||||
$this->seedLanguages(['PagLang A', 'PagLang B', 'PagLang C', 'PagLang D']);
|
||||
|
||||
$this->assertSame(
|
||||
['PagLang A', 'PagLang B'],
|
||||
array_column($this->db->getLanguagesPage('PagLang', 2, 0), 'name')
|
||||
);
|
||||
$this->assertSame(
|
||||
['PagLang C', 'PagLang D'],
|
||||
array_column($this->db->getLanguagesPage('PagLang', 2, 2), 'name')
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetLanguagesPageDeduplicatesCaseInsensitively(): void
|
||||
{
|
||||
$this->seedLanguages(['PagLangDup', 'paglanguedup']);
|
||||
|
||||
// Grouped by LOWER(name): two distinct lowercase names.
|
||||
$names = array_column($this->db->getLanguagesPage('PagLang', 100, 0), 'name');
|
||||
$lowered = array_map('strtolower', $names);
|
||||
|
||||
$this->assertSame(['paglangdup', 'paglanguedup'], $lowered);
|
||||
$this->assertSame(2, $this->db->countLanguagesWithCount('PagLang'));
|
||||
}
|
||||
|
||||
public function testGetLanguagesPageFiltersCaseInsensitively(): void
|
||||
{
|
||||
$this->seedLanguages(['PagLangX', 'PagLangY']);
|
||||
|
||||
// Lowercase query must still match the capitalised names.
|
||||
$rows = $this->db->getLanguagesPage('pagl', 100, 0);
|
||||
|
||||
$this->assertSame(['PagLangX', 'PagLangY'], array_column($rows, 'name'));
|
||||
}
|
||||
|
||||
public function testCountLanguagesWithCount(): void
|
||||
{
|
||||
$this->seedLanguages(['PagLangA', 'PagLangB', 'PagLangC']);
|
||||
|
||||
$this->assertSame(3, $this->db->countLanguagesWithCount('PagLang'));
|
||||
$this->assertSame(1, $this->db->countLanguagesWithCount('PagLangA'));
|
||||
$this->assertSame(0, $this->db->countLanguagesWithCount('zzz'));
|
||||
}
|
||||
|
||||
public function testGetAllLanguagesWithCountOptionalPaging(): void
|
||||
{
|
||||
$this->seedLanguages(['PagLangA', 'PagLangB', 'PagLangC']);
|
||||
|
||||
$unpaged = array_map('strtolower', array_column($this->db->getAllLanguagesWithCount(), 'name'));
|
||||
$this->assertContains('paglanga', $unpaged);
|
||||
$this->assertContains('paglangb', $unpaged);
|
||||
$this->assertContains('paglangc', $unpaged);
|
||||
|
||||
$page1 = array_column($this->db->getAllLanguagesWithCount(2, 0), 'name');
|
||||
$page2 = array_column($this->db->getAllLanguagesWithCount(2, 2), 'name');
|
||||
$this->assertCount(2, $page1);
|
||||
$this->assertCount(2, $page2);
|
||||
// Pages must not overlap.
|
||||
$this->assertSame([], array_intersect($page1, $page2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user