fix(#71): device specific sleep and setup images
Some checks failed
tests / ci (push) Has been cancelled

This commit is contained in:
Benjamin Nussbaum 2025-09-24 22:33:21 +02:00
parent 6ae3e023d4
commit 3e5ba47a12
35 changed files with 614 additions and 20 deletions

View file

@ -0,0 +1,197 @@
<?php
namespace App\Console\Commands;
use App\Models\DeviceModel;
use Bnussbau\TrmnlPipeline\Stages\BrowserStage;
use Bnussbau\TrmnlPipeline\Stages\ImageStage;
use Bnussbau\TrmnlPipeline\TrmnlPipeline;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use RuntimeException;
use Wnx\SidecarBrowsershot\BrowsershotLambda;
class GenerateDefaultImagesCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'images:generate-defaults {--force : Force regeneration of existing images}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate default images (setup-logo and sleep) for all device models from Blade templates';
/**
* Execute the console command.
*/
public function handle(): int
{
$this->info('Starting generation of default images for all device models...');
$deviceModels = DeviceModel::all();
if ($deviceModels->isEmpty()) {
$this->warn('No device models found in the database.');
return self::SUCCESS;
}
$this->info("Found {$deviceModels->count()} device models to process.");
// Create the target directory
$targetDir = 'images/default-screens';
if (! Storage::disk('public')->exists($targetDir)) {
Storage::disk('public')->makeDirectory($targetDir);
$this->info("Created directory: {$targetDir}");
}
$successCount = 0;
$skipCount = 0;
$errorCount = 0;
foreach ($deviceModels as $deviceModel) {
$this->info("Processing device model: {$deviceModel->label} (ID: {$deviceModel->id})");
try {
// Process setup-logo
$setupResult = $this->transformImage('setup-logo', $deviceModel, $targetDir);
if ($setupResult) {
++$successCount;
} else {
++$skipCount;
}
// Process sleep
$sleepResult = $this->transformImage('sleep', $deviceModel, $targetDir);
if ($sleepResult) {
++$successCount;
} else {
++$skipCount;
}
} catch (Exception $e) {
$this->error("Error processing device model {$deviceModel->label}: ".$e->getMessage());
++$errorCount;
}
}
$this->info("\nGeneration completed!");
$this->info("Successfully processed: {$successCount} images");
$this->info("Skipped (already exist): {$skipCount} images");
$this->info("Errors: {$errorCount} images");
return self::SUCCESS;
}
/**
* Transform a single image for a device model using Blade templates
*/
private function transformImage(string $imageType, DeviceModel $deviceModel, string $targetDir): bool
{
// Generate filename: {width}_{height}_{bit_depth}_{rotation}.{extension}
$extension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png';
$filename = "{$deviceModel->width}_{$deviceModel->height}_{$deviceModel->bit_depth}_{$deviceModel->rotation}.{$extension}";
$targetPath = "{$targetDir}/{$imageType}_{$filename}";
// Check if target already exists and force is not set
if (Storage::disk('public')->exists($targetPath) && ! $this->option('force')) {
$this->line(" Skipping {$imageType} - already exists: {$filename}");
return false;
}
try {
// Create custom Browsershot instance if using AWS Lambda
$browsershotInstance = null;
if (config('app.puppeteer_mode') === 'sidecar-aws') {
$browsershotInstance = new BrowsershotLambda();
}
// Generate HTML from Blade template
$html = $this->generateHtmlFromTemplate($imageType, $deviceModel);
// dump($html);
$browserStage = new BrowserStage($browsershotInstance);
$browserStage->html($html);
$browserStage
->width($deviceModel->width)
->height($deviceModel->height);
$browserStage->setBrowsershotOption('waitUntil', 'networkidle0');
if (config('app.puppeteer_docker')) {
$browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']);
}
$outputPath = Storage::disk('public')->path($targetPath);
$imageStage = new ImageStage();
$imageStage->format($extension)
->width($deviceModel->width)
->height($deviceModel->height)
->colors($deviceModel->colors)
->bitDepth($deviceModel->bit_depth)
->rotation($deviceModel->rotation)
// ->offsetX($deviceModel->offset_x)
// ->offsetY($deviceModel->offset_y)
->outputPath($outputPath);
(new TrmnlPipeline())->pipe($browserStage)
->pipe($imageStage)
->process();
if (! file_exists($outputPath)) {
throw new RuntimeException('Image file was not created: '.$outputPath);
}
if (filesize($outputPath) === 0) {
throw new RuntimeException('Image file is empty: '.$outputPath);
}
$this->line(" ✓ Generated {$imageType}: {$filename}");
return true;
} catch (Exception $e) {
$this->error(" ✗ Failed to generate {$imageType} for {$deviceModel->label}: ".$e->getMessage());
return false;
}
}
/**
* Generate HTML from Blade template for the given image type and device model
*/
private function generateHtmlFromTemplate(string $imageType, DeviceModel $deviceModel): string
{
// Map image type to template name
$templateName = match ($imageType) {
'setup-logo' => 'default-screens.setup',
'sleep' => 'default-screens.sleep',
default => throw new InvalidArgumentException("Invalid image type: {$imageType}")
};
// Determine device properties from DeviceModel
$deviceVariant = $deviceModel->name ?? 'og';
$colorDepth = $deviceModel->color_depth ?? '1bit'; // Use the accessor method
$scaleLevel = $deviceModel->scale_level; // Use the accessor method
$darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode
// Render the Blade template
return view($templateName, [
'noBleed' => false,
'darkMode' => $darkMode,
'deviceVariant' => $deviceVariant,
'colorDepth' => $colorDepth,
'scaleLevel' => $scaleLevel,
])->render();
}
}

