diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index ed24c2711..4c1a2f08a 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -13,6 +13,7 @@ scrollDebounce: null, isScrolling: false, destroyed: false, + morphUpdatedCleanup: null, deploymentFinishedCleanup: null, lastTouchY: 0, showTimestamps: true, @@ -212,10 +213,8 @@ }); // Apply search after Livewire updates. - // Livewire.hook() has no deregister API, so this callback survives - // wire:navigate. It is made harmless after teardown by the - // `destroyed` guard and by only reacting to DOM inside this root. - Livewire.hook('morph.updated', ({ el }) => { + // Livewire.hook() returns an unregister fn; keep it for destroy(). + this.morphUpdatedCleanup = Livewire.hook('morph.updated', ({ el }) => { if (this.destroyed) return; if (el.id !== 'logs' || !this.$root.contains(el)) return; this.$nextTick(() => { @@ -251,6 +250,10 @@ clearTimeout(this.scrollDebounce); this.scrollDebounce = null; } + if (typeof this.morphUpdatedCleanup === 'function') { + this.morphUpdatedCleanup(); + this.morphUpdatedCleanup = null; + } if (typeof this.deploymentFinishedCleanup === 'function') { this.deploymentFinishedCleanup(); this.deploymentFinishedCleanup = null; diff --git a/tests/Feature/DeploymentLogScrollTest.php b/tests/Feature/DeploymentLogScrollTest.php index c40752542..fe472bd6f 100644 --- a/tests/Feature/DeploymentLogScrollTest.php +++ b/tests/Feature/DeploymentLogScrollTest.php @@ -94,6 +94,11 @@ function showDeployment(string $status): TestResponse ->not->toContain("document.getElementById('logsContainer')") // morph.updated hook only acts on this component's own DOM. ->toContain('this.$root.contains(el)') + // Global Livewire hook is unregistered when Alpine tears down. + ->toContain('morphUpdatedCleanup: null') + ->toContain("this.morphUpdatedCleanup = Livewire.hook('morph.updated'") + ->toContain("typeof this.morphUpdatedCleanup === 'function'") + ->toContain('this.morphUpdatedCleanup()') // Continuation timeout is tracked so it can be cancelled. ->toContain('scrollTimeout'); });