Skip to content

Commit 120ee38

Browse files
committed
Theme Modules: Added testing coverage
1 parent cd84074 commit 120ee38

File tree

2 files changed

+237
-12
lines changed

2 files changed

+237
-12
lines changed

app/Theming/ThemeModule.php

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@
66

77
class ThemeModule
88
{
9-
protected string $name;
10-
protected string $description;
11-
protected string $folderName;
12-
protected string $version;
9+
public function __construct(
10+
public readonly string $name,
11+
public readonly string $description,
12+
public readonly string $folderName,
13+
public readonly string $version,
14+
) {
15+
}
1316

1417
/**
1518
* Create a ThemeModule instance from JSON data.
1619
*
1720
* @throws ThemeException
1821
*/
19-
public static function fromJson(array $data, string $folderName): static
22+
public static function fromJson(array $data, string $folderName): self
2023
{
2124
if (empty($data['name']) || !is_string($data['name'])) {
2225
throw new ThemeException("Module in folder \"{$folderName}\" is missing a valid 'name' property");
@@ -34,13 +37,12 @@ public static function fromJson(array $data, string $folderName): static
3437
throw new ThemeException("Module in folder \"{$folderName}\" has an invalid 'version' format. Expected semantic version format like '1.0.0' or 'v1.0.0'");
3538
}
3639

37-
$module = new static();
38-
$module->name = $data['name'];
39-
$module->description = $data['description'];
40-
$module->folderName = $folderName;
41-
$module->version = $data['version'];
42-
43-
return $module;
40+
return new self(
41+
name: $data['name'],
42+
description: $data['description'],
43+
folderName: $folderName,
44+
version: $data['version'],
45+
);
4446
}
4547

4648
/**

tests/Theme/ThemeModuleTests.php

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
<?php
2+
3+
namespace Tests\Theme;
4+
5+
use BookStack\Facades\Theme;
6+
use Tests\TestCase;
7+
8+
class ThemeModuleTests extends TestCase
9+
{
10+
public function test_modules_loaded_on_theme_load()
11+
{
12+
$this->usingThemeFolder(function ($themeFolder) {
13+
$a = theme_path('modules/a');
14+
$b = theme_path('modules/b');
15+
mkdir($a, 0777, true);
16+
mkdir($b, 0777, true);
17+
18+
file_put_contents($a . '/bookstack-module.json', json_encode([
19+
'name' => 'Module A',
20+
'description' => 'This is module A',
21+
'version' => '1.0.0',
22+
]));
23+
file_put_contents($b . '/bookstack-module.json', json_encode([
24+
'name' => 'Module B',
25+
'description' => 'This is module B',
26+
'version' => 'v0.5.0',
27+
]));
28+
29+
$this->refreshApplication();
30+
31+
$modules = Theme::getModules();
32+
$this->assertCount(2, $modules);
33+
34+
$moduleA = $modules['a'];
35+
$this->assertEquals('Module A', $moduleA->name);
36+
$this->assertEquals('This is module A', $moduleA->description);
37+
$this->assertEquals('1.0.0', $moduleA->version);
38+
});
39+
}
40+
41+
public function test_module_not_loaded_if_no_bookstack_module_json()
42+
{
43+
$this->usingThemeFolder(function ($themeFolder) {
44+
$moduleDir = theme_path('/modules/a');
45+
mkdir($moduleDir, 0777, true);
46+
file_put_contents($moduleDir . '/module.json', '{}');
47+
$this->refreshApplication();
48+
$modules = Theme::getModules();
49+
$this->assertCount(0, $modules);
50+
});
51+
}
52+
53+
public function test_language_text_overridable_via_module()
54+
{
55+
$this->usingModuleFolder(function (string $moduleFolderPath) {
56+
$translationPath = $moduleFolderPath . '/lang/en';
57+
mkdir($translationPath, 0777, true);
58+
file_put_contents($translationPath . '/entities.php', '<?php return ["books" => "SuperBeans"];');
59+
$this->refreshApplication();
60+
61+
$this->asAdmin()->get('/books')->assertSee('SuperBeans');
62+
});
63+
}
64+
65+
public function test_language_files_merge_with_theme_files_with_theme_taking_precedence()
66+
{
67+
$this->usingModuleFolder(function (string $moduleFolderPath) {
68+
$moduleTranslationPath = $moduleFolderPath . '/lang/en';
69+
mkdir($moduleTranslationPath, 0777, true);
70+
file_put_contents($moduleTranslationPath . '/entities.php', '<?php return ["books" => "SuperBeans", "recently_viewed" => "ViewedBiscuits"];');
71+
72+
$themeTranslationPath = theme_path('lang/en');
73+
mkdir($themeTranslationPath, 0777, true);
74+
file_put_contents($themeTranslationPath . '/entities.php', '<?php return ["books" => "WonderBeans"];');
75+
$this->refreshApplication();
76+
77+
$this->asAdmin()->get('/books')
78+
->assertSee('WonderBeans')
79+
->assertDontSee('SuperBeans')
80+
->assertSee('ViewedBiscuits');
81+
});
82+
}
83+
84+
public function test_view_files_overridable_from_module()
85+
{
86+
$this->usingModuleFolder(function (string $moduleFolderPath) {
87+
$viewsFolder = $moduleFolderPath . '/views/layouts/parts';
88+
mkdir($viewsFolder, 0777, true);
89+
file_put_contents($viewsFolder . '/header.blade.php', 'My custom header that says badgerriffic');
90+
$this->refreshApplication();
91+
$this->asAdmin()->get('/')->assertSee('badgerriffic');
92+
});
93+
}
94+
95+
public function test_theme_view_files_take_precedence_over_module_view_files()
96+
{
97+
$this->usingModuleFolder(function (string $moduleFolderPath) {
98+
$viewsFolder = $moduleFolderPath . '/views/layouts/parts';
99+
mkdir($viewsFolder, 0777, true);
100+
file_put_contents($viewsFolder . '/header.blade.php', 'My custom header that says badgerriffic');
101+
102+
$themeViewsFolder = theme_path('layouts/parts');
103+
mkdir($themeViewsFolder, 0777, true);
104+
file_put_contents($themeViewsFolder . '/header.blade.php', 'My theme header that says awesomeferrets');
105+
106+
$this->refreshApplication();
107+
$this->asAdmin()->get('/')
108+
->assertDontSee('badgerriffic')
109+
->assertSee('awesomeferrets');
110+
});
111+
}
112+
113+
public function test_theme_and_modules_views_can_be_used_at_the_same_time()
114+
{
115+
$this->usingModuleFolder(function (string $moduleFolderPath) {
116+
$viewsFolder = $moduleFolderPath . '/views/layouts/parts';
117+
mkdir($viewsFolder, 0777, true);
118+
file_put_contents($viewsFolder . '/base-body-start.blade.php', 'My custom header that says badgerriffic');
119+
120+
$themeViewsFolder = theme_path('layouts/parts');
121+
mkdir($themeViewsFolder, 0777, true);
122+
file_put_contents($themeViewsFolder . '/base-body-end.blade.php', 'My theme header that says awesomeferrets');
123+
124+
$this->refreshApplication();
125+
$this->asAdmin()->get('/')
126+
->assertSee('badgerriffic')
127+
->assertSee('awesomeferrets');
128+
});
129+
}
130+
131+
public function test_icons_can_be_overridden_from_module()
132+
{
133+
$this->usingModuleFolder(function (string $moduleFolderPath) {
134+
$iconsFolder = $moduleFolderPath . '/icons';
135+
mkdir($iconsFolder, 0777, true);
136+
file_put_contents($iconsFolder . '/books.svg', '<svg><path d="supericonpath"/></svg>');
137+
$this->refreshApplication();
138+
139+
$this->asAdmin()->get('/')->assertSee('supericonpath', false);
140+
});
141+
}
142+
143+
public function test_theme_icons_take_precedence_over_module_icons()
144+
{
145+
$this->usingModuleFolder(function (string $moduleFolderPath) {
146+
$iconsFolder = $moduleFolderPath . '/icons';
147+
mkdir($iconsFolder, 0777, true);
148+
file_put_contents($iconsFolder . '/books.svg', '<svg><path d="supericonpath"/></svg>');
149+
$this->refreshApplication();
150+
151+
$themeViewsFolder = theme_path('icons');
152+
mkdir($themeViewsFolder, 0777, true);
153+
file_put_contents($themeViewsFolder . '/books.svg', '<svg><path d="wackyiconpath"/></svg>');
154+
155+
156+
$this->asAdmin()->get('/')
157+
->assertSee('wackyiconpath', false)
158+
->assertDontSee('supericonpath', false);
159+
});
160+
}
161+
162+
public function test_public_folder_can_be_provided_from_module()
163+
{
164+
$this->usingModuleFolder(function (string $moduleFolderPath) {
165+
$publicFolder = $moduleFolderPath . '/public';
166+
mkdir($publicFolder, 0777, true);
167+
$themeName = basename(dirname(dirname($moduleFolderPath)));
168+
file_put_contents($publicFolder . '/test.txt', 'hellofrominsidethisfileimaghostwoooo!');
169+
$this->refreshApplication();
170+
171+
$resp = $this->asAdmin()->get("/theme/{$themeName}/test.txt")->streamedContent();
172+
$this->assertEquals('hellofrominsidethisfileimaghostwoooo!', $resp);
173+
});
174+
}
175+
176+
public function test_theme_public_files_take_precedence_over_modules()
177+
{
178+
$this->usingModuleFolder(function (string $moduleFolderPath) {
179+
$publicFolder = $moduleFolderPath . '/public';
180+
mkdir($publicFolder, 0777, true);
181+
$themeName = basename(theme_path());
182+
file_put_contents($publicFolder . '/test.txt', 'hellofrominsidethisfileimaghostwoooo!');
183+
184+
$themePublicFolder = theme_path('public');
185+
mkdir($themePublicFolder, 0777, true);
186+
file_put_contents($themePublicFolder . '/test.txt', 'imadifferentghostinsidethetheme,woooooo!');
187+
188+
$this->refreshApplication();
189+
190+
$resp = $this->asAdmin()->get("/theme/{$themeName}/test.txt")->streamedContent();
191+
$this->assertEquals('imadifferentghostinsidethetheme,woooooo!', $resp);
192+
});
193+
}
194+
195+
public function test_logical_functions_file_loaded_from_module_and_it_runs_alongside_theme_functions()
196+
{
197+
$this->usingModuleFolder(function (string $moduleFolderPath) {
198+
file_put_contents($moduleFolderPath . '/functions.php', "<?php\nTheme::listen(\BookStack\Theming\ThemeEvents::APP_BOOT, function(\$app) { \$app->alias('cat', 'dog');});");
199+
200+
$themeFunctionsFile = theme_path('functions.php');
201+
file_put_contents($themeFunctionsFile, "<?php\nTheme::listen(\BookStack\Theming\ThemeEvents::APP_BOOT, function(\$app) { \$app->alias('beans', 'cheese');});");
202+
203+
$this->refreshApplication();
204+
205+
$this->assertEquals('cat', $this->app->getAlias('dog'));
206+
$this->assertEquals('beans', $this->app->getAlias('cheese'));
207+
});
208+
}
209+
210+
protected function usingModuleFolder(callable $callback): void
211+
{
212+
$this->usingThemeFolder(function (string $themeFolder) use ($callback) {
213+
$moduleFolderPath = theme_path('modules/test-module');
214+
mkdir($moduleFolderPath, 0777, true);
215+
file_put_contents($moduleFolderPath . '/bookstack-module.json', json_encode([
216+
'name' => 'Test Module',
217+
'description' => 'This is a test module',
218+
'version' => 'v1.0.0',
219+
]));
220+
$callback($moduleFolderPath);
221+
});
222+
}
223+
}

0 commit comments

Comments
 (0)