View file

@ -31,6 +31,10 @@ final class DeviceModel extends Model
return null; return null;
} }
if ($this->bit_depth === 3) {
return '2bit';
}
// if higher then 4 return 4bit // if higher then 4 return 4bit
if ($this->bit_depth > 4) { if ($this->bit_depth > 4) {
return '4bit'; return '4bit';

View file

@ -12,6 +12,7 @@ use Bnussbau\TrmnlPipeline\TrmnlPipeline;
use Exception; use Exception;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use RuntimeException; use RuntimeException;
use Wnx\SidecarBrowsershot\BrowsershotLambda; use Wnx\SidecarBrowsershot\BrowsershotLambda;
@ -255,4 +256,151 @@ class ImageGenerationService
} }
} }
} }
/**
* Get device-specific default image path for setup or sleep mode
*/
public static function getDeviceSpecificDefaultImage(Device $device, string $imageType): ?string
{
// Validate image type
if (! in_array($imageType, ['setup-logo', 'sleep'])) {
return null;
}
// If device has a DeviceModel, try to find device-specific image
if ($device->deviceModel) {
$model = $device->deviceModel;
$extension = $model->mime_type === 'image/bmp' ? 'bmp' : 'png';
$filename = "{$model->width}_{$model->height}_{$model->bit_depth}_{$model->rotation}.{$extension}";
$deviceSpecificPath = "images/default-screens/{$imageType}_{$filename}";
if (Storage::disk('public')->exists($deviceSpecificPath)) {
return $deviceSpecificPath;
}
}
// Fallback to original hardcoded images
$fallbackPath = "images/{$imageType}.bmp";
if (Storage::disk('public')->exists($fallbackPath)) {
return $fallbackPath;
}
// Try PNG fallback
$fallbackPathPng = "images/{$imageType}.png";
if (Storage::disk('public')->exists($fallbackPathPng)) {
return $fallbackPathPng;
}
return null;
}
/**
* Generate a default screen image from Blade template
*/
public static function generateDefaultScreenImage(Device $device, string $imageType): string
{
// Validate image type
if (! in_array($imageType, ['setup-logo', 'sleep'])) {
throw new InvalidArgumentException("Invalid image type: {$imageType}");
}
$uuid = Uuid::uuid4()->toString();
try {
// Get image generation settings from DeviceModel if available, otherwise use device settings
$imageSettings = self::getImageSettings($device);
$fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png';
$outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension);
// Generate HTML from Blade template
$html = self::generateDefaultScreenHtml($device, $imageType);
// Create custom Browsershot instance if using AWS Lambda
$browsershotInstance = null;
if (config('app.puppeteer_mode') === 'sidecar-aws') {
$browsershotInstance = new BrowsershotLambda();
}
$browserStage = new BrowserStage($browsershotInstance);
$browserStage->html($html);
if (config('app.puppeteer_window_size_strategy') === 'v2') {
$browserStage
->width($imageSettings['width'])
->height($imageSettings['height']);
} else {
$browserStage->useDefaultDimensions();
}
if (config('app.puppeteer_wait_for_network_idle')) {
$browserStage->setBrowsershotOption('waitUntil', 'networkidle0');
}
if (config('app.puppeteer_docker')) {
$browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']);
}
$imageStage = new ImageStage();
$imageStage->format($fileExtension)
->width($imageSettings['width'])
->height($imageSettings['height'])
->colors($imageSettings['colors'])
->bitDepth($imageSettings['bit_depth'])
->rotation($imageSettings['rotation'])
->offsetX($imageSettings['offset_x'])
->offsetY($imageSettings['offset_y'])
->outputPath($outputPath);
(new TrmnlPipeline())->pipe($browserStage)
->pipe($imageStage)
->process();
if (! file_exists($outputPath)) {
throw new RuntimeException('Image file was not created: '.$outputPath);
}
if (filesize($outputPath) === 0) {
throw new RuntimeException('Image file is empty: '.$outputPath);
}
Log::info("Device $device->id: generated default screen image: $uuid for type: $imageType");
return $uuid;
} catch (Exception $e) {
Log::error('Failed to generate default screen image: '.$e->getMessage());
throw new RuntimeException('Failed to generate default screen image: '.$e->getMessage(), 0, $e);
}
}
/**
* Generate HTML from Blade template for default screens
*/
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',
default => throw new InvalidArgumentException("Invalid image type: {$imageType}")
};
// Determine device properties from DeviceModel or device settings
$deviceVariant = $device->deviceVariant();
$deviceOrientation = $device->rotate > 0 ? 'portrait' : 'landscape';
$colorDepth = $device->colorDepth() ?? '1bit';
$scaleLevel = $device->scaleLevel();
$darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode
// Render the Blade template
return view($templateName, [
'noBleed' => false,
'darkMode' => $darkMode,
'deviceVariant' => $deviceVariant,
'deviceOrientation' => $deviceOrientation,
'colorDepth' => $colorDepth,
'scaleLevel' => $scaleLevel,
])->render();
}
} }

