test(Settings::class) ->assertSet('tab', 'installation') ->assertSee(__('admin_settings.update_title')); }); it('remembers the tab across a reload, because it is in the URL', function () { // The whole point of #[Url]. Without it a refresh — or the browser's back // button — lands on the first tab every time. Livewire::withQueryParams(['tab' => 'account']) ->actingAs(operator('Owner'), 'operator') ->test(Settings::class) ->assertSet('tab', 'account') ->assertSee(__('admin_settings.account_title')) ->assertDontSee(__('admin_settings.update_title')); }); it('falls back to the first tab when the URL names one that does not exist', function () { // A query string is a string a stranger typed. Anything unknown must not // render a settings page with no section on it at all. Livewire::withQueryParams(['tab' => 'wat']) ->actingAs(operator('Owner'), 'operator') ->test(Settings::class) ->assertSet('tab', 'installation') ->assertSee(__('admin_settings.update_title')); }); it('carries two-factor enrolment as a tab rather than a page of its own', function () { Livewire::withQueryParams(['tab' => 'two-factor']) ->actingAs(operator('Owner'), 'operator') ->test(Settings::class) ->assertSet('tab', 'two-factor') ->assertSee(__('two_factor_setup.title')); }); it('does not offer the team tab to somebody who cannot manage staff', function () { // A tab that raises a 403 is worse than one that is not there — and an // operator who reaches ?tab=team anyway lands on the first tab rather than // on a page with nothing on it. $support = operator('Support'); expect($support->can('staff.manage'))->toBeFalse(); Livewire::withQueryParams(['tab' => 'team']) ->actingAs($support, 'operator') ->test(Settings::class) ->assertSet('tab', 'installation') ->assertViewHas('tabs', fn (array $tabs) => ! in_array('team', $tabs, true)); }); it('lists every tab it can render, and renders every tab it lists', function () { // The const IS the schema: it validates the query string, builds the bar // and decides what renders. A tab in the list with no label is a button // that says "admin_settings.tab.x". foreach (Settings::TABS as $tab) { $key = 'admin_settings.tab.'.str_replace('-', '_', $tab); expect(__($key))->not->toBe($key); } }); it('moves the tunnel to System and drops the two-factor entry from the menu', function () { // VPN is not a task of the day's operations — it is how the console reaches // the estate at all, so it belongs beside the other things that have to // work first. Two-factor left the menu entirely: it is a tab now. $groups = collect(Navigation::console()); $routesIn = fn (string $label) => $groups ->firstWhere('label', __($label))['items'] ?? []; $system = collect($routesIn('admin.nav_group.system'))->pluck(0); $operations = collect($routesIn('admin.nav_group.operations'))->pluck(0); expect($system)->toContain('admin.vpn') ->and($operations)->not->toContain('admin.vpn') ->and($groups->pluck('items')->flatten(1)->pluck(0)) ->not->toContain('admin.two-factor-setup'); }); it('keeps the standalone enrolment route, because the gate sends people there', function () { // RequireOperatorTwoFactor redirects an operator who has not enrolled yet, // and that operator may not open anything else — the settings page // included. Folding the page away entirely would have locked them out. expect(route('admin.two-factor-setup'))->toBeString(); $this->actingAs(operator('Owner'), 'operator') ->get(route('admin.two-factor-setup')) ->assertOk() ->assertSee(__('two_factor_setup.title')); }); it('tabs the integrations page the same way, and keeps the raw file to the Owner', function () { // Same treatment, same reason: one narrow column of six cards had an // operator scrolling past Stripe to reach DNS, with the raw .env editor at // the bottom of everything. $owner = operator('Owner'); Livewire::actingAs($owner, 'operator') ->test(App\Livewire\Admin\Integrations::class) ->assertSet('tab', 'services') ->assertViewHas('tabs', fn (array $tabs) => in_array('env', $tabs, true)); foreach (App\Livewire\Admin\Integrations::TABS as $tab) { expect(__('integrations.tab.'.$tab))->not->toBe('integrations.tab.'.$tab); } // An Admin can reach the page for the DNS zone but not the file that holds // every credential on the machine. $admin = operator('Admin'); expect($admin->can('hosts.manage'))->toBeTrue() ->and($admin->can('secrets.manage'))->toBeFalse(); Livewire::withQueryParams(['tab' => 'env']) ->actingAs($admin, 'operator') ->test(App\Livewire\Admin\Integrations::class) ->assertSet('tab', 'services') ->assertViewHas('tabs', fn (array $tabs) => ! in_array('env', $tabs, true)); }); it('puts the open tab in the address bar from the first render, on every tabbed page', function () { // Reported as "die Tabs fehlen in der Query beim Kunden". The binding was // there, but `except` hid the parameter while the default tab was open — so // a link to the page could not say which tab it meant unless the tab // happened not to be the first one, and the address bar looked like the // feature was missing. // // Asserted against the rendered page rather than the component: the // parameter is written by Livewire's JS from the `url` memo in the snapshot, // so the memo IS the behaviour. $customer = App\Models\Customer::factory()->create(); $pages = [ route('admin.settings'), route('admin.integrations'), route('admin.customer', $customer->uuid), ]; foreach ($pages as $url) { $html = html_entity_decode( (string) $this->actingAs(operator('Owner'), 'operator')->get($url)->getContent() ); expect($html)->toContain('"url":{"tab":') // No `except`: shown even on the default tab. ->and($html)->toMatch('/"tab":\{[^}]*"except":null/') // push, not replace: each tab is a step the back button can take. ->and($html)->toMatch('/"tab":\{[^}]*"use":"push"/'); } });