Put the open tab in the address bar, including the first one
tests / pest (push) Failing after 11m2s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Has been skipped Details

Reported as "die Tabs fehlen in der Query beim Kunden". The binding was
there and identical to the settings page — checked in the rendered
snapshot, which is where Livewire's JS reads it from — but `except` hid the
parameter while the DEFAULT tab was open. So arriving on a customer showed
no ?tab= at all, and a link to the page could not say which tab it meant
unless the tab happened not to be the first one. From the address bar that
is indistinguishable from the feature not existing.

No `except` now, on all three tabbed pages: the parameter is there from the
first render. And `history: true`, so each tab is a step the back button can
take rather than a silent replaceState — which also makes the change
unmistakable while clicking.

Asserted against the rendered page rather than the component, because the
parameter is written client-side from the `url` memo in the snapshot: the
memo IS the behaviour, and asserting the property would have passed
throughout the bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/betriebsmodus v1.3.43
nexxo 2026-07-29 23:46:13 +02:00
parent 0a89695189
commit 518bc9aa41
6 changed files with 67 additions and 6 deletions

View File

@ -1 +1 @@
1.3.42
1.3.43

View File

@ -50,7 +50,17 @@ class CustomerDetail extends Component
public Customer $customer;
#[Url(except: 'overview')]
/**
* Which tab is open, in the address bar.
*
* `history: true`, so each tab is a step the back button can take and no
* `except`, so the parameter is there from the first render rather than
* appearing only once somebody leaves the default tab. Hiding it on the
* default read as "the tab is not in the URL", which is exactly how it was
* reported: a link to this page could not say which tab it meant unless the
* tab happened not to be the first one.
*/
#[Url(history: true)]
public string $tab = 'overview';
// ---- Writing to them ----

View File

@ -76,7 +76,17 @@ class Integrations extends Component
*/
public const TABS = ['services', 'platform', 'env'];
#[Url(except: 'services')]
/**
* Which tab is open, in the address bar.
*
* `history: true`, so each tab is a step the back button can take and no
* `except`, so the parameter is there from the first render rather than
* appearing only once somebody leaves the default tab. Hiding it on the
* default read as "the tab is not in the URL", which is exactly how it was
* reported: a link to this page could not say which tab it meant unless the
* tab happened not to be the first one.
*/
#[Url(history: true)]
public string $tab = 'services';
use ConfirmsPassword {

View File

@ -64,7 +64,17 @@ class Settings extends Component
* needs no parameter, and a query string that appears the moment a page
* loads makes every link to it look different from the one in the menu.
*/
#[Url(except: 'installation')]
/**
* Which tab is open, in the address bar.
*
* `history: true`, so each tab is a step the back button can take and no
* `except`, so the parameter is there from the first render rather than
* appearing only once somebody leaves the default tab. Hiding it on the
* default read as "the tab is not in the URL", which is exactly how it was
* reported: a link to this page could not say which tab it meant unless the
* tab happened not to be the first one.
*/
#[Url(history: true)]
public string $tab = 'installation';
/** This page changes the signed-in OPERATOR's password, never a portal one. */

View File

@ -34,7 +34,7 @@ beforeEach(function () {
]);
});
it('opens on the details, and keeps that tab out of the address bar', function () {
it('opens on the details', function () {
Livewire::actingAs(operator('Owner'), 'operator')
->test(CustomerDetail::class, ['uuid' => $this->customer->uuid])
->assertSet('tab', 'overview')

View File

@ -13,7 +13,7 @@ use Livewire\Livewire;
* refreshes itself while an update runs, so a tab kept only in the component
* would drop them back to the first one mid-deployment.
*/
it('opens the installation tab by default and keeps it out of the address bar', function () {
it('opens the installation tab by default', function () {
Livewire::actingAs(operator('Owner'), 'operator')
->test(Settings::class)
->assertSet('tab', 'installation')
@ -133,3 +133,34 @@ it('tabs the integrations page the same way, and keeps the raw file to the Owner
->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"/');
}
});