runtimeMigratedComponentFiles() as $file) { $content = $this->readProjectFile($file); $this->assertStringNotContainsString('DOMContentLoaded', $content, $file); $this->assertStringNotContainsString("if (document.readyState === 'loading')", $content, $file); } } public function testMigratedComponentsExposeDestroyCleanupPaths(): void { $sidebar = $this->readProjectFile('web/js/components/app-toggle-sidebar.js'); $aside = $this->readProjectFile('web/js/components/app-toggle-aside-panels.js'); $search = $this->readProjectFile('web/js/components/app-global-search.js'); $bookmarkSave = $this->readProjectFile('modules/bookmarks/web/js/components/app-bookmark-save.js'); $bookmarkPanel = $this->readProjectFile('modules/bookmarks/web/js/components/app-bookmark-panel.js'); $multiSelect = $this->readProjectFile('web/js/components/app-multiselect-init.js'); $this->assertStringContainsString('destroy: () => {', $sidebar); $this->assertStringContainsString("cleanupFns.push(() => document.removeEventListener('keydown', onShortcut));", $sidebar); $this->assertStringContainsString("cleanupFns.push(() => mobileQuery.removeEventListener('change', onViewportChange));", $sidebar); $this->assertStringContainsString("cleanupFns.push(() => document.removeEventListener('keydown', onEscDrawer));", $sidebar); $this->assertStringContainsString('destroy: () => {', $aside); $this->assertStringContainsString("cleanupFns.push(() => document.removeEventListener('keydown', onKeyDown));", $aside); $this->assertStringContainsString('const destroy = () => {', $search); $this->assertStringContainsString("document.removeEventListener('keydown', onDocumentKeydown);", $search); $this->assertStringContainsString('const destroy = () => {', $bookmarkSave); $this->assertStringContainsString('abortController.abort();', $bookmarkSave); $this->assertStringContainsString('const destroy = () => {', $bookmarkPanel); $this->assertStringContainsString('abortController.abort();', $bookmarkPanel); $this->assertStringNotContainsString('export const init =', $bookmarkSave); $this->assertStringNotContainsString('export const init =', $bookmarkPanel); $this->assertStringContainsString('return {', $multiSelect); $this->assertStringContainsString('destroy: () => {', $multiSelect); } public function testRuntimeStorageContractUsesSharedUiStorageHelper(): void { $files = [ 'web/js/components/app-toggle-sidebar.js', 'web/js/components/app-toggle-aside-panels.js', 'web/js/components/app-toggle-details-aside.js', 'web/js/components/app-contrast-toggle.js', 'web/js/components/app-tabs.js', 'web/js/core/app-details-open-state.js', 'web/js/core/app-grid-factory.js', ]; foreach ($files as $file) { $content = $this->readProjectFile($file); $this->assertStringNotContainsString('window.localStorage', $content, $file); $this->assertStringContainsString('createUiStorage', $content, $file); } } public function testNoCustomWindowGlobalsRemainInWebJs(): void { $violations = $this->findPatternMatchesInFiles( 'web/js', '/window\.(AppSidebar|AppAsidePanels|requestFsLightboxRefresh|__fsLightboxRefreshPending|APP_ASSET_BASE|APP_BASE)\b/', ['js'] ); $this->assertSame([], $violations, 'Custom window globals found in web/js: ' . implode(', ', $violations)); } public function testHostBoundRuntimeComponentsAreRootStrictWithoutDocumentFallbackQuery(): void { $files = [ 'web/js/components/app-global-search.js', 'modules/bookmarks/web/js/components/app-bookmark-panel.js', 'web/js/components/app-tabs.js', ]; foreach ($files as $file) { $content = $this->readProjectFile($file); $this->assertDoesNotMatchRegularExpression('/\|\|\s*document\.querySelector/', $content, $file); } } public function testDomBindingUtilitiesExposeDestroyLifecycle(): void { $activeFilterChips = $this->readProjectFile('web/js/components/app-active-filter-chips.js'); $multiselectCascade = $this->readProjectFile('web/js/components/app-multiselect-cascade.js'); $this->assertStringContainsString('return { render, destroy };', $activeFilterChips); $this->assertStringContainsString('return {', $multiselectCascade); $this->assertStringContainsString('destroy: () => {', $multiselectCascade); } }