View file

@ -0,0 +1,22 @@
@props([
'noBleed' => false,
'darkMode' => false,
'deviceVariant' => 'og',
'deviceOrientation' => null,
'colorDepth' => '1bit',
'scaleLevel' => null,
])
<x-trmnl::screen colorDepth="{{$colorDepth}}" no-bleed="{{$noBleed}}" dark-mode="{{$darkMode}}"
device-variant="{{$deviceVariant}}" device-orientation="{{$deviceOrientation}}"
scale-level="{{$scaleLevel}}">
<x-trmnl::view>
<x-trmnl::layout>
<x-trmnl::richtext gapSize="large" align="center">
<x-trmnl::title>Welcome to BYOS Laravel!</x-trmnl::title>
<x-trmnl::content>Your device is connected.</x-trmnl::content>
</x-trmnl::richtext>
</x-trmnl::layout>
<x-trmnl::title-bar title="byos_laravel"/>
</x-trmnl::view>
</x-trmnl::screen>

View file

@ -0,0 +1,28 @@
@props([
'noBleed' => false,
'darkMode' => true,
'deviceVariant' => 'og',
'deviceOrientation' => null,
'colorDepth' => '1bit',
'scaleLevel' => null,
])
<x-trmnl::screen colorDepth="{{$colorDepth}}" no-bleed="{{$noBleed}}" dark-mode="{{$darkMode}}"
device-variant="{{$deviceVariant}}" device-orientation="{{$deviceOrientation}}"
scale-level="{{$scaleLevel}}">
<x-trmnl::view>
<x-trmnl::layout>
<x-trmnl::richtext gapSize="large" align="center">
<div class="image image-dither" alt="sleep">
<svg class="w-64 h-64" fill="#000" xmlns="http://www.w3.org/2000/svg" id="mdi-sleep"
viewBox="0 0 24 24">
<path
d="M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M15,16H9V14L12.39,10H9V8H15V10L11.62,14H15V16M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z"></path>
</svg>
</div>
<x-trmnl::title>Sleep Mode</x-trmnl::title>
</x-trmnl::richtext>
</x-trmnl::layout>
<x-trmnl::title-bar title="byos_laravel"/>
</x-trmnl::view>
</x-trmnl::screen>

