Pengujian Tabel
panelTable() membangun tabel yang sama dengan yang dibangun list page, menggunakan TableSchema dan TableQuery milik resource itu sendiri. Gunakan helper ini ketika pertanyaannya adalah "apa yang benar-benar akan dilihat pengguna ini" — row mana, dalam urutan apa, dan nilai apa yang muncul pada cell tertentu — bukan sekadar "apakah URL ini mengembalikan 200". List page yang menjawab 200 sambil menampilkan record dari seluruh tenant tetap merupakan test yang lulus sekaligus kebocoran data.
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('says which records the table is showing', function (): void {
$grace = User::factory()->create(['name' => 'Grace Hopper']);
panelTable(UserResource::class)
->assertCanSeeRecord($this->admin)
->assertCanSeeRecord($grace)
->assertCount(2);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Cara kerjanya
TestsTables::records() melakukan hal yang sama dengan list controller:
$schema = $this->resource::table(TableSchema::make());
$request = Request::create('/', 'GET', $this->state);
$request->setLaravelSession(app('session.store'));
return array_values(
(new TableQuery($schema, $request))
->paginate($this->resource::query())
->items(),
);2
3
4
5
6
7
8
9
10
Ada dua konsekuensi penting. Pertama, state diperlakukan sebagai query string, sehingga filter yang dapat ditetapkan helper adalah filter yang memang dapat ditetapkan URL, tidak lebih. Kedua, request membawa session karena tabel dengan persistSearchInSession() atau persistFiltersInSession() membacanya.
Menetapkan state
Empat setter tersedia dan semuanya mengembalikan clone. Objek bersifat immutable, sehingga variable yang menyimpan tabel tidak berubah hanya karena dibuat assertion chain baru darinya.
| Method | Signature | Padanan query string |
|---|---|---|
filter | filter(array $values): self | ?filters[verified]=false |
search | search(string $term): self | ?search=Grace |
sort | sort(string $column, string $direction = 'asc'): self | ?sort=name&direction=asc |
page | page(int $page): self | ?page=2 |
use PandaPanel\Tables\Filters\TernaryFilter;
$unverified = panelTable(UserResource::class)
->filter(['verified' => TernaryFilter::FALSE]);
$byName = $unverified->sort('name', 'desc')->page(2);
// `$unverified` is unchanged: filter() cloned, sort() cloned again.2
3
4
5
6
7
8
filter() melakukan merge, sehingga dua pemanggilan dapat dikomposisikan:
panelTable(UserResource::class)
->filter(['verified' => TernaryFilter::FALSE])
->filter(['is_admin' => TernaryFilter::TRUE]);2
3
Nilai filter yang tidak diterima schema akan diabaikan, bukan dipaksakan. Sifat ini layak diuji secara eksplisit, bukan diakali di dalam test:
it('ignores a ternary value outside the three it accepts', function (): void {
$all = panelTable(UserResource::class)->keys();
expect(panelTable(UserResource::class)->filter(['verified' => 'maybe'])->keys())
->toBe($all);
});2
3
4
5
6
Tidak ada perPage(). Page size, column visibility, grouping, dan per-column search ditetapkan oleh request; buat request dan baca viewData('page') jika perlu mengujinya.
Membaca hasil
| Method | Signature | Mengembalikan |
|---|---|---|
records | records(): array | list<Model> — record pada page saat ini, sesuai urutan |
keys | keys(): array | list<int|string> — primary key dari record tersebut |
row | row(Model $record): array | satu row dalam bentuk yang diterima frontend |
schema | schema(): TableSchema | tabel sebagaimana resource mendeklarasikannya, dibangun ulang |
$records = panelTable(UserResource::class)->sort('name')->records();
expect($records[0]->name)->toBe('Ada Lovelace');
$keys = panelTable(UserResource::class)->keys();
$row = panelTable(UserResource::class)->row($this->admin);2
3
4
5
6
7
row() adalah TableSchema::toRow(), sehingga bentuk datanya sama dengan row yang diterima browser:
[
'key' => 1,
'group' => null, // the band, when the table is grouped
'cells' => ['name' => …, 'email' => …],
'cellMeta' => [], // per-cell extras, beside the values
'actions' => [['name' => 'view', …], …],
]2
3
4
5
6
7
actions hanya berisi action yang benar-benar ditawarkan untuk record tersebut. Action yang ditolak oleh closure authorize() atau disembunyikan melalui visible() tidak ikut masuk ke row, bukan dirender lalu baru ditolak saat diklik.
Assertion
| Method | Signature | Gagal ketika |
|---|---|---|
assertCanSeeRecord | assertCanSeeRecord(Model $record): self | key tidak ada pada page saat ini |
assertCanNotSeeRecord | assertCanNotSeeRecord(Model $record): self | key justru ada pada page saat ini |
assertCanSeeRecords | assertCanSeeRecords(array $records): self | salah satu record tidak ada |
assertCount | assertCount(int $count): self | jumlah record pada page berbeda |
assertRecordsInOrder | assertRecordsInOrder(array $records): self | key berbeda, baik nilai maupun urutannya |
assertCellEquals | assertCellEquals(Model $record, string $column, mixed $expected): self | cell tidak identik (assertSame) dengan $expected |
assertColumnExists | assertColumnExists(string $column): self | schema tidak mendeklarasikan column dengan nama tersebut |
Ketujuhnya mengembalikan $this, sehingga dapat di-chain:
panelTable(UserResource::class)
->search('Grace')
->assertCanSeeRecord($grace)
->assertCanNotSeeRecord($this->admin)
->assertCount(1);2
3
4
5
Scope, bukan sekadar keberadaan
assertCanNotSeeRecord() memeriksa page saat ini secara keseluruhan. Jika digunakan sendirian, assertion ini hanya membuktikan "tidak ada di page ini", bukan "tidak dapat dijangkau". Jika yang sedang diuji adalah scope, gunakan page yang cukup besar untuk menampung seluruh data dan tambahkan pengujian akses berdasarkan key:
it('hides a record the resource query excludes', function (): void {
$outside = User::factory()->create(['is_admin' => true]);
panelTable(ScopedUserResource::class)->assertCanNotSeeRecord($outside);
// And the record is unreachable by key, which is the half a table
// assertion cannot see.
$this->get("/scope-host/scoped-users/{$outside->id}")->assertNotFound();
});2
3
4
5
6
7
8
9
Urutan
assertRecordsInOrder() membandingkan key menggunakan assertSame, sehingga sekaligus menjadi assertion sort dan jumlah record:
panelTable(UserResource::class)
->sort('name', 'asc')
->assertRecordsInOrder([$ada, $grace]);2
3
Cell
assertCellEquals() membaca row($record)['cells'][$column] lalu membandingkannya menggunakan assertSame. Nilainya mengikuti bentuk serialisasi column tersebut dan sering kali bukan string sederhana:
// TextColumn — a scalar
panelTable(UserResource::class)
->assertCellEquals($this->admin, 'email', $this->admin->email);
// TextInputColumn — an editable cell is the input's state
panelTable(UserResource::class)
->assertCellEquals($this->admin, 'name', ['value' => 'Ada Lovelace', 'disabled' => false]);2
3
4
5
6
7
Bentuk cell pada users table di aplikasi contoh dapat dijadikan peta ekspektasi:
| Tipe column | Cell |
|---|---|
TextColumn | nilai yang sudah diformat, atau null |
TextInputColumn, ToggleColumn | ['value' => …, 'disabled' => bool] |
BadgeColumn | ['value' => …, 'label' => …, 'color' => …] |
IconColumn | ['icon' => …, 'color' => …, 'label' => …] |
NumberColumn | ['display' => string, 'raw' => int|float] |
DateTimeColumn | ['display' => string, 'iso' => string] |
ImageColumn | ['url' => …, 'fallback' => …, 'alt' => …] |
CustomColumn | apa pun yang dikembalikan closure state() |
Menguji bentuk tersebut juga bernilai karena itulah kontrak yang digunakan component Vue:
expect(panelTable(UserResource::class)->row($this->admin)['cells']['created_at'])
->toHaveKeys(['display', 'iso']);2
Menguji melalui request
Helper mencakup apa yang dilakukan schema dan query. Ada tiga hal yang hanya dapat dibuktikan oleh request, dan ketiganya layak memiliki test.
Applied state. Tabel mengembalikan state yang benar-benar diterapkan. Ini cara membuktikan parameter berbahaya memang diabaikan, bukan hanya kebetulan tidak menimbulkan efek:
$page = $this->get('/admin/users?sort=password')->viewData('page');
expect($page['props']['state']['sort'])->toBeNull();2
3
Metadata pagination.
use Inertia\Testing\AssertableInertia;
User::factory()->count(12)->create(); // plus the administrator: thirteen
$this->get('/admin/users?perPage=10')
->assertInertia(fn (AssertableInertia $page) => $page
->where('pagination.page', 1)
->where('pagination.perPage', 10)
->where('pagination.total', 13)
->where('pagination.lastPage', 2)
->has('rows', 10));2
3
4
5
6
7
8
9
10
11
Query per row. Test N+1 yang tetap berguna ketika tabel berkembang adalah menghitung jumlah query pada page kecil dan page besar, lalu memastikan jumlahnya sama.
use Illuminate\Support\Facades\DB;
it('does not issue a query per row while serializing', function (): void {
$countQueries = function (): int {
DB::flushQueryLog();
DB::enableQueryLog();
test()->get('/admin/users?perPage=50')->assertOk();
$queries = count(DB::getQueryLog());
DB::disableQueryLog();
return $queries;
};
User::factory()->count(4)->create();
$few = $countQueries();
User::factory()->count(30)->create();
$many = $countQueries();
expect($many)->toBe($few);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Budget seperti expect($queries)->toBeLessThan(12) cenderung terus dinaikkan setiap kali seseorang menambahkan eager load. Perbandingan kesetaraan tidak mengalami masalah tersebut.
Hal yang perlu diperhatikan
row()menserialisasi model yang Anda berikan. Column yang membaca aliaswithCount— misalnyaNumberColumn::make('passkeys_count')->counts('passkeys')— menghasilkannulluntuk model langsung dari factory, tetapi menghasilkan['display' => '0', 'raw' => 0]untuk record yang diambil darirecords(), karena hanya yang kedua melewati query tabel. Ambil record darirecords()jika column menggunakan aggregate.schema()dibangun ulang pada setiap pemanggilan. Dua assertion tidak pernah berbagi resolved state. Artinya, menyimpan$table->schema()ke variable lalu memodifikasinya juga tidak memengaruhi assertion berikutnya.assertCellEquals()menggunakanassertSame.'1'tidak sama dengan1, dan['value' => true, 'disabled' => false]harus sama dalam tipe maupun urutan key.- Pengguna saat ini merupakan bagian dari hasil.
disabledUsing()pada toggle column,visible()pada row action, dan resource policy semuanya dievaluasi selamarow(). Record yang sama dapat menghasilkan row berbeda untuk user berbeda; inilah sifat yang perlu diuji, bukan dihilangkan dari test. assertCount()menghitung page, bukan seluruh tabel. Kombinasikan denganpage()atau assertpagination.totalmelalui request.
Lihat juga
- Helper pengujian dan setup pengujian
- Gambaran umum tabel, column, filter
- Editable column, sorting, search
- API tabel
- Negative security test — sisi hostile-input dari surface yang sama