- 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.
13 KiB
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
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
beforeEach(function () {
$this->setUpComponentTest();
});
Authentication Helper
actingAs(array $permissions = [], int $userId = 1) - Mock authenticated user:
$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:
$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:
$result = $this->assertActionExecutes($component, 'increment');
expect($result->state->data['count'])->toBe(1);
assertActionThrows() - Assert action throws exception:
$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:
// Note: Requires real component class with #[RequiresPermission] attribute
// ComponentFactory closures don't support attributes
$this->assertActionRequiresAuth($component, 'protectedAction');
assertActionRequiresPermission() - Assert action requires specific permission:
$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:
$result = $this->callAction($component, 'increment');
$this->assertStateEquals($result, ['count' => 1]);
assertStateHas(ComponentUpdate $result, string $key) - Assert state has key:
$this->assertStateHas($result, 'items');
assertStateValidates(ComponentUpdate $result) - Assert state passes validation:
$result = $this->callAction($component, 'updateData', ['value' => 'test']);
$this->assertStateValidates($result);
getStateValue(ComponentUpdate $result, string $key) - Get specific state value:
$count = $this->getStateValue($result, 'count');
expect($count)->toBe(5);
Event Assertions
assertEventDispatched(ComponentUpdate $result, string $eventName) - Assert event was dispatched:
$result = $this->callAction($component, 'submitForm');
$this->assertEventDispatched($result, 'form:submitted');
assertNoEventsDispatched(ComponentUpdate $result) - Assert no events were dispatched:
$result = $this->callAction($component, 'increment');
$this->assertNoEventsDispatched($result);
assertEventCount(ComponentUpdate $result, int $count) - Assert event count:
$result = $this->callAction($component, 'bulkOperation');
$this->assertEventCount($result, 3);
ComponentFactory
Builder Pattern
ComponentFactory::make() - Start builder:
$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 IDwithState(array $state)- Set initial state (cannot be empty!)withAction(string $name, callable $handler)- Add custom actionwithTemplate(string $template)- Set template namecreate()- Create component instance
Pre-configured Components
ComponentFactory::counter(int $initialCount = 0) - Counter component:
$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:
$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
// CSRF token automatically generated and validated
$result = $this->callAction($component, 'action');
// CSRF token: 'livecomponent:{componentId}' form ID
Automatic State Validation
// State automatically validated against derived schema
$result = $this->callAction($component, 'updateState');
// Schema derived on first getData() call
// Cached for subsequent validations
Authorization Integration
// 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
// ❌ 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
// ❌ 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
$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
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
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:
// ❌ 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:
// ❌ 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:
// 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:
// ❌ 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:
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.