diff options
Diffstat (limited to 'tests/Inc/ModuleTest.php')
| -rw-r--r-- | tests/Inc/ModuleTest.php | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/tests/Inc/ModuleTest.php b/tests/Inc/ModuleTest.php new file mode 100644 index 00000000..05791022 --- /dev/null +++ b/tests/Inc/ModuleTest.php @@ -0,0 +1,188 @@ +<?php + +use PHPUnit\Framework\TestCase; + +/** + * Tests for the global Module loader include (inc/module.inc.php). + * + * These tests exercise dependency resolution, activation/autoloading, page loading, + * and the various query helpers (getEnabled/getAll/getActivated/getDependencies,...). + * + * We create temporary test modules under ./modules with minimal config.json, + * page.inc.php, and inc/*.inc.php files, then clean them up in tearDown(). + * + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + */ +class ModuleTest extends TestCase +{ + private string $baseDir; + private array $created = []; + + protected function setUp(): void + { + ErrorHandler::reset(); + $this->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', + "<?php\nclass Page_TestDep extends Page { protected function doPreprocess(){} protected function doRender(){} }\n"); + // A helper class to be autoloaded only when TestMain is activated + $this->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', + "<?php\nclass Page_TestMain extends Page { protected function doPreprocess(){} protected function doRender(){} }\n"); + $this->writeFile('modules/TestMain/inc/abchelper.inc.php', + "<?php\nclass AbcHelper { public static function id(){ return 123; } }\n"); + // Provide optional assets that getCss/getScripts may include when activated + $this->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', + "<?php\nclass Page_BadMain extends Page { protected function doPreprocess(){} protected function doRender(){} }\n"); + } + + protected function tearDown(): void + { + // Remove created test modules + // Iterate in reverse to delete children before parents + foreach (array_reverse($this->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', + "<?php\nclass Page_Dep2 extends Page { protected function doPreprocess(){} protected function doRender(){} }\n"); + // Overwrite TestDep config to depend on dep2 + $this->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); + } +} |
