diff --git a/modules/audit/pages/audit/system-audit/filter-schema.php b/modules/audit/pages/audit/system-audit/filter-schema.php index 7d8bfe8..416bda1 100644 --- a/modules/audit/pages/audit/system-audit/filter-schema.php +++ b/modules/audit/pages/audit/system-audit/filter-schema.php @@ -46,6 +46,14 @@ return gridFilterSchema([ 'dir' => ['type' => 'dir', 'default' => 'desc'], ], 'toolbar' => [ + [ + // Dormant capability — kept in toolbar as hidden so Grid.js preserves + // the param across refetches if/when a UI is added. + 'key' => 'target_type', + 'type' => 'hidden', + 'input_id' => 'system-audit-target-type-filter', + 'default' => '', + ], [ 'key' => 'search', 'type' => 'text', diff --git a/tests/Architecture/FilterSchemaConsistencyTest.php b/tests/Architecture/FilterSchemaConsistencyTest.php new file mode 100644 index 0000000..cfc8268 --- /dev/null +++ b/tests/Architecture/FilterSchemaConsistencyTest.php @@ -0,0 +1,110 @@ +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 &$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); + } +} diff --git a/tests/Architecture/PageTemplateNamingTest.php b/tests/Architecture/PageTemplateNamingTest.php new file mode 100644 index 0000000..3c15e79 --- /dev/null +++ b/tests/Architecture/PageTemplateNamingTest.php @@ -0,0 +1,86 @@ +projectRootPath(); + $scanRoots = [$root . '/pages']; + $modulesDir = $root . '/modules'; + if (is_dir($modulesDir)) { + foreach (scandir($modulesDir) ?: [] as $moduleEntry) { + if ($moduleEntry === '.' || $moduleEntry === '..') { + continue; + } + $modulePages = $modulesDir . '/' . $moduleEntry . '/pages'; + if (is_dir($modulePages)) { + $scanRoots[] = $modulePages; + } + } + } + + $violations = []; + + foreach ($scanRoots as $scanRoot) { + if (!is_dir($scanRoot)) { + continue; + } + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($scanRoot, \FilesystemIterator::SKIP_DOTS)); + /** @var \SplFileInfo $file */ + foreach ($iterator as $file) { + if (!$file->isFile() || $file->getExtension() !== 'phtml') { + continue; + } + $name = $file->getFilename(); + $openCount = substr_count($name, '('); + $closeCount = substr_count($name, ')'); + $relativePath = str_replace($root . '/', '', $file->getPathname()); + + if ($openCount !== $closeCount) { + $violations[] = sprintf('%s — mismatched parens', $relativePath); + continue; + } + + // Partials (`_form.phtml`, shared includes) have zero paren groups — skip them. + if ($openCount === 0) { + continue; + } + + // Routed views must have exactly one paren group: `(