orchestra/testbench-browser-kit
BrowserKit Testbench component helps package developers write Laravel tests using laravel/browser-kit-testing. Swap your base TestCase for Orchestra\Testbench\BrowserKit\TestCase and use the fluent visit, see, and form APIs.
Reach for this package when your package (not a full app) ships routes, views, controllers or form-driven flows, and you want to exercise them the legacy BrowserKit way: visit(), click(), type(), press(). It is a thin bridge that swaps Testbench's base TestCase for one that includes laravel/browser-kit-testing's MakesHttpRequests + InteractsWithPages traits, so a package skeleton gets a real HTTP kernel, a session, and a DomCrawler to poke at.
Install (dev only), matching your Laravel major — not your Testbench major:
| Laravel | testbench-browser-kit |
|---|---|
| 6.x | 4.x |
| 7.x | 5.x |
| 8.x | 6.x |
| 9.x | 7.x |
| 10.x | 8.x |
| 11.x | 9.x |
| 12.x | 10.x |
composer require --dev "orchestra/testbench-browser-kit"
Look first at: the README "Usages" section (it is the whole API surface, inherited from Laravel's BrowserKit docs), then Orchestra\Testbench\BrowserKit\TestCase — it extends Orchestra\Testbench\TestCase, so every Testbench hook (getPackageProviders, getEnvironmentSetUp, defineRoutes, RefreshDatabase, …) still applies. There is nothing else to the package.
Minimal working setup — one base TestCase for the whole suite:
<?php
namespace Acme\Blog\Tests;
use Orchestra\Testbench\BrowserKit\TestCase as BaseTestCase;
use Illuminate\Routing\Router;
abstract class TestCase extends BaseTestCase
{
// Required: no default, you get "no host" style failures without it.
public $baseUrl = 'http://localhost';
protected function getPackageProviders($app)
{
return [\Acme\Blog\BlogServiceProvider::class];
}
protected function getEnvironmentSetUp($app)
{
$app['config']->set('view.paths', [__DIR__.'/fixtures/views']);
$app['config']->set('app.key', 'base64:'.base64_encode(random_bytes(32)));
}
protected function defineRoutes($router) // Testbench 8+
{
$router->middleware('web')->group(function (Router $router) {
$router->get('/', 'Acme\Blog\Http\Controllers\PostController@index')
->name('posts.index');
});
require __DIR__.'/fixtures/routes.php'; // if you keep them in a file
}
}
First use case — prove a package route renders package views:
public function test_index_lists_posts()
{
$this->visitRoute('posts.index')
->assertResponseOk()
->see('All posts');
}
If you are on an older Testbench without defineRoutes, register routes in setUp() after parent::setUp() via $this->app['router']->get(...) — same effect.
Layered base test case. Package tests rarely need every hook; put routing + provider + config in an abstract TestCase, then create thin subclasses per feature (BrowserTestCase, ApiTestCase) that add traits like RefreshDatabase. Because BrowserKit state (session, cookies, crawler) persists per test method, subclasses stay clean.
Chained page flows. The value over plain $this->get() is stateful interaction: one test walks the whole journey.
public function test_new_user_registration()
{
$this->visit('/register')
->type('Taylor', 'name')
->type('[email protected]', 'email')
->check('terms')
->select('GB', 'country')
->press('Register')
->seePageIs('/dashboard')
->seeRouteIs('dashboard')
->see('Hello, Taylor');
}
Auth via actingAs, not manual session. For package tests, define a minimal guard in getEnvironmentSetUp (or reuse your package's model) and then:
$this->actingAs($user) // default guard
->actingAs($user, 'api') // named guard
->withSession(['foo' => 'bar'])
->visit('/admin')
->see('Admin');
withSession() before the first visit() is the cheap way to pre-seed flashed data or CSRF tokens.
JSON endpoints — stay in the same test case. json(), get(), post() etc. chain into seeJson, seeJsonEquals, seeJsonStructure. Use seeJsonStructure with * for collection shapes; it is tolerant of extra keys, which keeps package tests from breaking when you add a field.
$this->json('POST', '/api/users', ['name' => 'Sally'])
->seeJsonStructure(['data' => ['*' => ['id', 'name', 'email']]]);
Mix chain API with raw assertions. When you need the actual response object (headers, status, view data), call() returns an Illuminate\Http\Response while still populating the crawler for later see() calls:
$response = $this->call('POST', '/posts', ['title' => 'Hi']);
$this->assertEquals(302, $response->status());
$this->assertRedirectedToRoute('posts.show', ['post' => 1]);
$this->assertSessionHas('status');
Compose with PHPUnit assertions and view inspection. assertViewHas, assertViewHasAll, assertViewMissing, assertHasOldInput, assertSessionHasErrors are the ones package authors actually use — they let you validate that your package's view composers and validation wiring behave, without asserting HTML strings.
Reusable intent helpers. Because chain methods return $this, wrap repeated journeys:
protected function loginAsAdmin(): static
{
return $this->actingAs($this->admin)->visit('/admin');
}
orchestra/testbench-browser-kit:^9.0 is for Laravel 11. Composer will let you install a mismatch; the failure shows up as kernel/container errors at runtime, not at install time. Pin it alongside orchestra/testbench.$baseUrl is not optional. Declare public $baseUrl = 'http://localhost'; in the base test case, no trailing slash, or visit() throws about a missing host.press(). Package routes registered under the web middleware group run through VerifyCsrfToken; a simulated POST has no token. Fix with $this->withoutMiddleware() per test, the WithoutMiddleware trait on the class, or by seeding withSession(['_token' => 'test'])->type('test', '_token').see() matches raw HTML, not text. &, entity-escaped output and whitespace inside tags all break substring matches. When in doubt assert on seePageIs()/assertViewHas() or dd($this->response->getContent()) to see what the crawler actually received.press() locates the button by visible text or name. Two buttons labelled "Save" make the assertion ambiguous — pass the name attribute instead.attach() takes a filesystem path, not UploadedFile::fake(). Create real temp files in tests/fixtures, and pair with Storage::fake() so your package's upload handler has a fake disk. Clean up in tearDown() if you generate files at runtime.withoutExceptionHandling is unnecessary here), but it means an expected 500-status test must not rely on the framework's exception handler formatting.visitRoute()/seeRouteIs() fail with RouteNotFoundException if you registered routes via a raw path string or after setUp().$this->response and $this->crawler. dump($this->response->getStatusCode(), $this->response->getContent()) beats guessing which see() failed; $this->crawler->filter('form')->html() shows what the form filler sees.$this->get('/')->assertSee('Laravel')). Orchestra keeps shipping aligned releases, but treat this as a compatibility layer — write new package tests with the modern get()/assertSee/assertViewHas API, and keep BrowserKit for genuinely interactive flows (multi-step forms, link-crawl navigation) or for porting existing suites. Its practical extension points are just overriding methods on your base TestCase; there is no config file to publish.How can I help you explore Laravel packages today?