feat: ColorBook als reiner Farbbestand mit Auflösung nach ID/Alias/Label, Verweisen, Zyklusprüfung und Änderungsmenge
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
fb46078d42
commit
2428801bc9
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use Closure;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* The global colors in Divi's format (GlobalData::get_global_colors(), formats §1): id => {color, folder, label,
|
||||
* lastUpdated, status, usedInPosts}. Takes the shape of get_global_colors() and of a snapshot alike (the same
|
||||
* entries), fills missing keys and keeps only these six. The five customizer colors carry lastUpdated "" – Divi
|
||||
* creates it fresh on every read, so design_rev and snapshots stay stable (ruling V3). Immutable – with() returns a
|
||||
* new book. rev() is the design_rev of the area "colors". Pure PHP.
|
||||
*/
|
||||
final class ColorBook
|
||||
{
|
||||
/** Customizer color ID => option name in et_divi (GlobalData::$customizer_colors). */
|
||||
public const CUSTOMIZER_IDS = [
|
||||
'gcid-primary-color' => 'accent_color',
|
||||
'gcid-secondary-color' => 'secondary_accent_color',
|
||||
'gcid-heading-color' => 'header_color',
|
||||
'gcid-body-color' => 'font_color',
|
||||
'gcid-link-color' => 'link_color',
|
||||
];
|
||||
|
||||
/** A color that refers to another one resolves at most this many references deep (Divi: depth 10). */
|
||||
private const MAX_DEPTH = 10;
|
||||
|
||||
private const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
/** @param array<string, array{color: string, folder: string, label: string, lastUpdated: string,
|
||||
* status: string, usedInPosts: list<mixed>}> $data */
|
||||
private function __construct(private array $data)
|
||||
{
|
||||
}
|
||||
|
||||
/** @param array<array-key, mixed>|object $raw get_global_colors() or a snapshot – entries may be objects. */
|
||||
public static function fromDivi(array|object $raw): self
|
||||
{
|
||||
$data = [];
|
||||
foreach (self::plain($raw) as $id => $entry) {
|
||||
if (is_array($entry)) {
|
||||
$data[(string) $id] = self::normalize((string) $id, $entry);
|
||||
}
|
||||
}
|
||||
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
/** @return array<string, array{color: string, folder: string, label: string, lastUpdated: string,
|
||||
* status: string, usedInPosts: list<mixed>}> */
|
||||
public function data(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/** Hash of the canonical JSON (keys sorted recursively), so the order of the entries does not matter. */
|
||||
public function rev(): string
|
||||
{
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- pure PHP (spec §4.4).
|
||||
$json = (string) json_encode(self::sorted($this->data), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return Rev::of('colors' . "\0" . $json);
|
||||
}
|
||||
|
||||
/** @return array{color: string, folder: string, label: string, lastUpdated: string, status: string,
|
||||
* usedInPosts: list<mixed>}|null */
|
||||
public function entry(string $id): ?array
|
||||
{
|
||||
return $this->data[$id] ?? null;
|
||||
}
|
||||
|
||||
/** The label of $id, else $id itself. */
|
||||
public function label(string $id): string
|
||||
{
|
||||
$label = $this->data[$id]['label'] ?? '';
|
||||
|
||||
return $label === '' ? $id : $label;
|
||||
}
|
||||
|
||||
public function isCustomizer(string $id): bool
|
||||
{
|
||||
return isset(self::CUSTOMIZER_IDS[$id]) || ($this->data[$id]['folder'] ?? '') === 'customizer';
|
||||
}
|
||||
|
||||
/**
|
||||
* The ID of the color $needle names: an ID, else an alias (primary, secondary, heading, body, link), else the
|
||||
* option name of a customizer color (accent_color …), else the one color of that label (case-insensitive).
|
||||
*
|
||||
* @throws InvalidArgumentException When several colors share the label (ruling S13a).
|
||||
*/
|
||||
public function find(string $needle): ?string
|
||||
{
|
||||
if (isset($this->data[$needle])) {
|
||||
return $needle;
|
||||
}
|
||||
|
||||
$key = mb_strtolower(trim($needle));
|
||||
$alias = DesignSystem::COLOR_ALIASES[$key] ?? array_search($key, self::CUSTOMIZER_IDS, true);
|
||||
if (is_string($alias) && isset($this->data[$alias])) {
|
||||
return $alias;
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
foreach ($this->data as $id => $entry) {
|
||||
if ($key !== '' && mb_strtolower(trim($entry['label'])) === $key) {
|
||||
$ids[] = $id;
|
||||
}
|
||||
}
|
||||
if (count($ids) > 1) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Name der globalen Farbe "%s" ist mehrdeutig; bitte die ID angeben: %s.',
|
||||
trim($needle),
|
||||
implode(', ', $ids)
|
||||
));
|
||||
}
|
||||
|
||||
return $ids[0] ?? null;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $entry Added or replaced; missing keys are filled like fromDivi() does. */
|
||||
public function with(string $id, array $entry): self
|
||||
{
|
||||
$data = $this->data;
|
||||
$data[$id] = self::normalize($id, $entry);
|
||||
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
/** @return list<string> IDs of the colors whose value is a reference to $id (relative colors). */
|
||||
public function referencing(string $id): array
|
||||
{
|
||||
return array_keys(array_filter(
|
||||
$this->data,
|
||||
static fn (array $entry): bool => self::targetOf($entry['color']) === $id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* The first chain of references that loops or is deeper than Divi resolves (more than 10 references), as the
|
||||
* IDs from its start to its end (a loop ends with the ID it began to repeat); null when there is none.
|
||||
*
|
||||
* @return list<string>|null
|
||||
*/
|
||||
public function cycle(): ?array
|
||||
{
|
||||
foreach (array_keys($this->data) as $start) {
|
||||
$chain = [$start];
|
||||
$id = $start;
|
||||
while (isset($this->data[$id]) && ($next = self::targetOf($this->data[$id]['color'])) !== null) {
|
||||
$chain[] = $next;
|
||||
if (in_array($next, array_slice($chain, 0, -1), true) || count($chain) - 1 > self::MAX_DEPTH) {
|
||||
return $chain;
|
||||
}
|
||||
$id = $next;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* New and changed entries compared to $before – what the normal write path hands to Divi (ruling V4). The
|
||||
* customizer colors' lastUpdated is "" in both books and never counts.
|
||||
*
|
||||
* @return array<string, array{color: string, folder: string, label: string, lastUpdated: string,
|
||||
* status: string, usedInPosts: list<mixed>}>
|
||||
*/
|
||||
public function changedSince(self $before): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->data,
|
||||
static fn (array $entry, string $id): bool => $before->entry($id) !== $entry,
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
}
|
||||
|
||||
/** A new color ID like the Visual Builder's: "gcid-" and 10 characters of [0-9a-z] (formats §1). */
|
||||
public static function newId(?Closure $random = null): string
|
||||
{
|
||||
$random ??= static function (int $length): string {
|
||||
$id = '';
|
||||
for ($i = 0; $i < $length; ++$i) {
|
||||
$id .= self::ALPHABET[random_int(0, 35)];
|
||||
}
|
||||
|
||||
return $id;
|
||||
};
|
||||
|
||||
return 'gcid-' . $random(10);
|
||||
}
|
||||
|
||||
/** lastUpdated of a color written now: Y-m-d\TH:i:s.v\Z in UTC (formats §1). */
|
||||
public static function now(): string
|
||||
{
|
||||
return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s.v\Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $entry
|
||||
* @return array{color: string, folder: string, label: string, lastUpdated: string, status: string,
|
||||
* usedInPosts: list<mixed>}
|
||||
*/
|
||||
private static function normalize(string $id, array $entry): array
|
||||
{
|
||||
$text = static fn (string $key, string $default): string => is_scalar($entry[$key] ?? null)
|
||||
? (string) $entry[$key]
|
||||
: $default;
|
||||
$folder = $text('folder', '');
|
||||
|
||||
return [
|
||||
'color' => $text('color', ''),
|
||||
'folder' => $folder,
|
||||
'label' => $text('label', ''),
|
||||
'lastUpdated' => isset(self::CUSTOMIZER_IDS[$id]) || $folder === 'customizer'
|
||||
? ''
|
||||
: $text('lastUpdated', ''),
|
||||
'status' => $text('status', 'active'),
|
||||
'usedInPosts' => is_array($entry['usedInPosts'] ?? null) ? array_values($entry['usedInPosts']) : [],
|
||||
];
|
||||
}
|
||||
|
||||
/** The color ID $color refers to when it is exactly one $variable(…)$ color reference, else null. */
|
||||
private static function targetOf(string $color): ?string
|
||||
{
|
||||
if (VariableRef::typeOf($color) !== 'color') {
|
||||
return null;
|
||||
}
|
||||
// typeOf() proved a whole reference: strip marker and end like Divi does, the JSON object is in between.
|
||||
$payload = json_decode(substr($color, strlen(VariableRef::MARKER), -strlen(VariableRef::END)), true);
|
||||
$name = is_array($payload) ? ($payload['value']['name'] ?? null) : null;
|
||||
|
||||
return is_string($name) ? $name : null;
|
||||
}
|
||||
|
||||
private static function sorted(mixed $value): mixed
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
$value = array_map([self::class, 'sorted'], $value);
|
||||
if (! array_is_list($value)) {
|
||||
ksort($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function plain(mixed $value): mixed
|
||||
{
|
||||
if (is_object($value)) {
|
||||
$value = get_object_vars($value);
|
||||
}
|
||||
|
||||
return is_array($value) ? array_map([self::class, 'plain'], $value) : $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
||||
use LIT\MCP\Integrations\Divi\Design\ColorBook;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use stdClass;
|
||||
|
||||
final class ColorBookTest extends TestCase
|
||||
{
|
||||
/** @return array<string, array<string, mixed>> The shape of GlobalData::get_global_colors() (formats §1). */
|
||||
public static function stored(string $customizerStamp = '2026-10-19T08:00:00.000Z'): array
|
||||
{
|
||||
$entry = static fn (
|
||||
string $color,
|
||||
string $label,
|
||||
string $folder = '',
|
||||
string $stamp = '2026-09-27T22:21:39.000Z',
|
||||
string $status = 'active'
|
||||
): array => [
|
||||
'color' => $color, 'folder' => $folder, 'label' => $label, 'lastUpdated' => $stamp,
|
||||
'status' => $status, 'usedInPosts' => [],
|
||||
];
|
||||
|
||||
return [
|
||||
'gcid-primary-color' => $entry('#2ea3f2', 'Primärfarbe', 'customizer', $customizerStamp),
|
||||
'gcid-secondary-color' => $entry('#2ea3f2', 'Sekundärfarbe', 'customizer', $customizerStamp),
|
||||
'gcid-q9swzy97df' => $entry('#5b3a29', 'Holz dunkel'),
|
||||
'gcid-989stx15zc' => $entry(self::ref('gcid-q9swzy97df', ['lightness' => 30]), 'Holz hell'),
|
||||
'gcid-aaaaaaaaaa' => $entry('#111111', 'Doppelt'),
|
||||
'gcid-bbbbbbbbbb' => $entry('#222222', 'doppelt', '', '2026-09-27T22:21:39.000Z', 'inactive'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, int|float> $settings */
|
||||
public static function ref(string $id, array $settings = []): string
|
||||
{
|
||||
return VariableRef::serialize('color', $id, $settings);
|
||||
}
|
||||
|
||||
public function testMissingKeysAreFilledAndOnlyTheSixKeysStay(): void
|
||||
{
|
||||
$book = ColorBook::fromDivi([
|
||||
'gcid-x1' => ['color' => '#fff', 'extra' => 'dropped'],
|
||||
'gcid-x2' => (object) ['color' => '#000', 'label' => 'Schwarz', 'usedInPosts' => [4 => 12]],
|
||||
'gcid-x3' => 'not an entry',
|
||||
]);
|
||||
|
||||
self::assertSame(['gcid-x1', 'gcid-x2'], array_keys($book->data()));
|
||||
self::assertSame([
|
||||
'color' => '#fff', 'folder' => '', 'label' => '', 'lastUpdated' => '', 'status' => 'active',
|
||||
'usedInPosts' => [],
|
||||
], $book->entry('gcid-x1'));
|
||||
self::assertSame([12], $book->entry('gcid-x2')['usedInPosts']);
|
||||
self::assertSame('Schwarz', $book->label('gcid-x2'));
|
||||
self::assertSame('gcid-x1', $book->label('gcid-x1'), 'a color without a label shows its ID');
|
||||
self::assertNull($book->entry('gcid-nope'));
|
||||
}
|
||||
|
||||
public function testObjectInputIsAcceptedLikeAnArray(): void
|
||||
{
|
||||
$raw = new stdClass();
|
||||
$raw->{'gcid-x1'} = (object) ['color' => '#fff', 'status' => 'inactive'];
|
||||
|
||||
self::assertSame(ColorBook::fromDivi(['gcid-x1' => ['color' => '#fff', 'status' => 'inactive']])->data(),
|
||||
ColorBook::fromDivi($raw)->data());
|
||||
}
|
||||
|
||||
public function testCustomizerLastUpdatedIsNormalizedSoRevStaysStable(): void
|
||||
{
|
||||
$now = ColorBook::fromDivi(self::stored('2026-10-19T08:00:00.000Z'));
|
||||
$later = ColorBook::fromDivi(self::stored('2026-10-19T09:30:00.000Z'));
|
||||
|
||||
self::assertSame('', $now->entry('gcid-primary-color')['lastUpdated']);
|
||||
self::assertSame('2026-09-27T22:21:39.000Z', $now->entry('gcid-q9swzy97df')['lastUpdated']);
|
||||
self::assertSame($now->rev(), $later->rev());
|
||||
self::assertSame($now->data(), $later->data());
|
||||
self::assertSame([], $later->changedSince($now));
|
||||
}
|
||||
|
||||
public function testRevIgnoresTheOrderOfEntriesAndChangesWithTheContent(): void
|
||||
{
|
||||
$stored = self::stored();
|
||||
$book = ColorBook::fromDivi($stored);
|
||||
$reversed = ColorBook::fromDivi(array_reverse($stored, true));
|
||||
|
||||
self::assertMatchesRegularExpression('/^[0-9a-f]{12}$/', $book->rev());
|
||||
self::assertSame($book->rev(), $reversed->rev());
|
||||
self::assertSame($book->rev(), ColorBook::fromDivi($stored)->rev());
|
||||
self::assertNotSame($book->rev(), $book->with('gcid-q9swzy97df', ['color' => '#000000'])->rev());
|
||||
}
|
||||
|
||||
public function testRealDiviFixtureAndTheSnapshotOfItAreTheSame(): void
|
||||
{
|
||||
$json = (string) file_get_contents(dirname(__DIR__, 4) . '/Fixtures/divi/stage4/global-colors.json');
|
||||
$fixture = json_decode($json, true);
|
||||
$read = ColorBook::fromDivi($fixture['get_global_colors']);
|
||||
$snapshot = ColorBook::fromDivi($read->data());
|
||||
|
||||
self::assertCount(9, $read->data());
|
||||
self::assertSame($read->rev(), $snapshot->rev());
|
||||
self::assertTrue($read->isCustomizer('gcid-link-color'));
|
||||
self::assertFalse($read->isCustomizer('gcid-q9swzy97df'));
|
||||
self::assertSame(['gcid-989stx15zc'], $read->referencing('gcid-q9swzy97df'));
|
||||
self::assertNull($read->cycle());
|
||||
}
|
||||
|
||||
public function testIsCustomizerByIdOrFolder(): void
|
||||
{
|
||||
$book = ColorBook::fromDivi([
|
||||
'gcid-body-color' => ['color' => '#666'],
|
||||
'gcid-legacy' => ['color' => '#666', 'folder' => 'customizer'],
|
||||
'gcid-x1' => ['color' => '#666'],
|
||||
]);
|
||||
|
||||
self::assertTrue($book->isCustomizer('gcid-body-color'));
|
||||
self::assertTrue($book->isCustomizer('gcid-legacy'));
|
||||
self::assertFalse($book->isCustomizer('gcid-x1'));
|
||||
self::assertTrue($book->isCustomizer('gcid-heading-color'), 'a customizer ID counts even if it is missing');
|
||||
self::assertSame('', $book->entry('gcid-legacy')['lastUpdated']);
|
||||
}
|
||||
|
||||
public function testFindByIdAliasOptionNameAndLabel(): void
|
||||
{
|
||||
$book = ColorBook::fromDivi(self::stored());
|
||||
|
||||
self::assertSame('gcid-q9swzy97df', $book->find('gcid-q9swzy97df'));
|
||||
self::assertSame('gcid-primary-color', $book->find('primary'));
|
||||
self::assertSame('gcid-primary-color', $book->find('PRIMARY'));
|
||||
self::assertSame('gcid-secondary-color', $book->find('secondary_accent_color'));
|
||||
self::assertSame('gcid-q9swzy97df', $book->find(' holz DUNKEL '));
|
||||
self::assertSame('gcid-primary-color', $book->find('Primärfarbe'));
|
||||
self::assertNull($book->find('heading'), 'an alias whose color does not exist finds nothing');
|
||||
self::assertNull($book->find('Fehlt'));
|
||||
self::assertNull($book->find(''));
|
||||
}
|
||||
|
||||
public function testAmbiguousLabelListsTheIds(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Name der globalen Farbe "Doppelt" ist mehrdeutig; bitte die ID angeben: gcid-aaaaaaaaaa, gcid-bbbbbbbbbb.'
|
||||
);
|
||||
|
||||
ColorBook::fromDivi(self::stored())->find('Doppelt');
|
||||
}
|
||||
|
||||
public function testWithAddsOrReplacesAndLeavesTheOriginalAlone(): void
|
||||
{
|
||||
$book = ColorBook::fromDivi(self::stored());
|
||||
$added = $book->with('gcid-new0000001', ['color' => '#abc', 'label' => 'Neu']);
|
||||
$replaced = $book->with('gcid-q9swzy97df', ['color' => '#000000', 'label' => 'Holz', 'status' => 'inactive']);
|
||||
|
||||
self::assertCount(6, $book->data());
|
||||
self::assertCount(7, $added->data());
|
||||
self::assertSame('active', $added->entry('gcid-new0000001')['status']);
|
||||
self::assertSame('inactive', $replaced->entry('gcid-q9swzy97df')['status']);
|
||||
self::assertSame('#5b3a29', $book->entry('gcid-q9swzy97df')['color']);
|
||||
self::assertSame('', $book->with('gcid-primary-color', ['color' => '#fff', 'lastUpdated' => 'x'])
|
||||
->entry('gcid-primary-color')['lastUpdated']);
|
||||
}
|
||||
|
||||
public function testReferencingFindsRelativeColorsIncludingSelfReferences(): void
|
||||
{
|
||||
$book = ColorBook::fromDivi(self::stored())
|
||||
->with('gcid-self000001', ['color' => self::ref('gcid-self000001')])
|
||||
->with('gcid-text000001', ['color' => 'Hinweis: gcid-q9swzy97df']);
|
||||
|
||||
self::assertSame(['gcid-989stx15zc'], $book->referencing('gcid-q9swzy97df'));
|
||||
self::assertSame(['gcid-self000001'], $book->referencing('gcid-self000001'));
|
||||
self::assertSame([], $book->referencing('gcid-989stx15zc'));
|
||||
}
|
||||
|
||||
public function testCycleFindsLoopsAndSelfReferences(): void
|
||||
{
|
||||
$loop = ColorBook::fromDivi([
|
||||
'gcid-a' => ['color' => self::ref('gcid-b')],
|
||||
'gcid-b' => ['color' => self::ref('gcid-c', ['hue' => 10])],
|
||||
'gcid-c' => ['color' => self::ref('gcid-b')],
|
||||
]);
|
||||
$self = ColorBook::fromDivi(['gcid-a' => ['color' => self::ref('gcid-a')]]);
|
||||
|
||||
self::assertSame(['gcid-a', 'gcid-b', 'gcid-c', 'gcid-b'], $loop->cycle());
|
||||
self::assertSame(['gcid-a', 'gcid-a'], $self->cycle());
|
||||
self::assertNull(ColorBook::fromDivi(self::stored())->cycle());
|
||||
}
|
||||
|
||||
public function testCycleStopsAtMoreThanTenReferences(): void
|
||||
{
|
||||
$chain = static function (int $references): ColorBook {
|
||||
$raw = [];
|
||||
for ($i = 0; $i < $references; ++$i) {
|
||||
$raw['gcid-c' . $i] = ['color' => self::ref('gcid-c' . ($i + 1))];
|
||||
}
|
||||
$raw['gcid-c' . $references] = ['color' => '#123456'];
|
||||
|
||||
return ColorBook::fromDivi($raw);
|
||||
};
|
||||
|
||||
self::assertNull($chain(10)->cycle(), 'ten references still resolve in Divi');
|
||||
$tooDeep = $chain(11)->cycle();
|
||||
self::assertCount(12, $tooDeep);
|
||||
self::assertSame(['gcid-c0', 'gcid-c1'], array_slice($tooDeep, 0, 2));
|
||||
}
|
||||
|
||||
public function testReferenceToAnUnknownColorIsNoCycle(): void
|
||||
{
|
||||
self::assertNull(ColorBook::fromDivi(['gcid-a' => ['color' => self::ref('gcid-missing')]])->cycle());
|
||||
}
|
||||
|
||||
public function testChangedSinceListsNewAndChangedEntriesOnly(): void
|
||||
{
|
||||
$before = ColorBook::fromDivi(self::stored());
|
||||
$after = $before
|
||||
->with('gcid-q9swzy97df', ['color' => '#000000', 'label' => 'Holz dunkel', 'lastUpdated' => 'x'])
|
||||
->with('gcid-new0000001', ['color' => '#abc', 'label' => 'Neu'])
|
||||
->with('gcid-aaaaaaaaaa', $before->entry('gcid-aaaaaaaaaa') ?? []);
|
||||
|
||||
self::assertSame(['gcid-q9swzy97df', 'gcid-new0000001'], array_keys($after->changedSince($before)));
|
||||
self::assertSame([], $before->changedSince($before));
|
||||
self::assertSame(['gcid-primary-color'], array_keys(
|
||||
$before->with('gcid-primary-color', ['color' => '#ff0000', 'folder' => 'customizer'])->changedSince($before)
|
||||
));
|
||||
}
|
||||
|
||||
public function testNewIdHasTheBuilderFormat(): void
|
||||
{
|
||||
self::assertMatchesRegularExpression('/^gcid-[0-9a-z]{10}$/', ColorBook::newId());
|
||||
self::assertNotSame(ColorBook::newId(), ColorBook::newId());
|
||||
self::assertSame(
|
||||
'gcid-abcdefghij',
|
||||
ColorBook::newId(static fn (int $length): string => substr('abcdefghijkl', 0, $length))
|
||||
);
|
||||
}
|
||||
|
||||
public function testNowHasDiviTimestampFormat(): void
|
||||
{
|
||||
self::assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/', ColorBook::now());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user