findPatternMatchesInFiles('web/js', '/window\.alert\s*\(/', ['js']); foreach ($this->moduleJsDirectories() as $moduleJsDir) { $violations = array_merge($violations, $this->findPatternMatchesInFiles($moduleJsDir, '/window\.alert\s*\(/', ['js'])); } $violations = array_values(array_unique($violations)); sort($violations); $this->assertSame([], $violations, "window.alert usage found in frontend JS scope:\n" . implode("\n", $violations)); } public function testFrontendJsDoesNotUseDomReadyAutoInitPatterns(): void { $violations = $this->findPatternMatchesInFiles('web/js', '/DOMContentLoaded|document\.readyState/', ['js']); foreach ($this->moduleJsDirectories() as $moduleJsDir) { $violations = array_merge($violations, $this->findPatternMatchesInFiles($moduleJsDir, '/DOMContentLoaded|document\.readyState/', ['js'])); } $violations = array_values(array_unique($violations)); sort($violations); $this->assertSame([], $violations, "DOM-ready auto-init pattern found in frontend 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 testFrontendJsUsesCentralHttpLayer(): void { $violations = $this->findDirectFetchCallsInFrontendScope(); $this->assertSame([], $violations, "Direct fetch(...) found outside approved HTTP infrastructure:\n" . implode("\n", $violations)); } public function testModuleImportPolicyForCoreAndSiblingModules(): void { $root = $this->projectRootPath(); $violations = []; foreach ($this->moduleJsDirectories() as $moduleJsDir) { $base = $root . '/' . $moduleJsDir; $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base, \FilesystemIterator::SKIP_DOTS)); /** @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, "Module 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 */ 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', ]; } /** * @return list */ private function moduleJsDirectories(): array { $root = $this->projectRootPath(); $modulesRoot = $root . '/modules'; $this->assertDirectoryExists($modulesRoot, 'Modules root not found.'); $dirs = []; $entries = scandir($modulesRoot); $this->assertNotFalse($entries, 'Could not read modules directory.'); foreach ($entries as $entry) { if ($entry === '.' || $entry === '..') { continue; } $jsPath = $modulesRoot . '/' . $entry . '/web/js'; if (is_dir($jsPath)) { $dirs[] = 'modules/' . $entry . '/web/js'; } } sort($dirs); return $dirs; } /** * @return list */ private function findDirectFetchCallsInFrontendScope(): array { $root = $this->projectRootPath(); $allowed = [ 'web/js/core/app-http.js', 'web/js/core/app-telemetry.js', ]; $scanRoots = array_merge(['web/js'], $this->moduleJsDirectories()); $violations = []; foreach ($scanRoots as $scanRoot) { $base = $root . '/' . $scanRoot; $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base, \FilesystemIterator::SKIP_DOTS)); /** @var \SplFileInfo $file */ foreach ($iterator as $file) { if (!$file->isFile() || $file->getExtension() !== 'js') { continue; } $relativePath = str_replace($root . '/', '', $file->getPathname()); if (in_array($relativePath, $allowed, true)) { continue; } $content = file_get_contents($file->getPathname()); $this->assertNotFalse($content, 'Could not read file: ' . $file->getPathname()); if (preg_match('/\bfetch\s*\(/', $content) === 1) { $violations[] = $relativePath; } } } $violations = array_values(array_unique($violations)); sort($violations); return $violations; } }