fix(audit L5): unter der Sperre Divis Laufzeit-Caches verwerfen (Options-Cache-Gruppe, GlobalData-/GlobalPreset-Static, et_theme_options) vor raw() und design_rev

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-20 00:26:40 +02:00
co-authored by Claude Sonnet 5.5
parent 6e84138979
commit 99c7aab238
28 changed files with 182 additions and 13 deletions
@@ -36,6 +36,11 @@ final class ColorArea implements DesignArea
return DiviFeature::ColorWrite;
}
public function refresh(): void
{
$this->colors->refresh();
}
public function raw(): array
{
return ColorBook::fromDivi($this->colors->read())->data();
@@ -25,6 +25,12 @@ interface DesignArea
/** The Divi feature a write of this area needs (DesignChange asks ReleaseGate::assertFeature() for it). */
public function feature(): DiviFeature;
/**
* Drops what this request cached of the inventory (Divi's static and option caches), called right after the
* lock is taken: another writer may have finished since the request began (audit L5).
*/
public function refresh(): void;
/** @return array<array-key, mixed> */
public function raw(): array;
@@ -93,6 +93,9 @@ final class DesignChange
);
}
try {
// Audit L5: drop Divi's request caches, so the inventory and design_rev below are the stored state.
$area->refresh();
return $this->attempt($ability, $area, $rev, $plan);
} finally {
$this->lock->release($area->name());
+6 -5
View File
@@ -15,8 +15,8 @@ defined('ABSPATH') || exit;
* DiviCheckRoute (add_option() reads first and writes after, so two requests could both win; audit L6). A lock
* older than STALE_AFTER seconds (a request that died) is taken over with a conditional UPDATE on the value just
* read, so only one taker wins; release() deletes the row only while it still holds this instance's token, so a
* holder that lost a stale lock cannot free the new holder's. A taken lock drops the cache of the autoloaded
* options, so the inventory read under the lock (and design_rev) is the stored one.
* holder that lost a stale lock cannot free the new holder's. A taken lock drops the options cache, so the
* inventory read under the lock (and design_rev) is the stored one.
*/
final class DesignLock
{
@@ -66,9 +66,10 @@ final class DesignLock
}
if ($taken) {
$this->tokens[$option] = $token;
// Audit L5: what the holder reads and rewrites must be the stored state, not the request-start cache
// (autoloaded options like Divi's live in "alloptions"); another writer may have finished since.
wp_cache_delete('alloptions', 'options');
// Audit L5: what the holder reads and rewrites must be the stored state, not the request's option cache
// ("alloptions" for autoloaded options, one entry each for the others); another writer may have
// finished since. A cache without group flushing loses everything – rare, and only under the lock.
wp_cache_supports('flush_group') ? wp_cache_flush_group('options') : wp_cache_flush();
}
return $taken;
@@ -81,6 +81,19 @@ class DesignSystemWriter implements DiviAdapter
);
}
/**
* Audit L5: GlobalPreset::get_data() keeps a static copy that can be older than another writer's finished save
* (the option cache is dropped by DesignLock). Called under the lock, before reading.
*/
public function refresh(): void
{
try {
(new \ReflectionProperty(GlobalPreset::class, '_data'))->setValue(null, null);
} catch (\ReflectionException) {
// Divi without that property: its reads are not cached this way.
}
}
/** @return array<array-key, mixed> The stored inventory (GlobalPreset::get_data(), formats §3). */
public function presets(): array
{
@@ -84,7 +84,32 @@ class GlobalColorsAdapter implements DiviAdapter
}
// Audit L5: set_global_colors() ends in et_update_option(), which writes Divi's request global back whole.
$GLOBALS['et_theme_options'] = get_option('et_divi');
$this->refresh();
GlobalData::set_global_colors($clean, ! $replace);
}
/**
* Audit L5: colors are read through et_get_option(), i.e. Divi's request global and GlobalData's static cache –
* both can be older than another writer's finished save. Called under the lock, before reading.
*/
public function refresh(): void
{
$GLOBALS['et_theme_options'] = get_option(self::OPTIONS);
self::forgetCached('colors');
}
/** Resets one entry of GlobalData's static runtime cache (private, so by reflection; gone = nothing to reset). */
private static function forgetCached(string $key): void
{
try {
$cache = new \ReflectionProperty(GlobalData::class, '_cached_data');
$data = $cache->getValue();
if (is_array($data) && array_key_exists($key, $data)) {
$data[$key] = null;
$cache->setValue(null, $data);
}
} catch (\ReflectionException) {
// Divi without that property: its reads are not cached this way.
}
}
}
@@ -86,4 +86,28 @@ class GlobalVariablesAdapter implements DiviAdapter
{
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed('variables_manager');
}
/**
* Audit L5: GlobalData keeps a static copy of the variables that can be older than another writer's finished
* save (the option cache is dropped by DesignLock). Called under the lock, before reading.
*/
public function refresh(): void
{
self::forgetCached('variables');
}
/** Resets one entry of GlobalData's static runtime cache (private, so by reflection; gone = nothing to reset). */
private static function forgetCached(string $key): void
{
try {
$cache = new \ReflectionProperty(GlobalData::class, '_cached_data');
$data = $cache->getValue();
if (is_array($data) && array_key_exists($key, $data)) {
$data[$key] = null;
$cache->setValue(null, $data);
}
} catch (\ReflectionException) {
// Divi without that property: its reads are not cached this way.
}
}
}
@@ -34,6 +34,11 @@ final class PresetArea implements DesignArea
return DiviFeature::PresetWrite;
}
public function refresh(): void
{
$this->writer->refresh();
}
public function raw(): array
{
return PresetBook::fromDivi($this->writer->presets())->data();
@@ -40,6 +40,11 @@ final class ThemeOptionsArea implements DesignArea
return DiviFeature::ThemeOptionWrite;
}
public function refresh(): void
{
// read() asks get_option(), whose cache the lock drops; write() reloads Divi's request global itself.
}
public function raw(): array
{
return ThemeOptionBook::fromDivi($this->options->read())->data();
@@ -40,6 +40,11 @@ final class VariableArea implements DesignArea
return DiviFeature::VariableWrite;
}
public function refresh(): void
{
$this->variables->refresh();
}
public function raw(): array
{
return VariableBook::fromDivi($this->variables->read())->data();
@@ -43,6 +43,7 @@ final class ColorAreaTest extends TestCase
public static function adapter(array &$stored, array &$saves): GlobalColorsAdapter
{
$colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('refresh');
$colors->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
});
@@ -47,6 +47,7 @@ final class ColorRestorePlannerTest extends TestCase
private static function planner(bool $visualBuilder = true, ?GlobalUsages $usages = null): ColorRestorePlanner
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn($visualBuilder);
return new ColorRestorePlanner(
@@ -56,6 +56,9 @@ final class DesignChangeTest extends TestCase
public array $restores = [];
/** @var list<string> refresh()/raw() in the order they happened */
public array $calls = [];
public function __construct(
private array &$stored,
private int $failWrites,
@@ -79,8 +82,15 @@ final class DesignChangeTest extends TestCase
return DiviFeature::PresetWrite;
}
public function refresh(): void
{
$this->calls[] = 'refresh';
}
public function raw(): array
{
$this->calls[] = 'raw';
return $this->stored;
}
@@ -205,6 +215,25 @@ final class DesignChangeTest extends TestCase
self::assertSame([], $area->writes);
}
/** Audit L5 (re-review): under the lock Divi's request caches are dropped before the inventory is read. */
public function testTheAreaDropsItsCachesUnderTheLockBeforeReading(): void
{
$stored = ['v' => 1];
$area = self::area($stored);
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->allows('save')->andReturn('presets-1');
$snapshots->allows('attachLog');
(new DesignChange($snapshots, new FakeChangeLog(1), new DesignLock()))->run(
'lit/divi-presets-set',
$area,
['confirm' => true, 'design_rev' => $area->rev($stored)],
self::plan(['v' => 2])
);
self::assertSame(['refresh', 'raw'], array_slice($area->calls, 0, 2));
}
/** Final review I-5: a confirm while another change of the area runs writes nothing. */
public function testAHeldLockRefusesTheConfirm(): void
{
@@ -102,26 +102,33 @@ final class DesignLockTest extends TestCase
public function testTheLockHolderReadsEtDiviFreshNotFromTheRequestStartCache(): void
{
$db = $this->table();
$stored = ['et_divi' => ['color' => 'alt']];
$stored = ['et_divi' => ['color' => 'alt'], 'et_divi_global_variables' => ['v' => 'alt']];
$cache = [];
// Like WordPress: every option is cached under its own key (not autoloaded ones) or in "alloptions".
Functions\when('get_option')->alias(static function (string $name) use (&$stored, &$cache) {
if (! isset($cache['options:' . $name]) && ! isset($cache['options:alloptions'][$name])) {
$cache['options:alloptions'][$name] = $stored[$name] ?? false;
}
$key = 'options:' . $name;
$cache[$key] ??= $stored[$name] ?? false;
return $cache['options:' . $name] ?? $cache['options:alloptions'][$name];
return $cache[$key];
});
Functions\when('wp_cache_delete')->alias(static function (string $key, string $group = '') use (&$cache): bool {
unset($cache[$group . ':' . $key]);
Functions\when('wp_cache_flush_group')->alias(static function (string $group) use (&$cache): bool {
foreach (array_keys($cache) as $key) {
if (str_starts_with($key, $group . ':')) {
unset($cache[$key]);
}
}
return true;
});
self::assertSame(['color' => 'alt'], get_option('et_divi'), 'request B cached the old array');
self::assertSame(['v' => 'alt'], get_option('et_divi_global_variables'));
$stored['et_divi'] = ['color' => 'neu']; // request A wrote and released meanwhile
$stored['et_divi_global_variables'] = ['v' => 'neu'];
self::assertTrue((new DesignLock())->acquire('colors'));
self::assertSame(['color' => 'neu'], get_option('et_divi'));
self::assertSame(['v' => 'neu'], get_option('et_divi_global_variables'), 'also options that are not autoloaded');
self::assertNotSame([], $db->rows);
}
}
@@ -18,6 +18,7 @@ final class DesignRightsTest extends TestCase
public function testPresetsNeedEditThemeOptionsAndTheVisualBuilder(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true, false);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
@@ -33,6 +34,7 @@ final class DesignRightsTest extends TestCase
public function testColorsNeedEditThemeOptionsAndTheVisualBuilder(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true, false);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
@@ -49,8 +51,10 @@ final class DesignRightsTest extends TestCase
public function testVariablesAlsoNeedTheVariablesManager(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn(true, false);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
@@ -63,8 +67,10 @@ final class DesignRightsTest extends TestCase
public function testVariablesAskTheThemeRightAndTheVisualBuilderFirst(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(false);
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn(true);
Functions\when('current_user_can')->justReturn(true);
@@ -53,6 +53,7 @@ final class DiviColorsSetAbilityTest extends TestCase
bool $visualBuilder = true
): DiviColorsSetAbility {
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn($visualBuilder);
$writer->allows('presets')->andReturn($presets === [] ? PresetBookTest::stored() : $presets);
$counter = 0;
@@ -297,6 +298,7 @@ final class DiviColorsSetAbilityTest extends TestCase
{
$calls = [];
$colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('refresh');
$colors->allows('read')->andReturn(ColorAreaTest::stored());
$colors->allows('save')->andReturnUsing(static function (array $entries, bool $replace) use (&$calls): void {
$calls[] = $replace;
@@ -80,6 +80,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
private static function readOnlyWriter(?array $stored = null): DesignSystemWriter&MockInterface
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($stored ?? PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')->never();
@@ -91,6 +92,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
private static function variableAdapter(bool $manage = true, string $radius = '8px'): GlobalVariablesAdapter&MockInterface
{
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('read')->andReturn(self::variables($radius));
$variables->allows('canManage')->andReturn($manage);
$variables->expects('save')->never();
@@ -246,6 +248,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
$calls = [];
$stored = self::storedWithVariable();
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -451,6 +454,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
{
$calls = [];
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -502,6 +506,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
public function testAPageErrorAfterThePresetStepSaysThePresetsAreChanged(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -633,6 +638,7 @@ final class DiviDesignDetachAbilityTest extends TestCase
private function abilityAfterThePresetStep(PageRewriter $pages): DiviDesignDetachAbility
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -42,6 +42,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
$older = PresetBookTest::stored();
unset($older['module']['divi/button']['items']['bbbbbbbbbb']);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')->with(Mockery::type('array'), true)->andReturn($older);
@@ -73,6 +74,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
{
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($current);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$snapshots = Mockery::mock(SnapshotStore::class);
@@ -108,6 +110,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
{
Functions\when('current_user_can')->justReturn(false);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->shouldNotReceive('savePresets');
$snapshots = Mockery::mock(SnapshotStore::class);
@@ -144,6 +147,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
{
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->shouldNotReceive('get');
@@ -171,6 +175,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
): DiviDesignRestoreAbility {
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->shouldNotReceive('savePresets');
@@ -206,6 +211,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
private static function colorStore(array $stored): GlobalColorsAdapter&MockInterface
{
$colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('refresh');
$colors->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
});
@@ -225,6 +231,7 @@ final class DiviDesignRestoreAbilityTest extends TestCase
private static function variableStore(array $stored, bool $manage = true): GlobalVariablesAdapter&MockInterface
{
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn($manage);
$variables->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
@@ -19,6 +19,7 @@ final class DiviPresetGetAbilityTest extends TestCase
private function ability(?GlobalUsages $usages = null): DiviPresetGetAbility
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$usages ??= Mockery::mock(GlobalUsages::class);
@@ -29,6 +29,7 @@ final class DiviPresetsAssignAbilityTest extends TestCase
private function ability(PageRewriter $pages): DiviPresetsAssignAbility
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
@@ -37,6 +37,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
Functions\when('current_user_can')->justReturn(true);
if ($writer === null) {
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn($stored === [] ? PresetBookTest::stored() : $stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
}
@@ -59,6 +60,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
public function testUnusedPresetIsDeletedAfterConfirm(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
@@ -153,6 +155,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
0, 1, [], 0, false);
Functions\when('current_user_can')->justReturn(true);
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$usages = Mockery::mock(GlobalUsages::class);
@@ -52,6 +52,7 @@ final class DiviPresetsSetAbilityTest extends TestCase
private function savingWriter(): DesignSystemWriter
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('savePresets')->andReturnUsing(function (array $rest, bool $mayRemove): array {
@@ -291,6 +292,7 @@ final class DiviPresetsSetAbilityTest extends TestCase
public function testNothingIsWrittenWhenOneItemHasErrors(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->shouldNotReceive('savePresets');
@@ -53,6 +53,7 @@ final class DiviVariablesSetAbilityTest extends TestCase
?FontCatalog $fonts = null
): DiviVariablesSetAbility {
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->allows('presets')->andReturn($presets === [] ? PresetBookTest::stored() : $presets);
$counter = 0;
@@ -313,6 +314,7 @@ final class DiviVariablesSetAbilityTest extends TestCase
{
$calls = [];
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn(true);
$variables->allows('read')->andReturn(VariableAreaTest::stored());
$variables->allows('save')->andReturnUsing(static function (array $data, array $expected) use (&$calls): void {
@@ -16,6 +16,7 @@ final class PresetAreaTest extends TestCase
public function testWritesTheRestFormatAndReadsBack(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$stored = PresetBookTest::stored();
$writer->allows('presets')->andReturn($stored);
$writer->expects('savePresets')
@@ -50,6 +51,7 @@ final class PresetAreaTest extends TestCase
public function testRestoreReplacesTheWholeInventory(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$stored = PresetBookTest::stored();
$writer->expects('savePresets')->with(PresetBook::fromDivi($stored)->toRest(), true)->andReturn($stored);
@@ -57,6 +57,7 @@ final class VariableAreaTest extends TestCase
public static function adapter(array &$stored, array &$saves, bool $manage = true): GlobalVariablesAdapter
{
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn($manage);
$variables->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
@@ -47,8 +47,10 @@ final class VariableRestorePlannerTest extends TestCase
private static function planner(bool $manage = true, ?GlobalUsages $usages = null): VariableRestorePlanner
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->allows('canUseVisualBuilder')->andReturn(true);
$variables = Mockery::mock(GlobalVariablesAdapter::class);
$variables->allows('refresh');
$variables->allows('canManage')->andReturn($manage);
return new VariableRestorePlanner($writer, $variables, $usages ?? UsageFixture::none());
@@ -57,6 +57,7 @@ final class DiviCompositionTest extends TestCase
private function presetSource(int $reads = 1): DesignSystemWriter
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->expects('presets')->times($reads)->andReturn([
'module' => ['divi/button' => ['default' => 'p-1', 'items' => [
'p-1' => ['id' => 'p-1', 'name' => 'Standard', 'moduleName' => 'divi/button'],
@@ -211,6 +212,7 @@ final class DiviCompositionTest extends TestCase
public function testThePresetBookIsNotReadWithoutAPresetInTheInput(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('refresh');
$writer->shouldNotReceive('presets');
$writer->shouldNotReceive('groupsOf');
+2
View File
@@ -34,6 +34,8 @@ abstract class TestCase extends BaseTestCase
'wp_slash' => static fn ($value) => $value,
'wp_unslash' => static fn ($value) => $value,
'wp_cache_delete' => true,
'wp_cache_supports' => true,
'wp_cache_flush_group' => true,
'sanitize_text_field' => static fn ($value): string => trim(strip_tags((string) $value)),
]);