New Branch Structure
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class CommonApplicationViewsTest extends TestCase
|
||||
{
|
||||
public function test_common_theme_owns_application_authentication_and_settings_views(): void
|
||||
{
|
||||
foreach ([
|
||||
'stylist-theme-common::components.layouts.app',
|
||||
'stylist-theme-common::components.settings.layout',
|
||||
'stylist-theme-common::partials.settings-heading',
|
||||
'stylist-theme-common::livewire.settings.profile',
|
||||
'stylist-theme-common::livewire.settings.appearance',
|
||||
'stylist-theme-common::livewire.settings.security',
|
||||
'stylist-theme-common::livewire.auth.login',
|
||||
'stylist-theme-common::livewire.auth.register',
|
||||
] as $view) {
|
||||
$this->assertTrue(view()->exists($view), "Expected common view [{$view}] to exist.");
|
||||
}
|
||||
}
|
||||
|
||||
public function test_authentication_and_settings_views_are_not_duplicated(): void
|
||||
{
|
||||
$this->assertDirectoryDoesNotExist(resource_path('views'));
|
||||
$this->assertDirectoryDoesNotExist(resource_path('themes/admin/views/livewire'));
|
||||
$this->assertDirectoryDoesNotExist(resource_path('themes/welcome/views/livewire'));
|
||||
|
||||
$commonViews = resource_path('themes/common/views');
|
||||
|
||||
foreach ([
|
||||
'livewire/auth/login.blade.php',
|
||||
'livewire/auth/register.blade.php',
|
||||
'livewire/auth/forgot-password.blade.php',
|
||||
'livewire/auth/reset-password.blade.php',
|
||||
'livewire/settings/profile.blade.php',
|
||||
'livewire/settings/appearance.blade.php',
|
||||
'livewire/settings/security.blade.php',
|
||||
] as $relativePath) {
|
||||
$matches = collect(File::allFiles(resource_path('themes')))
|
||||
->filter(fn ($file) => str_ends_with(
|
||||
str_replace('\\', '/', $file->getPathname()),
|
||||
'/views/'.$relativePath
|
||||
));
|
||||
|
||||
$this->assertCount(1, $matches, "Expected one authoritative view for [{$relativePath}].");
|
||||
$this->assertStringStartsWith(
|
||||
str_replace('\\', '/', $commonViews),
|
||||
str_replace('\\', '/', $matches->first()->getPathname())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
final class LegacyThemeNamespaceAliasTest extends TestCase
|
||||
{
|
||||
public function test_legacy_theme_namespace_resolves_to_application_theme_directory(): void
|
||||
{
|
||||
$this->assertTrue(view()->exists('stylist-module-theme-admin::dashboard'));
|
||||
$this->assertTrue(view()->exists('stylist-theme-admin::dashboard'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Modules\Stylist\Theme\Stylist;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class StylistModuleTest extends TestCase
|
||||
{
|
||||
public function test_stylist_service_resolves(): void
|
||||
{
|
||||
$this->assertInstanceOf(Stylist::class, app(Stylist::class));
|
||||
}
|
||||
|
||||
public function test_default_theme_resolves(): void
|
||||
{
|
||||
config(['stylist.default_theme' => 'default']);
|
||||
|
||||
$this->assertSame('default', app(Stylist::class)->current()->key());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ThemeAssetControllerTest extends TestCase
|
||||
{
|
||||
public function test_active_theme_css_is_served(): void
|
||||
{
|
||||
config(['stylist.themes_path' => resource_path('themes')]);
|
||||
app(Loader::class)->flush();
|
||||
|
||||
$this->get(route('stylist.theme.asset', [
|
||||
'theme' => 'saasland',
|
||||
'path' => 'css/theme.css',
|
||||
]))->assertOk()->assertHeader('X-Content-Type-Options', 'nosniff');
|
||||
}
|
||||
|
||||
public function test_asset_url_uses_public_themes_path(): void
|
||||
{
|
||||
$url = route('stylist.theme.asset', [
|
||||
'theme' => 'saasland',
|
||||
'path' => 'images/example.jpg',
|
||||
], false);
|
||||
|
||||
$this->assertSame('/themes/saasland/images/example.jpg', $url);
|
||||
}
|
||||
|
||||
public function test_non_asset_theme_files_are_not_served(): void
|
||||
{
|
||||
config(['stylist.themes_path' => resource_path('themes')]);
|
||||
app(Loader::class)->flush();
|
||||
|
||||
$this->get(route('stylist.theme.asset', [
|
||||
'theme' => 'saasland',
|
||||
'path' => 'views/home.blade.php',
|
||||
]))->assertNotFound();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Modules\Stylist\Html\ThemeHtmlBuilder;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ThemeHtmlBuilderTest extends TestCase
|
||||
{
|
||||
public function test_css_variables_are_rendered(): void
|
||||
{
|
||||
config(['stylist.fallback_variables.brand-primary' => '#111827']);
|
||||
|
||||
$html = app(ThemeHtmlBuilder::class)->variables()->toHtml();
|
||||
|
||||
$this->assertStringContainsString('--brand-primary: #111827;', $html);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Modules\Stylist\Diagnostics\ThemeInspector;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Modules\Stylist\View\ThemeViewResolver;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ThemeInheritanceTest extends TestCase
|
||||
{
|
||||
private string $themesPath;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->themesPath = storage_path('framework/testing/stylist-inheritance-'.uniqid());
|
||||
mkdir($this->themesPath, 0777, true);
|
||||
config([
|
||||
'stylist.themes_path' => $this->themesPath,
|
||||
'stylist.default_theme' => 'common',
|
||||
]);
|
||||
app(Loader::class)->flush();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
app(Filesystem::class)->deleteDirectory($this->themesPath);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_child_theme_resolves_views_through_parent_without_copying_them(): void
|
||||
{
|
||||
$this->theme('common', null, ['components/layouts/app.blade.php' => '<main>{{ $slot }}</main>']);
|
||||
$this->theme('admin', 'common', ['dashboard.blade.php' => '<h1>Dashboard</h1>']);
|
||||
app(Loader::class)->flush();
|
||||
|
||||
$candidates = app(ThemeViewResolver::class)->candidatesForTheme('admin', 'components.layouts.app');
|
||||
|
||||
$this->assertSame('stylist-theme-admin::components.layouts.app', $candidates[0]);
|
||||
$this->assertContains('stylist-theme-common::components.layouts.app', $candidates);
|
||||
$this->assertLessThan(
|
||||
array_search('stylist-theme-common::components.layouts.app', $candidates, true),
|
||||
array_search('stylist-theme-admin::components.layouts.app', $candidates, true),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_parent_alias_is_normalised_to_extends(): void
|
||||
{
|
||||
$path = $this->themesPath.'/child';
|
||||
mkdir($path, 0777, true);
|
||||
file_put_contents($path.'/theme.json', json_encode([
|
||||
'name' => 'Child',
|
||||
'parent' => 'common',
|
||||
]));
|
||||
|
||||
$theme = app(Loader::class)->fromPath($path);
|
||||
|
||||
$this->assertSame('common', $theme->parent());
|
||||
}
|
||||
|
||||
public function test_duplicate_view_ownership_is_reported(): void
|
||||
{
|
||||
$this->theme('common', null, ['livewire/auth/login.blade.php' => 'common']);
|
||||
$this->theme('admin', 'common', ['livewire/auth/login.blade.php' => 'admin']);
|
||||
app(Loader::class)->flush();
|
||||
|
||||
$duplicates = app(ThemeInspector::class)->duplicateViews();
|
||||
|
||||
$this->assertArrayHasKey('livewire/auth/login.blade.php', $duplicates);
|
||||
$this->assertEqualsCanonicalizing(['common', 'admin'], array_column($duplicates['livewire/auth/login.blade.php'], 'theme'));
|
||||
}
|
||||
|
||||
/** @param array<string, string> $views */
|
||||
private function theme(string $key, ?string $parent, array $views): void
|
||||
{
|
||||
$path = $this->themesPath.'/'.$key;
|
||||
mkdir($path.'/views', 0777, true);
|
||||
file_put_contents($path.'/theme.json', json_encode(array_filter([
|
||||
'name' => ucfirst($key),
|
||||
'slug' => $key,
|
||||
'extends' => $parent,
|
||||
], fn ($value) => $value !== null)));
|
||||
|
||||
foreach ($views as $relative => $contents) {
|
||||
$file = $path.'/views/'.$relative;
|
||||
mkdir(dirname($file), 0777, true);
|
||||
file_put_contents($file, $contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Modules\Stylist\Theme\Exceptions\InvalidThemeManifestException;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ThemeManifestTest extends TestCase
|
||||
{
|
||||
private string $themesPath;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->themesPath = storage_path('framework/testing/stylist-themes-'.uniqid());
|
||||
mkdir($this->themesPath, 0777, true);
|
||||
config(['stylist.themes_path' => $this->themesPath]);
|
||||
app(Loader::class)->flush();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
app(Filesystem::class)->deleteDirectory($this->themesPath);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_manifest_is_normalised(): void
|
||||
{
|
||||
$path = $this->themesPath.'/sample';
|
||||
mkdir($path.'/css', 0777, true);
|
||||
file_put_contents($path.'/theme.json', json_encode(['name' => 'Sample']));
|
||||
file_put_contents($path.'/css/theme.css', 'body{}');
|
||||
|
||||
$theme = app(Loader::class)->fromPath($path);
|
||||
|
||||
$this->assertSame('sample', $theme->key());
|
||||
$this->assertSame('css/theme.css', $theme->stylesheet());
|
||||
$this->assertSame('js/app.js', $theme->script());
|
||||
$this->assertSame('home', $theme->entryView());
|
||||
}
|
||||
|
||||
public function test_unsafe_manifest_path_is_rejected(): void
|
||||
{
|
||||
$path = $this->themesPath.'/unsafe';
|
||||
mkdir($path, 0777, true);
|
||||
file_put_contents($path.'/theme.json', json_encode([
|
||||
'name' => 'Unsafe',
|
||||
'stylesheet' => '../secret.css',
|
||||
]));
|
||||
|
||||
$this->expectException(InvalidThemeManifestException::class);
|
||||
app(Loader::class)->fromPath($path);
|
||||
}
|
||||
public function test_phase_one_manifest_optional_capabilities_are_normalised(): void
|
||||
{
|
||||
$path = $this->themesPath.'/design-gallery';
|
||||
mkdir($path.'/css', 0777, true);
|
||||
mkdir($path.'/js', 0777, true);
|
||||
file_put_contents($path.'/theme.json', json_encode([
|
||||
'key' => 'design-gallery',
|
||||
'name' => 'Design Gallery',
|
||||
'stylesheet' => 'css/app.css',
|
||||
'script' => 'js/app.js',
|
||||
'appearances' => ['light', 'dark', 'system'],
|
||||
'accents' => ['blue', 'emerald', 'neutral'],
|
||||
'default_appearance' => 'system',
|
||||
'default_accent' => 'neutral',
|
||||
// Phase 1 incorrectly duplicated accents here; Stylist must not
|
||||
// reject the whole theme because of that compact legacy list.
|
||||
'variants' => ['blue', 'emerald', 'neutral'],
|
||||
]));
|
||||
file_put_contents($path.'/css/app.css', 'body{}');
|
||||
file_put_contents($path.'/js/app.js', '');
|
||||
|
||||
$theme = app(Loader::class)->fromPath($path);
|
||||
|
||||
$this->assertSame(['light', 'dark', 'system'], array_keys($theme->appearances()));
|
||||
$this->assertSame(['blue', 'emerald', 'neutral'], array_keys($theme->accents()));
|
||||
$this->assertSame([], $theme->variants());
|
||||
$this->assertSame('system', $theme->defaultAppearance());
|
||||
$this->assertSame('neutral', $theme->defaultAccent());
|
||||
}
|
||||
|
||||
public function test_legacy_manifest_does_not_gain_appearance_or_accent_capabilities(): void
|
||||
{
|
||||
$path = $this->themesPath.'/legacy';
|
||||
mkdir($path.'/css', 0777, true);
|
||||
file_put_contents($path.'/theme.json', json_encode(['name' => 'Legacy']));
|
||||
file_put_contents($path.'/css/theme.css', 'body{}');
|
||||
|
||||
$theme = app(Loader::class)->fromPath($path);
|
||||
|
||||
$this->assertSame([], $theme->appearances());
|
||||
$this->assertSame([], $theme->accents());
|
||||
$this->assertSame('', $theme->defaultAppearance());
|
||||
$this->assertSame('', $theme->defaultAccent());
|
||||
}
|
||||
|
||||
public function test_defaults_must_reference_declared_optional_capabilities(): void
|
||||
{
|
||||
$path = $this->themesPath.'/invalid-default';
|
||||
mkdir($path, 0777, true);
|
||||
file_put_contents($path.'/theme.json', json_encode([
|
||||
'name' => 'Invalid Default',
|
||||
'appearances' => ['light', 'dark'],
|
||||
'default_appearance' => 'system',
|
||||
]));
|
||||
|
||||
$this->expectException(InvalidThemeManifestException::class);
|
||||
app(Loader::class)->fromPath($path);
|
||||
}
|
||||
|
||||
public function test_manifest_version_and_capabilities_are_normalised(): void
|
||||
{
|
||||
$path = $this->themesPath.'/capable';
|
||||
mkdir($path.'/css', 0777, true);
|
||||
mkdir($path.'/js', 0777, true);
|
||||
mkdir($path.'/views', 0777, true);
|
||||
file_put_contents($path.'/theme.json', json_encode([
|
||||
'manifest_version' => 1,
|
||||
'name' => 'Capable',
|
||||
'appearances' => ['light', 'dark'],
|
||||
'accents' => ['blue'],
|
||||
'capabilities' => ['marketplace' => true],
|
||||
]));
|
||||
file_put_contents($path.'/css/theme.css', 'body{}');
|
||||
file_put_contents($path.'/js/app.js', '');
|
||||
file_put_contents($path.'/views/home.blade.php', '<div>Capable</div>');
|
||||
|
||||
$theme = app(Loader::class)->fromPath($path);
|
||||
|
||||
$this->assertSame(1, $theme->manifestVersion());
|
||||
$this->assertTrue($theme->hasCapability('appearances'));
|
||||
$this->assertTrue($theme->hasCapability('accents'));
|
||||
$this->assertTrue($theme->hasCapability('marketplace'));
|
||||
$this->assertFalse($theme->hasCapability('preview'));
|
||||
}
|
||||
|
||||
public function test_unknown_manifest_fields_are_preserved_without_preventing_loading(): void
|
||||
{
|
||||
$path = $this->themesPath.'/future';
|
||||
mkdir($path.'/css', 0777, true);
|
||||
file_put_contents($path.'/theme.json', json_encode([
|
||||
'name' => 'Future',
|
||||
'future_builder_metadata' => ['anything' => true],
|
||||
]));
|
||||
file_put_contents($path.'/css/theme.css', 'body{}');
|
||||
|
||||
$theme = app(Loader::class)->fromPath($path);
|
||||
|
||||
$this->assertSame(['anything' => true], $theme->meta()['future_builder_metadata']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Modules\Stylist\Services\ThemePackageManager;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Tests\TestCase;
|
||||
use ZipArchive;
|
||||
|
||||
final class ThemePackageRoundTripTest extends TestCase
|
||||
{
|
||||
private string $themesPath;
|
||||
private string $workspace;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->themesPath = storage_path('framework/testing/stylist-installed-'.uniqid());
|
||||
$this->workspace = storage_path('framework/testing/stylist-package-'.uniqid());
|
||||
mkdir($this->themesPath, 0777, true);
|
||||
mkdir($this->workspace, 0777, true);
|
||||
config(['stylist.themes_path' => $this->themesPath]);
|
||||
app(Loader::class)->flush();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
app(Filesystem::class)->deleteDirectory($this->themesPath);
|
||||
app(Filesystem::class)->deleteDirectory($this->workspace);
|
||||
app(Loader::class)->flush();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_generated_theme_zip_installs_discovers_and_renders(): void
|
||||
{
|
||||
$source = $this->workspace.'/design-gallery';
|
||||
mkdir($source.'/css', 0777, true);
|
||||
mkdir($source.'/js', 0777, true);
|
||||
mkdir($source.'/views', 0777, true);
|
||||
|
||||
file_put_contents($source.'/theme.json', json_encode([
|
||||
'manifest_version' => 1,
|
||||
'key' => 'design-gallery',
|
||||
'name' => 'Design Gallery',
|
||||
'stylesheet' => 'css/app.css',
|
||||
'script' => 'js/app.js',
|
||||
'entry_view' => 'home',
|
||||
'appearances' => ['light', 'dark', 'system'],
|
||||
'accents' => ['blue', 'neutral'],
|
||||
'default_appearance' => 'system',
|
||||
'default_accent' => 'neutral',
|
||||
]));
|
||||
file_put_contents($source.'/css/app.css', 'body{}');
|
||||
file_put_contents($source.'/js/app.js', '');
|
||||
file_put_contents($source.'/views/home.blade.php', '<main>Design Gallery works</main>');
|
||||
|
||||
$zipPath = $this->workspace.'/design-gallery.zip';
|
||||
$zip = new ZipArchive();
|
||||
$this->assertTrue($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE));
|
||||
foreach (app(Filesystem::class)->allFiles($source) as $file) {
|
||||
$zip->addFile($file->getPathname(), 'design-gallery/'.$file->getRelativePathname());
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
$upload = new UploadedFile($zipPath, 'design-gallery.zip', 'application/zip', null, true);
|
||||
$theme = app(ThemePackageManager::class)->install($upload);
|
||||
|
||||
$this->assertSame('design-gallery', $theme->key());
|
||||
$this->assertTrue(app(Loader::class)->all()->has('design-gallery'));
|
||||
$this->assertStringContainsString(
|
||||
'Design Gallery works',
|
||||
stylist_theme_view('design-gallery', 'home')->render(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Modules\Stylist\View\ThemeViewResolver;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ThemeViewResolverTest extends TestCase
|
||||
{
|
||||
public function test_theme_view_candidates_prefer_active_theme_then_default(): void
|
||||
{
|
||||
config([
|
||||
'stylist.default_theme' => 'default',
|
||||
'stylist.themes_path' => resource_path('themes'),
|
||||
]);
|
||||
|
||||
$candidates = app(ThemeViewResolver::class)->candidates('components.page-hero');
|
||||
|
||||
$this->assertContains('stylist-theme-default::components.page-hero', $candidates);
|
||||
$this->assertContains('stylist::components.page-hero', $candidates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
final class WelcomeThemeViewTest extends TestCase
|
||||
{
|
||||
public function test_bundled_welcome_theme_view_namespace_is_registered(): void
|
||||
{
|
||||
$this->assertTrue(view()->exists('stylist-theme-welcome::welcome'));
|
||||
}
|
||||
|
||||
public function test_home_route_renders_the_welcome_theme(): void
|
||||
{
|
||||
$this->get('/')
|
||||
->assertOk()
|
||||
->assertViewIs('stylist-theme-welcome::welcome');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user