{{ __('updating.title') }}
{{ $body ?? __('updating.body') }}
diff --git a/tests/Feature/Admin/UpdateButtonTest.php b/tests/Feature/Admin/UpdateButtonTest.php index 30de3f0..7d21105 100644 --- a/tests/Feature/Admin/UpdateButtonTest.php +++ b/tests/Feature/Admin/UpdateButtonTest.php @@ -142,11 +142,12 @@ it('says a queued update is starting, and never counts down to it', function () $state = app(UpdateChannel::class)->state(); expect($state['running'])->toBeTrue() - ->and($state)->not->toHaveKey('next_check_at'); - - Livewire::actingAs(operator('Owner'), 'operator') - ->test(AdminSettings::class) - ->assertSee(__('admin_settings.update_starting')); + ->and($state)->not->toHaveKey('next_check_at') + // `instant` is what selects the sentence. Asserted here rather than on + // the settings component: the sentence moved into the full-page overlay + // in layouts/admin, because it was being shown in BOTH and an operator + // saw one panel appear and a second cover it seconds later. + ->and($state['instant'])->toBeTrue(); }); it('names the step the deployment is on, in the operator’s language', function () { @@ -161,9 +162,13 @@ it('names the step the deployment is on, in the operator’s language', function ]); writePhase('migrate'); - Livewire::actingAs(operator('Owner'), 'operator') - ->test(AdminSettings::class) - ->assertSee(__('admin_settings.update_step', [ + // Through the endpoint the overlay actually reads. That is the whole path + // now: no Livewire, no component state, no assets — because the thing being + // reported on restarts the containers serving the page. + $this->actingAs(operator('Owner'), 'operator') + ->getJson(route('admin.update.state')) + ->assertOk() + ->assertJsonPath('step', __('admin_settings.update_step', [ 'step' => __('admin_settings.update_phase.migrate'), ])); }); @@ -211,12 +216,14 @@ it('does not present the last failure’s step as this update’s progress', fun $state = app(UpdateChannel::class)->state(); expect($state['running'])->toBeTrue() - ->and($state['phase'])->toBeNull(); + ->and($state['phase'])->toBeNull() + // Queued, not instant — which is the sentence the overlay picks. + ->and($state['instant'])->toBeFalse(); - Livewire::actingAs(operator('Owner'), 'operator') - ->test(AdminSettings::class) - ->assertSee(__('admin_settings.update_starting_queued')) - ->assertDontSee(__('admin_settings.update_phase.migrate')); + // And the endpoint the overlay reads says nothing about a step either. + $this->actingAs(operator('Owner'), 'operator') + ->getJson(route('admin.update.state')) + ->assertJsonPath('step', null); }); it('ignores a step written before the run that is going on now', function () { @@ -492,10 +499,11 @@ it('tells a queued update apart from an immediate one, without either naming a t app(UpdateChannel::class)->request('owner@example.com'); - Livewire::actingAs(operator('Owner'), 'operator') - ->test(AdminSettings::class) - ->assertSee(__('admin_settings.update_starting_queued')) - ->assertDontSee(__('admin_settings.update_starting')); + // On the state, which is what the overlay in layouts/admin picks its + // sentence from. The settings card no longer says any of this — it said it + // at the same time as the overlay, in a smaller box, and an operator saw + // the page arrange itself twice. + expect(app(UpdateChannel::class)->state()['instant'])->toBeFalse(); }); it('does not believe a stale agent about being woken on demand', function () { @@ -790,3 +798,65 @@ it('keeps the deployment step in the overlay, where an operator needs it', funct ->toContain('x-text="runningSince"') ->toContain('x-text="log"'); }); + +it('has no dark mode of its own, in a product that has none', function () { + // Reported from a phone: the update screen — and only the update screen — + // came up dark blue. It carried a prefers-color-scheme block nothing else + // in this product has, so it was the one surface that flipped, and it + // flipped into Tailwind's slate ramp rather than the product's warm + // neutrals. + // Comments stripped first. The panel documents at length what it used to + // get wrong, naming the exact values — and a scan over the raw file reads + // that explanation as the offence it warns about. Twice now this file has + // had to learn the same thing: measure the thing, not the note about it. + $panel = preg_replace( + '/\{\{--.*?--\}\}/s', + '', + Illuminate\Support\Facades\File::get(resource_path('views/partials/updating-panel.blade.php')), + ); + + expect($panel)->not->toContain('prefers-color-scheme') + // The slate values it used. Named, so a paste of the same ramp is + // caught rather than merely discouraged by a comment. + ->not->toContain('#0f172a') + ->not->toContain('#64748b') + ->not->toContain('#e2e8f0') + ->not->toContain('#0b0f19') + // And the tokens it should be using instead. + ->toContain('#f6f6f8') + ->toContain('#17171c') + ->toContain('#6e6e7a'); +}); + +it('shows the step in one place, not in two of different sizes', function () { + // The settings card grew its own little "läuft gerade" block the instant a + // run started, and up to three seconds later the full-page overlay covered + // it saying the same thing. Reported as "first a small window, then the big + // one". + $card = Illuminate\Support\Facades\File::get(resource_path('views/livewire/admin/settings.blade.php')); + $layout = Illuminate\Support\Facades\File::get(resource_path('views/layouts/admin.blade.php')); + + expect($card)->not->toContain("admin_settings.update_offline_hint") + ->and($layout)->toContain('admin_settings.update_offline_hint'); +}); + +it('opens the overlay on the click, not on the next poll', function () { + // Three seconds is long enough to see the page arrange itself twice. + $settings = Illuminate\Support\Facades\File::get(app_path('Livewire/Admin/Settings.php')); + $layout = Illuminate\Support\Facades\File::get(resource_path('views/layouts/admin.blade.php')); + + expect($settings)->toContain("dispatch('update-started')") + ->and($layout)->toContain('@update-started.window="wasRunning = true"'); +}); + +it('does not cover the console because a button was pressed over nothing', function () { + // The overlay opens on the event, so the event must only fire once a run is + // actually pending. A second press, which the channel refuses, must not + // black out the page again. + writeStatus(['state' => 'idle', 'checked_at' => now()->toIso8601String(), 'behind' => 1]); + + $component = Livewire::actingAs(operator('Owner'), 'operator')->test(AdminSettings::class); + + $component->call('requestUpdate')->assertDispatched('update-started'); + $component->call('requestUpdate')->assertNotDispatched('update-started'); +});