diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index fcd5f12..cdfc9d2 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -311,7 +311,7 @@ class ImageGenerationService public static function getDeviceSpecificDefaultImage(Device $device, string $imageType): ?string { // Validate image type - if (! in_array($imageType, ['setup-logo', 'sleep', 'error'])) { + if (! in_array($imageType, ['setup-logo', 'sleep'])) { return null; } @@ -345,10 +345,10 @@ class ImageGenerationService /** * Generate a default screen image from Blade template */ - public static function generateDefaultScreenImage(Device $device, string $imageType, ?string $pluginName = null): string + public static function generateDefaultScreenImage(Device $device, string $imageType): string { // Validate image type - if (! in_array($imageType, ['setup-logo', 'sleep', 'error'])) { + if (! in_array($imageType, ['setup-logo', 'sleep'])) { throw new InvalidArgumentException("Invalid image type: {$imageType}"); } @@ -365,7 +365,7 @@ class ImageGenerationService $outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension); // Generate HTML from Blade template - $html = self::generateDefaultScreenHtml($device, $imageType, $pluginName); + $html = self::generateDefaultScreenHtml($device, $imageType); // Create custom Browsershot instance if using AWS Lambda $browsershotInstance = null; @@ -445,13 +445,12 @@ class ImageGenerationService /** * Generate HTML from Blade template for default screens */ - private static function generateDefaultScreenHtml(Device $device, string $imageType, ?string $pluginName = null): string + private static function generateDefaultScreenHtml(Device $device, string $imageType): string { // Map image type to template name $templateName = match ($imageType) { 'setup-logo' => 'default-screens.setup', 'sleep' => 'default-screens.sleep', - 'error' => 'default-screens.error', default => throw new InvalidArgumentException("Invalid image type: {$imageType}") }; @@ -462,22 +461,14 @@ class ImageGenerationService $scaleLevel = $device->scaleLevel(); $darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode - // Build view data - $viewData = [ + // Render the Blade template + return view($templateName, [ 'noBleed' => false, 'darkMode' => $darkMode, 'deviceVariant' => $deviceVariant, 'deviceOrientation' => $deviceOrientation, 'colorDepth' => $colorDepth, 'scaleLevel' => $scaleLevel, - ]; - - // Add plugin name for error screens - if ($imageType === 'error' && $pluginName !== null) { - $viewData['pluginName'] = $pluginName; - } - - // Render the Blade template - return view($templateName, $viewData)->render(); + ])->render(); } } diff --git a/resources/views/default-screens/error.blade.php b/resources/views/default-screens/error.blade.php deleted file mode 100644 index be8063a..0000000 --- a/resources/views/default-screens/error.blade.php +++ /dev/null @@ -1,23 +0,0 @@ -@props([ - 'noBleed' => false, - 'darkMode' => false, - 'deviceVariant' => 'og', - 'deviceOrientation' => null, - 'colorDepth' => '1bit', - 'scaleLevel' => null, - 'pluginName' => 'Recipe', -]) - - - - - - Error on {{ $pluginName }} - Unable to render content. Please check server logs. - - - - - diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php index 7257ab0..83a34fc 100644 --- a/resources/views/livewire/catalog/index.blade.php +++ b/resources/views/livewire/catalog/index.blade.php @@ -1,24 +1,20 @@ filter(function ($plugin) use ($currentVersion) { // Check if Laravel compatibility is true - if (! Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) { + if (!Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) { return false; } @@ -85,9 +81,8 @@ class extends Component }) ->sortBy('name') ->toArray(); - } catch (Exception $e) { - Log::error('Failed to load catalog from URL: '.$e->getMessage()); - + } catch (\Exception $e) { + Log::error('Failed to load catalog from URL: ' . $e->getMessage()); return []; } }); @@ -99,9 +94,8 @@ class extends Component $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); - if (! $plugin || ! $plugin['zip_url']) { + if (!$plugin || !$plugin['zip_url']) { $this->addError('installation', 'Plugin not found or no download URL available.'); - return; } @@ -119,8 +113,8 @@ class extends Component $this->dispatch('plugin-installed'); Flux::modal('import-from-catalog')->close(); - } catch (Exception $e) { - $this->addError('installation', 'Error installing plugin: '.$e->getMessage()); + } catch (\Exception $e) { + $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); } finally { $this->installingPlugin = ''; } @@ -130,27 +124,32 @@ class extends Component { $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); - if (! $plugin) { + if (!$plugin) { $this->addError('preview', 'Plugin not found.'); - return; } $this->previewingPlugin = $pluginId; $this->previewData = $plugin; + + // Store scroll position for restoration later + $this->dispatch('store-scroll-position'); } public function closePreview(): void { $this->previewingPlugin = ''; $this->previewData = []; + + // Restore scroll position when returning to catalog + $this->dispatch('restore-scroll-position'); } }; ?>
@if(empty($catalogPlugins))
- + No plugins available Catalog is empty
@@ -161,30 +160,30 @@ class extends Component @enderror @foreach($catalogPlugins as $plugin) -
+
@if($plugin['logo_url']) {{ $plugin['name'] }} @else -
- +
+
@endif
- {{ $plugin['name'] }} +

{{ $plugin['name'] }}

@if ($plugin['github']) - by {{ $plugin['github'] }} +

by {{ $plugin['github'] }}

@endif
@if($plugin['license']) - {{ $plugin['license'] }} + {{ $plugin['license'] }} @endif @if($plugin['repo_url']) - + @endif @@ -192,7 +191,7 @@ class extends Component
@if($plugin['description']) - {{ $plugin['description'] }} +

{{ $plugin['description'] }}

@endif
@@ -202,16 +201,14 @@ class extends Component Install - @if($plugin['screenshot_url']) - - - Preview - - - @endif + + + Preview + + @@ -239,20 +236,34 @@ class extends Component
-
- Preview of {{ $previewData['name'] }} -
- - @if($previewData['description']) -
- Description - {{ $previewData['description'] }} + @if($previewData['screenshot_url']) +
+ Preview of {{ $previewData['name'] }} +
+ @elseif($previewData['logo_url']) +
+ {{ $previewData['name'] }} logo +

No preview image available

+
+ @else +
+ +

No preview available

@endif -
+ @if($previewData['description']) +
+

Description

+

{{ $previewData['description'] }}

+
+ @endif + +
+ +@script + +@endscript diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index 9ecad1a..1b5dd50 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -1,28 +1,20 @@ page; - $response = Cache::remember($cacheKey, 43200, function () { + $this->recipes = Cache::remember('trmnl_recipes_newest', 43200, function () { $response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [ 'sort-by' => 'newest', - 'page' => $this->page, ]); - if (! $response->successful()) { - throw new RuntimeException('Failed to fetch TRMNL recipes'); + if (!$response->successful()) { + throw new \RuntimeException('Failed to fetch TRMNL recipes'); } - return $response->json(); + $json = $response->json(); + $data = $json['data'] ?? []; + return $this->mapRecipes($data); }); - - $data = $response['data'] ?? []; - $mapped = $this->mapRecipes($data); - - if ($this->page === 1) { - $this->recipes = $mapped; - } else { - $this->recipes = array_merge($this->recipes, $mapped); - } - - $this->hasMore = ! empty($response['next_page_url']); - } catch (Throwable $e) { - Log::error('TRMNL catalog load error: '.$e->getMessage()); - if ($this->page === 1) { - $this->recipes = []; - } - $this->hasMore = false; + } catch (\Throwable $e) { + Log::error('TRMNL catalog load error: ' . $e->getMessage()); + $this->recipes = []; } } @@ -84,65 +62,38 @@ class extends Component { $this->isSearching = true; try { - $cacheKey = 'trmnl_recipes_search_'.md5($term).'_page_'.$this->page; - $response = Cache::remember($cacheKey, 300, function () use ($term) { + $cacheKey = 'trmnl_recipes_search_' . md5($term); + $this->recipes = Cache::remember($cacheKey, 300, function () use ($term) { $response = Http::get('https://usetrmnl.com/recipes.json', [ 'search' => $term, 'sort-by' => 'newest', - 'page' => $this->page, ]); - if (! $response->successful()) { - throw new RuntimeException('Failed to search TRMNL recipes'); + if (!$response->successful()) { + throw new \RuntimeException('Failed to search TRMNL recipes'); } - return $response->json(); + $json = $response->json(); + $data = $json['data'] ?? []; + return $this->mapRecipes($data); }); - - $data = $response['data'] ?? []; - $mapped = $this->mapRecipes($data); - - if ($this->page === 1) { - $this->recipes = $mapped; - } else { - $this->recipes = array_merge($this->recipes, $mapped); - } - - $this->hasMore = ! empty($response['next_page_url']); - } catch (Throwable $e) { - Log::error('TRMNL catalog search error: '.$e->getMessage()); - if ($this->page === 1) { - $this->recipes = []; - } - $this->hasMore = false; + } catch (\Throwable $e) { + Log::error('TRMNL catalog search error: ' . $e->getMessage()); + $this->recipes = []; } finally { $this->isSearching = false; } } - public function loadMore(): void - { - $this->page++; - - $term = mb_trim($this->search); - if ($term === '' || mb_strlen($term) < 2) { - $this->loadNewest(); - } else { - $this->searchRecipes($term); - } - } - public function updatedSearch(): void { - $this->page = 1; - $term = mb_trim($this->search); + $term = trim($this->search); if ($term === '') { $this->loadNewest(); - return; } - if (mb_strlen($term) < 2) { + if (strlen($term) < 2) { // Require at least 2 chars to avoid noisy calls return; } @@ -170,78 +121,62 @@ class extends Component $this->dispatch('plugin-installed'); Flux::modal('import-from-trmnl-catalog')->close(); - } catch (Exception $e) { - Log::error('Plugin installation failed: '.$e->getMessage()); - $this->addError('installation', 'Error installing plugin: '.$e->getMessage()); + } catch (\Exception $e) { + Log::error('Plugin installation failed: ' . $e->getMessage()); + $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); } } public function previewRecipe(string $recipeId): void { + $recipe = collect($this->recipes)->firstWhere('id', $recipeId); + + if (!$recipe) { + $this->addError('preview', 'Recipe not found.'); + return; + } + $this->previewingRecipe = $recipeId; + $this->previewData = $recipe; + + // Store scroll position for restoration later + $this->dispatch('store-scroll-position'); + } + + public function closePreview(): void + { + $this->previewingRecipe = ''; $this->previewData = []; - try { - $response = Http::timeout(10)->get("https://usetrmnl.com/recipes/{$recipeId}.json"); - - if ($response->successful()) { - $item = $response->json()['data'] ?? []; - $this->previewData = $this->mapRecipe($item); - } else { - // Fallback to searching for the specific recipe if single endpoint doesn't exist - $response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [ - 'search' => $recipeId, - ]); - - if ($response->successful()) { - $data = $response->json()['data'] ?? []; - $item = collect($data)->firstWhere('id', $recipeId); - if ($item) { - $this->previewData = $this->mapRecipe($item); - } - } - } - } catch (Throwable $e) { - Log::error('TRMNL catalog preview fetch error: '.$e->getMessage()); - } - - if (empty($this->previewData)) { - $this->previewData = collect($this->recipes)->firstWhere('id', $recipeId) ?? []; - } + // Restore scroll position when returning to catalog + $this->dispatch('restore-scroll-position'); } /** - * @param array> $items + * @param array> $items * @return array> */ private function mapRecipes(array $items): array { return collect($items) - ->map(fn (array $item) => $this->mapRecipe($item)) + ->map(function (array $item) { + return [ + 'id' => $item['id'] ?? null, + 'name' => $item['name'] ?? 'Untitled', + 'icon_url' => $item['icon_url'] ?? null, + 'screenshot_url' => $item['screenshot_url'] ?? null, + 'author_bio' => is_array($item['author_bio'] ?? null) + ? strip_tags($item['author_bio']['description'] ?? null) + : null, + 'stats' => [ + 'installs' => data_get($item, 'stats.installs'), + 'forks' => data_get($item, 'stats.forks'), + ], + 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/' . $item['id']) : null, + ]; + }) ->toArray(); } - - /** - * @param array $item - * @return array - */ - private function mapRecipe(array $item): array - { - return [ - 'id' => $item['id'] ?? null, - 'name' => $item['name'] ?? 'Untitled', - 'icon_url' => $item['icon_url'] ?? null, - 'screenshot_url' => $item['screenshot_url'] ?? null, - 'author_bio' => is_array($item['author_bio'] ?? null) - ? strip_tags($item['author_bio']['description'] ?? null) - : null, - 'stats' => [ - 'installs' => data_get($item, 'stats.installs'), - 'forks' => data_get($item, 'stats.forks'), - ], - 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/'.$item['id']) : null, - ]; - } }; ?>
@@ -253,7 +188,7 @@ class extends Component icon="magnifying-glass" />
- Newest + Newest
@error('installation') @@ -262,22 +197,22 @@ class extends Component @if(empty($recipes))
- + No recipes found Try a different search term
@else
@foreach($recipes as $recipe) -
+
@php($thumb = $recipe['icon_url'] ?? $recipe['screenshot_url']) @if($thumb) {{ $recipe['name'] }} @else -
- +
+
@endif @@ -286,12 +221,12 @@ class extends Component
{{ $recipe['name'] }} @if(data_get($recipe, 'stats.installs')) - Installs: {{ data_get($recipe, 'stats.installs') }} · Forks: {{ data_get($recipe, 'stats.forks') }} + Installs: {{ data_get($recipe, 'stats.installs') }} · Forks: {{ data_get($recipe, 'stats.forks') }} @endif
@if($recipe['detail_url']) - + @endif @@ -311,7 +246,7 @@ class extends Component @endif - @if($recipe['id'] && ($recipe['screenshot_url'] ?? null)) + @if($recipe['id']) @endforeach
- - @if($hasMore) -
- - Load next page - Loading... - -
- @endif @endif -
-
- - Fetching recipe details... + @if($previewingRecipe && !empty($previewData)) +
+ Preview {{ $previewData['name'] ?? 'Recipe' }}
-
-
- @if($previewingRecipe && !empty($previewData)) -
- Preview {{ $previewData['name'] ?? 'Recipe' }} -
- -
+
+ @if($previewData['screenshot_url'])
Preview of {{ $previewData['name'] }}
- - @if($previewData['author_bio']) -
-
- Description - {{ $previewData['author_bio'] }} -
-
- @endif - - @if(data_get($previewData, 'stats.installs')) -
-
- Statistics - - Installs: {{ data_get($previewData, 'stats.installs') }} · - Forks: {{ data_get($previewData, 'stats.forks') }} - -
-
- @endif - -
- @if($previewData['detail_url']) - - View on TRMNL - - @endif - - - Install Recipe - - + @elseif($previewData['icon_url']) +
+ {{ $previewData['name'] }} icon + No preview image available
+ @else +
+ + No preview available +
+ @endif + + @if($previewData['author_bio']) +
+
+ Description + {{ $previewData['author_bio'] }} +
+
+ @endif + + @if(data_get($previewData, 'stats.installs')) +
+
+ Statistics + + Installs: {{ data_get($previewData, 'stats.installs') }} · + Forks: {{ data_get($previewData, 'stats.forks') }} + +
+
+ @endif + +
+ @if($previewData['detail_url']) + + View on TRMNL + + @endif + + + Install Recipe + +
- @endif -
+
+ @endif
+ +@script + +@endscript diff --git a/routes/api.php b/routes/api.php index b1d08b4..d1dbcac 100644 --- a/routes/api.php +++ b/routes/api.php @@ -95,16 +95,9 @@ Route::get('/display', function (Request $request) { // Check and update stale data if needed if ($plugin->isDataStale() || $plugin->current_image === null) { $plugin->updateDataPayload(); - try { - $markup = $plugin->render(device: $device); + $markup = $plugin->render(device: $device); - GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup); - } catch (Exception $e) { - Log::error("Failed to render plugin {$plugin->id} ({$plugin->name}): ".$e->getMessage()); - // Generate error display - $errorImageUuid = ImageGenerationService::generateDefaultScreenImage($device, 'error', $plugin->name); - $device->update(['current_screen_image' => $errorImageUuid]); - } + GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup); } $plugin->refresh(); @@ -127,17 +120,8 @@ Route::get('/display', function (Request $request) { } } - try { - $markup = $playlistItem->render(device: $device); - GenerateScreenJob::dispatchSync($device->id, null, $markup); - } catch (Exception $e) { - Log::error("Failed to render mashup playlist item {$playlistItem->id}: ".$e->getMessage()); - // For mashups, show error for the first plugin or a generic error - $firstPlugin = $plugins->first(); - $pluginName = $firstPlugin ? $firstPlugin->name : 'Recipe'; - $errorImageUuid = ImageGenerationService::generateDefaultScreenImage($device, 'error', $pluginName); - $device->update(['current_screen_image' => $errorImageUuid]); - } + $markup = $playlistItem->render(device: $device); + GenerateScreenJob::dispatchSync($device->id, null, $markup); $device->refresh(); diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php index 2925a5e..aff6758 100644 --- a/tests/Feature/Api/DeviceEndpointsTest.php +++ b/tests/Feature/Api/DeviceEndpointsTest.php @@ -7,7 +7,6 @@ use App\Models\Playlist; use App\Models\PlaylistItem; use App\Models\Plugin; use App\Models\User; -use App\Services\ImageGenerationService; use Bnussbau\TrmnlPipeline\TrmnlPipeline; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Storage; @@ -1024,163 +1023,3 @@ test('screens endpoint matches MAC address case-insensitively', function (): voi $response->assertOk(); Queue::assertPushed(GenerateScreenJob::class); }); - -test('display endpoint handles plugin rendering errors gracefully', function (): void { - TrmnlPipeline::fake(); - - $device = Device::factory()->create([ - 'mac_address' => '00:11:22:33:44:55', - 'api_key' => 'test-api-key', - 'proxy_cloud' => false, - ]); - - // Create a plugin with Blade markup that will cause an exception when accessing data[0] - // when data is not an array or doesn't have index 0 - $plugin = Plugin::factory()->create([ - 'name' => 'Broken Recipe', - 'data_strategy' => 'polling', - 'polling_url' => null, - 'data_stale_minutes' => 1, - 'markup_language' => 'blade', // Use Blade which will throw exception on invalid array access - 'render_markup' => '
{{ $data[0]["invalid"] }}
', // This will fail if data[0] doesn't exist - 'data_payload' => ['error' => 'Failed to fetch data'], // Not a list, so data[0] will fail - 'data_payload_updated_at' => now()->subMinutes(2), // Make it stale - 'current_image' => null, - ]); - - $playlist = Playlist::factory()->create([ - 'device_id' => $device->id, - 'name' => 'test_playlist', - 'is_active' => true, - 'weekdays' => null, - 'active_from' => null, - 'active_until' => null, - ]); - - PlaylistItem::factory()->create([ - 'playlist_id' => $playlist->id, - 'plugin_id' => $plugin->id, - 'order' => 1, - 'is_active' => true, - 'last_displayed_at' => null, - ]); - - $response = $this->withHeaders([ - 'id' => $device->mac_address, - 'access-token' => $device->api_key, - 'rssi' => -70, - 'battery_voltage' => 3.8, - 'fw-version' => '1.0.0', - ])->get('/api/display'); - - $response->assertOk(); - - // Verify error screen was generated and set on device - $device->refresh(); - expect($device->current_screen_image)->not->toBeNull(); - - // Verify the error image exists - $errorImagePath = Storage::disk('public')->path("images/generated/{$device->current_screen_image}.png"); - // The TrmnlPipeline is faked, so we just verify the UUID was set - expect($device->current_screen_image)->toBeString(); -}); - -test('display endpoint handles mashup rendering errors gracefully', function (): void { - TrmnlPipeline::fake(); - - $device = Device::factory()->create([ - 'mac_address' => '00:11:22:33:44:55', - 'api_key' => 'test-api-key', - 'proxy_cloud' => false, - ]); - - // Create plugins for mashup, one with invalid markup - $plugin1 = Plugin::factory()->create([ - 'name' => 'Working Plugin', - 'data_strategy' => 'polling', - 'polling_url' => null, - 'data_stale_minutes' => 1, - 'render_markup_view' => 'trmnl', - 'data_payload_updated_at' => now()->subMinutes(2), - 'current_image' => null, - ]); - - $plugin2 = Plugin::factory()->create([ - 'name' => 'Broken Plugin', - 'data_strategy' => 'polling', - 'polling_url' => null, - 'data_stale_minutes' => 1, - 'markup_language' => 'blade', // Use Blade which will throw exception on invalid array access - 'render_markup' => '
{{ $data[0]["invalid"] }}
', // This will fail - 'data_payload' => ['error' => 'Failed to fetch data'], - 'data_payload_updated_at' => now()->subMinutes(2), - 'current_image' => null, - ]); - - $playlist = Playlist::factory()->create([ - 'device_id' => $device->id, - 'name' => 'test_playlist', - 'is_active' => true, - 'weekdays' => null, - 'active_from' => null, - 'active_until' => null, - ]); - - // Create mashup playlist item - $playlistItem = PlaylistItem::createMashup( - $playlist, - '1Lx1R', - [$plugin1->id, $plugin2->id], - 'Test Mashup', - 1 - ); - - $response = $this->withHeaders([ - 'id' => $device->mac_address, - 'access-token' => $device->api_key, - 'rssi' => -70, - 'battery_voltage' => 3.8, - 'fw-version' => '1.0.0', - ])->get('/api/display'); - - $response->assertOk(); - - // Verify error screen was generated and set on device - $device->refresh(); - expect($device->current_screen_image)->not->toBeNull(); - - // Verify the error image UUID was set - expect($device->current_screen_image)->toBeString(); -}); - -test('generateDefaultScreenImage creates error screen with plugin name', function (): void { - TrmnlPipeline::fake(); - Storage::fake('public'); - Storage::disk('public')->makeDirectory('/images/generated'); - - $device = Device::factory()->create(); - - $errorUuid = ImageGenerationService::generateDefaultScreenImage($device, 'error', 'Test Recipe Name'); - - expect($errorUuid)->not->toBeEmpty(); - - // Verify the error image path would be created - $errorPath = "images/generated/{$errorUuid}.png"; - // Since TrmnlPipeline is faked, we just verify the UUID was generated - expect($errorUuid)->toBeString(); -}); - -test('generateDefaultScreenImage throws exception for invalid error image type', function (): void { - $device = Device::factory()->create(); - - expect(fn (): string => ImageGenerationService::generateDefaultScreenImage($device, 'invalid-error-type')) - ->toThrow(InvalidArgumentException::class); -}); - -test('getDeviceSpecificDefaultImage returns null for error type when no device-specific image exists', function (): void { - $device = new Device(); - $device->deviceModel = null; - - $result = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'error'); - expect($result)->toBeNull(); -}); diff --git a/tests/Feature/Livewire/Catalog/IndexTest.php b/tests/Feature/Livewire/Catalog/IndexTest.php index 1b2efba..22ab4b6 100644 --- a/tests/Feature/Livewire/Catalog/IndexTest.php +++ b/tests/Feature/Livewire/Catalog/IndexTest.php @@ -65,46 +65,6 @@ it('loads plugins from catalog URL', function (): void { $component->assertSee('testuser'); $component->assertSee('A test plugin'); $component->assertSee('MIT'); - $component->assertSee('Preview'); -}); - -it('hides preview button when screenshot_url is missing', function (): void { - // Clear cache first to ensure fresh data - Cache::forget('catalog_plugins'); - - // Mock the HTTP response for the catalog URL without screenshot_url - $catalogData = [ - 'test-plugin' => [ - 'name' => 'Test Plugin Without Screenshot', - 'author' => ['name' => 'Test Author', 'github' => 'testuser'], - 'author_bio' => [ - 'description' => 'A test plugin', - ], - 'license' => 'MIT', - 'trmnlp' => [ - 'zip_url' => 'https://example.com/plugin.zip', - ], - 'byos' => [ - 'byos_laravel' => [ - 'compatibility' => true, - ], - ], - 'logo_url' => 'https://example.com/logo.png', - 'screenshot_url' => null, - ], - ]; - - $yamlContent = Yaml::dump($catalogData); - - Http::fake([ - config('app.catalog_url') => Http::response($yamlContent, 200), - ]); - - Livewire::withoutLazyLoading(); - - Volt::test('catalog.index') - ->assertSee('Test Plugin Without Screenshot') - ->assertDontSeeHtml('variant="subtle" icon="eye"'); }); it('shows error when plugin not found', function (): void { @@ -154,46 +114,3 @@ it('shows error when zip_url is missing', function (): void { $component->assertHasErrors(); }); - -it('can preview a plugin', function (): void { - // Clear cache first to ensure fresh data - Cache::forget('catalog_plugins'); - - // Mock the HTTP response for the catalog URL - $catalogData = [ - 'test-plugin' => [ - 'name' => 'Test Plugin', - 'author' => ['name' => 'Test Author', 'github' => 'testuser'], - 'author_bio' => [ - 'description' => 'A test plugin description', - ], - 'license' => 'MIT', - 'trmnlp' => [ - 'zip_url' => 'https://example.com/plugin.zip', - ], - 'byos' => [ - 'byos_laravel' => [ - 'compatibility' => true, - ], - ], - 'logo_url' => 'https://example.com/logo.png', - 'screenshot_url' => 'https://example.com/screenshot.png', - ], - ]; - - $yamlContent = Yaml::dump($catalogData); - - Http::fake([ - config('app.catalog_url') => Http::response($yamlContent, 200), - ]); - - Livewire::withoutLazyLoading(); - - Volt::test('catalog.index') - ->assertSee('Test Plugin') - ->call('previewPlugin', 'test-plugin') - ->assertSet('previewingPlugin', 'test-plugin') - ->assertSet('previewData.name', 'Test Plugin') - ->assertSee('Preview Test Plugin') - ->assertSee('A test plugin description'); -}); diff --git a/tests/Feature/Volt/CatalogTrmnlTest.php b/tests/Feature/Volt/CatalogTrmnlTest.php index a80c63a..ba1b722 100644 --- a/tests/Feature/Volt/CatalogTrmnlTest.php +++ b/tests/Feature/Volt/CatalogTrmnlTest.php @@ -28,33 +28,9 @@ it('loads newest TRMNL recipes on mount', function (): void { Volt::test('catalog.trmnl') ->assertSee('Weather Chum') ->assertSee('Install') - ->assertDontSeeHtml('variant="subtle" icon="eye"') ->assertSee('Installs: 10'); }); -it('shows preview button when screenshot_url is provided', function (): void { - Http::fake([ - 'usetrmnl.com/recipes.json*' => Http::response([ - 'data' => [ - [ - 'id' => 123, - 'name' => 'Weather Chum', - 'icon_url' => 'https://example.com/icon.png', - 'screenshot_url' => 'https://example.com/screenshot.png', - 'author_bio' => null, - 'stats' => ['installs' => 10, 'forks' => 2], - ], - ], - ], 200), - ]); - - Livewire::withoutLazyLoading(); - - Volt::test('catalog.trmnl') - ->assertSee('Weather Chum') - ->assertSee('Preview'); -}); - it('searches TRMNL recipes when search term is provided', function (): void { Http::fake([ // First call (mount -> newest) @@ -176,111 +152,3 @@ it('shows error when plugin installation fails', function (): void { ->call('installPlugin', '123') ->assertSee('Error installing plugin'); // This will fail because the zip content is invalid }); - -it('previews a recipe with async fetch', function (): void { - Http::fake([ - 'usetrmnl.com/recipes.json*' => Http::response([ - 'data' => [ - [ - 'id' => 123, - 'name' => 'Weather Chum', - 'icon_url' => 'https://example.com/icon.png', - 'screenshot_url' => 'https://example.com/old.png', - 'author_bio' => null, - 'stats' => ['installs' => 10, 'forks' => 2], - ], - ], - ], 200), - 'usetrmnl.com/recipes/123.json' => Http::response([ - 'data' => [ - 'id' => 123, - 'name' => 'Weather Chum Updated', - 'icon_url' => 'https://example.com/icon.png', - 'screenshot_url' => 'https://example.com/new.png', - 'author_bio' => ['description' => 'New bio'], - 'stats' => ['installs' => 11, 'forks' => 3], - ], - ], 200), - ]); - - Livewire::withoutLazyLoading(); - - Volt::test('catalog.trmnl') - ->assertSee('Weather Chum') - ->call('previewRecipe', '123') - ->assertSet('previewingRecipe', '123') - ->assertSet('previewData.name', 'Weather Chum Updated') - ->assertSet('previewData.screenshot_url', 'https://example.com/new.png') - ->assertSee('Preview Weather Chum Updated') - ->assertSee('New bio'); -}); - -it('supports pagination and loading more recipes', function (): void { - Http::fake([ - 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::response([ - 'data' => [ - [ - 'id' => 1, - 'name' => 'Recipe Page 1', - 'icon_url' => null, - 'screenshot_url' => null, - 'author_bio' => null, - 'stats' => ['installs' => 1, 'forks' => 0], - ], - ], - 'next_page_url' => '/recipes.json?page=2', - ], 200), - 'usetrmnl.com/recipes.json?sort-by=newest&page=2' => Http::response([ - 'data' => [ - [ - 'id' => 2, - 'name' => 'Recipe Page 2', - 'icon_url' => null, - 'screenshot_url' => null, - 'author_bio' => null, - 'stats' => ['installs' => 2, 'forks' => 0], - ], - ], - 'next_page_url' => null, - ], 200), - ]); - - Livewire::withoutLazyLoading(); - - Volt::test('catalog.trmnl') - ->assertSee('Recipe Page 1') - ->assertDontSee('Recipe Page 2') - ->assertSee('Load next page') - ->call('loadMore') - ->assertSee('Recipe Page 1') - ->assertSee('Recipe Page 2') - ->assertDontSee('Load next page'); -}); - -it('resets pagination when search term changes', function (): void { - Http::fake([ - 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::sequence() - ->push([ - 'data' => [['id' => 1, 'name' => 'Initial 1']], - 'next_page_url' => '/recipes.json?page=2', - ]) - ->push([ - 'data' => [['id' => 3, 'name' => 'Initial 1 Again']], - 'next_page_url' => null, - ]), - 'usetrmnl.com/recipes.json?search=weather&sort-by=newest&page=1' => Http::response([ - 'data' => [['id' => 2, 'name' => 'Weather Result']], - 'next_page_url' => null, - ]), - ]); - - Livewire::withoutLazyLoading(); - - Volt::test('catalog.trmnl') - ->assertSee('Initial 1') - ->call('loadMore') - ->set('search', 'weather') - ->assertSee('Weather Result') - ->assertDontSee('Initial 1') - ->assertSet('page', 1); -});