Files
HickLab3D_Dashboard/tests/Unit/ProductImageRendererTest.php
T

105 lines
4.4 KiB
PHP

<?php
namespace Tests\Unit;
use App\Services\ProductImageRenderer;
use RuntimeException;
use Tests\TestCase;
class ProductImageRendererTest extends TestCase
{
/**
* @return list<array{v: array{0:array{0:float,1:float,2:float},1:array{0:float,1:float,2:float},2:array{0:float,1:float,2:float}}, color: array{0:int,1:int,2:int,3:int}}>
*/
private function oneRedTriangle(): array
{
return [[
'v' => [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0]],
'color' => [204, 0, 0, 255],
]];
}
public function test_renders_a_real_png_when_gd_is_available(): void
{
$renderer = new ProductImageRenderer(gdAvailable: true);
$result = $renderer->render($this->oneRedTriangle());
$this->assertSame('image/png', $result['mime']);
$this->assertSame('gd', $result['renderer']);
$this->assertSame(512, $result['width']);
$this->assertSame(512, $result['height']);
$this->assertStringStartsWith("\x89PNG\x0d\x0a\x1a\x0a", $result['bytes']);
// Decode it back and confirm an actual non-background pixel exists where the
// triangle should project — this is a real rasterisation, not a blank canvas.
$image = imagecreatefromstring($result['bytes']);
$this->assertNotFalse($image);
$backgroundRgb = imagecolorat($image, 5, 5);
// The projected triangle's centroid, computed from its known world
// coordinates and the corrected (non-flipped) isometric projection:
// vertices land at pixels (256,32), (480,161), (32,161).
$insideTriangleRgb = imagecolorat($image, 256, 120);
$this->assertNotSame($backgroundRgb, $insideTriangleRgb);
imagedestroy($image);
}
public function test_a_triangle_facing_away_from_the_camera_is_culled_and_never_drawn(): void
{
// Same triangle as oneRedTriangle() but with v1/v2 swapped, flipping
// its winding so the normal points -Z (away from the (1,1,1) camera).
$backFacing = [[
'v' => [[0.0, 0.0, 0.0], [0.0, 10.0, 0.0], [10.0, 0.0, 0.0]],
'color' => [204, 0, 0, 255],
]];
$renderer = new ProductImageRenderer(gdAvailable: true);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessageMatches('/faces away from the camera/i');
$renderer->render($backFacing);
}
public function test_falls_back_to_fixed_content_numeric_only_svg_when_gd_is_unavailable(): void
{
$renderer = new ProductImageRenderer(gdAvailable: false);
$result = $renderer->render($this->oneRedTriangle());
$this->assertSame('image/svg+xml', $result['mime']);
$this->assertSame('svg', $result['renderer']);
$this->assertStringContainsString('<svg', $result['bytes']);
// Shaded from the pure red base colour (204,0,0) by this face's normal
// angle to the fixed light direction, not the unshaded base colour.
$this->assertStringContainsString('fill="#bb0000"', $result['bytes']);
$this->assertStringNotContainsString('<script', $result['bytes']);
$this->assertStringNotContainsString('<image', $result['bytes']);
$this->assertStringNotContainsString('xlink:href', $result['bytes']);
$this->assertStringNotContainsString('url(', $result['bytes']);
// Only numeric coordinates and hex colours appear inside each polygon tag.
$this->assertMatchesRegularExpression('/<polygon points="[\d.,\s-]+" fill="#[0-9a-f]{6}"\/>/', $result['bytes']);
}
public function test_rejects_an_empty_scene_rather_than_rendering_a_blank_image(): void
{
$renderer = new ProductImageRenderer(gdAvailable: true);
$this->expectException(RuntimeException::class);
$renderer->render([]);
}
public function test_rejects_translucent_material_instead_of_silently_rendering_it_opaque(): void
{
$triangles = $this->oneRedTriangle();
$triangles[0]['color'][3] = 128;
$this->expectException(RuntimeException::class);
$this->expectExceptionMessageMatches('/transparen/i');
(new ProductImageRenderer(gdAvailable: true))->render($triangles);
}
public function test_renderer_name_reflects_gd_availability(): void
{
$this->assertSame('gd', (new ProductImageRenderer(gdAvailable: true))->rendererName());
$this->assertSame('svg', (new ProductImageRenderer(gdAvailable: false))->rendererName());
}
}