mirror of
https://github.com/usetrmnl/larapaper.git
synced 2026-08-13 14:28:54 -07:00
feat(#276): implement device pause functionality with custom duration (holiday mode)
This commit is contained in:
@@ -28,7 +28,7 @@ class RunDeviceDisplayCycle
|
||||
if ($device->isPauseActive()) {
|
||||
return [
|
||||
'image_path' => $this->defaultImagePath($device, 'sleep'),
|
||||
'refresh_time_override' => (int) now()->diffInSeconds($device->pause_until),
|
||||
'refresh_time_override' => min(Device::MAX_PAUSE_REFRESH_SECONDS, (int) now()->diffInSeconds($device->pause_until)),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ class Device extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const MAX_PAUSE_DAYS = 30;
|
||||
|
||||
public const MAX_PAUSE_REFRESH_SECONDS = 86400; // 24 hours
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
@@ -320,4 +324,9 @@ class Device extends Model
|
||||
{
|
||||
return $this->pause_until && $this->pause_until->isFuture();
|
||||
}
|
||||
|
||||
public function usesTouchBar(): bool
|
||||
{
|
||||
return $this->deviceModel?->name === 'v2';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ class User extends Authenticatable implements PasskeyUser // implements MustVeri
|
||||
->implode('');
|
||||
}
|
||||
|
||||
public function preferredTimezone(): string
|
||||
{
|
||||
return $this->timezone ?? config('app.timezone');
|
||||
}
|
||||
|
||||
public function devices(): HasMany
|
||||
{
|
||||
return $this->hasMany(Device::class);
|
||||
|
||||
Generated
+153
@@ -28,6 +28,11 @@
|
||||
"tailwindcss": "^4.0.7",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.9.6",
|
||||
"prettier-plugin-blade": "^3.2.2",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
|
||||
@@ -441,6 +446,20 @@
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@prettier/html-tags": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@prettier/html-tags/-/html-tags-2.2.0.tgz",
|
||||
"integrity": "sha512-6qLjt8ncestmSfEOeVf/kX5VhkinUsppybvkwn1X7wu+BjQ/GBSZYu7cytxdCvi5BaljCGIAr4rmfL5zVqdgyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@prettier/parse-srcset": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prettier/parse-srcset/-/parse-srcset-3.1.0.tgz",
|
||||
"integrity": "sha512-FIRv2rZotO9NP/r66taYqD1zICpPiBbrECqjKV/xqNotqDxF7fLzzUeK+1RSRx9Tenk0DajR/GcRmTJ2qtHaKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.6.tgz",
|
||||
@@ -1540,6 +1559,17 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/html-element-attributes": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/html-element-attributes/-/html-element-attributes-3.5.0.tgz",
|
||||
"integrity": "sha512-rU2BFhp0kQla9sqPBI46C+zbP6PFOtD7z6XNAJ6as+cGecCDXLx0W3aIs6XdPLmBBG/Fy1meRi/n65Exofz4Qw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
@@ -1960,6 +1990,129 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.9.6",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
|
||||
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-blade": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-blade/-/prettier-plugin-blade-3.2.2.tgz",
|
||||
"integrity": "sha512-CPAlkLTAnvEZfvmRZ1CgdeePpfhxz/Bggei2lSAYcrsBFBhSZ04Ky0ttL4VB1u2q3iKh411vBi0T/Wx3GHWq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@prettier/html-tags": "^2.2.0",
|
||||
"@prettier/parse-srcset": "^3.1.0",
|
||||
"html-element-attributes": "^3.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@prettier/plugin-php": "^0.24.0",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.0 || ^0.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@prettier/plugin-php": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-tailwindcss": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.8.1.tgz",
|
||||
"integrity": "sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "*",
|
||||
"@prettier/plugin-hermes": "*",
|
||||
"@prettier/plugin-oxc": "*",
|
||||
"@prettier/plugin-pug": "*",
|
||||
"@shopify/prettier-plugin-liquid": "*",
|
||||
"@trivago/prettier-plugin-sort-imports": "*",
|
||||
"@zackad/prettier-plugin-twig": "*",
|
||||
"prettier": "^3.0",
|
||||
"prettier-plugin-astro": "*",
|
||||
"prettier-plugin-css-order": "*",
|
||||
"prettier-plugin-jsdoc": "*",
|
||||
"prettier-plugin-marko": "*",
|
||||
"prettier-plugin-multiline-arrays": "*",
|
||||
"prettier-plugin-organize-attributes": "*",
|
||||
"prettier-plugin-organize-imports": "*",
|
||||
"prettier-plugin-sort-imports": "*",
|
||||
"prettier-plugin-svelte": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@ianvs/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-hermes": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-oxc": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@shopify/prettier-plugin-liquid": {
|
||||
"optional": true
|
||||
},
|
||||
"@trivago/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@zackad/prettier-plugin-twig": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-astro": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-css-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-jsdoc": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-marko": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-multiline-arrays": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-attributes": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-svelte": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "25.3.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.3.0.tgz",
|
||||
|
||||
@@ -34,5 +34,10 @@
|
||||
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
|
||||
"lightningcss-linux-x64-gnu": "^1.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.9.6",
|
||||
"prettier-plugin-blade": "^3.2.2",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceModel;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
|
||||
new class extends Component
|
||||
@@ -28,7 +31,11 @@ new class extends Component
|
||||
|
||||
public $deviceModels;
|
||||
|
||||
public ?int $pause_duration;
|
||||
public ?int $pause_duration = null;
|
||||
|
||||
public ?string $pause_until_date = null;
|
||||
|
||||
public ?string $pause_until_time = null;
|
||||
|
||||
protected $rules = [
|
||||
'mac_address' => 'required',
|
||||
@@ -40,7 +47,7 @@ new class extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->devices = auth()->user()->devices;
|
||||
$this->devices = auth()->user()->devices()->with('deviceModel')->get();
|
||||
$this->deviceModels = DeviceModel::orderBy('label')->get()->sortBy(function ($deviceModel) {
|
||||
// Put TRMNL models at the top, then sort alphabetically within each group
|
||||
$isTrmnl = str_starts_with($deviceModel->label, 'TRMNL');
|
||||
@@ -51,12 +58,25 @@ new class extends Component
|
||||
return view('livewire.devices.manage');
|
||||
}
|
||||
|
||||
public function updatedDeviceModelId(): void
|
||||
#[Computed]
|
||||
public function timezone(): string
|
||||
{
|
||||
// Convert empty string to null for custom selection
|
||||
if (empty($this->device_model_id)) {
|
||||
return auth()->user()->preferredTimezone();
|
||||
}
|
||||
|
||||
public function updated(string $property): void
|
||||
{
|
||||
if ($property === 'device_model_id' && empty($this->device_model_id)) {
|
||||
$this->device_model_id = null;
|
||||
}
|
||||
|
||||
if ($property === 'pause_duration' && $this->pause_duration !== null) {
|
||||
$this->pause_until_date = $this->pause_until_time = null;
|
||||
}
|
||||
|
||||
if (in_array($property, ['pause_until_date', 'pause_until_time'], true) && $this->{$property}) {
|
||||
$this->pause_duration = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function createDevice(): void
|
||||
@@ -87,7 +107,7 @@ new class extends Component
|
||||
$this->reset();
|
||||
Flux::modal('create-device')->close();
|
||||
|
||||
$this->devices = auth()->user()->devices;
|
||||
$this->devices = auth()->user()->devices()->with('deviceModel')->get();
|
||||
Flux::toast(variant: 'success', text: 'Device created successfully.');
|
||||
}
|
||||
|
||||
@@ -103,18 +123,47 @@ new class extends Component
|
||||
// }
|
||||
}
|
||||
|
||||
public function pauseDevice($deviceId): void
|
||||
public function pauseDevice(int $deviceId): void
|
||||
{
|
||||
$this->validate([
|
||||
'pause_duration' => 'required|integer',
|
||||
]);
|
||||
$device = auth()->user()->devices()->findOrFail($deviceId);
|
||||
$pauseUntil = now()->addMinutes($this->pause_duration);
|
||||
$device->update(['pause_until' => $pauseUntil]);
|
||||
$this->reset('pause_duration');
|
||||
$device->update(['pause_until' => $this->resolvePauseUntil()]);
|
||||
$this->reset('pause_duration', 'pause_until_date', 'pause_until_time');
|
||||
Flux::modal('pause-device-'.$deviceId)->close();
|
||||
$this->devices = auth()->user()->devices;
|
||||
Flux::toast(variant: 'success', text: 'Device paused until '.$pauseUntil->format('H:i'));
|
||||
$this->devices = auth()->user()->devices()->with('deviceModel')->get();
|
||||
|
||||
$pauseUntil = $device->pause_until->timezone($this->timezone);
|
||||
Flux::toast(variant: 'success', text: "Device paused until {$pauseUntil} {$this->timezone}");
|
||||
}
|
||||
|
||||
public function unpauseDevice(int $deviceId): void
|
||||
{
|
||||
$device = auth()->user()->devices()->findOrFail($deviceId);
|
||||
$device->update(['pause_until' => null]);
|
||||
Flux::modal('unpause-device-'.$deviceId)->close();
|
||||
$this->devices = auth()->user()->devices()->with('deviceModel')->get();
|
||||
Flux::toast(variant: 'success', text: 'Pause cleared. Wake your device manually to resume.');
|
||||
}
|
||||
|
||||
private function resolvePauseUntil(): Carbon
|
||||
{
|
||||
if (filled($this->pause_until_date) && filled($this->pause_until_time)) {
|
||||
$now = now($this->timezone);
|
||||
$pauseUntil = Carbon::parse("{$this->pause_until_date} {$this->pause_until_time}", $this->timezone);
|
||||
|
||||
if ($pauseUntil->lte($now) || $pauseUntil->gt($now->copy()->addDays(Device::MAX_PAUSE_DAYS))) {
|
||||
throw ValidationException::withMessages([
|
||||
'pause_until_date' => $pauseUntil->lte($now)
|
||||
? 'The pause time must be in the future.'
|
||||
: 'The pause time cannot be more than '.Device::MAX_PAUSE_DAYS.' days in the future.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $pauseUntil->utc();
|
||||
}
|
||||
|
||||
$this->validate(['pause_duration' => 'required|integer']);
|
||||
|
||||
return now()->addMinutes((int) $this->pause_duration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,9 +314,11 @@ new class extends Component
|
||||
<flux:button href="{{ route('devices.configure', $device) }}" wire:navigate icon="eye" iconVariant="outline">
|
||||
</flux:button>
|
||||
@if($device->isPauseActive())
|
||||
<flux:tooltip content="Device paused until: {{ $device->pause_until?->format('H:i') }}">
|
||||
<flux:button icon="pause-circle"/>
|
||||
</flux:tooltip>
|
||||
<flux:modal.trigger name="unpause-device-{{ $device->id }}">
|
||||
<flux:tooltip content="Device paused until: {{ $device->pause_until->diffForHumans() }}">
|
||||
<flux:button icon="pause-circle"/>
|
||||
</flux:tooltip>
|
||||
</flux:modal.trigger>
|
||||
@else
|
||||
<flux:modal.trigger name="pause-device-{{ $device->id }}">
|
||||
<flux:button icon="pause-circle" iconVariant="outline">
|
||||
@@ -314,6 +365,26 @@ new class extends Component
|
||||
<flux:radio value="480" label="480 min"/>
|
||||
</flux:radio.group>
|
||||
</div>
|
||||
|
||||
<flux:separator text="or" class="my-4"/>
|
||||
|
||||
<div class="mb-4 grid grid-cols-2 gap-4">
|
||||
<flux:input
|
||||
type="date"
|
||||
label="Date"
|
||||
wire:model.live="pause_until_date"
|
||||
min="{{ now($this->timezone)->toDateString() }}"
|
||||
max="{{ now($this->timezone)->addDays(\App\Models\Device::MAX_PAUSE_DAYS)->toDateString() }}"
|
||||
/>
|
||||
<flux:input
|
||||
type="time"
|
||||
label="Time"
|
||||
wire:model.live="pause_until_time"
|
||||
/>
|
||||
</div>
|
||||
<flux:text class="text-sm text-zinc-500 mb-4">Timezone: {{ $this->timezone }}.</flux:text>
|
||||
<flux:error name="pause_until_date"/>
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:modal.close>
|
||||
@@ -324,5 +395,36 @@ new class extends Component
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
<flux:modal name="unpause-device-{{ $device->id }}">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">Pause Active</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:callout variant="info" icon="pause-circle">
|
||||
<flux:callout.heading>Paused until {{ $device->pause_until?->timezone($this->timezone) }} {{ $this->timezone }}</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
@if($device->usesTouchBar())
|
||||
To exit pause early, click "End pause" and press the touch bar in the middle of your device.
|
||||
@else
|
||||
To exit pause early, click "End pause" and press the physical screen button on your device.
|
||||
@endif
|
||||
</flux:callout.text>
|
||||
<x-slot name="actions">
|
||||
<flux:button wire:click="unpauseDevice({{ $device->id }})" variant="primary">
|
||||
End pause
|
||||
</flux:button>
|
||||
</x-slot>
|
||||
</flux:callout>
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:modal.close>
|
||||
<flux:button variant="ghost">Close</flux:button>
|
||||
</flux:modal.close>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@@ -1601,6 +1601,29 @@ test('device returns sleep.png and correct refresh time when paused', function (
|
||||
expect($json['refresh_rate'])->toBeLessThanOrEqual(3600); // ~60 min
|
||||
});
|
||||
|
||||
test('device paused for long period returns refresh rate capped at 24 hours', function (): void {
|
||||
$device = Device::factory()->create([
|
||||
'mac_address' => '00:11:22:33:44:55',
|
||||
'api_key' => 'test-api-key',
|
||||
'pause_until' => now()->addDays(7),
|
||||
]);
|
||||
|
||||
$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();
|
||||
$json = $response->json();
|
||||
|
||||
expect($json['filename'])->toMatch('/^[a-f0-9-]+\.png$/');
|
||||
expect($json['image_url'])->toContain('images/generated/');
|
||||
expect($json['refresh_rate'])->toBeLessThanOrEqual(86400);
|
||||
});
|
||||
|
||||
test('screens endpoint accepts nullable file_name', function (): void {
|
||||
Queue::fake();
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceModel;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
|
||||
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
|
||||
@@ -103,3 +105,109 @@ test('user cannot toggle proxy cloud for other users devices', function (): void
|
||||
$response->assertStatus(403);
|
||||
expect($device->fresh()->proxy_cloud)->toBeFalse();
|
||||
});
|
||||
|
||||
test('user can pause device with preset duration', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$device = Device::factory()->create(['user_id' => $user->id]);
|
||||
$this->actingAs($user);
|
||||
|
||||
Carbon::setTestNow('2026-08-04 12:00:00');
|
||||
|
||||
Livewire::test('devices.manage')
|
||||
->set('pause_duration', 60)
|
||||
->call('pauseDevice', $device->id)
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($device->fresh()->pause_until?->equalTo(now()->addMinutes(60)))->toBeTrue();
|
||||
});
|
||||
|
||||
test('user can pause device with custom date and time in user timezone', function (): void {
|
||||
$user = User::factory()->create(['timezone' => 'Europe/Berlin']);
|
||||
$device = Device::factory()->create(['user_id' => $user->id]);
|
||||
$this->actingAs($user);
|
||||
|
||||
Carbon::setTestNow(Carbon::parse('2026-08-04 10:00:00', 'UTC'));
|
||||
|
||||
Livewire::test('devices.manage')
|
||||
->set('pause_until_date', '2026-08-10')
|
||||
->set('pause_until_time', '14:30')
|
||||
->call('pauseDevice', $device->id)
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($device->fresh()->pause_until?->utc()->format('Y-m-d H:i'))->toBe('2026-08-10 12:30');
|
||||
});
|
||||
|
||||
test('custom pause rejects datetime more than 30 days in the future', function (): void {
|
||||
$user = User::factory()->create(['timezone' => 'UTC']);
|
||||
$device = Device::factory()->create(['user_id' => $user->id]);
|
||||
$this->actingAs($user);
|
||||
|
||||
Carbon::setTestNow('2026-08-04 12:00:00');
|
||||
|
||||
Livewire::test('devices.manage')
|
||||
->set('pause_until_date', '2026-09-10')
|
||||
->set('pause_until_time', '14:30')
|
||||
->call('pauseDevice', $device->id)
|
||||
->assertHasErrors(['pause_until_date']);
|
||||
|
||||
expect($device->fresh()->pause_until)->toBeNull();
|
||||
});
|
||||
|
||||
test('custom pause rejects datetime in the past', function (): void {
|
||||
$user = User::factory()->create(['timezone' => 'UTC']);
|
||||
$device = Device::factory()->create(['user_id' => $user->id]);
|
||||
$this->actingAs($user);
|
||||
|
||||
Carbon::setTestNow('2026-08-04 12:00:00');
|
||||
|
||||
Livewire::test('devices.manage')
|
||||
->set('pause_until_date', '2026-08-04')
|
||||
->set('pause_until_time', '08:00')
|
||||
->call('pauseDevice', $device->id)
|
||||
->assertHasErrors(['pause_until_date']);
|
||||
|
||||
expect($device->fresh()->pause_until)->toBeNull();
|
||||
});
|
||||
|
||||
test('user can end pause early', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$device = Device::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'pause_until' => now()->addHour(),
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test('devices.manage')
|
||||
->call('unpauseDevice', $device->id)
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($device->fresh()->pause_until)->toBeNull();
|
||||
});
|
||||
|
||||
test('unpause modal shows touch bar instructions for v2 devices', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$deviceModel = DeviceModel::query()->where('name', 'v2')->firstOrFail();
|
||||
$device = Device::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'device_model_id' => $deviceModel->id,
|
||||
'pause_until' => now()->addHour(),
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test('devices.manage')
|
||||
->assertSee('touch bar in the middle')
|
||||
->assertDontSee('physical screen button');
|
||||
});
|
||||
|
||||
test('unpause modal shows screen button instructions for non-v2 devices', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$device = Device::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'pause_until' => now()->addHour(),
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test('devices.manage')
|
||||
->assertSee('physical screen button')
|
||||
->assertDontSee('touch bar in the middle');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user