# 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
<?php

use Tests\Framework\LiveComponents\ComponentFactory;
use Tests\Framework\LiveComponents\ComponentTestCase;

// Use ComponentTestCase trait
uses(ComponentTestCase::class);

// Setup before each test
beforeEach(function () {
    $this->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
<?php

use Tests\Framework\LiveComponents\ComponentFactory;
use Tests\Framework\LiveComponents\ComponentTestCase;

uses(ComponentTestCase::class);

beforeEach(function () {
    $this->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.
