feat: Divi adapters for roles and library with a live compatibility check
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,14 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Core;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Detects the themes/plugins LIT MCP integrates with.
|
||||
* Detects the themes/plugins LIT MCP integrates with; for Divi also the internals that are missing
|
||||
* (DiviCompatibility).
|
||||
*/
|
||||
class EnvironmentProbe
|
||||
{
|
||||
/** @return list<array{id: string, active: bool, version: ?string}> */
|
||||
public function __construct(private ?DiviCompatibility $divi = null)
|
||||
{
|
||||
}
|
||||
|
||||
/** @return list<array{id: string, active: bool, version: ?string, problems: list<string>}> */
|
||||
public function components(): array
|
||||
{
|
||||
$theme = wp_get_theme();
|
||||
@@ -22,16 +29,23 @@ class EnvironmentProbe
|
||||
$tecActive = class_exists('Tribe__Events__Main');
|
||||
|
||||
return [
|
||||
['id' => 'divi', 'active' => $isDivi, 'version' => $diviVersion],
|
||||
[
|
||||
'id' => 'divi',
|
||||
'active' => $isDivi,
|
||||
'version' => $diviVersion,
|
||||
'problems' => $isDivi ? ($this->divi ?? new DiviCompatibility())->problems() : [],
|
||||
],
|
||||
[
|
||||
'id' => 'pods',
|
||||
'active' => defined('PODS_VERSION'),
|
||||
'version' => defined('PODS_VERSION') ? (string) PODS_VERSION : null,
|
||||
'problems' => [],
|
||||
],
|
||||
[
|
||||
'id' => 'the-events-calendar',
|
||||
'active' => $tecActive,
|
||||
'version' => $tecActive ? (string) constant('Tribe__Events__Main::VERSION') : null,
|
||||
'problems' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ final class SiteStatusAbility implements Ability
|
||||
public function description(): string
|
||||
{
|
||||
return 'Überblick: Website, WordPress-Version, eigener Benutzer und Rollen, '
|
||||
. 'erkannte Integrationen (Divi, Pods, The Events Calendar).';
|
||||
. 'erkannte Integrationen (Divi, Pods, The Events Calendar). '
|
||||
. 'Bei Divi nennt problems Divi-Funktionen, die nach einem Update fehlen.';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||
use Throwable;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Live check of the Divi internals LIT MCP relies on (decisions note §5, E15): classes,
|
||||
* functions, the library post type and taxonomies. lit-site-status shows the failed checks, so a
|
||||
* Divi update that moves one of them is visible before a tool reports "unsupported".
|
||||
*/
|
||||
final class DiviCompatibility
|
||||
{
|
||||
/** @param array<string, callable(): bool>|null $checks Label => check; null = defaults(). */
|
||||
public function __construct(private ?array $checks = null)
|
||||
{
|
||||
}
|
||||
|
||||
/** @return list<string> Labels of the checks that failed or threw. */
|
||||
public function problems(): array
|
||||
{
|
||||
$problems = [];
|
||||
foreach ($this->checks ?? self::defaults() as $label => $check) {
|
||||
try {
|
||||
$ok = (bool) $check();
|
||||
} catch (Throwable) {
|
||||
$ok = false;
|
||||
}
|
||||
if (! $ok) {
|
||||
$problems[] = (string) $label;
|
||||
}
|
||||
}
|
||||
|
||||
return $problems;
|
||||
}
|
||||
|
||||
/** @return array<string, callable(): bool> */
|
||||
public static function defaults(): array
|
||||
{
|
||||
return [
|
||||
'ModuleRegistration (Modul-Katalog)' => static fn (): bool => DiviModules::isAvailable(),
|
||||
'DynamicContentOptions (Dynamic Content)' =>
|
||||
static fn (): bool => class_exists(DynamicContentMap::REGISTRY),
|
||||
'et_pb_is_allowed (Divi-Rollen)' => static fn (): bool => function_exists('et_pb_is_allowed'),
|
||||
'Post-Typ et_pb_layout (Bibliothek)' =>
|
||||
static fn (): bool => post_type_exists(DiviLibraryStore::POST_TYPE),
|
||||
'Taxonomien scope, layout_type, module_width (Bibliothek)' =>
|
||||
static fn (): bool => (new DiviLibraryStore())->missingTaxonomies() === [],
|
||||
'GlobalLayout::is_global_layout_template (globale Module)' =>
|
||||
static fn (): bool => method_exists(DiviLibraryStore::GLOBAL_LAYOUT, 'is_global_layout_template'),
|
||||
'ET_Core_PageResource (CSS-Cache)' => static fn (): bool => class_exists('ET_Core_PageResource'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Adapter for the Divi Library (E4, E15): post type et_pb_layout, the taxonomies scope,
|
||||
* layout_type, module_width, layout_category, layout_tag and the meta Divi's et_pb_submit_layout()
|
||||
* writes (F1, F2, fixtures stage3). "Global" is Divi's own definition
|
||||
* GlobalLayout::is_global_layout_template() (F5) with the scope term as fallback when the class is
|
||||
* gone. find() filters by the scope term only, so an entry Divi counts as global through its D4
|
||||
* meta fallback can show up under scope "normal" (preflight M-12, accepted). Library entries are
|
||||
* always "publish" (F2). Not final so abilities can be tested with a mock.
|
||||
*/
|
||||
class DiviLibraryStore
|
||||
{
|
||||
public const POST_TYPE = 'et_pb_layout';
|
||||
|
||||
public const GLOBAL_LAYOUT = 'ET\Builder\Packages\GlobalLayout\GlobalLayout';
|
||||
|
||||
public const LAYOUT_TYPES = ['layout', 'section', 'row', 'module'];
|
||||
|
||||
/** Taxonomies every new entry needs (et_pb_submit_layout). */
|
||||
public const TAXONOMIES = ['scope', 'layout_type', 'module_width'];
|
||||
|
||||
public function isGlobal(int $id): bool
|
||||
{
|
||||
if (method_exists(self::GLOBAL_LAYOUT, 'is_global_layout_template')) {
|
||||
return (bool) call_user_func([self::GLOBAL_LAYOUT, 'is_global_layout_template'], $id);
|
||||
}
|
||||
|
||||
return in_array('global', $this->termSlugs($id, 'scope'), true);
|
||||
}
|
||||
|
||||
/** "layout", "section", "row" or "module"; null when the entry has no known type. */
|
||||
public function layoutType(int $id): ?string
|
||||
{
|
||||
foreach ($this->termSlugs($id, 'layout_type') as $slug) {
|
||||
if (in_array($slug, self::LAYOUT_TYPES, true)) {
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function termSlugs(int $id, string $taxonomy): array
|
||||
{
|
||||
return array_map(static fn (WP_Term $term): string => (string) $term->slug, $this->terms($id, $taxonomy));
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function termNames(int $id, string $taxonomy): array
|
||||
{
|
||||
return array_map(static fn (WP_Term $term): string => (string) $term->name, $this->terms($id, $taxonomy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Published entries, $limit + 1 of them (Cursor::page() tells whether there are more).
|
||||
*
|
||||
* @param array{layout_type?: string, scope?: string, search?: string, category?: string} $filters
|
||||
* scope: "global" or "normal"; category: name of a layout_category term.
|
||||
* @return list<WP_Post>
|
||||
*/
|
||||
public function find(array $filters, int $limit, int $offset): array
|
||||
{
|
||||
$taxQuery = [];
|
||||
if (in_array($filters['layout_type'] ?? '', self::LAYOUT_TYPES, true)) {
|
||||
$taxQuery[] = ['taxonomy' => 'layout_type', 'field' => 'slug', 'terms' => [$filters['layout_type']]];
|
||||
}
|
||||
if (($filters['scope'] ?? '') === 'global') {
|
||||
$taxQuery[] = ['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global']];
|
||||
} elseif (($filters['scope'] ?? '') === 'normal') {
|
||||
$taxQuery[] = ['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global'], 'operator' => 'NOT IN'];
|
||||
}
|
||||
if (($filters['category'] ?? '') !== '') {
|
||||
$taxQuery[] = ['taxonomy' => 'layout_category', 'field' => 'name', 'terms' => [$filters['category']]];
|
||||
}
|
||||
|
||||
$args = [
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $limit + 1,
|
||||
'offset' => $offset,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'no_found_rows' => true,
|
||||
'suppress_filters' => false,
|
||||
];
|
||||
if (($filters['search'] ?? '') !== '') {
|
||||
$args['s'] = $filters['search'];
|
||||
}
|
||||
if ($taxQuery !== []) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- the library is small.
|
||||
$args['tax_query'] = array_merge(['relation' => 'AND'], $taxQuery);
|
||||
}
|
||||
|
||||
return array_values(array_filter(get_posts($args), static fn (mixed $post): bool => $post instanceof WP_Post));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: int, title: string, layout_type: ?string, scope: string, module_type: ?string,
|
||||
* categories: list<string>, tags: list<string>, modified: string}
|
||||
*/
|
||||
public function describe(WP_Post $post): array
|
||||
{
|
||||
$id = (int) $post->ID;
|
||||
$moduleType = (string) get_post_meta($id, '_et_pb_module_type', true);
|
||||
|
||||
return [
|
||||
'id' => $id,
|
||||
'title' => (string) $post->post_title,
|
||||
'layout_type' => $this->layoutType($id),
|
||||
'scope' => $this->isGlobal($id) ? 'global' : 'normal',
|
||||
'module_type' => $moduleType === '' ? null : $moduleType,
|
||||
'categories' => $this->termNames($id, 'layout_category'),
|
||||
'tags' => $this->termNames($id, 'layout_tag'),
|
||||
'modified' => (string) $post->post_modified_gmt,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Library taxonomies not registered in this request (F2: Divi registers them in
|
||||
* ET_Builder_Library; fixture finding 6: not in every context).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function missingTaxonomies(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
self::TAXONOMIES,
|
||||
static fn (string $taxonomy): bool => ! taxonomy_exists($taxonomy)
|
||||
));
|
||||
}
|
||||
|
||||
/** @return list<WP_Term> */
|
||||
private function terms(int $id, string $taxonomy): array
|
||||
{
|
||||
$terms = get_the_terms($id, $taxonomy);
|
||||
|
||||
return is_array($terms)
|
||||
? array_values(array_filter($terms, static fn (mixed $term): bool => $term instanceof WP_Term))
|
||||
: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Library;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Adapter for Divi's role editor (E3, E15): et_pb_is_allowed() (Divi 5.2.1 builder/core.php:1128)
|
||||
* answers for the current user's role, "dangerous" options (theme_builder,
|
||||
* read_dynamic_content_custom_fields) default to off for every role but administrator. When
|
||||
* Divi no longer has the function, nothing is allowed – DiviCompatibility reports it.
|
||||
* Not final so tests can pass a check.
|
||||
*/
|
||||
class DiviRoles
|
||||
{
|
||||
private const LABELS = [
|
||||
'divi_library' => 'Divi-Bibliothek',
|
||||
'save_library' => 'In der Bibliothek speichern',
|
||||
'add_library' => 'Aus der Bibliothek hinzufügen',
|
||||
'edit_global_library' => 'Globale Bibliothekselemente bearbeiten',
|
||||
'read_dynamic_content_custom_fields' => 'Dynamic Content: benutzerdefinierte Felder lesen',
|
||||
];
|
||||
|
||||
/** @param (Closure(string): bool)|null $check Test seam; null = Divi's et_pb_is_allowed(). */
|
||||
public function __construct(private ?Closure $check = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function allows(string $capability): bool
|
||||
{
|
||||
if ($this->check !== null) {
|
||||
return ($this->check)($capability);
|
||||
}
|
||||
|
||||
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed($capability);
|
||||
}
|
||||
|
||||
/** Null when the role allows $capability, else a forbidden error naming the option. */
|
||||
public function refusal(string $capability): ?WP_Error
|
||||
{
|
||||
if ($this->allows($capability)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Errors::forbidden(
|
||||
sprintf('Die Divi-Rolle dieses Kontos erlaubt „%s“ nicht.', self::LABELS[$capability] ?? $capability),
|
||||
'Einen Administrator bitten, das Recht unter Divi → Rollen-Editor freizugeben.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@ defined('ABSPATH') || exit;
|
||||
*/
|
||||
class DynamicContentMap
|
||||
{
|
||||
private const REGISTRY = 'ET\Builder\Packages\Module\Layout\Components\DynamicContent\DynamicContentOptions';
|
||||
/** Divi's dynamic-content registry class; also checked by DiviCompatibility. */
|
||||
public const REGISTRY = 'ET\Builder\Packages\Module\Layout\Components\DynamicContent\DynamicContentOptions';
|
||||
|
||||
/**
|
||||
* @param (Closure(int): mixed)|null $registry Registry lookup by post ID (tests); null = Divi's.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Core;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Core\EnvironmentProbe;
|
||||
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class EnvironmentProbeTest extends TestCase
|
||||
{
|
||||
public function testDiviComponentCarriesTheCompatibilityFindings(): void
|
||||
{
|
||||
$theme = Mockery::mock();
|
||||
$theme->allows('get_template')->andReturn('Divi');
|
||||
$theme->allows('parent')->andReturn(false);
|
||||
$theme->allows('get')->with('Version')->andReturn('5.2.1');
|
||||
Functions\when('wp_get_theme')->justReturn($theme);
|
||||
|
||||
$probe = new EnvironmentProbe(new DiviCompatibility(['GlobalLayout' => static fn (): bool => false]));
|
||||
$components = array_column($probe->components(), null, 'id');
|
||||
|
||||
self::assertSame(['GlobalLayout'], $components['divi']['problems']);
|
||||
self::assertSame([], $components['pods']['problems']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
final class DiviCompatibilityTest extends TestCase
|
||||
{
|
||||
public function testListsFailedAndThrowingChecks(): void
|
||||
{
|
||||
$compatibility = new DiviCompatibility([
|
||||
'A' => static fn (): bool => true,
|
||||
'B' => static fn (): bool => false,
|
||||
'C' => static function (): bool {
|
||||
throw new RuntimeException('Divi changed');
|
||||
},
|
||||
]);
|
||||
|
||||
self::assertSame(['B', 'C'], $compatibility->problems());
|
||||
}
|
||||
|
||||
public function testDefaultsCoverTheDiviInternalsInUse(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
'ModuleRegistration (Modul-Katalog)',
|
||||
'DynamicContentOptions (Dynamic Content)',
|
||||
'et_pb_is_allowed (Divi-Rollen)',
|
||||
'Post-Typ et_pb_layout (Bibliothek)',
|
||||
'Taxonomien scope, layout_type, module_width (Bibliothek)',
|
||||
'GlobalLayout::is_global_layout_template (globale Module)',
|
||||
'ET_Core_PageResource (CSS-Cache)',
|
||||
],
|
||||
array_keys(DiviCompatibility::defaults())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
|
||||
final class DiviLibraryStoreTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$terms = [
|
||||
1058 => [
|
||||
'scope' => [new WP_Term(['slug' => 'global', 'name' => 'global'])],
|
||||
'layout_type' => [new WP_Term(['slug' => 'section', 'name' => 'section'])],
|
||||
'layout_category' => [new WP_Term(['slug' => 'kopf', 'name' => 'Kopfbereiche'])],
|
||||
],
|
||||
1055 => [
|
||||
'scope' => [new WP_Term(['slug' => 'non_global', 'name' => 'non_global'])],
|
||||
'layout_type' => [new WP_Term(['slug' => 'section', 'name' => 'section'])],
|
||||
],
|
||||
1057 => [],
|
||||
];
|
||||
Functions\when('get_the_terms')->alias(
|
||||
static fn (int $id, string $taxonomy): array|false => $terms[$id][$taxonomy] ?? false
|
||||
);
|
||||
Functions\when('get_post_meta')->alias(
|
||||
static fn (int $id, string $key): string => $id === 1057 && $key === '_et_pb_module_type' ? 'divi/blurb' : ''
|
||||
);
|
||||
}
|
||||
|
||||
public function testGlobalScopeAndLayoutTypeFromTheTerms(): void
|
||||
{
|
||||
$store = new DiviLibraryStore();
|
||||
|
||||
self::assertTrue($store->isGlobal(1058));
|
||||
self::assertFalse($store->isGlobal(1055));
|
||||
self::assertSame('section', $store->layoutType(1058));
|
||||
self::assertNull($store->layoutType(1057));
|
||||
self::assertSame(['Kopfbereiche'], $store->termNames(1058, 'layout_category'));
|
||||
}
|
||||
|
||||
public function testDescribesAnEntry(): void
|
||||
{
|
||||
$post = new WP_Post([
|
||||
'ID' => 1057, 'post_type' => 'et_pb_layout', 'post_title' => '[Fixture] Library-Modul',
|
||||
'post_modified_gmt' => '2026-09-26 20:56:29',
|
||||
]);
|
||||
|
||||
self::assertSame(
|
||||
[
|
||||
'id' => 1057, 'title' => '[Fixture] Library-Modul', 'layout_type' => null, 'scope' => 'normal',
|
||||
'module_type' => 'divi/blurb', 'categories' => [], 'tags' => [], 'modified' => '2026-09-26 20:56:29',
|
||||
],
|
||||
(new DiviLibraryStore())->describe($post)
|
||||
);
|
||||
}
|
||||
|
||||
public function testFindQueriesPublishedEntriesWithFilters(): void
|
||||
{
|
||||
$captured = [];
|
||||
Functions\when('get_posts')->alias(static function (array $args) use (&$captured): array {
|
||||
$captured = $args;
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
(new DiviLibraryStore())->find(
|
||||
['layout_type' => 'section', 'scope' => 'normal', 'search' => 'Kopf', 'category' => 'Kopfbereiche'],
|
||||
20,
|
||||
40
|
||||
);
|
||||
|
||||
self::assertSame('et_pb_layout', $captured['post_type']);
|
||||
self::assertSame('publish', $captured['post_status']);
|
||||
self::assertSame(21, $captured['posts_per_page']);
|
||||
self::assertSame(40, $captured['offset']);
|
||||
self::assertSame('Kopf', $captured['s']);
|
||||
self::assertSame(
|
||||
[
|
||||
'relation' => 'AND',
|
||||
['taxonomy' => 'layout_type', 'field' => 'slug', 'terms' => ['section']],
|
||||
['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global'], 'operator' => 'NOT IN'],
|
||||
['taxonomy' => 'layout_category', 'field' => 'name', 'terms' => ['Kopfbereiche']],
|
||||
],
|
||||
$captured['tax_query']
|
||||
);
|
||||
}
|
||||
|
||||
public function testReportsMissingTaxonomies(): void
|
||||
{
|
||||
Functions\when('taxonomy_exists')->alias(static fn (string $taxonomy): bool => $taxonomy !== 'scope');
|
||||
|
||||
self::assertSame(['scope'], (new DiviLibraryStore())->missingTaxonomies());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
|
||||
final class DiviRolesTest extends TestCase
|
||||
{
|
||||
public function testAsksTheCheckAndExplainsARefusal(): void
|
||||
{
|
||||
$roles = new DiviRoles(static fn (string $cap): bool => $cap !== 'edit_global_library');
|
||||
|
||||
self::assertTrue($roles->allows('divi_library'));
|
||||
self::assertNull($roles->refusal('divi_library'));
|
||||
$refusal = $roles->refusal('edit_global_library');
|
||||
self::assertToolError($refusal, 'forbidden');
|
||||
self::assertStringContainsString('Globale Bibliothekselemente bearbeiten', $refusal->get_error_message());
|
||||
self::assertStringContainsString('Rollen-Editor', $refusal->get_error_message());
|
||||
}
|
||||
|
||||
public function testWithoutDiviNothingIsAllowed(): void
|
||||
{
|
||||
// Unit tests have no et_pb_is_allowed(): a missing Divi function never means "allowed".
|
||||
self::assertFalse((new DiviRoles())->allows('divi_library'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user