Helper Pengujian
Dua belas free function beserta empat class di baliknya dikirim bersama package dan dimuat otomatis, sehingga test tidak memerlukan import maupun base class khusus. Gunakan helper ini ketika pertanyaannya berkaitan dengan panel, bukan sekadar HTTP: row apa yang akan ditampilkan tabel, apakah sebuah field wajib, apakah action dapat dijangkau, atau apakah pengguna menerima notifikasi. Semuanya tetap melewati TableSchema, TableQuery, FormSchema, InfolistSchema, dan Action yang sebenarnya — helper ini hanya menyediakan cara yang lebih nyaman untuk bertanya, bukan implementasi kedua dari jawabannya.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
use App\Models\User;
use App\Panels\Admin\Resources\Users\UserResource;
use PandaPanel\Core\PanelManager;
beforeEach(function (): void {
app(PanelManager::class)->setCurrentPanel(panel('admin'));
$this->admin = User::factory()->create(['is_admin' => true, 'name' => 'Ada Lovelace']);
$this->actingAs($this->admin);
});
it('shows the resource, its form and its actions to an administrator', function (): void {
panelTable(UserResource::class)->assertCanSeeRecord($this->admin)->assertCount(1);
panelForm(UserResource::class)->assertFieldIsRequired('name');
panelRecordActions(UserResource::class)->assertExists('edit');
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Mengapa menggunakan function
Jika menggunakan trait, setiap test class yang membutuhkan helper harus menggunakan trait tersebut, dan cara membacanya akan berbeda antara closure Pest dan test berbasis class. Free function terlihat sama pada keduanya, dan test yang hanya membutuhkan satu helper tidak perlu ikut mengaktifkan semuanya.
Setiap function dilindungi dengan guard:
if (! function_exists('panelTable')) {
/**
* @param class-string<PanelResource> $resource
*/
function panelTable(string $resource): TestsTables
{
return TestsTables::for($resource);
}
}2
3
4
5
6
7
8
9
Artinya, aplikasi yang sudah memiliki panelTable() sendiri tetap mempertahankannya. Ini adalah escape hatch, bukan pola yang disarankan: helper dengan nama yang sama tetapi perilaku berbeda akan membingungkan siapa pun yang sudah membaca dokumentasi ini.
Semua function
| Function | Signature | Mengembalikan |
|---|---|---|
panelTable | panelTable(string $resource): TestsTables | query builder tabel beserta objek assertion |
panelForm | panelForm(string $resource, string $page = 'create'): TestsSchemas | form untuk page tersebut |
panelInfolistLabels | panelInfolistLabels(string $resource, Model $record): array | list<string> — semua label entry, sedalam apa pun nested-nya |
panelRecordActions | panelRecordActions(string $resource): TestsActions | scope row action |
panelTableActions | panelTableActions(string $resource): TestsActions | scope header, toolbar, dan empty state |
panelBulkActions | panelBulkActions(string $resource): TestsActions | scope selection |
panelInfolistActions | panelInfolistActions(string $resource): TestsActions | scope record page |
fakePanelNotifications | fakePanelNotifications(): void | — mulai merekam broadcast |
assertPanelNotificationSentTo | assertPanelNotificationSentTo(Authenticatable $user, ?string $title = null): void | — |
assertNoPanelNotifications | assertNoPanelNotifications(): void | — |
assertPanelNotificationStoredFor | assertPanelNotificationStoredFor(Authenticatable $user, ?string $title = null): void | — |
assertNoPanelNotificationsStoredFor | assertNoPanelNotificationsStoredFor(Authenticatable $user): void | — |
$resource selalu berupa class-string<PandaPanel\Resources\Resource>.
panelTable()
use PandaPanel\Tables\Filters\TernaryFilter;
panelTable(UserResource::class)
->filter(['verified' => TernaryFilter::FALSE])
->search('Grace')
->sort('name', 'asc')
->page(1)
->assertCanSeeRecord($grace)
->assertCanNotSeeRecord($ada)
->assertCount(1);2
3
4
5
6
7
8
9
10
State ditetapkan dengan cara yang sama seperti URL menetapkannya. Jadi apa pun yang dapat dilakukan helper juga dapat dilakukan request — dan apa yang tidak dapat dilakukan helper juga tidak dapat dilakukan pengguna. Referensi lengkap: Pengujian tabel.
panelForm()
panelForm(UserResource::class) // the create form
panelForm(UserResource::class, 'edit') // the edit form
panelForm(UserResource::class, 'view') // the view page's read-only schema
panelForm(UserResource::class)
->assertHasField('email')
->assertFieldIsRequired('name')
->assertDehydratesTo(['name' => 'Ada', 'unknown' => 'x'], ['name' => 'Ada']);2
3
4
5
6
7
8
Nama page adalah nilai yang dibandingkan oleh hiddenOn() dan visibleOn(). create, edit, dan view adalah tiga nama yang digunakan page bawaan framework. Referensi lengkap: Pengujian form.
panelInfolistLabels()
$labels = panelInfolistLabels(UserResource::class, $this->admin);
expect($labels)->toContain('Two-factor')
->and($labels)->toContain('Account summary');2
3
4
Helper ini menserialisasi infolist untuk record tersebut lalu menelusuri tree, masuk ke schema dan tabs, kemudian mengumpulkan label dari setiap komponen dengan key component bernilai entry. Layout bukan entry sehingga tidak ikut muncul; begitu pula child dari repeatable, karena child tersebut milik item, bukan langsung milik record.
Empat scope action
panelRecordActions(UserResource::class) // one row
panelTableActions(UserResource::class) // header, toolbar, empty state
panelBulkActions(UserResource::class) // a selection
panelInfolistActions(UserResource::class) // a record page2
3
4
Scope tersebut tidak dapat saling dipertukarkan. Action yang dideklarasikan sebagai row action tidak otomatis ada sebagai bulk action, apa pun nama yang dikirim request. Referensi lengkap: Pengujian action.
Assertion notifikasi
fakePanelNotifications();
Notification::make('saved')->title('Saved.')->send($this->admin);
assertPanelNotificationSentTo($this->admin, 'Saved.');
assertNoPanelNotificationsStoredFor($this->admin);2
3
4
5
6
"Terkirim" dan "tersimpan" adalah dua pertanyaan berbeda. Referensi lengkap: Pengujian notifikasi.
Class di balik helper
Free function di atas hanyalah wrapper tipis. Test yang lebih nyaman menyimpan objek dapat menggunakan class langsung:
use PandaPanel\Testing\TestsActions;
use PandaPanel\Testing\TestsNotifications;
use PandaPanel\Testing\TestsSchemas;
use PandaPanel\Testing\TestsTables;
$table = TestsTables::for(UserResource::class)->search('Ada');
$form = TestsSchemas::form(UserResource::class, 'edit');
$labels = TestsSchemas::infolistLabels(UserResource::class, $record);
$rowActions = TestsActions::record(UserResource::class);
$tableActions = TestsActions::table(UserResource::class);
$bulkActions = TestsActions::bulk(UserResource::class);
$infolistActions = TestsActions::infolist(UserResource::class);
TestsNotifications::fake();
TestsNotifications::assertSentTo($user, 'Export ready');
TestsNotifications::assertNothingSent();
TestsNotifications::assertStoredFor($user, 'Export ready');
TestsNotifications::assertNothingStoredFor($user);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| Free function | Method class |
|---|---|
panelTable($r) | TestsTables::for($r) |
panelForm($r, $page) | TestsSchemas::form($r, $page) |
panelInfolistLabels($r, $record) | TestsSchemas::infolistLabels($r, $record) |
panelRecordActions($r) | TestsActions::record($r) |
panelTableActions($r) | TestsActions::table($r) |
panelBulkActions($r) | TestsActions::bulk($r) |
panelInfolistActions($r) | TestsActions::infolist($r) |
fakePanelNotifications() | TestsNotifications::fake() |
assertPanelNotificationSentTo() | TestsNotifications::assertSentTo() |
assertNoPanelNotifications() | TestsNotifications::assertNothingSent() |
assertPanelNotificationStoredFor() | TestsNotifications::assertStoredFor() |
assertNoPanelNotificationsStoredFor() | TestsNotifications::assertNothingStoredFor() |
TestsTables, TestsSchemas, dan TestsActions adalah final dengan constructor private. Semuanya dibangun melalui named constructor di atas dan bukan melalui new. TestsNotifications sepenuhnya static.
Mengakses schema di bawahnya
Kedua helper berbasis schema mengekspos objek yang mereka bangun untuk kebutuhan yang tidak ditutupi assertion bawaan:
use PandaPanel\Tables\TableSchema;
use PandaPanel\Forms\FormSchema;
$table = panelTable(UserResource::class)->schema(); // TableSchema
$form = panelForm(UserResource::class)->schema(); // FormSchema
expect($table->getColumn('email'))->not->toBeNull()
->and(array_keys($form->validationRules()))->toContain('email');2
3
4
5
6
7
8
TestsTables::schema() membangun TableSchema baru pada setiap pemanggilan karena schema menyimpan resolved state. Menggunakan ulang satu instance lintas assertion akan membuat assertion berikutnya bergantung pada state sebelumnya. TestsSchemas::schema() melakukan hal yang sama dan membangunnya persis seperti resource page:
$this->resource::form(
FormSchema::make()->model($this->resource::getModel())->forPage($this->page),
);2
3
Hal yang tidak dilakukan helper
Lebih baik nyatakan keterbatasannya dengan jelas daripada mencoba menggunakannya untuk sesuatu yang memang tidak tersedia:
- Tidak ada
perPage().TestsTableshanya menetapkanfilters,search,sort,direction, danpage. Page size, column visibility, grouping, serta column-level search dikendalikan oleh request; buat request jika perlu mengujinya. - Tidak ada argumen panel. Semua helper membaca panel yang sedang aktif melalui
panel(). Resource yang terdaftar di dua panel dengan konfigurasi per-panel berbeda akan diuji terhadap panel yang saat itu aktif — lihat Setup pengujian. - Tidak ada helper relation manager. Relation manager diuji melalui endpoint-nya atau dengan memanggil static method terkait secara langsung.
- Tidak ada helper widget. Padanan publiknya adalah
PandaPanel\Pages\WidgetCollection::for([...])->definitions(). - Tidak ada helper page.
Page::canAccess()dan requestget()ke route page sudah mencakup kebutuhan tersebut. Lihat Pengujian otorisasi.
Hal yang perlu diperhatikan
- Helper bertanya, bukan mensimulasikan.
panelTable()->records()menjalankanResource::query()melaluiTableQuery. Jika scope resource rusak, helper juga akan menunjukkan hasil yang rusak dengan cara yang sama — dan justru itulah tujuannya. row()menserialisasi model yang Anda berikan. Column berbasis aggregate (->counts('passkeys')) membacanulluntukUser::factory()->create()biasa, tetapi membaca nilai sebenarnya jika record diambil darirecords(), karena hanya record kedua yang dimuat melalui query tabel.call()melakukan otorisasi terlebih dahulu. Jika pengguna saat ini tidak diizinkan menjalankan action, test akan gagal, bukan diam-diam melewati action.assertDoesNotHaveField()juga memeriksa rules. Confirmed password field menambahkan key_confirmationkevalidationRules()tanpa menambahkan field. Karena ituassertDoesNotHaveField('password_confirmation')gagal pada resource yang menggunakanPasswordInput::make('password')->confirmed().- Notifikasi membutuhkan fake terlebih dahulu.
fakePanelNotifications()memasangEvent::fake([PanelNotificationSent::class]), dan hanya event yang dikirim setelah itu yang direkam.