summaryrefslogtreecommitdiffstats
path: root/tests/Inc/ModuleTest.php
blob: 057910226f71325150dea2f6fdbcf82ba7de5213 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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);
	}
}