
Filament Actions 測試完整指南從 callAction 到 TestAction 的實戰與源碼解析【免費下載鏈接】filamentA powerful open-source UI framework for Laravel ? Build and ship apps admin panels fast with Livewire項目地址: https://gitcode.com/GitHub_Trending/fi/filamentFilament 的 Action 系統貫穿表格、表單、模態框與 infolist 等幾乎所有交互場景而 docs/10-testing/05-testing-actions.md 正是針對這一系統編寫的官方測試指南。本文以該文檔為骨架結合packages/actions包內的測試宏TestsActions、TestAction定位對象以及tests/src/Actions下的真實測試用例系統講解如何用 Pest Livewire 測試助手對 Filament Action 進行端到端斷言幫助你在讀完本文后能獨立編寫覆蓋觸發、表單、校驗、可見性、禁用、狀態、外觀、參數等維度的完整 Action 測試套件。測試基礎為什么 Filament 的 Action 可以用 Livewire 測試Filament 的所有組件最終都掛載在一個 Livewire 組件上因此測試 Filament 與測試 Livewire 組件是同一件事——全程使用 Livewire 的測試助手。在 Pest 中借助其 Livewire 插件提供的livewire()函數在 PHPUnit 中則替換為Livewire::test()方法即可參見 docs/10-testing/01-overview.md。需要特別區分的是資源類、Schema 組件、Action 本身都不是 Livewire 組件但頁面含資源Pages目錄下的類、RelationManager、Widget 是。因此測試 Action 時傳入livewire()的應是承載該 Action 的頁面或組件類例如EditInvoice::class、ListInvoices::class、ManageInvoices::class。Action 的所有測試宏都實現在 packages/actions/src/Testing/TestsActions.php它以mixin Testable方式混入 Livewire 的測試對象所以-callAction()、-assertActionExists()等方法都可以直接鏈式調用。調用 ActioncallAction / mountAction / callMountedAction用名稱或類調用最簡單的方式是把 Action 的名稱字符串或類名傳給callAction()use function Pest\Livewire\livewire; it(can send invoices, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [ invoice $invoice, ]) -callAction(send); expect($invoice-refresh()) -isSent()-toBeTrue(); });callAction的底層執行序列見 TestsActions.php是先assertActionVisible()斷言可見 →parseNestedActions()解析嵌套 Action →mountAction()掛載 → 若存在模態框表單且有$data則fillForm()填充 → 最后callMountedAction()真正提交。這也解釋了為何callAction()在內部會自動完成掛載 填充 提交三步。只掛載不調用如果只想打開 Action 的模態框而不提交使用mountAction()對已經掛載的 Action 提交使用callMountedAction()。這在先斷言模態框內容、再提交的場景中非常常用livewire(EditInvoice::class, [invoice $invoice]) -mountAction(send) -assertMountedActionModalSee($recipientEmail) -callMountedAction();從源碼看mountAction會逐個調用 Livewire 的mountAction($name, $arguments, $context)方法mountedActions是組件上記錄掛載棧的狀態而callMountedAction則調用callMountedAction($arguments)并直接取得當前掛載的 Action 實例。向 Action 傳入數據模態框表單Action 模態框中的表單數據通過data:命名參數傳入it(can send invoices, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [ invoice $invoice, ]) -callAction(send, data: [ email $email fake()-email(), ]) -assertHasNoFormErrors(); expect($invoice-refresh()) -isSent()-toBeTrue() -recipient_email-toBe($email); });若只想預填數據而不立即觸發可先用mountAction()掛載再用fillForm()填充在callAction的實現里filled($data)時執行的也正是同一個fillForm()兩者行為一致。斷言表單校驗錯誤assertHasNoFormErrors()斷言提交 Action 表單時沒有產生校驗錯誤assertHasFormErrors()斷言產生了指定校驗錯誤用法與 Livewire 的assertHasErrors()類似第二個參數為字段名、第三個為規則名it(can validate invoice recipient email, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -callAction(send, data: [ email Str::random(), ]) -assertHasFormErrors([email [email]]); });此外還有一對別名方法assertHasActionErrors()/assertHasNoActionErrors()它們內部直接委托給assertHasFormErrors()/assertHasNoFormErrors()見 TestsActions.php。斷言表單被預填assertSchemaStateSet()用于斷言 Action 的 Schema 狀態已被預填為期望值非常適合驗證默認值邏輯it(can send invoices to the primary contact by default, function () { $invoice Invoice::factory()-create(); $recipientEmail $invoice-company-primaryContact-email; livewire(EditInvoice::class, [invoice $invoice]) -mountAction(send) -assertSchemaStateSet([ email $recipientEmail, ]) -callMountedAction() -assertHasNoFormErrors(); expect($invoice-refresh()) -isSent()-toBeTrue() -recipient_email-toBe($recipientEmail); });定位 ActionTestAction 對象當 Action 不在頁面最頂層例如位于表格行內、表格頭部、批量操作區、infolist 的 schema 組件內時字符串名稱無法唯一定位。此時使用Filament\Actions\Testing\TestAction對象。其核心方法見 packages/actions/src/Testing/TestAction.php包括方法作用源碼位置TestAction::make($name)創建定位對象指定 Action 名稱TestAction.php-table($record true)定位表格內的 Action不傳參時定位表格頭部 ActionTestAction.php-bulk(bool $condition true)定位表格批量bulkActionTestAction.php-schemaComponent($component, ?string $schema null)定位 Schema表單/infolist組件內的 ActionTestAction.php-arguments(array \| Closure \| null $arguments)指定/斷言 Action 參數TestAction.php在序列化toArray()時table()會把記錄鍵寫入context.recordKey模型取getKey()數組取主鍵bulk()寫入context.bulkschemaComponent()則生成context.schemaComponent最終由parseNestedActions()解析并傳給 Livewire 的mountAction()。測試表格行內 Actionuse Filament\Actions\Testing\TestAction; use function Pest\Livewire\livewire; $invoice Invoice::factory()-create(); livewire(ListInvoices::class) -callAction(TestAction::make(send)-table($invoice)); livewire(ListInvoices::class) -assertActionVisible(TestAction::make(send)-table($invoice)); livewire(ListInvoices::class) -assertActionExists(TestAction::make(send)-table($invoice));table($invoice)傳模型時測試宏會通過getTableRecordKey()將其轉換為表格記錄鍵見 TestsActions.php。測試表格頭部 Action頭部 Action 不針對特定記錄table()不帶參數即可livewire(ListInvoices::class) -callAction(TestAction::make(create)-table()); livewire(ListInvoices::class) -assertActionVisible(TestAction::make(create)-table()); livewire(ListInvoices::class) -assertActionExists(TestAction::make(create)-table());測試表格批量 Action批量 Action 需要先用selectTableRecords()勾選記錄再用table()-bulk()組合定位$invoices Invoice::factory()-count(3)-create(); livewire(ListInvoices::class) -selectTableRecords($invoices-pluck(id)-toArray()) -callAction(TestAction::make(send)-table()-bulk()); livewire(ListInvoices::class) -assertActionVisible(TestAction::make(send)-table()-bulk()); livewire(ListInvoices::class) -assertActionExists(TestAction::make(send)-table()-bulk());測試 Schemainfolist / 表單內的 Action若 Action 屬于某個 infolist entry 的belowContent()之類的 Schema 組件用schemaComponent()指定組件名$invoice Invoice::factory()-create(); livewire(EditInvoice::class) -callAction(TestAction::make(send)-schemaComponent(customer_id)); livewire(EditInvoice::class) -assertActionVisible(TestAction::make(send)-schemaComponent(customer_id)); livewire(EditInvoice::class) -assertActionExists(TestAction::make(send)-schemaComponent(customer_id));schemaComponent()的第二個參數可指定所在 schema 的名稱TestAction::make(...)-schemaComponent(form-actions, schema: content)這在資源頁的getFormActions()場景下是必需的詳見 docs/10-testing/02-testing-resources.md 中Testing create / edit pagegetFormActions()一節自定義的Action::make(createAndVerifyEmail)位于CreateUser頁contentschema 的form-actions鍵中需寫成-callAction(TestAction::make(createAndVerifyEmail)-schemaComponent(form-actions, schema: content))。測試另一個 Action 的模態框內嵌 Action如果 Action 位于另一個 Action 模態框的schema()/form()內例如內嵌在模態框某個字段的belowContent()則按嵌套順序傳入一個TestAction數組由parseNestedActions()逐層解析$invoice Invoice::factory()-create(); livewire(ManageInvoices::class) -callAction([ TestAction::make(view)-table($invoice), TestAction::make(send)-schemaComponent(customer.name), ]); livewire(ManageInvoices::class) -assertActionVisible([ TestAction::make(view)-table($invoice), TestAction::make(send)-schemaComponent(customer.name), ]); livewire(ManageInvoices::class) -assertActionExists([ TestAction::make(view)-table($invoice), TestAction::make(send)-schemaComponent(customer.name), ]);源碼層面parseNestedActions()對TestAction調用toArray(defaultSchema: ...)其中嵌套 Action 的默認 schema 名為mountedActionSchema{n}見 TestsActions.php從而將內嵌 Action 正確綁定到外層 Action 的模態框 schema 上。測試 Action 參數Action 定義時若聲明了arguments如Action::make(send)-arguments([...])測試中可用arguments()指定期望的參數值傳Closure時還能通過checkArguments()做自定義參數斷言TestAction.phpuse Filament\Actions\Testing\TestAction; $invoice Invoice::factory()-create(); livewire(ManageInvoices::class) -callAction(TestAction::make(send)-arguments([invoice $invoice-getKey()])); livewire(ManageInvoices::class) -assertActionVisible(TestAction::make(send)-arguments([invoice $invoice-getKey()])); livewire(ManageInvoices::class) -assertActionExists(TestAction::make(send)-arguments([invoice $invoice-getKey()]));斷言模態框內容要檢查模態框渲染出的內容應先mountAction()callAction()會關閉模態框然后使用以下四個斷言斷言方法說明匹配方式assertMountedActionModalSee($values)斷言模態框 HTML 包含給定內容默認對內容做e()轉義后匹配assertMountedActionModalDontSee($values)斷言模態框 HTML 不包含給定內容默認轉義后匹配assertMountedActionModalSeeHtml($values)斷言模態框 HTML 包含給定 HTML不轉義直接匹配assertMountedActionModalDontSeeHtml($values)斷言模態框 HTML 不包含給定 HTML不轉義it(confirms the target address before sending, function () { $invoice Invoice::factory()-create(); $recipientEmail $invoice-company-primaryContact-email; livewire(EditInvoice::class, [invoice $invoice]) -mountAction(send) -assertMountedActionModalSee($recipientEmail); });底層實現中這四者都依賴getMountedActionModalHtml()從 Livewire 最近一次響應的partials中提取action-modals或帶嵌套索引的action-modals.{index}部分未找到時直接Assert::fail()見 TestsActions.php。斷言存在性與可見性存在 / 不存在assertActionExists()與assertActionDoesNotExist()用于斷言 Action 是否注冊it(can send but not unsend invoices, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionExists(send) -assertActionDoesNotExist(unsend); });assertActionExists()可追加一個閉包作為真值測試用于斷言 Action 的具體配置。閉包接收解析出的Filament\Actions\Action實例可調用其 getteruse Filament\Actions\Action; it(has the correct description, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionExists(send, function (Action $action): bool { return $action-getModalDescription() This will send an email to the customer\s primary address, with the invoice attached as a PDF; }); });從 TestsActions.php 的實現看該斷言先通過組件上的getAction()HasActions契約方法見 packages/actions/src/Contracts/HasActions.php解析出 Action 實例并斷言其類型再對checkActionUsing閉包做assertTrue。assertActionDoesNotExist()則捕獲ActionNotResolvableException解析不到即視為不存在。可見 / 隱藏assertActionVisible()/assertActionHidden()分別斷言$action-isVisible()/$action-isHidden()it(can only print invoices, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionHidden(send) -assertActionVisible(print); });源碼中兩者都是assertActionExists()加一個checkActionUsing閉包的語法糖TestsActions.php失敗消息會明確提示Failed asserting that an action with name [...] is visible/hidden on the [...] component.。斷言啟用 / 禁用狀態與順序assertActionEnabled()/assertActionDisabled()斷言isEnabled()/isDisabled()it(can only print a sent invoice, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionDisabled(send) -assertActionEnabled(print); });assertActionListInOrder()斷言一組 Action 以正確的順序存在支持 Action 組自動展開見 TestsActions.phpit(can have actions in order, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionListInOrder([send, export]); });斷言 Action 外觀標簽、圖標、顏色、URL斷言方法底層 getterassertActionHasLabel($actions, $label)/assertActionDoesNotHaveLabel(...)getLabel()assertActionHasIcon($actions, $icon)/assertActionDoesNotHaveIcon(...)getIcon()assertActionHasColor($actions, $color)/assertActionDoesNotHaveColor(...)getColor()assertActionHasUrl($actions, $url)/assertActionDoesNotHaveUrl(...)getUrl()assertActionShouldOpenUrlInNewTab(...)/assertActionShouldNotOpenUrlInNewTab(...)shouldOpenUrlInNewTab()標簽斷言it(send action has correct label, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionHasLabel(send, Email Invoice) -assertActionDoesNotHaveLabel(send, Send); });圖標斷言圖標既支持字符串也支持BackedEnum枚舉時取-value比較見 TestsActions.phpit(when enabled the send button has correct icon, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionEnabled(send) -assertActionHasIcon(send, envelope-open) -assertActionDoesNotHaveIcon(send, envelope); });顏色斷言顏色名取字符串本身自定義色數組會歸一化為customit(actions display proper colors, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionHasColor(delete, danger) -assertActionDoesNotHaveColor(print, danger); });URL 與新標簽頁打開斷言it(links to the correct Filament sites, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionHasUrl(filament, https://filamentphp.com/) -assertActionDoesNotHaveUrl(filament, https://github.com/filamentphp/filament) -assertActionShouldOpenUrlInNewTab(filament) -assertActionShouldNotOpenUrlInNewTab(github); });斷言 Action 被 halt中斷在 Action 的action()閉包中調用halt()會拋出Halt異常中斷執行見 packages/actions/src/Action.phpcancel()則拋出Cancel常用于條件不滿足就中止的業務邏輯。測試中用assertActionHalted()斷言該 Action 仍處于掛載中斷狀態it(stops sending if invoice has no email address, function () { $invoice Invoice::factory([email null])-create(); livewire(EditInvoice::class, [invoice $invoice]) -callAction(send) -assertActionHalted(send); });從源碼看assertActionHalted()就是assertActionMounted()的別名TestsActions.php舊名assertActionHeld()已標記廢棄。倉庫測試中也有對應用例見 tests/src/Actions/ActionTest.php 的 can call an action and halt先斷言事件halt-called被派發再assertActionHalted(halt)。在測試中使用 Action 類名Filament 內置了大量預置 ActionCreateAction、EditAction、DeleteAction等位于 packages/actions/src 下它們可以直接以類名傳入測試方法use Filament\Actions\CreateAction; livewire(ManageInvoices::class) -callAction(CreateAction::class);通過#[ActionName]屬性暴露自定義 Action 名對于自帶make()方法的普通 Action 類Filament 無法高效地通過運行make()來探測名稱因此提供了#[ActionName]屬性見 packages/actions/src/ActionName.php屬性值必須與測試中使用的 Action 名一致use Filament\Actions\Action; use Filament\Actions\ActionName; #[ActionName(send)] class SendInvoiceAction { public static function make(): Action { return Action::make(send) -requiresConfirmation() -action(function () { // ... }); } }之后即可在測試中使用類名use App\Filament\Resources\Invoices\Actions\SendInvoiceAction; use Filament\Actions\Testing\TestAction; $invoice Invoice::factory()-create(); livewire(ManageInvoices::class) -callAction(TestAction::make(SendInvoiceAction::class)-table($invoice));parseNestedActions()在解析時會讀取類上的ActionName屬性并替換為真實名稱見 TestsActions.phpassertActionListInOrder()也做了同樣的名稱解析。通過getDefaultName()讓 Action 類自報名稱若自定義 Action 類直接繼承Filament\Actions\Action可重寫靜態方法getDefaultName()基類默認返回null見 packages/actions/src/Action.php。這樣既能讓 Filament 發現名稱也允許實例化時省略make()的名稱參數use Filament\Actions\Action; class SendInvoiceAction extends Action { public static function getDefaultName(): string { return send; } protected function setUp(): void { parent::setUp(); $this -requiresConfirmation() -action(function () { // ... }); } }Action::make($name ?? static::getDefaultName())Action.php與測試解析中的$actionName::getDefaultName()TestsActions.php共同構成了這條名稱發現鏈路。測試要點小結先確認承載 Action 的 Livewire 組件類再傳給livewire()Action 本身不是 Livewire 組件頂層 Action 用字符串名即可表格 / schema / 嵌套場景務必使用TestAction的table()、bulk()、schemaComponent()、arguments()組合定位需要斷言模態框內容時用mountAction()而非callAction()校驗、預填、外觀、可見性、禁用、halt 等維度均有對應的斷言宏且大多是對assertActionExists() 閉包的包裝可讀性與失敗信息都經過優化自定義 Action 類接入測試體系有兩種方式#[ActionName]屬性普通類或getDefaultName()繼承Action的類。如需進一步了解 Action 的定義、模態框與嵌套行為可繼續閱讀 docs/10-testing/02-testing-resources.md資源頁getFormActions()的測試、docs/10-testing/03-testing-tables.md表格與表格 Action 測試以及 docs/10-testing/04-testing-schemas.mdSchema 組件測試。【免費下載鏈接】filamentA powerful open-source UI framework for Laravel ? Build and ship apps admin panels fast with Livewire項目地址: https://gitcode.com/GitHub_Trending/fi/filament創作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考