- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
505 lines
13 KiB
Markdown
505 lines
13 KiB
Markdown
# 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.
|