View file

@ -60,12 +60,22 @@ Route::get('/display', function (Request $request) {
} }
if ($device->isPauseActive()) { if ($device->isPauseActive()) {
$image_path = 'images/sleep.png'; $image_path = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'sleep');
$filename = 'sleep.png'; if (! $image_path) {
// Generate from template if no device-specific image exists
$image_uuid = ImageGenerationService::generateDefaultScreenImage($device, 'sleep');
$image_path = 'images/generated/'.$image_uuid.'.png';
}
$filename = basename($image_path);
$refreshTimeOverride = (int) now()->diffInSeconds($device->pause_until); $refreshTimeOverride = (int) now()->diffInSeconds($device->pause_until);
} elseif ($device->isSleepModeActive()) { } elseif ($device->isSleepModeActive()) {
$image_path = 'images/sleep.png'; $image_path = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'sleep');
$filename = 'sleep.png'; if (! $image_path) {
// Generate from template if no device-specific image exists
$image_uuid = ImageGenerationService::generateDefaultScreenImage($device, 'sleep');
$image_path = 'images/generated/'.$image_uuid.'.png';
}
$filename = basename($image_path);
$refreshTimeOverride = $device->getSleepModeEndsInSeconds() ?? $device->default_refresh_interval; $refreshTimeOverride = $device->getSleepModeEndsInSeconds() ?? $device->default_refresh_interval;
} else { } else {
// Get current screen image from a mirror device or continue if not available // Get current screen image from a mirror device or continue if not available
@ -125,8 +135,13 @@ Route::get('/display', function (Request $request) {
$image_uuid = $device->current_screen_image; $image_uuid = $device->current_screen_image;
} }
if (! $image_uuid) { if (! $image_uuid) {
$image_path = 'images/setup-logo.bmp'; $image_path = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'setup-logo');
$filename = 'setup-logo.bmp'; if (! $image_path) {
// Generate from template if no device-specific image exists
$image_uuid = ImageGenerationService::generateDefaultScreenImage($device, 'setup-logo');
$image_path = 'images/generated/'.$image_uuid.'.png';
}
$filename = basename($image_path);
} else { } else {
// Determine image format based on device settings // Determine image format based on device settings
$preferred_format = 'png'; // Default to PNG for newer firmware $preferred_format = 'png'; // Default to PNG for newer firmware
@ -225,7 +240,7 @@ Route::get('/setup', function (Request $request) {
'status' => 200, 'status' => 200,
'api_key' => $device->api_key, 'api_key' => $device->api_key,
'friendly_id' => $device->friendly_id, 'friendly_id' => $device->friendly_id,
'image_url' => url('storage/images/setup-logo.png'), 'image_url' => url('storage/'.ImageGenerationService::getDeviceSpecificDefaultImage($device, 'setup-logo')),
'message' => 'Welcome to TRMNL BYOS', 'message' => 'Welcome to TRMNL BYOS',
]); ]);
}); });
@ -444,8 +459,13 @@ Route::get('/current_screen', function (Request $request) {
$image_uuid = $device->current_screen_image; $image_uuid = $device->current_screen_image;
if (! $image_uuid) { if (! $image_uuid) {
$image_path = 'images/setup-logo.bmp'; $image_path = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'setup-logo');
$filename = 'setup-logo.bmp'; if (! $image_path) {
// Generate from template if no device-specific image exists
$image_uuid = ImageGenerationService::generateDefaultScreenImage($device, 'setup-logo');
$image_path = 'images/generated/'.$image_uuid.'.png';
}
$filename = basename($image_path);
} else { } else {
// Determine image format based on device settings // Determine image format based on device settings
$preferred_format = 'png'; // Default to PNG for newer firmware $preferred_format = 'png'; // Default to PNG for newer firmware

View file

@ -1,4 +0,0 @@
*
!private/
!public/
!.gitignore

View file

@ -1,3 +1,2 @@
* *
!images/
!.gitignore !.gitignore

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

View file

@ -812,10 +812,10 @@ test('device in sleep mode returns sleep image and correct refresh rate', functi
'fw-version' => '1.0.0', 'fw-version' => '1.0.0',
])->get('/api/display'); ])->get('/api/display');
$response->assertOk() $response->assertOk();
->assertJson([
'filename' => 'sleep.png', // The filename should be a UUID-based PNG file since we're generating from template
]); expect($response['filename'])->toMatch('/^[a-f0-9-]+\.png$/');
expect($response['refresh_rate'])->toBeGreaterThan(0); expect($response['refresh_rate'])->toBeGreaterThan(0);
Carbon\Carbon::setTestNow(); // Clear test time Carbon\Carbon::setTestNow(); // Clear test time
@ -867,8 +867,10 @@ test('device returns sleep.png and correct refresh time when paused', function (
$response->assertOk(); $response->assertOk();
$json = $response->json(); $json = $response->json();
expect($json['filename'])->toBe('sleep.png');
expect($json['image_url'])->toContain('sleep.png'); // The filename should be a UUID-based PNG file since we're generating from template
expect($json['filename'])->toMatch('/^[a-f0-9-]+\.png$/');
expect($json['image_url'])->toContain('images/generated/');
expect($json['refresh_rate'])->toBeLessThanOrEqual(3600); // ~60 min expect($json['refresh_rate'])->toBeLessThanOrEqual(3600); // ~60 min
}); });

