id()); self::assertFalse($integration->isAvailable(), 'no Divi classes in unit tests'); self::assertSame( self::EXPECTED, array_map(static fn (Ability $ability): string => $ability->name(), $integration->abilities()) ); foreach ($integration->abilities() as $ability) { self::assertSame('object', $ability->inputSchema()['type'], $ability->name()); self::assertFalse($ability->inputSchema()['additionalProperties'], $ability->name()); self::assertNotNull($ability->outputSchema(), $ability->name()); } } /** Spec §3: the ports the core uses (Plugin wires them) are Divi's own implementations. */ public function testProvidesThePortsTheCoreUses(): void { $integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()); self::assertInstanceOf(BuilderDetector::class, $integration->builderDetector()); self::assertInstanceOf(DiviBuilderDetector::class, $integration->builderDetector()); $sources = $integration->mediaUsageSources(); self::assertCount(1, $sources); self::assertInstanceOf(MediaUsageSource::class, $sources[0]); self::assertInstanceOf(DiviLogoUsage::class, $sources[0]); self::assertInstanceOf(EnvironmentComponent::class, $integration->environmentComponent()); self::assertInstanceOf(DiviEnvironmentComponent::class, $integration->environmentComponent()); } /** The content tools leave the same post types alone as before the schema stopped knowing Divi. */ public function testDiviPostTypesAddedToTheSchemaGiveTheFormerExcludedList(): void { self::assertSame( [ 'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part', 'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face', 'et_pb_layout', 'et_theme_builder', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout', 'et_pb_canvas', '_et_pb_speculation', ], array_merge(ContentSchema::EXCLUDED, DiviIntegration::internalPostTypes()) ); } public function testTheSchemaAsksDiviWhetherTheBuilderIsOnForAPostType(): void { Functions\when('et_builder_enabled_for_post_type')->alias(static fn (string $type): bool => $type === 'page'); self::assertTrue(DiviIntegration::builderEnabledFor('page')); self::assertFalse(DiviIntegration::builderEnabledFor('post')); } public function testProvidesTheDiviCompatibilityProfile(): void { $integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()); self::assertInstanceOf(ProvidesCompatibility::class, $integration); self::assertEquals([DiviProfile::profile()], $integration->compatibilities()); } public function testProvidesTheLibraryGroup(): void { Functions\when('__')->returnArg(); $library = (new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))->library(); self::assertSame('divi-library', $library->id()); self::assertSame('Divi-Bibliothek', $library->label()); self::assertFalse($library->isAvailable(), 'no Divi classes in unit tests'); self::assertSame( [ 'lit/library-list', 'lit/library-save', 'lit/library-insert', 'lit/library-find-similar', 'lit/library-make-global', ], array_map(static fn (Ability $ability): string => $ability->name(), $library->abilities()) ); foreach ($library->abilities() as $ability) { self::assertFalse($ability->inputSchema()['additionalProperties'], $ability->name()); self::assertNotNull($ability->outputSchema(), $ability->name()); } } public function testProvidesTheThemeBuilderGroup(): void { Functions\when('__')->returnArg(); $group = (new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))->themeBuilder(); self::assertSame('divi-theme-builder', $group->id()); self::assertSame('Divi Theme Builder', $group->label()); self::assertFalse($group->isAvailable(), 'no Divi classes in unit tests'); self::assertSame( ['lit/tb-list', 'lit/tb-create-template', 'lit/tb-assign', 'lit/tb-activate'], array_map(static fn (Ability $ability): string => $ability->name(), $group->abilities()) ); foreach ($group->abilities() as $ability) { self::assertFalse($ability->inputSchema()['additionalProperties'], $ability->name()); self::assertNotNull($ability->outputSchema(), $ability->name()); self::assertFalse($ability->isDirect(), $ability->name()); } } /** * Review I-2 (ruling Q17 M1): the library usages follow the Theme Builder access – without it (here: no Divi * role check in unit tests, so canUse() fails) TB layouts are counted and block, never named. * ThemeBuilderAccessTest covers usageState() itself. */ public function testGlobalUsagesFollowTheThemeBuilderAccess(): void { Functions\when('current_user_can')->justReturn(true); Functions\when('get_post')->justReturn(null); Functions\when('_get_non_cached_ids')->returnArg(1); Functions\when('_prime_post_caches')->justReturn(null); Functions\when('wp_cache_delete')->justReturn(true); $db = Mockery::mock(\wpdb::class)->makePartial(); $db->allows('get_results')->andReturnUsing(static fn (string $sql): array => str_contains($sql, 'post_content FROM') ? [ ['ID' => '1053', 'post_content' => ''], ['ID' => '1061', 'post_content' => ''], ] : [ ['ID' => '1053', 'post_status' => 'publish', 'post_type' => 'page'], ['ID' => '1061', 'post_status' => 'publish', 'post_type' => 'et_header_layout'], ]); $report = (new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden())) ->globalUsages($db) ->report(1058); self::assertSame([1053], array_column($report->items, 'post_id')); self::assertSame([], $report->blocked); self::assertSame(1, $report->blockedHidden); } /** A Tools-tab state bound to $policy, with lit/divi-insert registered (the Divi group available). */ private static function tools(ExposurePolicy $policy = new ExposurePolicy()): ToolAvailability { $insert = Mockery::mock(Ability::class); $insert->allows('name')->andReturn('lit/divi-insert'); $insert->allows('isDirect')->andReturn(true); $divi = Mockery::mock(Integration::class); $divi->allows('id')->andReturn('divi'); $divi->allows('isAvailable')->andReturn(true); $divi->allows('abilities')->andReturn([$insert]); $tools = new ToolAvailability(); $tools->bind(new IntegrationRegistry($divi), $policy); return $tools; } /** Ruling P9 (E9): lit/divi-node-copy follows lit/divi-insert's Tools-tab state. */ public function testNodeCopyFollowsLitDiviInsert(): void { $integration = new DiviIntegration( Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden(), self::tools(new ExposurePolicy(['lit/divi-insert' => ExposurePolicy::DISABLED])) ); $copy = null; foreach ($integration->abilities() as $ability) { if ($ability->name() === 'lit/divi-node-copy') { $copy = $ability; } } self::assertNotNull($copy); self::assertToolError($copy->permission(['post_id' => 7, 'source_post_id' => 8]), 'unsupported'); } /** Security M2: content-create's divi input follows lit/divi-insert, asked when content-create asks. */ public function testTheShortFormFollowsLitDiviInsert(): void { $schema = Mockery::mock(ContentSchema::class); $open = new DiviIntegration($schema, FakeChangeLog::forbidden(), self::tools()); $closed = new DiviIntegration( $schema, FakeChangeLog::forbidden(), self::tools(new ExposurePolicy(['lit/divi-insert' => ExposurePolicy::DISABLED])) ); self::assertTrue($open->contentComposer()->writable()); self::assertFalse($closed->contentComposer()->writable()); self::assertTrue((new DiviIntegration($schema, FakeChangeLog::forbidden()))->contentComposer()->writable()); } public function testGivesContentCreateTheDiviShortForm(): void { $integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()); $composer = $integration->contentComposer(); self::assertInstanceOf(DiviContentComposer::class, $composer); self::assertFalse($composer->available(), 'no Divi classes in unit tests'); } /** E1/E3: the Divi read tools reach library entries; ruling P9: a hidden library leaves them not found. */ public function testTheReadToolsReachTheLibraryUnlessItIsHidden(): void { LibraryFixtures::given([LibraryFixtures::item(1055)]); $schema = Mockery::mock(ContentSchema::class); $schema->allows('isManaged')->andReturn(false); $outline = static fn (DiviIntegration $divi): mixed => $divi->abilities()[0]->permission(['post_id' => 1055]); // No Divi in unit tests: DiviRoles allows nothing, so the entry is not found either way (ruling P12); // the wiring shows in which gate the integration asks: an unbound ToolAvailability throws when asked. $visible = new DiviIntegration($schema, FakeChangeLog::forbidden(), self::tools()); self::assertToolError($outline($visible), 'not_found'); self::assertToolError($outline(new DiviIntegration($schema, FakeChangeLog::forbidden())), 'not_found'); $hidden = new DiviIntegration( $schema, FakeChangeLog::forbidden(), self::tools(new ExposurePolicy(['@group:divi-library' => ExposurePolicy::DISABLED])) ); $refusal = $outline($hidden); self::assertInstanceOf(WP_Error::class, $refusal); self::assertToolError($refusal, 'not_found'); $this->expectExceptionMessage('ToolAvailability asked before Plugin bound it.'); $outline(new DiviIntegration($schema, FakeChangeLog::forbidden(), new ToolAvailability())); } /** * Ruling P13: library IDs follow the integration's library access – hidden with the group disabled * and without the library right (no Divi in unit tests: DiviRoles allows nothing). The positive * cases, "nur lesen" included, are in LibraryAccessTest (defining et_pb_is_allowed here would leak). */ public function testLibraryIdsFollowTheLibraryAccess(): void { LibraryFixtures::given([]); $schema = Mockery::mock(ContentSchema::class); $hidden = new DiviIntegration( $schema, FakeChangeLog::forbidden(), self::tools(new ExposurePolicy(['@group:divi-library' => ExposurePolicy::DISABLED])) ); self::assertFalse($hidden->libraryIdsVisible()); self::assertFalse((new DiviIntegration($schema, FakeChangeLog::forbidden()))->libraryIdsVisible()); $this->expectExceptionMessage('ToolAvailability asked before Plugin bound it.'); (new DiviIntegration($schema, FakeChangeLog::forbidden(), new ToolAvailability()))->libraryIdsVisible(); } /** Bootable: Divi's own hooks – upgrader cache reset, check route, tab "Divi" after the plugin's tabs. */ public function testBootRegistersDivisOwnHooks(): void { $upgraded = null; $tabs = null; Actions\expectAdded('upgrader_process_complete')->once()->whenHappen( static function (callable $callback) use (&$upgraded): void { $upgraded = $callback; } ); Actions\expectAdded('rest_api_init')->once()->with(Mockery::on( static fn (array $callback): bool => $callback[0] instanceof DiviCheckRoute )); Filters\expectAdded(SettingsPage::TABS_FILTER)->once()->whenHappen( static function (callable $callback) use (&$tabs): void { $tabs = $callback; } ); Functions\expect('delete_transient')->once()->with(ReleaseResolver::CACHE)->andReturn(true); $base = Mockery::mock(AdminTab::class); (new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))->boot(); self::assertIsCallable($upgraded); $upgraded(); self::assertIsCallable($tabs); $all = $tabs([$base]); self::assertCount(2, $all); self::assertSame($base, $all[0]); self::assertInstanceOf(DiviTab::class, $all[1]); } public function testTheFeatureOverviewFindsTheToolsOfAllIntegrations(): void { $integration = new DiviIntegration( Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden(), allAbilities: static fn (): array => [ new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageWrite]), new FeatureFakeTool('lit/library-insert', true, [DiviFeature::PageWrite, DiviFeature::LibraryRead]), ] ); self::assertSame( ['page-write' => ['lit-divi-insert', 'lit-library-insert'], 'library-read' => ['lit-library-insert']], $integration->featureOverview()->toolsByFeature() ); } public function testWithoutAnAbilityListTheOverviewKnowsNoTools(): void { $integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()); self::assertSame([], $integration->featureOverview()->toolsByFeature()); } }