Setup Pengujian
Semua yang dilakukan panel pada dasarnya adalah request Laravel biasa. Karena itu, sebagian besar test suite panel menggunakan hal-hal yang sudah familiar: actingAs(), factory, get(), dan assertion terhadap database. Package menambahkan sejumlah helper yang dimuat otomatis untuk menjawab pertanyaan yang sulit diuji hanya melalui HTTP — "apakah pengguna ini melihat row tersebut", "apakah field ini wajib", "apakah action ini dapat dijalankan" — serta satu prinsip tentang apa yang layak diuji. Halaman ini menjelaskan cara menyiapkan suite sampai helper tersebut siap digunakan.
Contoh minimal yang berfungsi
Tidak ada yang perlu di-install dan tidak ada helper yang perlu di-import:
<?php
declare(strict_types=1);
use App\Models\User;
use App\Panels\Admin\Resources\Users\UserResource;
it('lists users to an administrator and refuses everybody else', function (): void {
$admin = User::factory()->create(['is_admin' => true]);
$member = User::factory()->create();
$this->actingAs($admin)->get('/admin/users')->assertOk();
$this->actingAs($member)->get('/admin/users')->assertForbidden();
$this->actingAs($admin);
panelTable(UserResource::class)
->assertCanSeeRecord($admin)
->assertCanSeeRecord($member);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
panelTable() tidak membutuhkan statement use. Helper ini adalah free function yang didaftarkan melalui autoload.files milik Composer, sehingga autoloader memuatnya di setiap process yang membutuhkan package ini — termasuk process test — tanpa base class, trait, maupun service provider khusus.
Apa yang dikirim package dan dari mana asalnya
composer.json mendeklarasikan dua file yang dimuat otomatis:
"autoload": {
"psr-4": { "PandaPanel\\": "src/" },
"files": [
"src/Support/helpers.php",
"src/Testing/helpers.php"
]
}2
3
4
5
6
7
src/Testing/helpers.php mendefinisikan dua belas function. Masing-masing dilindungi function_exists dan mendelegasikan pekerjaan ke empat public class:
| Class | Free function |
|---|---|
PandaPanel\Testing\TestsTables | panelTable() |
PandaPanel\Testing\TestsSchemas | panelForm(), panelInfolistLabels() |
PandaPanel\Testing\TestsActions | panelRecordActions(), panelTableActions(), panelBulkActions(), panelInfolistActions() |
PandaPanel\Testing\TestsNotifications | fakePanelNotifications(), assertPanelNotificationSentTo(), assertNoPanelNotifications(), assertPanelNotificationStoredFor(), assertNoPanelNotificationsStoredFor() |
Helper tersebut dikirim bersama package, bukan hanya disimpan di directory tests/ repository ini, karena pertanyaan yang dijawab sama pentingnya di suite aplikasi pengguna. Lihat Helper pengujian untuk seluruh signature.
Test suite aplikasi
Aplikasi Laravel sudah memiliki test suite. Panel tidak membutuhkan konfigurasi tambahan selain dua hal yang memang dibutuhkan feature test: database dan pengguna yang sudah login.
// tests/Pest.php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
pest()
->extend(TestCase::class)
->use(RefreshDatabase::class)
->in('Feature');2
3
4
5
6
7
8
9
Panel membaca auth()->user() untuk setiap pertanyaan otorisasi — gate milik panel, resource policy, closure authorize() milik action, hingga disabledUsing() pada editable column. Test yang lupa memanggil actingAs() berarti sedang mengajukan seluruh pertanyaan tersebut kepada guest, sehingga jawabannya hampir selalu "tidak".
use App\Models\User;
beforeEach(function (): void {
$this->admin = User::factory()->create(['is_admin' => true]);
$this->actingAs($this->admin);
});2
3
4
5
6
7
Panel context di luar request
Middleware ResolvePanel mengikat current panel pada awal setiap route panel. Helper yang dipanggil di luar request tidak memiliki middleware di belakangnya. Akibatnya, fitur yang bergantung pada current panel — konfigurasi resource per-panel, tenancy scoping, strict authorization — akan membaca kondisi "tidak ada panel" kecuali test menetapkannya sendiri.
use PandaPanel\Core\PanelManager;
app(PanelManager::class)->setCurrentPanel(panel('admin'));2
3
| Pemanggilan | Signature | Mengembalikan |
|---|---|---|
panel() | panel(?string $id = null): ?Panel | current panel, atau null di luar panel |
panel('admin') | — | panel tersebut; melempar PanelRegistrationException jika id tidak terdaftar |
PanelManager::setCurrentPanel() | setCurrentPanel(?Panel $panel): void | — |
PanelManager::currentPanel() | currentPanel(): ?Panel | panel yang sedang terikat |
PanelManager::has() | has(string $id): bool | apakah panel dengan id tersebut terdaftar |
PanelManager::get() | get(string $id): Panel | panel, atau melempar exception |
Lakukan ini di beforeEach untuk file test yang memanggil helper secara langsung, bukan melalui URL:
use PandaPanel\Core\PanelManager;
beforeEach(function (): void {
app(PanelManager::class)->setCurrentPanel(panel('admin'));
$this->actingAs(User::factory()->create(['is_admin' => true]));
});2
3
4
5
6
7
Mengembalikan current panel ke null adalah cara menguji ketiadaan perilaku yang bergantung pada panel:
app(PanelManager::class)->setCurrentPanel(null);
// A tenant-scoped resource is unscoped here, because tenancy is a property
// of the panel rather than of the resource.
expect(DocumentResource::query()->count())->toBe(3);2
3
4
5
Membaca response Inertia
Page panel adalah response Inertia. Ada dua cara membaca response dan keduanya melihat bagian berbeda:
use Inertia\Testing\AssertableInertia;
$this->get('/admin/users')
->assertInertia(fn (AssertableInertia $page) => $page
->component('panel/resources/Index')
->where('state.sort', null)
->has('rows', 10));2
3
4
5
6
7
// The raw page object, which is where flash data lives — beside `props`,
// not inside it, so the Inertia assertions above cannot reach it.
$page = $this->get('/admin/users')->viewData('page');
$rows = $page['props']['rows'];
$toast = $page['flash']['toast'] ?? null;2
3
4
5
6
Mengambil satu column dari rows adalah pola yang paling sering digunakan di test suite repository ini:
/**
* @return list<string>
*/
function namesOn(string $url): array
{
return collect(test()->get($url)->viewData('page')['props']['rows'])
->pluck('cells.name.value')
->all();
}2
3
4
5
6
7
8
9
Digunakan cells.name.value, bukan cells.name, karena name adalah TextInputColumn pada aplikasi contoh. Editable cell diserialisasi sebagai ['value' => …, 'disabled' => …]. Lihat Pengujian tabel.
Partial reload — mekanisme yang digunakan lazy widget untuk mengambil data — membutuhkan asset version. Tanpanya, Inertia menjawab 409 dan meminta browser melakukan full visit:
$version = $this->get('/admin')->viewData('page')['version'];
$this->get('/admin', [
'X-Inertia' => 'true',
'X-Inertia-Version' => $version,
'X-Inertia-Partial-Component' => 'panel/Dashboard',
'X-Inertia-Partial-Data' => 'widgetData',
])->assertOk();2
3
4
5
6
7
8
Mendaftarkan panel di dalam test
Test yang membutuhkan panel yang tidak dimiliki aplikasi — misalnya resource dengan scope khusus, panel tenancy, atau fixture yang sengaja memiliki satu konfigurasi salah — dapat membangun panel sendiri dan mendaftarkan route-nya:
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelManager;
use PandaPanel\Routing\PanelRouteRegistrar;
beforeEach(function (): void {
$manager = app(PanelManager::class);
if (! $manager->has('scope-host')) {
$panel = $manager->register(
Panel::make('scope-host')
->path('scope-host')
->settings(false)
->resources([ScopedUserResource::class]),
);
app(PanelRouteRegistrar::class)->register($panel);
}
$manager->setCurrentPanel($manager->get('scope-host'));
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Ada tiga hal penting di sini:
if (! $manager->has(...)). Registry bertahan antar-test dalam satu process; mendaftarkan panel yang sama dua kali akan melempar exception.->settings(false)menghilangkan tiga account page yang secara default dimiliki setiap panel, sehingga discovery assertion hanya membahas apa yang memang dideklarasikan fixture.PanelRouteRegistrar::register()membuat panel benar-benar dapat dijangkau melalui URL.Panel::make()sendiri hanya membuat object, bukan route. Jika test kemudian mencari route berdasarkan nama, panggilRoute::getRoutes()->refreshNameLookups()setelah registration.
Menjalankan suite
Script bawaan package dari composer.json:
| Command | Yang dijalankan |
|---|---|
composer test | vendor/bin/pest |
composer test-coverage | vendor/bin/pest --coverage |
composer analyse | vendor/bin/phpstan analyse --memory-limit=1G |
composer format | vendor/bin/pint |
composer format-check | vendor/bin/pint --test |
composer ci | format-check, lalu analyse, lalu test |
vendor/bin/pest tests/Feature/Panel
vendor/bin/pest --filter=ResourceQuery
vendor/bin/pest --compact2
3
Frontend diperiksa melalui script package.json, dan tidak ada job PHP yang dapat menggantikannya:
npm ci
npm run format:check
npm run lint
npm run typecheck
npm run build
npm run ci # all four, in that order2
3
4
5
6
Lihat Matrix CI untuk cara command tersebut dikombinasikan di GitHub Actions.
Cara test suite package ini dibangun
Bagian ini berguna jika Anda berkontribusi pada package atau ingin membuat package sendiri yang menguji panel dengan pola serupa. Isi phpunit.xml:
<testsuites>
<testsuite name="PandaPanel Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include><directory>src</directory></include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="testing"/>
</php>2
3
4
5
6
7
8
9
10
11
12
tests/TestCase.php mewarisi Orchestra\Testbench\TestCase dan melakukan lima hal:
| Method | Alasan |
|---|---|
getPackageProviders($app) | mendaftarkan Inertia, Fortify, PandaPanelServiceProvider, dan Fortify provider milik aplikasi contoh |
applicationBasePath() | mengarahkan base_path() ke root package sehingga icon registry, generator stub, dan TypeScript yang dipublish adalah file nyata, bukan skeleton kosong Testbench |
resolveApplicationConfiguration($app) | useAppPath(examples/app), useDatabasePath(examples/database), useBootstrapPath(…/testbench-core/laravel/bootstrap), useStoragePath(build/testbench/storage) |
defineEnvironment($app) | sqlite :memory:, array session dan cache, sync queue, array mail, binding FakeVite, serta dua panel contoh di panda-panel.panels |
defineRoutes($router) / defineDatabaseMigrations() | route dan migration milik aplikasi contoh, ditambah milik Fortify, Passkeys, dan package |
tests/Pest.php memanggil TestCase::prepareWritableDirectories() sebelum aplikasi pertama dibangun. Laravel menulis package manifest ketika aplikasi masih dalam proses konstruksi, sehingga directory target harus sudah tersedia sebelum framework mencoba menulis ke sana.
Sisi aplikasi berada di examples/, dan hanya untuk development di-autoload di bawah namespace App\. Menggunakan example sebagai test application memastikan contoh tersebut benar-benar dieksekusi oleh suite dan tidak membusuk sebagai code yang tidak pernah dijalankan: App\Models\User, App\Panels\Admin, App\Panels\App, dan App\Policies\UserPolicy. Setiap snippet di halaman testing yang menyebut UserResource menunjuk resource yang benar-benar dijalankan suite.
FakeVite di-bind menggantikan Illuminate\Foundation\Vite, sehingga test PHP tidak bergantung pada npm run build sudah dijalankan sebelumnya.
Hal yang perlu diperhatikan
- Panggil
actingAs()sebelum helper. Semua helper mengajukan pertanyaan otorisasi terhadap pengguna saat ini. Helper yang dipanggil sebelumactingAs()sedang mengujinya sebagai guest. - Tetapkan panel ketika tidak sedang membuat request. Tanpa
setCurrentPanel(), konfigurasi per-panel dan tenancy berperilaku seperti tidak ada panel. Itu kondisi valid, tetapi biasanya bukan kondisi yang ingin diuji. panel:cachetidak terlibat. Manifest adalah optimisasi production. Test melakukan discovery dari disk, sehingga resource yang ditambahkan di tengah suite tetap dapat ditemukan.- Flash bukan prop.
assertInertia()tidak dapat melihatnya. BacaviewData('page')['flash']. - Registry melempar exception pada duplikasi. Lindungi registration fixture dengan
has(). Dua resource yang mengklaim slug sama, atau page dengan slug yang bertabrakan dengan resource, menghasilkanPanelRegistrationExceptionsaat registration. Event::fake()tanpa argumen merusak panel. Ia membungkam model event yang dibutuhkan resource.fakePanelNotifications()sengaja hanya mem-fake satu event class.