081ca83613
CI / test (push) Has been cancelled
Two related bugs that surfaced on the first 13.3" device's first photo: 1) Web-UI portrait preview was 90° sideways. DeviceApiController:: renderBinToPng rotated whenever the device was Portrait — correct for V1 (landscape-native, Portrait => renderer rotated, so preview un-rotates) but wrong for V2 (portrait-native — the renderer doesn't rotate, so the preview shouldn't either). Now mirrors the render-pipeline check: rotate only when `orientation !== model->nativeOrientation()`. Two new functional tests pin the V2 portrait and V2 landscape PNG dimensions to guard against regressions. 2) Cropped photo letterboxed on the 13.3" panel. CropEditor / StickerCanvas / FrameCard had V1 dimensions hardcoded (1600×960 = 5:3 aspect). V2 is 4:3 (1200×1600 portrait / 1600×1200 landscape), so a "full crop" came out the wrong shape and the server's white-canvas composite added bars. New `panelDims(model, orientation)` helper in @/types is the single source of truth on the frontend; matches DeviceModel::width/height on the server. Threaded `model` through Device serializer → Device type → UploadView → CropEditor / StickerCanvas, and HomeView → FrameCard. FrameCard tests updated to cover all four model × orientation placeholders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
139 lines
4.6 KiB
PHP
139 lines
4.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Service;
|
|
|
|
use App\Entity\Device;
|
|
use App\Entity\Image;
|
|
use App\Enum\DeviceModel;
|
|
use App\Enum\Orientation;
|
|
use App\Enum\RotationMode;
|
|
use App\Service\DeviceSerializer;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* The serializer is the wire-shape contract between REST and Mercure. A test
|
|
* here doubles as a contract regression check: the SPA splats the same JSON
|
|
* for both, so any silent rename/removal here would break live updates the
|
|
* moment a device polls.
|
|
*/
|
|
class DeviceSerializerTest extends TestCase
|
|
{
|
|
private DeviceSerializer $serializer;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->serializer = new DeviceSerializer();
|
|
}
|
|
|
|
public function test_includes_all_expected_fields(): void
|
|
{
|
|
$device = $this->makeDevice();
|
|
|
|
$payload = $this->serializer->serialize($device);
|
|
|
|
$this->assertEqualsCanonicalizing(
|
|
['id', 'mac', 'name', 'model', 'orientation', 'rotationIntervalMinutes',
|
|
'wakeTimes', 'timezone', 'uniquenessWindow', 'rotationMode',
|
|
'prioritizeNeverShown', 'linkedAt', 'lastSeenAt',
|
|
'nextPollExpectedAt', 'lockedImageId', 'currentImageId'],
|
|
array_keys($payload),
|
|
);
|
|
}
|
|
|
|
public function test_serializes_model_field(): void
|
|
{
|
|
$device = $this->makeDevice();
|
|
$device->setModel(\App\Enum\DeviceModel::V2);
|
|
$payload = $this->serializer->serialize($device);
|
|
$this->assertSame('v2', $payload['model']);
|
|
}
|
|
|
|
public function test_serializes_scalars_in_expected_shapes(): void
|
|
{
|
|
$device = $this->makeDevice();
|
|
$device->setName('Living Room');
|
|
$device->setOrientation(Orientation::Portrait);
|
|
$device->setRotationIntervalMinutes(15);
|
|
$device->setWakeTimes([6 * 60, 18 * 60]);
|
|
$device->setTimezone('America/Chicago');
|
|
$device->setUniquenessWindow(7);
|
|
$device->setRotationMode(RotationMode::Random);
|
|
$device->setPrioritizeNeverShown(true);
|
|
|
|
$payload = $this->serializer->serialize($device);
|
|
|
|
$this->assertSame('Living Room', $payload['name']);
|
|
$this->assertSame('portrait', $payload['orientation']);
|
|
$this->assertSame(15, $payload['rotationIntervalMinutes']);
|
|
$this->assertSame([360, 1080], $payload['wakeTimes']);
|
|
$this->assertSame('America/Chicago', $payload['timezone']);
|
|
$this->assertSame(7, $payload['uniquenessWindow']);
|
|
$this->assertSame('random', $payload['rotationMode']);
|
|
$this->assertTrue($payload['prioritizeNeverShown']);
|
|
}
|
|
|
|
public function test_serializes_nullable_timestamps_as_null_when_unset(): void
|
|
{
|
|
$device = $this->makeDevice();
|
|
|
|
$payload = $this->serializer->serialize($device);
|
|
|
|
$this->assertNull($payload['lastSeenAt']);
|
|
$this->assertNull($payload['nextPollExpectedAt']);
|
|
$this->assertNull($payload['lockedImageId']);
|
|
$this->assertNull($payload['currentImageId']);
|
|
}
|
|
|
|
public function test_serializes_timestamps_as_iso_8601(): void
|
|
{
|
|
$device = $this->makeDevice();
|
|
$device->markSeen();
|
|
$device->setNextPollExpectedAt(new \DateTimeImmutable('2026-05-07T12:34:56+00:00'));
|
|
|
|
$payload = $this->serializer->serialize($device);
|
|
|
|
$this->assertMatchesRegularExpression(
|
|
'/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/',
|
|
$payload['lastSeenAt'],
|
|
);
|
|
$this->assertSame('2026-05-07T12:34:56+00:00', $payload['nextPollExpectedAt']);
|
|
}
|
|
|
|
public function test_lockedImageId_and_currentImageId_reflect_assignments(): void
|
|
{
|
|
$device = $this->makeDevice();
|
|
|
|
$image = $this->makeImage(42);
|
|
$device->setLockedImage($image);
|
|
$device->setCurrentImage($image);
|
|
|
|
$payload = $this->serializer->serialize($device);
|
|
|
|
$this->assertSame(42, $payload['lockedImageId']);
|
|
$this->assertSame(42, $payload['currentImageId']);
|
|
}
|
|
|
|
private function makeDevice(): Device
|
|
{
|
|
$device = new Device();
|
|
$device->setMac('AA:BB:CC:DD:EE:FF');
|
|
$device->setName('Test');
|
|
$device->setModel(DeviceModel::V1);
|
|
$device->setOrientation(Orientation::Landscape);
|
|
// Persisted-id is null in unit-tests; serializer must tolerate it.
|
|
return $device;
|
|
}
|
|
|
|
private function makeImage(int $id): Image
|
|
{
|
|
$image = new Image();
|
|
// Image::$id is set by Doctrine — bypass via reflection in unit context.
|
|
$ref = new \ReflectionProperty(Image::class, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($image, $id);
|
|
return $image;
|
|
}
|
|
}
|