I am sorry to drop here a description from ai chatbot (claude code), but this bug haunted me for years.
I did not even know it was from this package, until i tried the fix.
I just post the description here in case it is useful for other developers.
Description
When the Alert output is enabled (the default), feature tests that use Laravel's view assertions can fail with:
The response is not a view.
…even though the response is HTTP 200, the session has no errors, and the controller did return a View.
Root cause
BeyondCode\QueryDetector\Outputs\Alert::output() injects the JS alert by calling:
$response->setContent($content);
Illuminate\Http\Response::setContent() unconditionally executes:
$this->original = $content;
at the top of the method. When $content is a string (as it is in Alert::output()), the original View object that Laravel had stored in $response->original is replaced by a raw string. From that point on, TestResponse::assertViewIs() / assertViewHas() / assertViewMissing() report "The response is not a view.", because they check $response->original instanceof View.
This is the same class of bug Livewire had with its auto-injected scripts, which they fixed in v3.4.0 (livewire/livewire PR #7758) by preserving $response->original after setContent.
Reproduction
-
Laravel app with beyondcode/laravel-query-detector installed, QUERY_DETECTOR_ENABLED=true, and Alert::class listed in config/querydetector.php output.
-
A route returning view('foo') that triggers ≥ threshold N+1 queries.
-
Test:
$response = $this->get('/foo');
$response->assertStatus(200);
$response->assertViewIs('foo'); // ❌ "The response is not a view."
The failure happens only when N+1 queries are actually detected (i.e. the Alert output runs). Below the threshold the test passes, which makes it look intermittent.
Symptoms that make this hard to diagnose
assertStatus(200) passes.
assertSessionHasNoErrors() passes.
- Nothing is logged to
laravel.log if only Alert::class is configured.
- The error message ("The response is not a view") points at the test, not the package, so users blame their view / Livewire / @include / a recent refactor.
Suggested fixes (in order of preference)
-
Restore $response->original after setContent inside Alert::output():
$original = $response->original;
$response->setContent($content);
$response->original = $original;
$response->headers->remove('Content-Length');
This matches the approach Livewire adopted in v3.4.0.
-
Skip the Alert output in the testing environment, e.g. early-return when app()->runningUnitTests() (or when app()->environment('testing')). The injected JS alert() is never visible during tests anyway.
-
Document the caveat in docs/usage.md: recommend setting QUERY_DETECTOR_ENABLED=false for the testing environment (via phpunit.xml <env> or .env.testing) until a fix lands.
Workaround I'm using (until a fix is released)
Adding to phpunit.xml:
<env name="QUERY_DETECTOR_ENABLED" value="false"/>
This deactivates the entire detector during tests and the view assertions work again. The detector still runs normally in local/development.
Environment
- PHP 8.4
- Laravel 12
- Livewire 3 (with PowerGrid)
beyondcode/laravel-query-detector (latest)
I am sorry to drop here a description from ai chatbot (claude code), but this bug haunted me for years.
I did not even know it was from this package, until i tried the fix.
I just post the description here in case it is useful for other developers.
Description
When the
Alertoutput is enabled (the default), feature tests that use Laravel's view assertions can fail with:…even though the response is HTTP 200, the session has no errors, and the controller did return a
View.Root cause
BeyondCode\QueryDetector\Outputs\Alert::output()injects the JS alert by calling:Illuminate\Http\Response::setContent()unconditionally executes:at the top of the method. When
$contentis a string (as it is inAlert::output()), the originalViewobject that Laravel had stored in$response->originalis replaced by a raw string. From that point on,TestResponse::assertViewIs()/assertViewHas()/assertViewMissing()report "The response is not a view.", because they check$response->original instanceof View.This is the same class of bug Livewire had with its auto-injected scripts, which they fixed in v3.4.0 (livewire/livewire PR #7758) by preserving
$response->originalaftersetContent.Reproduction
Laravel app with
beyondcode/laravel-query-detectorinstalled,QUERY_DETECTOR_ENABLED=true, andAlert::classlisted inconfig/querydetector.phpoutput.A route returning
view('foo')that triggers ≥ threshold N+1 queries.Test:
The failure happens only when N+1 queries are actually detected (i.e. the
Alertoutput runs). Below the threshold the test passes, which makes it look intermittent.Symptoms that make this hard to diagnose
assertStatus(200)passes.assertSessionHasNoErrors()passes.laravel.logif onlyAlert::classis configured.Suggested fixes (in order of preference)
Restore
$response->originalaftersetContentinsideAlert::output():This matches the approach Livewire adopted in v3.4.0.
Skip the
Alertoutput in the testing environment, e.g. early-return whenapp()->runningUnitTests()(or whenapp()->environment('testing')). The injected JSalert()is never visible during tests anyway.Document the caveat in
docs/usage.md: recommend settingQUERY_DETECTOR_ENABLED=falsefor thetestingenvironment (viaphpunit.xml<env>or.env.testing) until a fix lands.Workaround I'm using (until a fix is released)
Adding to
phpunit.xml:This deactivates the entire detector during tests and the view assertions work again. The detector still runs normally in
local/development.Environment
beyondcode/laravel-query-detector(latest)