diff --git a/README.md b/README.md index 3c3cb8f..e0ede47 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,29 @@ Please review [`Collection` source code](./src/Enqueue/Collection.php) to find o +### Retrieving registered/enqueued assets' collection + +The `Assets` class provides a `Assets::collection()` method that returns a `Collection` instance with all the assets that have been enqueued/registered. The collection allows us to act on individual assets (retrieved via `Collection::oneByName()` or `Collection::oneByHandle()`) or collectively on all or some assets (filtered using one of the many methods `Collection` provides, see above). + +Here's an example on how this could be leveraged for obtaining "automatic bulk registration" for all our assets in few lines of code that loop files in the assets directory: + +```php +$assets = Assets::forTheme('/dist')->useDependencyExtractionData(); + +foreach (glob($assets->context()->basePath() . '*.{css,js}', GLOB_BRACE) as $file) { + str_ends_with($file, '.css') + ? $assets->registerStyle(basename($file, '.css')) + : $assets->registerScript(basename($file, '.js')); +} + +add_action('admin_enqueue_scripts'), fn () => $assets->collection()->keep('*-admin')->enqueue()); +add_action('wp_enqueue_scripts'), fn () => $assets->collection()->keep('*-view')->enqueue()); +``` + +Please note: because `Collection` has an immutable design, do not store the result of `Assets::collection()` but always call that method to retrieve an up-to-date collection. + + + ### Debug diff --git a/psalm.xml b/psalm.xml index 6ca6724..eae66bf 100644 --- a/psalm.xml +++ b/psalm.xml @@ -25,5 +25,6 @@ + diff --git a/src/Assets.php b/src/Assets.php index c69bbc9..d68cef6 100644 --- a/src/Assets.php +++ b/src/Assets.php @@ -14,10 +14,6 @@ namespace Brain\Assets; -use Brain\Assets\Enqueue\Collection; -use Brain\Assets\Enqueue\JsEnqueue; -use Brain\Assets\Enqueue\Strategy; - class Assets { public const CSS = 'css'; @@ -26,6 +22,7 @@ class Assets public const VIDEO = 'videos'; public const FONT = 'fonts'; + private Enqueue\Collection $collection; private UrlResolver\MinifyResolver|null $minifyResolver = null; private string $handlePrefix; private bool $addVersion = true; @@ -33,6 +30,7 @@ class Assets private bool $useDepExtractionData = false; /** @var array */ private array $subFolders = []; + private bool $removalConfigured = false; /** * @param string $mainPluginFilePath @@ -138,6 +136,15 @@ final protected function __construct(private Factory $factory) { // Store separately from name, so we can enable & disable as well as changing it. $this->handlePrefix = $this->context()->name(); + $this->collection = Enqueue\Collection::new($this); + } + + /** + * @return Enqueue\Collection + */ + public function collection(): Enqueue\Collection + { + return clone $this->collection; } /** @@ -542,8 +549,8 @@ public function enqueueScript( * @param string $name * @param string $url * @param array $deps - * @param Strategy|bool|array|string|null $strategy - * @return JsEnqueue + * @param Enqueue\Strategy|bool|array|string|null $strategy + * @return Enqueue\JsEnqueue */ public function registerExternalScript( string $name, @@ -572,6 +579,42 @@ public function enqueueExternalScript( return $this->doEnqueueOrRegisterScript('enqueue', $name, $url, $deps, $strategy); } + /** + * @param string $name + * @return static + */ + public function dequeueStyle(string $name): static + { + return $this->deregisterOrDequeue($name, self::CSS, deregister: false); + } + + /** + * @param string $name + * @return static + */ + public function deregisterStyle(string $name): static + { + return $this->deregisterOrDequeue($name, self::CSS, deregister: true); + } + + /** + * @param string $name + * @return static + */ + public function dequeueScript(string $name): static + { + return $this->deregisterOrDequeue($name, self::JS, deregister: false); + } + + /** + * @param string $name + * @return static + */ + public function deregisterScript(string $name): static + { + return $this->deregisterOrDequeue($name, self::JS, deregister: true); + } + /** * @param string $name * @return string @@ -591,10 +634,13 @@ public function handleForName(string $name): string /** * @param array $jsDeps * @param array $cssDeps - * @return Collection + * @return Enqueue\Collection */ - public function registerAllFromManifest(array $jsDeps = [], array $cssDeps = []): Collection - { + public function registerAllFromManifest( + array $jsDeps = [], + array $cssDeps = [] + ): Enqueue\Collection { + $collection = []; $urls = $this->factory->manifestUrlResolver()->resolveAll(); foreach ($urls as $name => $url) { @@ -605,7 +651,7 @@ public function registerAllFromManifest(array $jsDeps = [], array $cssDeps = []) $collection[] = $registered; } - $registeredAll = Collection::new($this, ...$collection); + $registeredAll = Enqueue\Collection::new($this, ...$collection); do_action('brain.assets.registered-all-from-manifest', $registeredAll); return $registeredAll; @@ -627,22 +673,32 @@ private function doEnqueueOrRegisterStyle( string $media ): Enqueue\CssEnqueue { + $isEnqueue = $type === 'enqueue'; $handle = $this->handleForName($name); + + $existing = $this->maybeRegistered($handle, $isEnqueue, self::CSS); + if ($existing instanceof Enqueue\CssEnqueue) { + return $existing; + } + [$url, $useMinify] = ($url === null) ? $this->assetUrlForEnqueue($name, self::CSS) : [$this->adjustAbsoluteUrl($url), false]; $deps = $this->prepareDeps($deps, $url, $useMinify); - $isEnqueue = $type === 'enqueue'; /** @var callable $callback */ $callback = $isEnqueue ? 'wp_enqueue_style' : 'wp_register_style'; $callback($handle, $url, $deps, null, $media); - return $isEnqueue + $enqueued = $isEnqueue ? Enqueue\CssEnqueue::new($handle) : Enqueue\CssEnqueue::newRegistration($handle); + $this->collection = $this->collection->append($enqueued); + $this->setupRemoval(); + + return $enqueued; } /** @@ -650,7 +706,7 @@ private function doEnqueueOrRegisterStyle( * @param string $name * @param string|null $url * @param array $deps - * @param Strategy|bool|array|string|null $strategy + * @param Enqueue\Strategy|bool|array|string|null $strategy * @return Enqueue\JsEnqueue */ private function doEnqueueOrRegisterScript( @@ -661,7 +717,14 @@ private function doEnqueueOrRegisterScript( Enqueue\Strategy|bool|array|string|null $strategy ): Enqueue\JsEnqueue { + $isEnqueue = $type === 'enqueue'; $handle = $this->handleForName($name); + + $existing = $this->maybeRegistered($handle, $isEnqueue, self::JS); + if ($existing instanceof Enqueue\JsEnqueue) { + return $existing; + } + [$url, $useMinify] = ($url === null) ? $this->assetUrlForEnqueue($name, self::JS) : [$this->adjustAbsoluteUrl($url), false]; @@ -669,15 +732,42 @@ private function doEnqueueOrRegisterScript( $strategy = Enqueue\Strategy::new($strategy); $deps = $this->prepareDeps($deps, $url, $useMinify); - $isEnqueue = $type === 'enqueue'; /** @var callable $callback */ $callback = $isEnqueue ? 'wp_enqueue_script' : 'wp_register_script'; $callback($handle, $url, $deps, null, $strategy->toArray()); - return $isEnqueue + $enqueued = $isEnqueue ? Enqueue\JsEnqueue::new($handle, $strategy) : Enqueue\JsEnqueue::newRegistration($handle, $strategy); + $this->collection = $this->collection->append($enqueued); + $this->setupRemoval(); + + return $enqueued; + } + + /** + * @param string $name + * @param 'css'|'js' $type + * @param bool $deregister + * @return static + */ + private function deregisterOrDequeue(string $name, string $type, bool $deregister): static + { + $handle = $this->handleForName($name); + $existing = $this->maybeRegistered($handle, null, $type); + if ($existing instanceof Enqueue\Enqueue) { + $deregister ? $existing->deregister() : $existing->dequeue(); + + return $this; + } + + match ($type) { + 'css' => $deregister ? wp_deregister_style($handle) : wp_dequeue_script($handle), + 'js' => $deregister ? wp_deregister_script($handle) : wp_dequeue_script($handle), + }; + + return $this; } /** @@ -934,4 +1024,45 @@ private function unminifiedUrl(string $url): ?string return null; } + + /** + * @param string $handle + * @param bool|null $enqueue + * @param 'css'|'js' $type + * @return Enqueue\Enqueue|null + */ + private function maybeRegistered(string $handle, ?bool $enqueue, string $type): ?Enqueue\Enqueue + { + $existing = $this->collection()->oneByHandle($handle, $type); + if (($existing !== null) || ($enqueue === null)) { + if (($existing !== null) && ($enqueue === true) && !$existing->isEnqueued()) { + $existing->enqueue(); + } + + return $existing; + } + + if (($type === 'css') && wp_styles()->query($handle)) { + wp_dequeue_style($handle); + wp_deregister_style($handle); + } elseif (($type === 'js') && wp_scripts()->query($handle)) { + wp_dequeue_script($handle); + wp_deregister_script($handle); + } + + return null; + } + + /** + * @return void + */ + private function setupRemoval(): void + { + $this->removalConfigured or $this->removalConfigured = add_action( + 'brain.assets.deregistered', + function (Enqueue\Enqueue $enqueue): void { + $this->collection = $this->collection->remove($enqueue); + } + ); + } } diff --git a/src/Enqueue/AbstractEnqueue.php b/src/Enqueue/AbstractEnqueue.php index 3272ebc..73f019c 100644 --- a/src/Enqueue/AbstractEnqueue.php +++ b/src/Enqueue/AbstractEnqueue.php @@ -66,6 +66,7 @@ final public function deregister(): void $this->isCss() ? wp_deregister_style($this->handle()) : wp_deregister_script($this->handle()); + do_action('brain.assets.deregistered', $this); } /** diff --git a/src/Enqueue/Collection.php b/src/Enqueue/Collection.php index f6aa6f8..a759eea 100644 --- a/src/Enqueue/Collection.php +++ b/src/Enqueue/Collection.php @@ -42,6 +42,42 @@ final protected function __construct( $this->collection = $enqueues; } + /** + * @param Enqueue $enqueue + * @return static + */ + public function append(Enqueue $enqueue): static + { + $collection = []; + $enqueueId = $this->id($enqueue); + foreach ($this->collection as $item) { + if ($enqueueId === $this->id($item)) { + return clone $this; + } + $collection[] = $item; + } + $collection[] = $enqueue; + + return new static($this->assets, ...$collection); + } + + /** + * @param Enqueue $enqueue + * @return static + */ + public function remove(Enqueue $enqueue): static + { + $collection = []; + $enqueueId = $this->id($enqueue); + foreach ($this->collection as $item) { + if ($this->id($item) !== $enqueueId) { + $collection[] = $enqueue; + } + } + + return new static($this->assets, ...$collection); + } + /** * @param string $pattern * @param 'css'|'js'|null $type @@ -152,13 +188,11 @@ public function merge(Collection $collection): static { $merged = []; foreach ($this->collection as $enqueue) { - $id = $enqueue->handle() . ($enqueue->isJs() ? '--js' : '--css'); - $merged[$id] = $enqueue; + $merged[$this->id($enqueue)] = $enqueue; } foreach ($collection->collection as $enqueue) { - $id = $enqueue->handle() . ($enqueue->isJs() ? '--js' : '--css'); - $merged[$id] = $enqueue; + $merged[$this->id($enqueue)] = $enqueue; } return static::new($this->assets, ...array_values($merged)); @@ -172,13 +206,11 @@ public function diff(Collection $collection): static { $diff = []; foreach ($this->collection as $enqueue) { - $id = $enqueue->handle() . ($enqueue->isJs() ? '--js' : '--css'); - $diff[$id] = $enqueue; + $diff[$this->id($enqueue)] = $enqueue; } foreach ($collection->collection as $enqueue) { - $id = $enqueue->handle() . ($enqueue->isJs() ? '--js' : '--css'); - unset($diff[$id]); + unset($diff[$this->id($enqueue)]); } return static::new($this->assets, ...array_values($diff)); @@ -321,7 +353,7 @@ public function lastOf(callable $callback): ?Enqueue /** * @param string $name * @param 'css'|'js'|null $type - * @return Enqueue|null + * @return ($type is 'css' ? CssEnqueue|null : ($type is 'js' ? JsEnqueue|null : Enqueue)) */ public function oneByName(string $name, ?string $type = null): ?Enqueue { @@ -348,7 +380,7 @@ public function oneByName(string $name, ?string $type = null): ?Enqueue /** * @param string $handle * @param 'css'|'js'|null $type - * @return Enqueue|null + * @return ($type is 'css' ? CssEnqueue|null : ($type is 'js' ? JsEnqueue|null : Enqueue|null)) */ public function oneByHandle(string $handle, ?string $type = null): ?Enqueue { @@ -410,6 +442,16 @@ public function enqueue(): void } } + /** + * @return void + */ + public function dequeue(): void + { + foreach ($this->collection as $enqueue) { + $enqueue->dequeue(); + } + } + /** * @param non-empty-string $name * @param 'css'|'js'|null $type @@ -561,6 +603,15 @@ private function isRegex(string $pattern): bool return array_unique($flags) === $flags; } + /** + * @param Enqueue $enqueue + * @return string + */ + private function id(Enqueue $enqueue): string + { + return $enqueue->handle() . ($enqueue->isJs() ? '--js' : '--css'); + } + /** * @param string $str * @param 'handle'|'name' $varName diff --git a/src/Enqueue/Enqueue.php b/src/Enqueue/Enqueue.php index ffedcf3..cd19e7e 100644 --- a/src/Enqueue/Enqueue.php +++ b/src/Enqueue/Enqueue.php @@ -41,6 +41,11 @@ public function isEnqueued(): bool; */ public function dequeue(): static; + /** + * @return void + */ + public function deregister(): void; + /** * @return static */ diff --git a/src/Enqueue/JsEnqueue.php b/src/Enqueue/JsEnqueue.php index 1ebd56e..99baf49 100644 --- a/src/Enqueue/JsEnqueue.php +++ b/src/Enqueue/JsEnqueue.php @@ -63,9 +63,9 @@ public function handle(): string /** * @param string $condition - * @return JsEnqueue + * @return static */ - public function withCondition(string $condition): JsEnqueue + public function withCondition(string $condition): static { wp_scripts()->add_data($this->handle, 'conditional', $condition); @@ -74,9 +74,9 @@ public function withCondition(string $condition): JsEnqueue /** * @param string $jsCode - * @return JsEnqueue + * @return static */ - public function prependInline(string $jsCode): JsEnqueue + public function prependInline(string $jsCode): static { wp_add_inline_script($this->handle, $jsCode, 'before'); @@ -85,9 +85,9 @@ public function prependInline(string $jsCode): JsEnqueue /** * @param string $jsCode - * @return JsEnqueue + * @return static */ - public function appendInline(string $jsCode): JsEnqueue + public function appendInline(string $jsCode): static { wp_add_inline_script($this->handle, $jsCode, 'after'); $this->removeStrategy(); @@ -98,9 +98,9 @@ public function appendInline(string $jsCode): JsEnqueue /** * @param string $objectName * @param array $data - * @return JsEnqueue + * @return static */ - public function localize(string $objectName, array $data): JsEnqueue + public function localize(string $objectName, array $data): static { wp_localize_script($this->handle, $objectName, $data); @@ -108,31 +108,46 @@ public function localize(string $objectName, array $data): JsEnqueue } /** - * @return JsEnqueue + * @param Strategy $strategy + * @return static */ - public function useAsync(): JsEnqueue + public function useStrategy(Strategy $strategy): static { - $this->useStrategyAttribute(Strategy::ASYNC); + $this->strategy = $strategy; + $strategyName = match (true) { + $strategy->isDefer() => Strategy::DEFER, + $strategy->isAsync() => Strategy::ASYNC, + default => false, + }; + + wp_scripts()->add_data($this->handle, 'strategy', $strategyName); + wp_scripts()->add_data($this->handle, 'group', $strategy->inFooter() ? 1 : false); return $this; } /** - * @return JsEnqueue + * @return static */ - public function useDefer(): JsEnqueue + public function useAsync(): static { - $this->useStrategyAttribute(Strategy::DEFER); + return $this->useStrategy(Strategy::newAsync($this->strategy?->inFooter() ?? false)); + } - return $this; + /** + * @return static + */ + public function useDefer(): static + { + return $this->useStrategy(Strategy::newDefer($this->strategy?->inFooter() ?? false)); } /** * @param string $name * @param string $value - * @return JsEnqueue + * @return static */ - public function useAttribute(string $name, ?string $value): JsEnqueue + public function useAttribute(string $name, ?string $value): static { $nameLower = strtolower($name); if (($nameLower !== 'async') && ($nameLower !== 'defer')) { @@ -152,31 +167,15 @@ public function useAttribute(string $name, ?string $value): JsEnqueue /** * @param callable $callback - * @return JsEnqueue + * @return static */ - public function addFilter(callable $callback): JsEnqueue + public function addFilter(callable $callback): static { $this->setupFilters()->add($callback); return $this; } - /** - * @param 'async'|'defer' $strategy - * @return void - * - * phpcs:disable Generic.Metrics.CyclomaticComplexity - */ - private function useStrategyAttribute(string $strategyName): void - { - wp_scripts()->add_data($this->handle, 'strategy', $strategyName); - - $this->strategy = match ($strategyName) { - Strategy::ASYNC => Strategy::newAsync($this->strategy?->inFooter() ?? false), - Strategy::DEFER => Strategy::newDefer($this->strategy?->inFooter() ?? false), - }; - } - /** * @return void */ diff --git a/tests/src/TestCase.php b/tests/src/TestCase.php index 6c461db..f6fdd24 100644 --- a/tests/src/TestCase.php +++ b/tests/src/TestCase.php @@ -114,6 +114,85 @@ protected function factoryPathFinder(bool $useAlt, bool $debug): PathFinder return PathFinder::new($this->factoryContext($useAlt, $debug)); } + /** + * @param string $handle + * @param array $extra + * @return \_WP_Dependency + * + * phpcs:disable Inpsyde.CodeQuality.NestingLevel + */ + protected function factoryWpDependency(string $handle, array $extra = []): \_WP_Dependency + { + // phpcs:enable Inpsyde.CodeQuality.NestingLevel + \Mockery::spy(\_WP_Dependency::class); + + // phpcs:disable + /** @psalm-suppress PropertyNotSetInConstructor */ + return new class ($handle, $extra) extends \_WP_Dependency + { + /** @var string */ + public $handle; + /** @var array */ + public $extra; + + public function __construct(string $handle, array $extra) + { + $this->handle = $handle; + $this->extra = $extra; + } + + public function add_data(mixed $name, mixed $data): bool + { + if (is_scalar($name)) { + $this->extra[$name] = $data; + + return true; + } + + return false; + } + }; + // phpcs:enable + } + + /** + * @param 'style'|'script' $type + * @param array> $deps + * @return WpAssetsStub + */ + protected function mockWpDependencies(string $type, array $deps = []): WpAssetsStub + { + assert(in_array($type, ['style', 'script'], true)); + + $stub = new WpAssetsStub(); + foreach ($deps as $status => $elements) { + assert(in_array($status, ['registered', 'enqueued', 'to_do', 'done'], true)); + foreach ($elements as $elementData) { + $handle = $elementData[0]; + $extra = $elementData[1] ?? []; + assert(is_string($handle) && ($handle !== '')); + assert(is_array($extra)); + $dep = $this->factoryWpDependency($handle, $extra); + $stub->addWpDependencyStub($dep, $status); + } + } + + Monkey\Functions\expect("wp_{$type}s")->zeroOrMoreTimes()->andReturn($stub); + Monkey\Functions\expect("wp_{$type}_is") + ->zeroOrMoreTimes() + ->andReturnUsing( + static function ( + string $handle, + string $status = 'enqueued' + ) use ($stub): \_WP_Dependency|bool { + + return $stub->query($handle, $status); + } + ); + + return $stub; + } + /** * @param string $url * @return array{path: string|null, scheme: string|null, ver: string|null} diff --git a/tests/src/WpAssetsStub.php b/tests/src/WpAssetsStub.php index 01ada85..c8c8f32 100644 --- a/tests/src/WpAssetsStub.php +++ b/tests/src/WpAssetsStub.php @@ -16,17 +16,59 @@ class WpAssetsStub { + /** @var array> */ public array $data = []; + /** @var array}> */ + public array $dependencies = []; /** * @param string $handle - * @param mixed ...$args + * @param string $key + * @param mixed $value * * phpcs:disable PSR1.Methods.CamelCapsMethodName */ - public function add_data(string $handle, mixed ...$args): void + public function add_data(string $handle, string $key, mixed $value): void { // phpcs:enable PSR1.Methods.CamelCapsMethodName - $this->data[$handle] = $args; + isset($this->data[$handle]) or $this->data[$handle] = []; + $this->data[$handle][$key] = $value; + + [$dep] = $this->dependencies[$handle] ?? [null]; + /** @psalm-suppress MixedArgument */ + $dep?->add_data($key, $value); + } + + /** + * @param string $handle + * @param string $type + * @return \_WP_Dependency|bool + */ + public function query(string $handle, string $type = 'registered'): \_WP_Dependency|bool + { + [$dep, $types] = $this->dependencies[$handle] ?? [null, []]; + if (($dep !== null) && ($type === 'registered')) { + return $dep; + } + + return ($dep !== null) && in_array($type, $types, true); + } + + /** + * @param \_WP_Dependency $dependency + * @param 'enqueued'|'registered'|'to_do'|'done' $status + * @return static + */ + public function addWpDependencyStub(\_WP_Dependency $dependency, string $status): static + { + $types = match ($status) { + 'registered' => ['registered'], + 'enqueued' => ['registered', 'enqueued'], + 'to_do' => ['registered', 'enqueued', 'to_do'], + 'done' => ['registered', 'enqueued', 'done'], + }; + $this->dependencies[$dependency->handle] = [$dependency, $types]; + + return $this; } } diff --git a/tests/unit/AssetsTest.php b/tests/unit/AssetsTest.php index a646ecb..08022d4 100644 --- a/tests/unit/AssetsTest.php +++ b/tests/unit/AssetsTest.php @@ -18,7 +18,7 @@ use Brain\Assets\Enqueue\Enqueue; use Brain\Assets\Tests\AssetsTestCase; use Brain\Assets\Tests\TestCase; -use Brain\Monkey\Functions; +use Brain\Monkey; /** * @ runTestsInSeparateProcesses @@ -150,7 +150,9 @@ public function testPluginForManifest(): void */ public function testPluginEnqueueCss(): void { - Functions\expect('wp_enqueue_style') + $this->mockWpDependencies('style'); + + Monkey\Functions\expect('wp_enqueue_style') ->once() ->with('foo-admin', \Mockery::type('string'), ['jquery'], null, 'all') ->andReturnUsing( @@ -175,7 +177,9 @@ function (string $handle, string $url): void { */ public function testPluginEnqueueCssNoVerNoMin(): void { - Functions\expect('wp_enqueue_style') + $this->mockWpDependencies('style'); + + Monkey\Functions\expect('wp_enqueue_style') ->once() ->with('foo-admin', \Mockery::type('string'), ['jquery'], null, 'all') ->andReturnUsing( @@ -200,7 +204,9 @@ function (string $handle, string $url): void { */ public function testPluginEnqueueCssMinNotFound(): void { - Functions\expect('wp_enqueue_style') + $this->mockWpDependencies('style'); + + Monkey\Functions\expect('wp_enqueue_style') ->once() ->with('foo-no-min', \Mockery::type('string'), [], null, 'screen') ->andReturnUsing( @@ -358,7 +364,9 @@ public function testThemeJsUrlWithDebug(): void */ public function testThemeEnqueueScript(): void { - Functions\expect('wp_enqueue_script') + $this->mockWpDependencies('script'); + + Monkey\Functions\expect('wp_enqueue_script') ->once() ->with('parent-theme', \Mockery::type('string'), ['jquery'], null, true) ->andReturnUsing( @@ -371,7 +379,7 @@ function (string $handle, string $url): void { } ); - Functions\expect('wp_localize_script') + Monkey\Functions\expect('wp_localize_script') ->once() ->with('parent-theme', 'ThemeData', ['foo' => 'bar']); @@ -486,7 +494,7 @@ public function testChildThemeFromParentAbsoluteUrl(): void } /** - * @return void + * @test */ public function testChildThemeFromExternalAbsoluteUrl(): void { @@ -499,7 +507,7 @@ public function testChildThemeFromExternalAbsoluteUrl(): void } /** - * @return void + * @test */ public function testChildThemeFromExternalAbsoluteUrlWithoutForceSecure(): void { @@ -533,7 +541,9 @@ public function testLibraryAutomaticManifest(): void */ public function testLibraryEnqueue(): void { - Functions\expect('wp_enqueue_style') + $this->mockWpDependencies('style'); + + Monkey\Functions\expect('wp_enqueue_style') ->once() ->with('foo', "{$this->baseUrl}/foo.abcde.css", [], null, 'screen'); @@ -543,12 +553,73 @@ public function testLibraryEnqueue(): void ->enqueueStyle('foo', [], 'screen'); } + /** + * @test + */ + public function testLibraryEnqueueHappenOnce(): void + { + $this->mockWpDependencies('style'); + Monkey\Functions\expect('wp_register_style')->once(); + Monkey\Functions\expect('wp_enqueue_style')->once(); + + $assets = $this->factoryLibraryAssets(); + + $assets->registerStyle('foo'); + $assets->registerStyle('foo.css'); + $assets->enqueueStyle('foo'); + $assets->enqueueStyle('foo.css'); + $assets->registerStyle('foo'); + $assets->enqueueStyle('foo'); + } + + /** + * @test + */ + public function testLibraryEnqueueRegisteredFromWp(): void + { + $this->mockWpDependencies('style', ['registered' => [['lib-foo']]]); + $this->mockWpDependencies('script', ['registered' => [['lib-some-script']]]); + + Monkey\Functions\expect('wp_dequeue_style')->once()->with('lib-foo'); + Monkey\Functions\expect('wp_deregister_style')->once()->with('lib-foo'); + Monkey\Functions\expect('wp_dequeue_script')->once()->with('lib-some-script'); + Monkey\Functions\expect('wp_deregister_script')->once()->with('lib-some-script'); + Monkey\Functions\expect('wp_enqueue_style')->once(); + Monkey\Functions\expect('wp_enqueue_script')->once(); + + $assets = $this->factoryLibraryAssets(); + + $assets->enqueueStyle('foo'); + $assets->enqueueScript('some-script'); + + static::assertSame('lib-foo', $assets->collection()->first(Assets::CSS)?->handle()); + static::assertSame('lib-some-script', $assets->collection()->first(Assets::JS)?->handle()); + } + + /** + * @test + */ + public function testLibraryEnqueueEnqueuedFromWp(): void + { + $this->mockWpDependencies('style', ['enqueued' => [['lib-foo']]]); + + Monkey\Functions\expect('wp_dequeue_style')->once()->with('lib-foo'); + Monkey\Functions\expect('wp_deregister_style')->once()->with('lib-foo'); + Monkey\Functions\expect('wp_enqueue_style')->once(); + + $assets = $this->factoryLibraryAssets(); + + $assets->enqueueStyle('foo'); + } + /** * @test */ public function testLibraryEnqueueFromDepInfo(): void { - Functions\expect('wp_enqueue_script') + $this->mockWpDependencies('script'); + + Monkey\Functions\expect('wp_enqueue_script') ->once() ->with( 'lib-some-script', @@ -568,7 +639,9 @@ public function testLibraryEnqueueFromDepInfo(): void */ public function testLibraryEnqueueFromDepInfoWithStrategy(): void { - Functions\expect('wp_enqueue_script') + $this->mockWpDependencies('script'); + + Monkey\Functions\expect('wp_enqueue_script') ->once() ->with( 'lib-some-script', @@ -588,9 +661,11 @@ public function testLibraryEnqueueFromDepInfoWithStrategy(): void */ public function testRegisterAndEnqueueManifestPlusDependencyDataExtraction(): void { + $this->mockWpDependencies('script'); + $assets = $this->factoryManifestsAssets()->useDependencyExtractionData(); - Functions\expect('wp_register_script') + Monkey\Functions\expect('wp_register_script') ->once() ->with( 'hello-world-block-a', @@ -600,7 +675,7 @@ public function testRegisterAndEnqueueManifestPlusDependencyDataExtraction(): vo ['in_footer' => true] ); - Functions\expect('wp_enqueue_script') + Monkey\Functions\expect('wp_enqueue_script') ->once() ->with('hello-world-block-a'); @@ -629,6 +704,9 @@ public function testRegisterAllFromManifestPlusDependencyDataExtraction(): void 'hello-world-front-style', ]; + $this->mockWpDependencies('style'); + $this->mockWpDependencies('script'); + $aString = \Mockery::type('string'); /** @psalm-suppress InvalidArgument */ $aHandle = \Mockery::anyOf(...$handles); @@ -646,7 +724,7 @@ public function testRegisterAllFromManifestPlusDependencyDataExtraction(): void static::assertSame($query, 'v=a29c9d677e174811e603'); }; - Functions\expect('wp_register_script') + Monkey\Functions\expect('wp_register_script') ->twice() ->with($aHandle, $aString, [], null, ['in_footer' => true]) ->andReturnUsing($dataCheck) @@ -655,13 +733,13 @@ public function testRegisterAllFromManifestPlusDependencyDataExtraction(): void ->with($aHandle, $aString, $deps, null, ['in_footer' => true]) ->andReturnUsing($dataCheck); - Functions\expect('wp_register_style') + Monkey\Functions\expect('wp_register_style') ->times(4) ->with($aHandle, $aString, [], null, 'all') ->andReturnUsing($dataCheck); - Functions\expect('wp_enqueue_script')->times(4)->with($aHandle); - Functions\expect('wp_enqueue_style')->times(4)->with($aHandle); + Monkey\Functions\expect('wp_enqueue_script')->times(4)->with($aHandle); + Monkey\Functions\expect('wp_enqueue_style')->times(4)->with($aHandle); $collection = $this ->factoryManifestsAssets() @@ -695,6 +773,108 @@ public function testRegisterAllFromManifestPlusDependencyDataExtraction(): void $collection->enqueue(); } + /** + * @test + */ + public function testAssetsCollection(): void + { + $this->mockWpDependencies('style'); + $this->mockWpDependencies('script'); + + $assets = $this->factoryLibraryAssets(); + + Monkey\Functions\expect('wp_register_style')->once(); + Monkey\Functions\expect('wp_enqueue_style')->once(); + Monkey\Functions\expect('wp_dequeue_style')->once(); + Monkey\Functions\expect('wp_deregister_style')->once(); + + $onDeregister = null; + Monkey\Actions\expectAdded('brain.assets.deregistered') + ->once() + ->whenHappen(static function (callable $callback) use (&$onDeregister): void { + $onDeregister = $callback; + }); + Monkey\Actions\expectDone('brain.assets.deregistered') + ->once() + ->with(\Mockery::type(Enqueue::class)) + ->whenHappen(static function (Enqueue $enqueue) use (&$onDeregister): void { + /** @var callable $onDeregister */ + $onDeregister($enqueue); + }); + + $enqueue = $assets->registerStyle('foo'); + $retrieved = $assets->collection()->cssOnly()->first(); + + static::assertSame($enqueue, $retrieved); + static::assertFalse($retrieved->isEnqueued()); + + $enqueue->enqueue(); + + static::assertTrue($retrieved->isEnqueued()); + static::assertTrue($assets->collection()->cssOnly()->first()->isEnqueued()); + + $enqueue->deregister(); + + static::assertFalse($retrieved->isEnqueued()); + static::assertCount(0, $assets->collection()); + } + + /** + * @test + */ + public function testAssetsCollectionsFromManifest(): void + { + $this->mockWpDependencies('style'); + $this->mockWpDependencies('script'); + Monkey\Functions\expect('wp_register_style')->times(4); + Monkey\Functions\expect('wp_register_script')->times(4); + + $assets = $this->factoryManifestsAssets(); + $fromManifest = $assets->useDependencyExtractionData()->registerAllFromManifest(); + $collection = $assets->collection(); + + static::assertCount(8, $fromManifest); + static::assertCount(8, $collection); + static::assertNotSame($fromManifest, $collection); + static::assertSame([], $fromManifest->diff($collection)->handles()); + static::assertSame([], $collection->diff($fromManifest)->handles()); + static::assertSame($fromManifest->handles(), $collection->merge($fromManifest)->handles()); + } + + /** + * @test + */ + public function testAssetsCollectionsBulkRegistration(): void + { + $this->mockWpDependencies('style'); + $this->mockWpDependencies('script'); + Monkey\Functions\expect('wp_register_script')->times(4); + Monkey\Functions\expect('wp_register_style')->times(4); + $handles = [ + 'hello-world-admin', + 'hello-world-admin-style', + 'hello-world-block-a', + 'hello-world-block-a-style', + 'hello-world-block-b', + 'hello-world-block-b-style', + 'hello-world-front', + 'hello-world-front-style', + ]; + + $assets = $this->factoryManifestsAssets()->useDependencyExtractionData(); + + foreach (glob($assets->context()->basePath() . '*.{css,js}', GLOB_BRACE) as $file) { + str_ends_with($file, '.css') + ? $assets->registerStyle(basename($file, '.css')) + : $assets->registerScript(basename($file, '.js')); + } + + $collection = $assets->collection(); + + static::assertCount(8, $collection); + static::assertSame([], array_diff($collection->handles(), $handles)); + } + /** * @return Assets */ @@ -702,11 +882,11 @@ private function factoryPluginAssets(): Assets { $pluginFilePath = static::fixturesPath('/plugins/foo/plugin.php'); - Functions\expect('plugin_basename') + Monkey\Functions\expect('plugin_basename') ->with($pluginFilePath) ->andReturn('foo/plugin.php'); - Functions\expect('plugins_url') + Monkey\Functions\expect('plugins_url') ->with('/assets/', $pluginFilePath) ->andReturn('http://example.com/wp-content/plugins/foo/assets'); @@ -721,9 +901,12 @@ private function factoryThemeAssets(): Assets $themePath = static::fixturesPath('/themes/parent'); $themeDir = '/wp-content/themes/parent'; - Functions\when('get_template')->justReturn('parent'); - Functions\when('get_template_directory')->justReturn($themePath); - Functions\when('get_template_directory_uri')->justReturn('http://example.com' . $themeDir); + Monkey\Functions\when('get_template') + ->justReturn('parent'); + Monkey\Functions\when('get_template_directory') + ->justReturn($themePath); + Monkey\Functions\when('get_template_directory_uri') + ->justReturn('http://example.com' . $themeDir); return Assets::forTheme('/assets')->tryMinUrls(); } @@ -733,20 +916,21 @@ private function factoryThemeAssets(): Assets */ private function factoryChildThemeAssets(): Assets { - $themeFolder = '/wp-content/themes/parent'; - $childThemeFolder = '/wp-content/themes/child'; - $baseUrl = 'https://example.com'; $basePath = static::fixturesPath(); - Functions\when('get_template')->justReturn('parent'); - Functions\when('get_stylesheet')->justReturn('child'); + $themeFolder = '/wp-content/themes/parent'; + $childThemeFolder = '/wp-content/themes/child'; + $childThemeUrl = $baseUrl . $childThemeFolder; + + Monkey\Functions\when('get_template')->justReturn('parent'); + Monkey\Functions\when('get_stylesheet')->justReturn('child'); - Functions\when('get_stylesheet_directory')->justReturn($basePath . '/themes/child'); - Functions\when('get_stylesheet_directory_uri')->justReturn($baseUrl . $childThemeFolder); + Monkey\Functions\when('get_stylesheet_directory')->justReturn($basePath . '/themes/child'); + Monkey\Functions\when('get_stylesheet_directory_uri')->justReturn($childThemeUrl); - Functions\when('get_template_directory')->justReturn($basePath . '/themes/parent'); - Functions\when('get_template_directory_uri')->justReturn($baseUrl . $themeFolder); + Monkey\Functions\when('get_template_directory')->justReturn($basePath . '/themes/parent'); + Monkey\Functions\when('get_template_directory_uri')->justReturn($baseUrl . $themeFolder); return Assets::forChildTheme('/', '/assets'); } diff --git a/tests/unit/Enqueue/CollectionTest.php b/tests/unit/Enqueue/CollectionTest.php index 080a629..c947161 100644 --- a/tests/unit/Enqueue/CollectionTest.php +++ b/tests/unit/Enqueue/CollectionTest.php @@ -228,6 +228,8 @@ public function testByHandle(): void */ private function factoryCollection(): Collection { + $this->mockWpDependencies('style'); + $this->mockWpDependencies('script'); Monkey\Functions\expect('wp_register_style')->times(4); Monkey\Functions\expect('wp_register_script')->times(4); diff --git a/tests/unit/Enqueue/CssEnqueueTest.php b/tests/unit/Enqueue/CssEnqueueTest.php index 7f49610..2e9130d 100644 --- a/tests/unit/Enqueue/CssEnqueueTest.php +++ b/tests/unit/Enqueue/CssEnqueueTest.php @@ -17,8 +17,7 @@ use Brain\Assets\Enqueue\CssEnqueue; use Brain\Assets\Tests\TestCase; use Brain\Assets\Tests\WpAssetsStub; -use Brain\Monkey\Functions; -use Brain\Monkey\Filters; +use Brain\Monkey; class CssEnqueueTest extends TestCase { @@ -30,13 +29,7 @@ class CssEnqueueTest extends TestCase protected function setUp(): void { parent::setUp(); - - Functions\when('wp_styles')->alias( - function (): WpAssetsStub { - $this->wpStyles or $this->wpStyles = new WpAssetsStub(); - return $this->wpStyles; - } - ); + $this->wpStyles = $this->mockWpDependencies('style'); } /** @@ -55,7 +48,7 @@ public function testConditional(): void { CssEnqueue::new('h1')->withCondition('lt IE 9'); - static::assertSame(['conditional', 'lt IE 9'], $this->wpStyles?->data['h1']); + static::assertSame('lt IE 9', $this->wpStyles?->data['h1']['conditional'] ?? null); } /** @@ -65,7 +58,7 @@ public function testAlternate(): void { CssEnqueue::new('h2')->asAlternate(); - static::assertSame(['alt', true], $this->wpStyles?->data['h2']); + static::assertSame(true, $this->wpStyles?->data['h2']['alt'] ?? null); } /** @@ -75,7 +68,7 @@ public function testTitle(): void { CssEnqueue::new('h3')->withTitle('Hello'); - static::assertSame(['title', 'Hello'], $this->wpStyles?->data['h3']); + static::assertSame('Hello', $this->wpStyles?->data['h3']['title'] ?? null); } /** @@ -85,7 +78,7 @@ public function testInline(): void { $inline = 'p { display:none }'; - Functions\expect('wp_add_inline_style')->once()->with('h4', $inline); + Monkey\Functions\expect('wp_add_inline_style')->once()->with('h4', $inline); CssEnqueue::new('h4')->appendInline($inline); } @@ -100,7 +93,7 @@ public function testFilters(): void /** @var callable|null $filterCallback */ $filterCallback = null; - Filters\expectAdded('style_loader_tag') + Monkey\Filters\expectAdded('style_loader_tag') ->once() ->whenHappen( static function (callable $callback) use (&$filterCallback): void { @@ -108,7 +101,7 @@ static function (callable $callback) use (&$filterCallback): void { } ); - Filters\expectApplied('style_loader_tag') + Monkey\Filters\expectApplied('style_loader_tag') ->once() ->with($tag, $handle) ->andReturnUsing( diff --git a/tests/unit/Enqueue/JsEnqueueTest.php b/tests/unit/Enqueue/JsEnqueueTest.php index 375f1d3..d45e92c 100644 --- a/tests/unit/Enqueue/JsEnqueueTest.php +++ b/tests/unit/Enqueue/JsEnqueueTest.php @@ -29,13 +29,7 @@ class JsEnqueueTest extends TestCase protected function setUp(): void { parent::setUp(); - - Monkey\Functions\when('wp_scripts')->alias( - function (): WpAssetsStub { - $this->wpScripts or $this->wpScripts = new WpAssetsStub(); - return $this->wpScripts; - } - ); + $this->wpScripts = $this->mockWpDependencies('script'); } /** @@ -54,7 +48,7 @@ public function testConditional(): void { JsEnqueue::new('h1')->withCondition('lt IE 9'); - static::assertSame(['conditional', 'lt IE 9'], $this->wpScripts?->data['h1']); + static::assertSame('lt IE 9', $this->wpScripts()->data['h1']['conditional'] ?? null); } /** @@ -161,7 +155,7 @@ static function (string $tag, string $handle) use (&$tagCb): mixed { . '">' . $after; - static::assertSame(['strategy', 'async'], $this->wpScripts?->data[$handle]); + static::assertSame('async', $this->wpScripts()->data[$handle]['strategy'] ?? null); static::assertSame($expected, $filtered); } @@ -171,25 +165,35 @@ static function (string $tag, string $handle) use (&$tagCb): mixed { public function testAsyncDefer(): void { $enqueue = JsEnqueue::new('handle')->useDefer(); - static::assertSame(['strategy', 'defer'], $this->wpScripts?->data['handle']); + static::assertSame('defer', $this->wpScripts()->data['handle']['strategy'] ?? null); $enqueue->useAsync(); - static::assertSame(['strategy', 'async'], $this->wpScripts?->data['handle']); + static::assertSame('async', $this->wpScripts()->data['handle']['strategy'] ?? null); $enqueue->useAttribute('defer', null); - static::assertSame(['strategy', 'defer'], $this->wpScripts?->data['handle']); + static::assertSame('defer', $this->wpScripts()->data['handle']['strategy'] ?? null); $enqueue->useAttribute('async', 'true'); - static::assertSame(['strategy', 'async'], $this->wpScripts?->data['handle']); + static::assertSame('async', $this->wpScripts()->data['handle']['strategy'] ?? null); $enqueue->useAttribute('async', 'false'); - static::assertSame(['strategy', false], $this->wpScripts?->data['handle']); + static::assertSame(false, $this->wpScripts()->data['handle']['strategy'] ?? null); $enqueue->useDefer(); $enqueue->useDefer(); - static::assertSame(['strategy', 'defer'], $this->wpScripts?->data['handle']); + static::assertSame('defer', $this->wpScripts()->data['handle']['strategy'] ?? null); $enqueue->useAsync(); - static::assertSame(['strategy', 'async'], $this->wpScripts?->data['handle']); + static::assertSame('async', $this->wpScripts()->data['handle']['strategy'] ?? null); + } + + /** + * @return WpAssetsStub + */ + private function wpScripts(): WpAssetsStub + { + assert($this->wpScripts instanceof WpAssetsStub); + + return $this->wpScripts; } }