# LiveComponents Test Harness Comprehensive test harness für LiveComponents mit ComponentTestCase trait und ComponentFactory. ## Übersicht Der Test-Harness bietet: - **ComponentTestCase trait**: Umfassende Test-Helper-Methoden - **ComponentFactory**: Builder-Pattern für Test-Component-Erstellung - **Automatische Setup**: CSRF, Authorization, State Validation Integration - **Assertions**: State, Action, Authorization und Event Assertions ## Quick Start ```php setUpComponentTest(); }); it('executes component action', function () { $component = ComponentFactory::counter(initialCount: 5); $result = $this->callAction($component, 'increment'); expect($result->state->data['count'])->toBe(6); }); ``` ## ComponentTestCase Trait ### Setup Methode **`setUpComponentTest()`** - Initialisiert Test-Environment: - Erstellt Session mit CSRF-Token-Generator - Initialisiert LiveComponentHandler mit allen Dependencies (CSRF, Auth, Validation) - Setzt EventDispatcher, AuthorizationChecker, StateValidator, SchemaCache auf ```php beforeEach(function () { $this->setUpComponentTest(); }); ``` ### Authentication Helper **`actingAs(array $permissions = [], int $userId = 1)`** - Mock authenticated user: ```php $this->actingAs(['posts.edit', 'posts.delete']); $result = $this->callAction($component, 'deletePost', ['id' => 123]); ``` ### Action Execution **`callAction(LiveComponentContract $component, string $method, array $params = [])`** - Execute action with automatic CSRF: ```php $component = ComponentFactory::counter(); // Automatic CSRF token generation $result = $this->callAction($component, 'increment'); // With parameters $result = $this->callAction($component, 'addItem', ['item' => 'New Task']); ``` ### Action Assertions **`assertActionExecutes()`** - Assert action executes successfully: ```php $result = $this->assertActionExecutes($component, 'increment'); expect($result->state->data['count'])->toBe(1); ``` **`assertActionThrows()`** - Assert action throws exception: ```php $component = ComponentFactory::make() ->withId('error:component') ->withState(['data' => 'test']) ->withAction('fail', function() { throw new \RuntimeException('Expected error'); }) ->create(); $this->assertActionThrows($component, 'fail', \RuntimeException::class); ``` **`assertActionRequiresAuth()`** - Assert action requires authentication: ```php // Note: Requires real component class with #[RequiresPermission] attribute // ComponentFactory closures don't support attributes $this->assertActionRequiresAuth($component, 'protectedAction'); ``` **`assertActionRequiresPermission()`** - Assert action requires specific permission: ```php $this->actingAs(['posts.view']); // Insufficient permission $this->assertActionRequiresPermission( $component, 'deletePost', ['posts.view'] // Should fail with only 'view' permission ); ``` ### State Assertions **`assertStateEquals(ComponentUpdate $result, array $expected)`** - Assert state matches expected: ```php $result = $this->callAction($component, 'increment'); $this->assertStateEquals($result, ['count' => 1]); ``` **`assertStateHas(ComponentUpdate $result, string $key)`** - Assert state has key: ```php $this->assertStateHas($result, 'items'); ``` **`assertStateValidates(ComponentUpdate $result)`** - Assert state passes validation: ```php $result = $this->callAction($component, 'updateData', ['value' => 'test']); $this->assertStateValidates($result); ``` **`getStateValue(ComponentUpdate $result, string $key)`** - Get specific state value: ```php $count = $this->getStateValue($result, 'count'); expect($count)->toBe(5); ``` ### Event Assertions **`assertEventDispatched(ComponentUpdate $result, string $eventName)`** - Assert event was dispatched: ```php $result = $this->callAction($component, 'submitForm'); $this->assertEventDispatched($result, 'form:submitted'); ``` **`assertNoEventsDispatched(ComponentUpdate $result)`** - Assert no events were dispatched: ```php $result = $this->callAction($component, 'increment'); $this->assertNoEventsDispatched($result); ``` **`assertEventCount(ComponentUpdate $result, int $count)`** - Assert event count: ```php $result = $this->callAction($component, 'bulkOperation'); $this->assertEventCount($result, 3); ``` ## ComponentFactory ### Builder Pattern **`ComponentFactory::make()`** - Start builder: ```php $component = ComponentFactory::make() ->withId('posts:manager') ->withState(['posts' => [], 'count' => 0]) ->withAction('addPost', function(string $title) { $this->state['posts'][] = $title; $this->state['count']++; return ComponentData::fromArray($this->state); }) ->create(); ``` ### Builder Methods - **`withId(string $id)`** - Set component ID - **`withState(array $state)`** - Set initial state (cannot be empty!) - **`withAction(string $name, callable $handler)`** - Add custom action - **`withTemplate(string $template)`** - Set template name - **`create()`** - Create component instance ### Pre-configured Components **`ComponentFactory::counter(int $initialCount = 0)`** - Counter component: ```php $component = ComponentFactory::counter(initialCount: 5); // Actions: increment, decrement, reset $result = $this->callAction($component, 'increment'); expect($result->state->data['count'])->toBe(6); ``` **`ComponentFactory::list(array $initialItems = [])`** - List component: ```php $component = ComponentFactory::list(['item1', 'item2']); // Actions: addItem, removeItem, clear $result = $this->callAction($component, 'addItem', ['item' => 'item3']); expect($result->state->data['items'])->toHaveCount(3); ``` ## Integration Features ### Automatic CSRF Protection ```php // CSRF token automatically generated and validated $result = $this->callAction($component, 'action'); // CSRF token: 'livecomponent:{componentId}' form ID ``` ### Automatic State Validation ```php // State automatically validated against derived schema $result = $this->callAction($component, 'updateState'); // Schema derived on first getData() call // Cached for subsequent validations ``` ### Authorization Integration ```php // Mock authenticated user with permissions $this->actingAs(['admin.access']); // Authorization automatically checked for #[RequiresPermission] attributes $result = $this->callAction($component, 'adminAction'); ``` ## Best Practices ### State Must Not Be Empty ```php // ❌ Empty state causes schema derivation error $component = ComponentFactory::make() ->withState([]) ->create(); // ✅ Always provide at least one state field $component = ComponentFactory::make() ->withState(['initialized' => true]) ->create(); ``` ### Authorization Testing Requires Real Classes ```php // ❌ Closures don't support attributes for authorization $component = ComponentFactory::make() ->withAction('protectedAction', #[RequiresPermission('admin')] // Attribute wird ignoriert function() { } ) ->create(); // ✅ Use real component class for authorization testing final readonly class TestComponent implements LiveComponentContract { #[RequiresPermission('admin')] public function protectedAction(): ComponentData { // Implementation } } ``` ### Action Closures Have Access to Component State ```php $component = ComponentFactory::make() ->withState(['count' => 0]) ->withAction('increment', function() { // $this->state available via closure binding $this->state['count']++; return ComponentData::fromArray($this->state); }) ->create(); ``` ### Multiple Actions in Sequence ```php it('handles multiple actions', function () { $component = ComponentFactory::counter(); $result1 = $this->callAction($component, 'increment'); $result2 = $this->callAction($component, 'increment'); $result3 = $this->callAction($component, 'decrement'); // Note: Component state is immutable // Each call returns new state, doesn't mutate original expect($result1->state->data['count'])->toBe(1); expect($result2->state->data['count'])->toBe(2); expect($result3->state->data['count'])->toBe(1); }); ``` ## Test Organization ``` tests/ ├── Framework/LiveComponents/ │ ├── ComponentTestCase.php # Trait with helper methods │ └── ComponentFactory.php # Builder for test components └── Feature/Framework/LiveComponents/ ├── TestHarnessDemo.php # Demo of all features ├── SimpleTestHarnessTest.php # Simple examples └── ExceptionTestHarnessTest.php # Exception handling examples ``` ## Complete Example ```php setUpComponentTest(); }); describe('Shopping Cart Component', function () { it('adds items to cart', function () { $component = ComponentFactory::make() ->withId('shopping-cart') ->withState(['items' => [], 'total' => 0]) ->withAction('addItem', function(string $product, int $price) { $this->state['items'][] = ['product' => $product, 'price' => $price]; $this->state['total'] += $price; return ComponentData::fromArray($this->state); }) ->create(); $result = $this->callAction($component, 'addItem', [ 'product' => 'Laptop', 'price' => 999 ]); $this->assertStateHas($result, 'items'); expect($result->state->data['items'])->toHaveCount(1); expect($result->state->data['total'])->toBe(999); }); it('requires authentication for checkout', function () { $component = ComponentFactory::make() ->withId('shopping-cart') ->withState(['items' => [['product' => 'Laptop', 'price' => 999]]]) ->withAction('checkout', function() { // Checkout logic return ComponentData::fromArray($this->state); }) ->create(); // Without authentication // Note: For authorization testing, use real component classes // With authentication $this->actingAs(['checkout.access']); $result = $this->assertActionExecutes($component, 'checkout'); }); }); ``` ## Known Limitations ### 1. Closure Attributes Attributes on closures passed to `withAction()` are not supported for authorization checks: ```php // ❌ Doesn't work - attribute ignored $component = ComponentFactory::make() ->withAction('protectedAction', #[RequiresPermission('admin')] function() { } ) ->create(); ``` **Workaround**: Create real component class for authorization testing. ### 2. Empty State Not Allowed Components must have at least one state field for schema derivation: ```php // ❌ Throws InvalidArgumentException: 'Schema cannot be empty' $component = ComponentFactory::make() ->withState([]) ->create(); // ✅ Provide at least one field $component = ComponentFactory::make() ->withState(['initialized' => true]) ->create(); ``` ### 3. Magic Method Reflection ComponentFactory uses `__call()` for actions, which limits reflection-based parameter analysis. The handler falls back to direct parameter passing for magic methods. ## Performance Considerations - **Schema Caching**: Schema derived once per component class and cached - **CSRF Generation**: CSRF token generated per test, not reused - **Session State**: Session state reset in `setUpComponentTest()` - **Event Dispatcher**: Events collected per action call, not persisted ## Troubleshooting ### "Method not found" Error ``` BadMethodCallException: Method increment not found on component ``` **Fix**: Ensure `method_exists()` check supports `__call()` magic methods: ```php // LiveComponentHandler checks both real and magic methods if (!method_exists($component, $method) && !is_callable([$component, $method])) { throw new \BadMethodCallException(...); } ``` ### "Schema cannot be empty" Error ``` InvalidArgumentException: Schema cannot be empty ``` **Fix**: Provide non-empty state: ```php // ❌ Empty state ->withState([]) // ✅ Non-empty state ->withState(['data' => 'test']) ``` ### Reflection Exception for Actions ``` ReflectionException: Method increment() does not exist ``` **Fix**: Handler catches ReflectionException and falls back to direct call: ```php try { $reflection = new \ReflectionMethod($component, $method); // Parameter analysis } catch (\ReflectionException $e) { // Direct call for magic methods return $component->$method(...$params->toArray()); } ``` ## Summary Der Test-Harness bietet: - ✅ **Einfache Component-Erstellung** via ComponentFactory - ✅ **Umfassende Assertions** für State, Actions, Events - ✅ **Automatische Integration** mit CSRF, Auth, Validation - ✅ **Flexible Test-Components** via Builder Pattern - ✅ **Pre-configured Components** (Counter, List) - ⚠️ **Known Limitations** mit Closure-Attributes **Framework-Integration**: Vollständig integriert mit LiveComponentHandler, StateValidator, AuthorizationChecker und EventDispatcher.