Pengujian Form
panelForm() menanyakan kepada form milik resource apa yang benar-benar dideklarasikannya: field apa saja yang tersedia pada page tertentu, aturan validasi apa yang berlaku, dan — pertanyaan yang sering tersembunyi di balik bug form — data apa yang benar-benar akan ditulis. Form dan infolist adalah struktur rekursif; sebuah field bisa berada empat lapis di dalam layout, sehingga test seharusnya dapat menjawab pertanyaan penting tanpa harus menelusuri tree tersebut secara manual.
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->actingAs(User::factory()->create(['is_admin' => true]));
});
it('declares the fields the create form needs', function (): void {
panelForm(UserResource::class)
->assertHasField('name')
->assertHasField('email')
->assertDoesNotHaveField('remember_token')
->assertFieldIsRequired('name');
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Cara kerjanya
TestsSchemas::schema() membangun schema dengan cara yang sama seperti CreateRecord, EditRecord, dan ViewRecord:
$this->resource::form(
FormSchema::make()->model($this->resource::getModel())->forPage($this->page),
);2
3
fields() dan validationRules() adalah method yang sama dengan yang dipanggil controller. Artinya, field yang ditemukan helper ini adalah field yang benar-benar ikut divalidasi dan didehidrasi.
Memilih page
panelForm(UserResource::class) // 'create' — the default
panelForm(UserResource::class, 'edit')
panelForm(UserResource::class, 'view')2
3
Nama page adalah string yang dibandingkan oleh hiddenOn() dan visibleOn(). FormSchema::getPage() juga dapat dibaca dari dalam method form() milik resource. Aplikasi contoh menggunakannya untuk membuat password wajib saat create dan opsional saat edit:
$isCreate = $schema->getPage() === 'create';Karena itu, kedua page layak diuji secara terpisah setiap kali schema memiliki cabang berdasarkan page:
it('requires a password when creating and not when editing', function (): void {
panelForm(UserResource::class)->assertFieldIsRequired('password');
expect(panelForm(UserResource::class, 'edit')->schema()->validationRules()['password'])
->not->toContain('required');
});2
3
4
5
6
create, edit, dan view adalah tiga nama yang digunakan page resource bawaan framework. Custom page yang menetapkan protected static string $page = 'wizard' juga dapat diuji menggunakan nama tersebut.
Semua method
| Method | Signature | Mengembalikan |
|---|---|---|
schema | schema(): FormSchema | schema yang sudah dibangun |
fieldNames | fieldNames(?Model $record = null): array | list<string> — seluruh field yang terlihat, sudah diratakan |
assertHasField | assertHasField(string $name, ?Model $record = null): self | $this |
assertDoesNotHaveField | assertDoesNotHaveField(string $name, ?Model $record = null): self | $this |
assertFieldIsRequired | assertFieldIsRequired(string $name, ?Model $record = null): self | $this |
dehydrate | dehydrate(array $validated, ?Model $record = null): array | atribut yang akan ditulis |
assertDehydratesTo | assertDehydratesTo(array $validated, array $expected, ?Model $record = null): self | $this |
infolistLabels | static infolistLabels(string $resource, Model $record): array | list<string> — seluruh label entry infolist |
Semua method menerima ?Model $record secara opsional, karena visibility dapat bergantung pada record yang sedang diedit. hiddenUsing(), visibleUsing(), dan rulesUsing() semuanya menerima record tersebut.
fieldNames()
Daftar datar dalam urutan deklarasi, dengan seluruh layout sudah dibuka:
expect(panelForm(UserResource::class)->fieldNames())
->toBe(['name', 'email', 'verified', 'is_admin', 'password']);2
Dua section dan sebuah nested layout tetap menghasilkan satu daftar. Field yang disembunyikan pada page saat ini tidak ikut muncul.
assertHasField() dan assertDoesNotHaveField()
panelForm(UserResource::class)
->assertHasField('email')
->assertDoesNotHaveField('two_factor_secret');2
3
assertDoesNotHaveField() melakukan dua assertion sekaligus: nama tersebut tidak ada di fieldNames() dan bukan key di validationRules(). Jadi field yang tidak ada bukan sekadar tersembunyi — field tersebut juga tidak ada dalam aturan validasi maupun payload hasil dehidrasi. Request yang mengirim key tersebut tidak dapat membuatnya tiba-tiba dianggap sebagai field. Inilah sifat yang penting untuk dibuktikan.
Kirim record jika keberadaan field bergantung pada record tersebut:
$locked = User::factory()->create(['is_admin' => true]);
panelForm(UserResource::class, 'edit')->assertHasField('is_admin', $locked);2
3
assertFieldIsRequired()
Method ini membaca validationRules()[$name], mengubah setiap rule menjadi string, lalu memastikan required termasuk di dalamnya:
panelForm(UserResource::class)->assertFieldIsRequired('name');Rule object diubah menjadi string terlebih dahulu, sehingga Rule::unique(...) di dalam daftar tidak mengganggu perbandingan. Untuk pemeriksaan yang lebih spesifik daripada sekadar "required", assert langsung terhadap rule-nya:
$rules = array_map(strval(...), panelForm(UserResource::class)->schema()->validationRules()['name']);
expect($rules)->toBe(['required', 'string', 'max:255']);2
3
dehydrate() dan assertDehydratesTo()
Di sinilah banyak bug form sebenarnya berada: field lolos validasi tetapi tidak tersimpan, atau justru tersimpan dengan nama atribut yang berbeda.
$written = panelForm(UserResource::class)->dehydrate([
'name' => 'Ada',
'email' => 'ada@example.test',
'unknown' => 'discarded',
]);
expect($written)->toHaveKey('name')
->and($written)->not->toHaveKey('unknown');2
3
4
5
6
7
8
assertDehydratesTo() membandingkan seluruh payload menggunakan assertSame. Ini cara yang tepat untuk membuktikan bahwa schema benar-benar membuang data yang tidak pernah dideklarasikan:
panelForm(UserResource::class)->assertDehydratesTo(
['name' => 'Ada', 'unknown' => 'discarded'],
['name' => 'Ada'],
);2
3
4
Method ini juga berguna untuk memastikan rename atribut. Pada aplikasi contoh, toggle verified menulis ke email_verified_at:
$written = panelForm(UserResource::class)->dehydrate([
'name' => 'Ada',
'verified' => true,
]);
expect($written)->toHaveKey('email_verified_at')
->and($written)->not->toHaveKey('verified');2
3
4
5
6
7
Kirim record untuk kasus edit, karena sebuah field dapat memilih untuk tidak melakukan write:
$user = User::factory()->create();
$written = panelForm(UserResource::class, 'edit')
->dehydrate(['name' => 'Ada', 'password' => ''], $user);
// `optionalWhenFilled()` drops it rather than blanking the hash.
expect($written)->not->toHaveKey('password');2
3
4
5
6
7
infolistLabels()
Method static pada class yang sama, dan juga tersedia melalui panelInfolistLabels():
use PandaPanel\Testing\TestsSchemas;
$labels = TestsSchemas::infolistLabels(UserResource::class, $user);
// or: panelInfolistLabels(UserResource::class, $user);
expect($labels)->toContain('Two-factor');2
3
4
5
6
Method ini menserialisasi infolist untuk record tersebut lalu menelusuri schema dan tabs secara rekursif, mengumpulkan label dari setiap komponen yang memiliki key component bernilai entry. Komponen layout bukan entry, sehingga tidak ikut muncul.
Menguji endpoint
Helper menguji sisi schema. Sisi controller tetap diuji melalui HTTP biasa, dan kedua sisi sama-sama dibutuhkan: schema yang mendeklarasikan field dengan benar belum membuktikan bahwa request yang masuk benar-benar difilter melalui schema tersebut.
it('persists no attribute the create form never declared', function (): void {
$this->post('/admin/users/create', [
'name' => 'Mallory',
'email' => 'mallory@example.test',
'password' => 'Password123!',
'password_confirmation' => 'Password123!',
// None of these is a field on the form.
'remember_token' => 'stolen-token',
'two_factor_secret' => 'stolen-secret',
'id' => 9999,
]);
$user = User::query()->where('email', 'mallory@example.test')->firstOrFail();
expect($user->remember_token)->not->toBe('stolen-token')
->and($user->two_factor_secret)->toBeNull()
->and($user->id)->not->toBe(9999);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Validation failure kembali sebagai session error karena form page pada panel adalah Inertia page, bukan JSON endpoint:
$this->post('/admin/users/create', ['name' => 'Nameless'])
->assertSessionHasErrors();
$this->put("/admin/users/{$target->id}/edit", [
'name' => $target->name,
'email' => $other->email,
])->assertSessionHasErrors('email');2
3
4
5
6
7
Sebagai referensi, route-nya adalah POST /{panel}/{resource}/create dan PUT /{panel}/{resource}/{record}/edit.
Hal yang perlu diperhatikan
password_confirmationadalah rule tanpa field.FormSchema::validationRules()menambahkan['nullable', 'string']di bawah{field}_confirmationuntukPasswordInput::make(…)->confirmed(). Karena itu key tersebut ada di rules tetapi tidak ada difieldNames(), sehinggaassertDoesNotHaveField('password_confirmation')justru gagal — assertion keduanya memang memeriksa rules. Gunakanexpect(panelForm(...)->fieldNames())->not->toContain('password_confirmation')untuk kasus ini.- Relationship group memberi namespace pada child-nya.
Relationship::make('profile')->schema([TextInput::make('name')])berdampingan dengan top-levelnamemenghasilkan rule keynamedanprofile.name, bukan duplicate-name error. - Nama field duplikat melempar exception.
validationRules()memanggilassertUniqueFieldNames(), sehingga dua field dengan nama yang sama menghasilkanPanelSchemaExceptionyang menyebut nama field tersebut — exception muncul saat test meminta rules, walaupun deklarasi kedua berada jauh di dalam nested layout. dehydrate()tidak melakukan validasi. Method ini menerima input yang sudah divalidasi. Mengirim nilai yang seharusnya ditolak rule hanya memberi tahu apa yang akan ditulis schema, bukan apakah request akan sampai ke tahap tersebut.- Rule untuk elemen dan struktur nested memiliki key sendiri. List field memvalidasi elemennya melalui
field.*, sedangkan child repeater melaluiitems.*.title. Cari key tersebut, jangan menganggap seluruh rule berada di bawah nama field induknya.
Lihat juga
- Helper pengujian dan setup pengujian
- Gambaran umum form, validasi, hidrasi
- Visibility — nilai yang dibandingkan oleh
hiddenOn()danvisibleOn() - Gambaran umum infolist
- Negative security test