111 lines
4.0 KiB
PHP
111 lines
4.0 KiB
PHP
|
|
<?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);
|
||
|
|
}
|
||
|
|
}
|