mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-01 04:02:09 +02:00
TASK-000: Remove legacy core patching system - Delete src/core/Module/CoreCodePatcher.php - Delete src/core/Module/CoreCodePatchableModuleInterface.php TASK-001: Split ModuleInterface into specialized provider interfaces - Add ServiceProviderInterface (boot, getEventSubscribers) - Add RouteProviderInterface (registerRoutes) - Add CommandProviderInterface (registerCommands) - Add NavbarProviderInterface (registerNavbar) - Add StreamMiddlewareProviderInterface - ModuleLoader::bootAll() detects supported providers via instanceof TASK-002: Refactor EventDispatcher - Add ListenerProvider with priority support - Add AbstractEvent base class - Add StoppableEventInterface support - Preserve legacy subscribe/publish API compatibility TASK-003: Extend ServiceContainer - Add ServiceContainer::decorate() - Support priority-based decorator chains - Add protected service registry: - db - settings - config - auth TASK-004: Add core event catalog - ModuleLoadedEvent - ModuleBootedEvent - PackageInstalledEvent - UserAuthenticatedEvent - UserLoggedOutEvent - StreamStartingEvent - StreamStartedEvent - StreamStoppedEvent - SettingsChangedEvent TASK-005: Formalize subsystem boundaries - Add BoundaryInterface - Add MinistraBootstrap implementation - Define exported service contracts TASK-006: Add stream middleware pipeline - Add StreamContext - Add StreamMiddlewareInterface - Add StreamPipeline - Support middleware priority ordering TASK-007: Add module hot reload - Dispatch PackageInstalledEvent after installation - Add ModuleManager::hotReload() - Load and boot newly installed modules without service restart TASK-008: Standardize container contracts - Add container interfaces: - ContainerInterface - ContainerExceptionInterface - NotFoundExceptionInterface - Add NotFoundException implementation - ServiceContainer::get() throws NotFoundException TASK-009: Refactor navbar registration - CoreNavbarProvider implements NavbarProviderInterface - Add registerNavbar() instance method - ModuleLoader uses provider-based registration TASK-010: Add module priority support - Add priority field to module.json - Default value: 0 - Higher priority modules load earlier - Resolve load order by: 1. Priority DESC 2. Module name ASC TASK-011: Add optional dependencies - Add optional_dependencies field to module.json - Missing optional dependencies do not block module loading - Process optional dependencies after required dependencies Tests - Add ServiceContainer tests - Add EventDispatcher tests - Add StreamPipeline tests - Add ModuleLoader priority tests Documentation - Rewrite module architecture documentation - Update examples and extension development guides - Document lifecycle, events, services, middleware, and dependencies
136 lines
4.7 KiB
PHP
136 lines
4.7 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class EventDispatcherTest extends TestCase {
|
|
|
|
protected function setUp(): void {
|
|
EventDispatcher::clear();
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
EventDispatcher::clear();
|
|
}
|
|
|
|
// ── dispatch / listen ──────────────────────────────────────
|
|
|
|
public function testDispatchCallsMatchingListener(): void {
|
|
$received = null;
|
|
|
|
EventDispatcher::listen(TestPlainEvent::class, function (TestPlainEvent $e) use (&$received) {
|
|
$received = $e->value;
|
|
});
|
|
|
|
EventDispatcher::dispatch(new TestPlainEvent('hello'));
|
|
|
|
$this->assertSame('hello', $received);
|
|
}
|
|
|
|
public function testDispatchReturnsEvent(): void {
|
|
$event = new TestPlainEvent('x');
|
|
$returned = EventDispatcher::dispatch($event);
|
|
|
|
$this->assertSame($event, $returned);
|
|
}
|
|
|
|
public function testDispatchDoesNotCallUnrelatedListeners(): void {
|
|
$called = false;
|
|
EventDispatcher::listen(TestPlainEvent::class, function () use (&$called) {
|
|
$called = true;
|
|
});
|
|
|
|
EventDispatcher::dispatch(new TestOtherEvent());
|
|
|
|
$this->assertFalse($called);
|
|
}
|
|
|
|
// ── Priority ordering ──────────────────────────────────────
|
|
|
|
public function testListenersAreCalledInPriorityDescendingOrder(): void {
|
|
$order = [];
|
|
|
|
EventDispatcher::listen(TestPlainEvent::class, function () use (&$order) { $order[] = 'low'; }, 10);
|
|
EventDispatcher::listen(TestPlainEvent::class, function () use (&$order) { $order[] = 'high'; }, 50);
|
|
EventDispatcher::listen(TestPlainEvent::class, function () use (&$order) { $order[] = 'mid'; }, 30);
|
|
|
|
EventDispatcher::dispatch(new TestPlainEvent('x'));
|
|
|
|
$this->assertSame(['high', 'mid', 'low'], $order);
|
|
}
|
|
|
|
// ── StoppableEventInterface ────────────────────────────────
|
|
|
|
public function testStoppableEventStopsPropagation(): void {
|
|
$calls = 0;
|
|
|
|
EventDispatcher::listen(TestStoppableEvent::class, function (TestStoppableEvent $e) use (&$calls) {
|
|
$calls++;
|
|
$e->stopPropagation();
|
|
}, 10);
|
|
|
|
EventDispatcher::listen(TestStoppableEvent::class, function () use (&$calls) {
|
|
$calls++;
|
|
}, 5);
|
|
|
|
EventDispatcher::dispatch(new TestStoppableEvent());
|
|
|
|
$this->assertSame(1, $calls);
|
|
}
|
|
|
|
// ── hasListeners ───────────────────────────────────────────
|
|
|
|
public function testHasListenersReturnsTrueWhenRegistered(): void {
|
|
EventDispatcher::listen(TestPlainEvent::class, fn() => null);
|
|
|
|
$this->assertTrue(EventDispatcher::hasListeners(TestPlainEvent::class));
|
|
}
|
|
|
|
public function testHasListenersReturnsFalseWhenNone(): void {
|
|
$this->assertFalse(EventDispatcher::hasListeners(TestPlainEvent::class));
|
|
}
|
|
|
|
// ── unlisten / clear ───────────────────────────────────────
|
|
|
|
public function testUnlistenRemovesAllListenersForEvent(): void {
|
|
$called = false;
|
|
EventDispatcher::listen(TestPlainEvent::class, fn() => $called = true);
|
|
EventDispatcher::unlisten(TestPlainEvent::class);
|
|
|
|
EventDispatcher::dispatch(new TestPlainEvent('x'));
|
|
|
|
$this->assertFalse($called);
|
|
}
|
|
|
|
public function testClearRemovesAllListeners(): void {
|
|
$called = false;
|
|
EventDispatcher::listen(TestPlainEvent::class, fn() => $called = true);
|
|
EventDispatcher::clear();
|
|
|
|
EventDispatcher::dispatch(new TestPlainEvent('x'));
|
|
|
|
$this->assertFalse($called);
|
|
}
|
|
|
|
// ── Multiple listeners on same event ───────────────────────
|
|
|
|
public function testMultipleListenersAllCalled(): void {
|
|
$results = [];
|
|
EventDispatcher::listen(TestPlainEvent::class, function () use (&$results) { $results[] = 'a'; });
|
|
EventDispatcher::listen(TestPlainEvent::class, function () use (&$results) { $results[] = 'b'; });
|
|
|
|
EventDispatcher::dispatch(new TestPlainEvent('x'));
|
|
|
|
$this->assertCount(2, $results);
|
|
}
|
|
}
|
|
|
|
// ── Test event fixtures ────────────────────────────────────────────
|
|
|
|
final class TestPlainEvent {
|
|
public function __construct(public readonly string $value = '') {}
|
|
}
|
|
|
|
final class TestOtherEvent {}
|
|
|
|
final class TestStoppableEvent extends AbstractEvent {}
|