createComponent(CounterComponent::class); * $result = $this->callAction($component, 'increment'); * $this->assertStateEquals($result, ['count' => 1]); * }); * ``` */ trait ComponentTestCase { protected Session $session; protected LiveComponentHandler $handler; protected ComponentEventDispatcher $eventDispatcher; protected string $csrfToken; /** * Setup test environment * * Creates session, handler, and generates CSRF token. * Call this in beforeEach() hook. */ protected function setUpComponentTest(): void { // Create session $sessionId = SessionId::fromString(bin2hex(random_bytes(16))); $clock = new SystemClock(); $randomGenerator = new SecureRandomGenerator(); $csrfGenerator = new CsrfTokenGenerator($randomGenerator); $this->session = Session::fromArray($sessionId, $clock, $csrfGenerator, []); // Create handler dependencies $this->eventDispatcher = new ComponentEventDispatcher(); $authChecker = new SessionBasedAuthorizationChecker($this->session); $stateValidator = new DefaultStateValidator(); $schemaCache = new SchemaCache(); $this->handler = new LiveComponentHandler( $this->eventDispatcher, $this->session, $authChecker, $stateValidator, $schemaCache ); } /** * Authenticate user with permissions * * @param array $permissions User permissions * @param int $userId User ID */ protected function actingAs(array $permissions = [], int $userId = 1): self { $this->session->set('user', [ 'id' => $userId, 'permissions' => $permissions, ]); return $this; } /** * Call component action * * Automatically generates CSRF token for the component. * * @param LiveComponentContract $component Component instance * @param string $method Action method name * @param array $params Action parameters * @return ComponentUpdate Action result */ protected function callAction( LiveComponentContract $component, string $method, array $params = [] ): ComponentUpdate { // Generate CSRF token for component $formId = 'livecomponent:' . $component->getId()->toString(); $csrfToken = $this->session->csrf->generateToken($formId); $actionParams = ActionParameters::fromArray($params, $csrfToken); return $this->handler->handle($component, $method, $actionParams); } /** * Assert action executes successfully * * @param LiveComponentContract $component Component instance * @param string $method Action method name * @param array $params Action parameters */ protected function assertActionExecutes( LiveComponentContract $component, string $method, array $params = [] ): ComponentUpdate { $result = $this->callAction($component, $method, $params); expect($result)->toBeInstanceOf(ComponentUpdate::class); return $result; } /** * Assert action throws exception * * @param LiveComponentContract $component Component instance * @param string $method Action method name * @param string $exceptionClass Expected exception class * @param array $params Action parameters */ protected function assertActionThrows( LiveComponentContract $component, string $method, string $exceptionClass, array $params = [] ): void { $thrown = false; $caughtException = null; try { $this->callAction($component, $method, $params); } catch (\Throwable $e) { $thrown = true; $caughtException = $e; } if (! $thrown) { throw new \AssertionError("Expected exception {$exceptionClass} to be thrown, but no exception was thrown"); } if (! ($caughtException instanceof $exceptionClass)) { throw new \AssertionError( "Expected exception of type {$exceptionClass}, got " . get_class($caughtException) ); } } /** * Assert action requires authentication * * @param LiveComponentContract $component Component with protected action * @param string $method Protected action method */ protected function assertActionRequiresAuth( LiveComponentContract $component, string $method ): void { $this->assertActionThrows( $component, $method, UnauthorizedActionException::class ); } /** * Assert action requires permission * * @param LiveComponentContract $component Component with protected action * @param string $method Protected action method * @param array $withoutPermissions User permissions (should fail) */ protected function assertActionRequiresPermission( LiveComponentContract $component, string $method, array $withoutPermissions = [] ): void { $this->actingAs($withoutPermissions); $this->assertActionThrows( $component, $method, UnauthorizedActionException::class ); } /** * Assert state equals expected values * * @param ComponentUpdate $result Action result * @param array $expected Expected state data */ protected function assertStateEquals(ComponentUpdate $result, array $expected): void { $actualState = $result->state->data; foreach ($expected as $key => $value) { expect($actualState)->toHaveKey($key); expect($actualState[$key])->toBe($value); } } /** * Assert state has key * * @param ComponentUpdate $result Action result * @param string $key State key */ protected function assertStateHas(ComponentUpdate $result, string $key): void { expect($result->state->data)->toHaveKey($key); } /** * Assert state validates against schema * * @param ComponentUpdate $result Action result */ protected function assertStateValidates(ComponentUpdate $result): void { // If we got here without StateValidationException, validation passed expect($result)->toBeInstanceOf(ComponentUpdate::class); } /** * Assert event was dispatched * * @param ComponentUpdate $result Action result * @param string $eventName Event name */ protected function assertEventDispatched(ComponentUpdate $result, string $eventName): void { $events = $result->events; $found = false; foreach ($events as $event) { if ($event->name === $eventName) { $found = true; break; } } expect($found)->toBeTrue("Event '{$eventName}' was not dispatched"); } /** * Assert no events were dispatched * * @param ComponentUpdate $result Action result */ protected function assertNoEventsDispatched(ComponentUpdate $result): void { expect($result->events)->toBeEmpty(); } /** * Assert event count * * @param ComponentUpdate $result Action result * @param int $count Expected event count */ protected function assertEventCount(ComponentUpdate $result, int $count): void { expect($result->events)->toHaveCount($count); } /** * Get state value from result * * @param ComponentUpdate $result Action result * @param string $key State key * @return mixed State value */ protected function getStateValue(ComponentUpdate $result, string $key): mixed { return $result->state->data[$key] ?? null; } }