ids() ); } /** Divi, Pods and WP Fastest Cache boot (hooks, orphan cleanup); a second boot registers nothing twice. */ public function testTheBootableIntegrationsBootExactlyOnce(): void { Actions\expectAdded('upgrader_process_complete')->once(); Actions\expectAdded('lit_mcp_compat_changed')->once(); Filters\expectAdded(SettingsPage::TABS_FILTER)->never(); Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->times(3); // Divi, Pods and WP Fastest Cache Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once(); Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once(); $registry = self::registry(); $registry->boot(); $registry->boot(); } /** * No temporal coupling: building and booting the integrations never asks the Tools-tab state (an unbound * ToolAvailability throws when asked); bound afterwards, it answers from the bound policy. */ public function testTheToolsTabStateIsAskedOnlyAfterBind(): void { Functions\stubTranslationFunctions(); $tools = new ToolAvailability(); $registry = self::registry($tools); $registry->boot(); $tools->bind($registry, new ExposurePolicy(['@group:divi-library' => ExposurePolicy::DISABLED])); self::assertFalse($tools->groupEnabled('divi-library')); self::assertTrue($tools->groupWritable('divi-theme-builder')); self::assertFalse($tools->allows('lit/divi-insert'), 'no Divi in unit tests: lit/divi-insert not registered'); } /** Pods has no tools: it is in the registry for its profile only and shows up as no group. */ public function testPodsIsRegisteredWithoutBecomingAToolGroup(): void { $registry = self::registry(); self::assertContains('pods', $registry->ids()); self::assertArrayNotHasKey('pods', $registry->grouped()); } /** WP Fastest Cache is registered with its profile; its tool group appears only while the plugin is loaded. */ public function testFastestCacheIsRegisteredAndNoToolGroupWithoutTheCachePlugin(): void { $registry = self::registry(); self::assertContains('wpfc', $registry->ids()); self::assertArrayNotHasKey('wpfc', $registry->grouped()); } /** * The purger of the real wiring asks the registry's Compatibility at call time: with WP Fastest Cache loaded and * its manifest met, the whole-cache purge runs (a registry captured by value would still be null: always locked). * * @runInSeparateProcess * @preserveGlobalState disabled */ public function testTheWiredPurgerAsksTheCompatibilityOfTheRegistry(): void { define('WPFC_MAIN_PATH', '/plugins/wp-fastest-cache/'); eval('function wpfc_clear_post_cache_by_id($id = false) { $GLOBALS["wpfc_calls"][] = "post:" . $id; }' . 'function wpfc_clear_all_cache($minified = false) { $GLOBALS["wpfc_calls"][] = "all"; }' . 'class WpFastestCache { public function singleDeleteCache($c = false, $p = false, $parents = true) {}' . ' public function getWpContentDir($path = false) { return ""; } }'); $GLOBALS['wpfc_calls'] = []; $GLOBALS['wp_fastest_cache'] = new \WpFastestCache(); $GLOBALS['wp_fastest_cache_options'] = (object) ['wpFastestCacheStatus' => 'on']; Functions\when('get_file_data')->justReturn(['Version' => '1.5.5']); Functions\when('get_option')->justReturn(false); Functions\when('get_transient')->justReturn(false); Functions\when('set_transient')->justReturn(true); Functions\when('is_multisite')->justReturn(false); Functions\when('current_user_can')->justReturn(true); $integrations = (new ReflectionProperty(IntegrationRegistry::class, 'integrations'))->getValue(self::registry(log: new FakeChangeLog(1))); $wpfc = array_values(array_filter( $integrations, static fn ($integration): bool => $integration->id() === 'wpfc' ))[0]; $result = $wpfc->abilities()[0]->execute(['scope' => 'all', 'confirm' => true]); self::assertSame('purged', $result['outcome']); self::assertSame(['all'], $GLOBALS['wpfc_calls']); } /** lit-site-status keeps its order: Divi, Pods, WP Fastest Cache, The Events Calendar. */ public function testSiteStatusListsDiviPodsFastestCacheAndTheEventsCalendarInThatOrder(): void { Functions\when('get_bloginfo')->justReturn('x'); Functions\when('home_url')->justReturn('https://example.test'); Functions\when('wp_get_current_user')->justReturn(new WP_User(1, ['administrator'])); Functions\when('get_option')->justReturn(['administrator']); Functions\when('apply_filters')->returnArg(2); $theme = Mockery::mock(); $theme->allows('get_template')->andReturn('twentytwentyfive'); Functions\when('wp_get_theme')->justReturn($theme); $status = array_values(array_filter( self::registry()->abilities(), static fn ($ability): bool => $ability instanceof SiteStatusAbility ))[0]; $components = $status->execute([])['components']; self::assertSame(['divi', 'pods', 'wpfc', 'the-events-calendar'], array_column($components, 'id')); $pods = array_column($components, null, 'id')['pods']; self::assertFalse($pods['active'], 'no Pods in unit tests'); self::assertSame([], $pods['problems']); $wpfc = array_column($components, null, 'id')['wpfc']; self::assertFalse($wpfc['active'], 'no WP Fastest Cache in unit tests'); self::assertSame([], $wpfc['problems']); } public function testTheCompatibilityCoversAllProfilesWithoutIdCollision(): void { $compat = self::registry()->compatibility(); $ids = array_map(static fn (Profile $p): string => $p->id, $compat->profiles()); self::assertSame(['divi', 'pods', 'wpfc'], $ids); self::assertSame($ids, array_values(array_unique($ids))); self::assertSame('wpfc', $compat->owner($compat->capability('wpfc-purge-all'))?->id); self::assertNotNull($compat->capability('page-write')); self::assertNotNull($compat->capability('pods-field-read')); self::assertSame('divi', $compat->owner($compat->capability('page-write'))?->id); self::assertSame('pods', $compat->owner($compat->capability('pods-field-write'))?->id); self::assertNull($compat->capability('nobody-knows-me')); } /** The lock of unknown IDs lives in CapabilityGate: without it a core tool with an odd ID would run unlocked. */ public function testBothGuardsAreWiredDiviFirst(): void { $guards = Plugin::guards(self::registry(), FakeChangeLog::forbidden()); self::assertCount(2, $guards); self::assertInstanceOf(DiviFeatureGate::class, $guards[0]); self::assertInstanceOf(CapabilityGate::class, $guards[1]); } /** The tab "Compatibility" gets the registry's Compatibility: one section per profile, Divi and Pods. */ public function testTheCompatibilityTabShowsEveryProfileOfTheRegistry(): void { Functions\stubEscapeFunctions(); Functions\stubTranslationFunctions(); $theme = Mockery::mock(); $theme->allows('get_template')->andReturn('twentytwentyfive'); Functions\when('wp_get_theme')->justReturn($theme); $tab = Plugin::compatibilityTab(self::registry(), FakeChangeLog::forbidden()); ob_start(); $tab->render(); $html = (string) ob_get_clean(); self::assertInstanceOf(CompatibilityTab::class, $tab); self::assertSame('compatibility', $tab->slug()); // Both plugins are inactive in this registry: one overview row each, no section. self::assertStringContainsString('data-label="Plugin">Divi', $html); self::assertStringContainsString('data-label="Plugin">Pods', $html); self::assertStringNotContainsString('getValue($source); } private static function podsCompat(bool $usable): Compatibility { return new Compatibility([new Profile( id: 'pods', label: 'Pods', tier: Tier::LockOnFailure, capabilities: PodsFeature::cases(), active: static fn (): bool => $usable )]); } public function testWithoutPodsTheMetaSourcesStayWritableAndPodsIsNotASource(): void { $sources = Plugin::schemaSources(self::podsCompat(false), false); self::assertCount(2, $sources); self::assertInstanceOf(RegisteredMetaSource::class, $sources[0]); self::assertInstanceOf(SampledMetaSource::class, $sources[1]); self::assertFalse(self::readOnly($sources[0])); self::assertFalse(self::readOnly($sources[1])); } public function testWithUsablePodsItsSourceComesFirstAndTheMetaSourcesStayWritable(): void { $sources = Plugin::schemaSources(self::podsCompat(true), true); self::assertInstanceOf(PodsFieldSource::class, $sources[0]); self::assertFalse(self::readOnly($sources[1])); self::assertFalse(self::readOnly($sources[2])); } /** Pods active but its schema locked: its meta keys must not be written past Pods. */ public function testWithActiveButLockedPodsTheMetaSourcesAreReadOnly(): void { $sources = Plugin::schemaSources(self::podsCompat(false), true); self::assertInstanceOf(PodsFieldSource::class, $sources[0]); self::assertTrue(self::readOnly($sources[1])); self::assertTrue(self::readOnly($sources[2])); } }