View file

@ -0,0 +1,89 @@
<?php
use App\Models\Device;
use App\Models\DeviceModel;
use App\Services\ImageGenerationService;
use Illuminate\Support\Facades\Storage;
test('command transforms default images for all device models', function () {
// Ensure we have device models
$deviceModels = DeviceModel::all();
expect($deviceModels)->not->toBeEmpty();
// Run the command
$this->artisan('images:generate-defaults')
->assertExitCode(0);
// Check that the default-screens directory was created
expect(Storage::disk('public')->exists('images/default-screens'))->toBeTrue();
// Check that images were generated for each device model
foreach ($deviceModels as $deviceModel) {
$extension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png';
$filename = "{$deviceModel->width}_{$deviceModel->height}_{$deviceModel->bit_depth}_{$deviceModel->rotation}.{$extension}";
$setupPath = "images/default-screens/setup-logo_{$filename}";
$sleepPath = "images/default-screens/sleep_{$filename}";
expect(Storage::disk('public')->exists($setupPath))->toBeTrue();
expect(Storage::disk('public')->exists($sleepPath))->toBeTrue();
}
});
test('getDeviceSpecificDefaultImage returns correct path for device with model', function () {
$deviceModel = DeviceModel::first();
expect($deviceModel)->not->toBeNull();
$device = new Device();
$device->deviceModel = $deviceModel;
$setupImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'setup-logo');
$sleepImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'sleep');
expect($setupImage)->toContain('images/default-screens/setup-logo_');
expect($sleepImage)->toContain('images/default-screens/sleep_');
});
test('getDeviceSpecificDefaultImage falls back to original images for device without model', function () {
$device = new Device();
$device->deviceModel = null;
$setupImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'setup-logo');
$sleepImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'sleep');
expect($setupImage)->toBe('images/setup-logo.bmp');
expect($sleepImage)->toBe('images/sleep.bmp');
});
test('generateDefaultScreenImage creates images from Blade templates', function () {
$device = Device::factory()->create();
$setupUuid = ImageGenerationService::generateDefaultScreenImage($device, 'setup-logo');
$sleepUuid = ImageGenerationService::generateDefaultScreenImage($device, 'sleep');
expect($setupUuid)->not->toBeEmpty();
expect($sleepUuid)->not->toBeEmpty();
expect($setupUuid)->not->toBe($sleepUuid);
// Check that the generated images exist
$setupPath = "images/generated/{$setupUuid}.png";
$sleepPath = "images/generated/{$sleepUuid}.png";
expect(Storage::disk('public')->exists($setupPath))->toBeTrue();
expect(Storage::disk('public')->exists($sleepPath))->toBeTrue();
});
test('generateDefaultScreenImage throws exception for invalid image type', function () {
$device = Device::factory()->create();
expect(fn () => ImageGenerationService::generateDefaultScreenImage($device, 'invalid-type'))
->toThrow(InvalidArgumentException::class);
});
test('getDeviceSpecificDefaultImage returns null for invalid image type', function () {
$device = new Device();
$device->deviceModel = DeviceModel::first();
$result = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'invalid-type');
expect($result)->toBeNull();
});

