feat(js): add app-http contracts and migrate helpdesk runtime layer

This commit is contained in:
2026-04-20 22:31:13 +02:00
parent ec2f03e0cf
commit f189ef9df6
34 changed files with 2023 additions and 1134 deletions

View File

@@ -0,0 +1,116 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
final class FrontendJsContractsTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testNoWindowAlertCallsInCoreAndHelpdeskJs(): void
{
$violations = array_merge(
$this->findPatternMatchesInFiles('web/js', '/window\.alert\s*\(/', ['js']),
$this->findPatternMatchesInFiles('modules/helpdesk/web/js', '/window\.alert\s*\(/', ['js'])
);
$violations = array_values(array_unique($violations));
sort($violations);
$this->assertSame([], $violations, "window.alert usage found in JS scope:\n" . implode("\n", $violations));
}
public function testHelpdeskRuntimeComponentsDoNotAutoInitialize(): void
{
foreach ($this->helpdeskRuntimeComponentFiles() as $file) {
$content = $this->readProjectFile($file);
$this->assertStringNotContainsString('DOMContentLoaded', $content, $file);
$this->assertStringNotContainsString("if (document.readyState === 'loading')", $content, $file);
$this->assertStringNotContainsString('const container = document.querySelector', $content, $file);
}
}
public function testHelpdeskRuntimeComponentsExposeDestroyLifecycle(): void
{
foreach ($this->helpdeskRuntimeComponentFiles() as $file) {
$content = $this->readProjectFile($file);
$this->assertStringContainsString('destroy:', $content, $file);
}
}
public function testHelpdeskJsUsesCentralHttpLayer(): void
{
$violations = $this->findPatternMatchesInFiles('modules/helpdesk/web/js', '/\bfetch\s*\(/', ['js']);
$this->assertSame([], $violations, "Direct fetch(...) found in helpdesk JS:\n" . implode("\n", $violations));
}
public function testHelpdeskImportPolicyForCoreAndSiblingModules(): void
{
$root = $this->projectRootPath();
$base = $root . '/modules/helpdesk/web/js';
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base, \FilesystemIterator::SKIP_DOTS));
$violations = [];
/** @var \SplFileInfo $file */
foreach ($iterator as $file) {
if (!$file->isFile() || $file->getExtension() !== 'js') {
continue;
}
$content = file_get_contents($file->getPathname());
$this->assertNotFalse($content, 'Could not read file: ' . $file->getPathname());
$relativePath = str_replace($root . '/', '', $file->getPathname());
preg_match_all('/^\s*import\s+(?:[^\"\']+\s+from\s+)?[\"\']([^\"\']+)[\"\']/m', $content, $matches);
foreach ($matches[1] as $importPathRaw) {
$importPath = (string) $importPathRaw;
if (str_starts_with($importPath, '/js/')) {
continue;
}
if (str_starts_with($importPath, './') || str_starts_with($importPath, '../')) {
if (preg_match('#(?:^|/)js/(core|components|pages)/#', $importPath) === 1) {
$violations[] = $relativePath . ' => relative core import: ' . $importPath;
}
continue;
}
$violations[] = $relativePath . ' => invalid import path: ' . $importPath;
}
}
$violations = array_values(array_unique($violations));
sort($violations);
$this->assertSame([], $violations, "Helpdesk import policy violations:\n" . implode("\n", $violations));
}
public function testLookupFieldComponentExportsRuntimeInitializerWithoutAutoInit(): void
{
$content = $this->readProjectFile('web/js/components/app-lookup-field.js');
$this->assertStringContainsString('export function initLookupFields', $content);
$this->assertStringContainsString('destroy:', $content);
$this->assertStringNotContainsString('DOMContentLoaded', $content);
}
/**
* @return list<string>
*/
private function helpdeskRuntimeComponentFiles(): array
{
return [
'modules/helpdesk/web/js/helpdesk-detail.js',
'modules/helpdesk/web/js/helpdesk-domain-detail.js',
'modules/helpdesk/web/js/helpdesk-ticket.js',
'modules/helpdesk/web/js/helpdesk-team.js',
'modules/helpdesk/web/js/helpdesk-risk-radar.js',
'modules/helpdesk/web/js/helpdesk-settings.js',
'modules/helpdesk/web/js/handover-domain-select.js',
'modules/helpdesk/web/js/handover-schema-editor.js',
];
}
}

View File

@@ -32,8 +32,17 @@ trait FrontendRuntimeContractSupport
'web/js/components/app-auto-submit.js',
'modules/bookmarks/web/js/components/app-bookmark-save.js',
'modules/bookmarks/web/js/components/app-bookmark-panel.js',
'web/js/components/app-lookup-field.js',
'web/js/components/app-password-toggle.js',
'web/js/components/app-page-editor.js',
'modules/helpdesk/web/js/helpdesk-detail.js',
'modules/helpdesk/web/js/helpdesk-domain-detail.js',
'modules/helpdesk/web/js/helpdesk-ticket.js',
'modules/helpdesk/web/js/helpdesk-team.js',
'modules/helpdesk/web/js/helpdesk-risk-radar.js',
'modules/helpdesk/web/js/helpdesk-settings.js',
'modules/helpdesk/web/js/handover-domain-select.js',
'modules/helpdesk/web/js/handover-schema-editor.js',
];
}
}

View File

@@ -19,7 +19,11 @@ class ListTemplateContractTest extends TestCase
$usesPartial || str_contains($content, 'renderGridFilterToolbar('),
$file . ' does not render toolbar directly and does not include shared list-filters partial.'
);
$this->assertStringContainsString('initStandardListPage(', $moduleContent, $file);
$this->assertTrue(
str_contains($moduleContent, 'initStandardListPage(')
|| str_contains($moduleContent, 'createListPageModule('),
$file . ' does not initialize list module via initStandardListPage/createListPageModule.'
);
$this->assertStringNotContainsString('gridFiltersFromSchema(', $content, $file);
$this->assertSame(0, preg_match('/<select id=\"[^\"]*-filter\"/', $content), $file);
$this->assertSame(0, preg_match('/<input[^>]*id=\"[^\"]*-filter\"/', $content), $file);