baseDir = getcwd() . '/modules'; // Ensure a clean environment: the Module registry initializes lazily in Module::init(), // and we haven't referenced Module yet in this process. $this->makeModule('TestDep', [ 'dependencies' => [], 'client-plugin' => false, 'category' => 'utilities', ]); $this->writeFile('modules/TestDep/page.inc.php', "makeModule('TestMain', [ 'dependencies' => ['testdep'], // must reference the lowercase id key 'client-plugin' => true, 'category' => 'main', 'css' => [], 'scripts' => [], 'collapse' => true, ]); $this->writeFile('modules/TestMain/page.inc.php', "writeFile('modules/TestMain/inc/abchelper.inc.php', "writeFile('modules/TestMain/style.css', '/* css */'); $this->writeFile('modules/TestMain/clientscript.js', '// js'); // A module with a missing dependency to test failure path $this->makeModule('BadMain', [ 'dependencies' => ['no_such_dep'] ]); $this->writeFile('modules/BadMain/page.inc.php', "created) as $path) { if (is_file($path)) { @unlink($path); } elseif (is_dir($path)) { @rmdir($path); } } $this->created = []; } private function makeModule(string $name, array $config): void { $dir = $this->baseDir . '/' . $name; $inc = $dir . '/inc'; if (!is_dir($dir)) { mkdir($dir, 0777, true); $this->created[] = $dir; } if (!is_dir($inc)) { mkdir($inc, 0777, true); $this->created[] = $inc; } $this->writeFile($dir . '/config.json', json_encode($config)); } private function writeFile(string $path, string $content): void { file_put_contents($path, $content); $this->created[] = $path; } public function testInitAndGetAndActivationAndNewPage(): void { // Load module list from filesystem require_once 'inc/module.inc.php'; Module::init(); // Resolve/activate and check availability $this->assertTrue(Module::isAvailable('testmain', true)); $main = Module::get('testmain'); $this->assertNotFalse($main); $this->assertSame('TestMain', $main->getIdentifier()); // getDisplayName falls back to !!name!! without dictionary entry $this->assertSame('!!TestMain!!', $main->getDisplayName()); // Category helpers $this->assertSame('main', $main->getCategory()); $this->assertSame('Cat:main', $main->getCategoryName()); $this->assertTrue($main->doCollapse()); $this->assertSame('modules/TestMain', $main->getDir()); // Test autoloader from module/inc $this->assertTrue(class_exists('AbcHelper'), 'Autoloader should expose AbcHelper from module/inc'); $this->assertSame(123, AbcHelper::id()); // Page loader $page = $main->newPage(); $this->assertInstanceOf(Page::class, $page); $this->assertInstanceOf(Page_TestMain::class, $page); // Assets only reported when directly activated and client-plugin true $css = $main->getCss(); $js = $main->getScripts(); $this->assertContains('style.css', $css); $this->assertContains('clientscript.js', $js); } public function testDependencyResolutionAndQueries(): void { require_once 'inc/module.inc.php'; Module::init(); // BadMain is not available due to missing dep // The Module loader triggers a user-level warning in this case; expect it so the test doesn't fail set_error_handler(function ($errno, $errstr, $errfile, $errline) { if ($errstr === 'Disabling module BadMain: Dependency no_such_dep failed.') return; throw new ErrorException($errstr, 0, $errno, $errfile, $errline); }); $this->assertFalse(Module::isAvailable('badmain')); $this->assertFalse(Module::get('badmain')); // TestDep alone is available $this->assertTrue(Module::isAvailable('testdep')); $dep = Module::get('testdep'); $this->assertNotFalse($dep); // Enabled list contains both TestDep and TestMain when we activate main Module::isAvailable('testmain', true); $enabled = Module::getEnabled(true); $ids = array_map(function($m){ return $m->getIdentifier(); }, $enabled); $this->assertContains('TestDep', $ids); $this->assertContains('TestMain', $ids); // All includes even those with missing deps (after resolution attempt) $all = Module::getAll(); $allIds = array_map(function($m){ return $m->getIdentifier(); }, $all); $this->assertContains('TestDep', $allIds); $this->assertContains('TestMain', $allIds); $this->assertContains('BadMain', $allIds); // present but unusable // Activated contains modules with an activated depth marker $activated = Module::getActivated(); $this->assertNotEmpty($activated); $actVals = array_values($activated); $this->assertContainsOnlyInstancesOf(Module::class, $actVals); } public function testTransitiveDependenciesListed(): void { // Create Dep2 and make TestDep depend on it; then init fresh process registry $this->makeModule('Dep2', [ 'dependencies' => [] ]); $this->writeFile('modules/Dep2/page.inc.php', "writeFile('modules/TestDep/config.json', json_encode(['dependencies' => ['dep2']])); require_once 'inc/module.inc.php'; Module::init(); // Activate main (which depends on testdep which depends on dep2) Module::isAvailable('testmain', true); $main = Module::get('testmain'); $list = $main->getDependencies(); // Should include transitive dependency $this->assertContains('testdep', $list); $this->assertContains('dep2', $list); } }