id, "Module directory '{$module['id']}' has mismatched manifest id '{$module['manifest']->id}'" ); } } public function testManifestHasVersion(): void { foreach (self::$modules as $module) { self::assertMatchesRegularExpression( '/^\d+\.\d+\.\d+$/', $module['manifest']->version, "Module '{$module['id']}' must have a semver version (got '{$module['manifest']->version}')" ); } } public function testManifestHasAllRequiredKeys(): void { $requiredKeys = [ 'id', 'version', 'enabled_by_default', 'load_order', 'routes', 'public_paths', 'container_registrars', 'ui_slots', 'search_resources', 'asset_groups', 'scheduler_jobs', 'layout_context_providers', 'session_providers', 'permissions', ]; foreach (self::$modules as $module) { foreach ($requiredKeys as $key) { self::assertArrayHasKey( $key, $module['raw'], "Module '{$module['id']}' manifest is missing required key '{$key}'" ); } } } public function testRequiresFieldContainsOnlyValidModuleIds(): void { foreach (self::$modules as $module) { foreach ($module['manifest']->requires as $index => $requiredId) { self::assertIsString($requiredId); self::assertNotEmpty( trim($requiredId), "Module '{$module['id']}' requires[{$index}] must be a non-empty string" ); self::assertMatchesRegularExpression( '/^[a-z][a-z0-9_-]*$/', $requiredId, "Module '{$module['id']}' requires[{$index}] has invalid module id format: '{$requiredId}'" ); } } self::addToAssertionCount(1); } public function testModuleHasLibDirectory(): void { foreach (self::$modules as $module) { self::assertDirectoryExists( $module['path'] . '/lib', "Module '{$module['id']}' must have a lib/ directory for PHP classes" ); } } public function testRoutesHavePathAndTarget(): void { foreach (self::$modules as $module) { foreach ($module['manifest']->routes as $idx => $route) { self::assertArrayHasKey( 'path', $route, "Module '{$module['id']}' route[{$idx}] is missing 'path'" ); self::assertArrayHasKey( 'target', $route, "Module '{$module['id']}' route[{$idx}] is missing 'target'" ); self::assertNotEmpty( trim((string) $route['path']), "Module '{$module['id']}' route[{$idx}] has empty 'path'" ); self::assertNotEmpty( trim((string) $route['target']), "Module '{$module['id']}' route[{$idx}] has empty 'target'" ); } } } public function testModulesWithRoutesHavePagesDirectory(): void { foreach (self::$modules as $module) { if (empty($module['manifest']->routes)) { continue; } self::assertDirectoryExists( $module['path'] . '/pages', "Module '{$module['id']}' declares routes but has no pages/ directory" ); } } }