View file

@ -0,0 +1,89 @@
<?php
use App\Models\Device;
use App\Models\DeviceModel;
use App\Services\ImageGenerationService;
use Illuminate\Support\Facades\Storage;
test('command transforms default images for all device models', function () {
// Ensure we have device models
$deviceModels = DeviceModel::all();
expect($deviceModels)->not->toBeEmpty();
// Run the command
$this->artisan('images:generate-defaults')
->assertExitCode(0);
// Check that the default-screens directory was created
expect(Storage::disk('public')->exists('images/default-screens'))->toBeTrue();
// Check that images were generated for each device model
foreach ($deviceModels as $deviceModel) {
$extension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png';
$filename = "{$deviceModel->width}_{$deviceModel->height}_{$deviceModel->bit_depth}_{$deviceModel->rotation}.{$extension}";
$setupPath = "images/default-screens/setup-logo_{$filename}";
$sleepPath = "images/default-screens/sleep_{$filename}";
expect(Storage::disk('public')->exists($setupPath))->toBeTrue();
expect(Storage::disk('public')->exists($sleepPath))->toBeTrue();
}
});
test('getDeviceSpecificDefaultImage returns correct path for device with model', function () {
$deviceModel = DeviceModel::first();
expect($deviceModel)->not->toBeNull();
$device = new Device();
$device->deviceModel = $deviceModel;
$setupImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'setup-logo');
$sleepImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'sleep');
expect($setupImage)->toContain('images/default-screens/setup-logo_');
expect($sleepImage)->toContain('images/default-screens/sleep_');
});
test('getDeviceSpecificDefaultImage falls back to original images for device without model', function () {
$device = new Device();
$device->deviceModel = null;
$setupImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'setup-logo');
$sleepImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'sleep');
expect($setupImage)->toBe('images/setup-logo.bmp');
expect($sleepImage)->toBe('images/sleep.bmp');
});
test('generateDefaultScreenImage creates images from Blade templates', function () {
$device = Device::factory()->create();
$setupUuid = ImageGenerationService::generateDefaultScreenImage($device, 'setup-logo');
$sleepUuid = ImageGenerationService::generateDefaultScreenImage($device, 'sleep');
expect($setupUuid)->not->toBeEmpty();
expect($sleepUuid)->not->toBeEmpty();
expect($setupUuid)->not->toBe($sleepUuid);
// Check that the generated images exist
$setupPath = "images/generated/{$setupUuid}.png";
$sleepPath = "images/generated/{$sleepUuid}.png";
expect(Storage::disk('public')->exists($setupPath))->toBeTrue();
expect(Storage::disk('public')->exists($sleepPath))->toBeTrue();
})->skipOnCI();
test('generateDefaultScreenImage throws exception for invalid image type', function () {
$device = Device::factory()->create();
expect(fn () => ImageGenerationService::generateDefaultScreenImage($device, 'invalid-type'))
->toThrow(InvalidArgumentException::class);
});
test('getDeviceSpecificDefaultImage returns null for invalid image type', function () {
$device = new Device();
$device->deviceModel = DeviceModel::first();
$result = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'invalid-type');
expect($result)->toBeNull();
});