Files
breadcrumb-the-shire/tests/Architecture/FilterSchemaConsistencyTest.php

111 lines
4.0 KiB
PHP
Raw Normal View History

<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
/**
* Every query key declared in a list's `filter-schema.php` must also appear
* in the `toolbar` section (either as a visible field or hidden). Reason:
*
* - Backend reads filter values via `gridParseFiltersFromSchemaFile()`, which
* parses against the `query` schema.
* - Frontend's Grid.js URL-sync only forwards params that are declared in the
* `toolbar` section (derived via `gridSchemaClientFilters()`). If a query
* param is not in `toolbar`, Grid.js drops it on every subsequent request
* (pagination, sort, filter change) the filter appears to work on page
* load but silently breaks after any user interaction.
*
* This test ran us into real production pain before it existed: the address
* book's tenant-register tabs worked on first load but lost the filter on
* every grid refetch. Declaring `tenant` as a hidden toolbar field fixed it.
* This test prevents repeat occurrences.
*
* Exempted keys (framework-provided, always preserved by Grid.js):
* limit, offset, order, dir
*/
class FilterSchemaConsistencyTest extends TestCase
{
use ProjectFileAssertionSupport;
private const FRAMEWORK_KEYS = ['limit', 'offset', 'order', 'dir'];
public function testEveryQueryKeyHasToolbarDeclaration(): void
{
$root = $this->projectRootPath();
$schemaFiles = [];
$this->collectFilterSchemas($root . '/pages', $schemaFiles);
$this->collectFilterSchemas($root . '/modules', $schemaFiles);
$this->assertNotEmpty($schemaFiles, 'No filter-schema.php files found — regex stale?');
$violations = [];
foreach ($schemaFiles as $schemaFile) {
$schema = @include $schemaFile;
if (!is_array($schema)) {
$violations[] = sprintf('%s — filter-schema.php did not return an array', $this->relative($schemaFile, $root));
continue;
}
$queryKeys = array_keys(is_array($schema['query'] ?? null) ? $schema['query'] : []);
$toolbarFields = is_array($schema['toolbar'] ?? null) ? $schema['toolbar'] : [];
// Schemas without a toolbar are for non-Grid endpoints (e.g. global
// search data endpoint consumed by a custom JS dialog). Skip those —
// they never participate in Grid.js URL sync.
if ($toolbarFields === []) {
continue;
}
$toolbarKeys = [];
foreach ($toolbarFields as $field) {
if (!is_array($field)) {
continue;
}
$key = trim((string) ($field['key'] ?? ''));
if ($key !== '') {
$toolbarKeys[] = $key;
}
}
$missing = array_diff($queryKeys, $toolbarKeys, self::FRAMEWORK_KEYS);
if ($missing) {
$violations[] = sprintf(
"%s — query keys missing from toolbar: %s.\n Add them to the 'toolbar' section (type 'hidden' is fine) so Grid.js forwards them on refetch.",
$this->relative($schemaFile, $root),
implode(', ', $missing)
);
}
}
$this->assertSame(
[],
$violations,
"Filter-schema consistency violations:\n" . implode("\n\n", $violations)
);
}
/**
* @param list<string> &$out
*/
private function collectFilterSchemas(string $base, array &$out): void
{
if (!is_dir($base)) {
return;
}
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base, \FilesystemIterator::SKIP_DOTS));
/** @var \SplFileInfo $file */
foreach ($iterator as $file) {
if ($file->isFile() && $file->getFilename() === 'filter-schema.php') {
$out[] = $file->getPathname();
}
}
}
private function relative(string $path, string $root): string
{
return str_replace($root . '/', '', $path);
}
}