Files
breadcrumb-the-shire/tests/Architecture/PageTemplateNamingTest.php
fs 21c331cd06 test(architecture): lock down two routing/filter silent-failure modes
Encodes two bugs that recently cost debugging time as architecture tests so
they cannot recur.

PageTemplateNamingTest
  MintyPHP's Router splits page filenames on parens. `view(default).phtml` →
  view="view", template="default". A nested group like `view($id)(none).phtml`
  produces an empty view name, resolves to the wrong file, and the response
  silently becomes an empty body. URL parameters belong only in the .php
  action filename. This test flags any .phtml with more than one paren group.

FilterSchemaConsistencyTest
  Every query key declared in a list's filter-schema.php must also appear in
  the toolbar section (standard pagination/sort keys exempted). If a filter
  param is not in the toolbar, Grid.js's URL-sync drops it on every refetch —
  the filter works on initial page load and breaks after any user interaction.
  This test caught one dormant violation (system-audit's target_type), which
  is now declared as a hidden toolbar field for future use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:37:26 +02:00

87 lines
3.2 KiB
PHP

<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
/**
* MintyPHP's Router parses page filenames by splitting on parentheses. URL
* parameters belong ONLY in the `.php` action filename (e.g. `view($id).php`);
* the `.phtml` view filename must contain exactly ONE paren group for the
* template name (`view(default).phtml` or `view(none).phtml`).
*
* Nested paren groups like `view($id)(none).phtml` cause the router's regex
* split to produce an empty view name — the file silently resolves to the
* wrong path and the response is an empty body with no visible error.
*
* Reference: vendor/mintyphp/core/src/Router.php::extractParts()
*/
class PageTemplateNamingTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testPhtmlFilenamesHaveExactlyOneParenGroup(): void
{
$root = $this->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: `<view>(<template>).phtml`.
if ($openCount !== 1) {
$violations[] = sprintf(
'%s — found %d paren groups, expected exactly 1 (template only). '
. 'URL parameters belong in the .php action filename, not the .phtml view. '
. "Nested paren groups break MintyPHP's Router silently (empty response).",
$relativePath,
$openCount
);
}
}
}
$this->assertSame(
[],
$violations,
".phtml filename convention violations:\n" . implode("\n", $violations)
);
}
}