mirror of
https://github.com/twigphp/Twig.git
synced 2026-11-01 08:35:42 +00:00
9e50ff7083
`Cva::apply()` splits the resolved classes with `preg_split()` and deduplicates them with `array_unique()` on every call. Components call `html_cva()` on every render and apply the same few variant combinations, so most of that work is repeated: on an admin dashboard built with the Symfony UX Toolkit Shadcn kit, the 486 calls per request resolve to only 38 distinct class strings (92% repeats), around 550 bytes and 37 classes each. Splitting and deduplicating them is about 1.5 µs out of a 2 µs call.
The result now goes into a static array keyed by the resolved class string, the only thing it depends on. The array is static because `html_cva()` creates a new `Cva` on every call. It holds at most 1,000 entries and is emptied when full.
- `Cva::apply()` time per dashboard request (hrtime around the method, cache emptied between requests like PHP-FPM, 6 interleaved runs of 20 requests each side): **1.27-1.35 ms** -> **0.69-0.73 ms** (-46%); with the cache kept between requests, as in a long-running worker: **0.58-0.61 ms**
- standalone benchmark (500 buttons using the Shadcn button recipe, 24 distinct variant/size pairs, cache emptied between renders, 5 interleaved runs each side, identical output): **510-517 ms** -> **432-434 ms** (-16%)
- Blackfire (5 samples of 5 requests): wall time **2.52 s** -> **2.40 s**
On the whole dashboard request, this saves about 0.6 ms out of 46 ms (~1.3%).
```php
<?php
// Run from extra/html-extra: php bench.php
use Twig\Environment;
use Twig\Extra\Html\Cva;
use Twig\Extra\Html\HtmlExtension;
use Twig\Loader\ArrayLoader;
require __DIR__.'/vendor/autoload.php';
// The button recipe of the Symfony UX Toolkit Shadcn kit
$twig = new Environment(new ArrayLoader([
'button.html.twig' => <<<'TWIG'
{%- set style = html_cva(
base: "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline: 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
icon: 'size-8',
'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
) -%}
<button class="{{ style.apply({variant: variant, size: size}) }}">{{ label }}</button>
TWIG,
// 500 buttons, 24 distinct variant/size pairs
'page.html.twig' => <<<'TWIG'
{% for i in 1..500 %}{{ include('button.html.twig', {variant: ['default', 'outline', 'secondary', 'ghost', 'destructive', 'link'][i % 6], size: ['default', 'sm', 'lg', 'icon'][i % 4], label: 'Button ' ~ i}) }}{% endfor %}
TWIG,
]));
$twig->addExtension(new HtmlExtension());
// PHP-FPM empties static properties at the end of every request
$render = static function () use ($twig): string {
if (property_exists(Cva::class, 'classListCache')) {
(new \ReflectionProperty(Cva::class, 'classListCache'))->setValue(null, []);
}
return $twig->render('page.html.twig');
};
$html = $render();
$times = [];
for ($run = 0; $run < 7; ++$run) {
$start = hrtime(true);
for ($i = 0; $i < 100; ++$i) {
$render();
}
$times[] = (hrtime(true) - $start) / 1e6;
}
sort($times);
printf("100 renders of 500 buttons: median %.0f ms (min %.0f, max %.0f) | sha1 %s\n", $times[3], $times[0], $times[6], sha1($html));
```
Blackfire profiles (dashboard page):
- before: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/57d327e3-02f6-4d6d-b27c-a39d3ce0438e/graph
- after: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/f63c01a9-adab-48bd-a524-67d576ab5ebf/graph
- comparison: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/compare/57d327e3-02f6-4d6d-b27c-a39d3ce0438e...f63c01a9-adab-48bd-a524-67d576ab5ebf/graph
This dashboard is available at https://github.com/Kocal/sf-ux-perfs-twig-components.
685 lines
22 KiB
PHP
685 lines
22 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of Twig.
|
|
*
|
|
* (c) Fabien Potencier
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Twig\Extra\Html\Tests;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Twig\Extra\Html\Cva;
|
|
|
|
class CvaTest extends TestCase
|
|
{
|
|
/**
|
|
* @dataProvider recipeProvider
|
|
*/
|
|
public function testRecipes(array $recipe, array $recipes, string $expected): void
|
|
{
|
|
$recipeClass = new Cva($recipe['base'] ?? '', $recipe['variants'] ?? [], $recipe['compounds'] ?? [], $recipe['defaultVariants'] ?? []);
|
|
|
|
$this->assertEquals($expected, $recipeClass->apply($recipes));
|
|
}
|
|
|
|
public function testApply(): void
|
|
{
|
|
$recipe = new Cva('font-semibold border rounded', [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
], [
|
|
[
|
|
'colors' => ['primary'],
|
|
'sizes' => ['sm'],
|
|
'class' => 'text-red-500',
|
|
],
|
|
]);
|
|
|
|
$this->assertEquals('font-semibold border rounded text-primary text-sm text-red-500', $recipe->apply(['colors' => 'primary', 'sizes' => 'sm']));
|
|
}
|
|
|
|
public function testApplySameRecipesAgain(): void
|
|
{
|
|
$createRecipe = static fn () => new Cva("font-semibold border\nrounded border", [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
]);
|
|
$recipe = $createRecipe();
|
|
|
|
$this->assertSame('font-semibold border rounded text-primary', $recipe->apply(['colors' => 'primary']));
|
|
$this->assertSame('font-semibold border rounded text-secondary', $recipe->apply(['colors' => 'secondary']));
|
|
$this->assertSame('font-semibold border rounded text-primary', $recipe->apply(['colors' => 'primary']));
|
|
$this->assertSame('font-semibold border rounded text-primary', $createRecipe()->apply(['colors' => 'primary']));
|
|
}
|
|
|
|
public function testApplyWithNullString(): void
|
|
{
|
|
$recipe = new Cva('font-semibold border rounded', [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
], [
|
|
[
|
|
'colors' => ['primary'],
|
|
'sizes' => ['sm'],
|
|
'class' => 'text-red-500',
|
|
],
|
|
]);
|
|
|
|
$this->assertEquals('font-semibold border rounded text-primary text-sm text-red-500 flex justify-center', $recipe->apply(['colors' => 'primary', 'sizes' => 'sm'], 'flex', null, 'justify-center'));
|
|
}
|
|
|
|
public static function recipeProvider(): iterable
|
|
{
|
|
yield 'base null' => [
|
|
['variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
]],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'text-primary text-sm',
|
|
];
|
|
|
|
yield 'base empty' => [
|
|
[
|
|
'base' => '',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
]],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'text-primary text-sm',
|
|
];
|
|
|
|
yield 'base array' => [
|
|
[
|
|
'base' => ['font-semibold', 'border', 'rounded'],
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
]],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'font-semibold border rounded text-primary text-sm',
|
|
];
|
|
|
|
yield 'no recipes match' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'red', 'sizes' => 'test'],
|
|
'font-semibold border rounded',
|
|
];
|
|
|
|
yield 'simple variants' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'font-semibold border rounded text-primary text-sm',
|
|
];
|
|
|
|
yield 'simple variants as array' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => ['text-primary', 'uppercase'],
|
|
'secondary' => ['text-secondary', 'uppercase'],
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'font-semibold border rounded text-primary uppercase text-sm',
|
|
];
|
|
|
|
yield 'simple variants with custom' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'secondary', 'sizes' => 'md'],
|
|
'font-semibold border rounded text-secondary text-md',
|
|
];
|
|
|
|
yield 'compound variants' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
],
|
|
'compounds' => [
|
|
[
|
|
'colors' => 'primary',
|
|
'sizes' => ['sm'],
|
|
'class' => 'text-red-100',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'font-semibold border rounded text-primary text-sm text-red-100',
|
|
];
|
|
|
|
yield 'compound variants with true' => [
|
|
[
|
|
'base' => 'button',
|
|
'variants' => [
|
|
'colors' => [
|
|
'blue' => 'btn-blue',
|
|
'red' => 'btn-red',
|
|
],
|
|
'disabled' => [
|
|
'true' => 'disabled',
|
|
],
|
|
],
|
|
'compounds' => [
|
|
[
|
|
'colors' => 'blue',
|
|
'disabled' => ['true'],
|
|
'class' => 'font-bold',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'blue', 'disabled' => 'true'],
|
|
'button btn-blue disabled font-bold',
|
|
];
|
|
|
|
yield 'compound variants as array' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
],
|
|
'compounds' => [
|
|
[
|
|
'colors' => ['primary'],
|
|
'sizes' => ['sm'],
|
|
'class' => ['text-red-900', 'bold'],
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'font-semibold border rounded text-primary text-sm text-red-900 bold',
|
|
];
|
|
|
|
yield 'multiple compound variants' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
],
|
|
'compounds' => [
|
|
[
|
|
'colors' => ['primary'],
|
|
'sizes' => ['sm'],
|
|
'class' => 'text-red-300',
|
|
],
|
|
[
|
|
'colors' => ['primary'],
|
|
'sizes' => ['md'],
|
|
'class' => 'text-blue-300',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'font-semibold border rounded text-primary text-sm text-red-300',
|
|
];
|
|
|
|
yield 'compound with multiple variants' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
],
|
|
'compounds' => [
|
|
[
|
|
'colors' => ['primary', 'secondary'],
|
|
'sizes' => ['sm'],
|
|
'class' => 'text-red-800',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'font-semibold border rounded text-primary text-sm text-red-800',
|
|
];
|
|
|
|
yield 'compound doesn\'t match' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
],
|
|
'compounds' => [
|
|
[
|
|
'colors' => ['danger', 'secondary'],
|
|
'sizes' => ['sm'],
|
|
'class' => 'text-red-500',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'font-semibold border rounded text-primary text-sm',
|
|
];
|
|
|
|
yield 'default variables' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
'rounded' => [
|
|
'sm' => 'rounded-sm',
|
|
'md' => 'rounded-md',
|
|
'lg' => 'rounded-lg',
|
|
],
|
|
],
|
|
'compounds' => [
|
|
[
|
|
'colors' => ['danger', 'secondary'],
|
|
'sizes' => 'sm',
|
|
'class' => 'text-red-500',
|
|
],
|
|
],
|
|
'defaultVariants' => [
|
|
'colors' => 'primary',
|
|
'sizes' => 'sm',
|
|
'rounded' => 'md',
|
|
],
|
|
],
|
|
['colors' => 'primary', 'sizes' => 'sm'],
|
|
'font-semibold border rounded text-primary text-sm rounded-md',
|
|
];
|
|
|
|
yield 'default variables all overwrite' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
'rounded' => [
|
|
'sm' => 'rounded-sm',
|
|
'md' => 'rounded-md',
|
|
'lg' => 'rounded-lg',
|
|
],
|
|
],
|
|
'compounds' => [
|
|
[
|
|
'colors' => ['danger', 'secondary'],
|
|
'sizes' => ['sm'],
|
|
'class' => 'text-red-500',
|
|
],
|
|
],
|
|
'defaultVariants' => [
|
|
'colors' => 'primary',
|
|
'sizes' => 'sm',
|
|
'rounded' => 'md',
|
|
],
|
|
],
|
|
['colors' => 'primary', 'sizes' => 'sm', 'rounded' => 'lg'],
|
|
'font-semibold border rounded text-primary text-sm rounded-lg',
|
|
];
|
|
|
|
yield 'default variables without matching variants' => [
|
|
[
|
|
'base' => 'font-semibold border rounded',
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'sizes' => [
|
|
'sm' => 'text-sm',
|
|
'md' => 'text-md',
|
|
'lg' => 'text-lg',
|
|
],
|
|
'rounded' => [
|
|
'sm' => 'rounded-sm',
|
|
'md' => 'rounded-md',
|
|
'lg' => 'rounded-lg',
|
|
],
|
|
],
|
|
'compounds' => [
|
|
[
|
|
'colors' => ['danger', 'secondary'],
|
|
'sizes' => ['sm'],
|
|
'class' => 'text-red-500',
|
|
],
|
|
],
|
|
'defaultVariants' => [
|
|
'colors' => 'primary',
|
|
'sizes' => 'sm',
|
|
'rounded' => 'md',
|
|
],
|
|
],
|
|
[],
|
|
'font-semibold border rounded text-primary text-sm rounded-md',
|
|
];
|
|
|
|
yield 'default variables with boolean' => [
|
|
[
|
|
'base' => 'button',
|
|
'variants' => [
|
|
'colors' => [
|
|
'blue' => 'btn-blue',
|
|
'red' => 'btn-red',
|
|
],
|
|
'disabled' => [
|
|
'true' => 'disabled',
|
|
'false' => 'opacity-100',
|
|
],
|
|
],
|
|
'defaultVariants' => [
|
|
'colors' => 'blue',
|
|
'disabled' => 'false',
|
|
],
|
|
],
|
|
[],
|
|
'button btn-blue opacity-100',
|
|
];
|
|
|
|
yield 'boolean string variants true / true' => [
|
|
[
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'disabled' => [
|
|
'true' => 'disable',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'disabled' => true],
|
|
'text-primary disable',
|
|
];
|
|
|
|
yield 'boolean string variants true / false' => [
|
|
[
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'disabled' => [
|
|
'true' => 'disable',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'disabled' => false],
|
|
'text-primary',
|
|
];
|
|
|
|
yield 'boolean string variants false / true' => [
|
|
[
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'disabled' => [
|
|
'false' => 'disable',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'disabled' => true],
|
|
'text-primary',
|
|
];
|
|
|
|
yield 'boolean string variants false / false' => [
|
|
[
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'disabled' => [
|
|
'false' => 'disable',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'disabled' => false],
|
|
'text-primary disable',
|
|
];
|
|
|
|
yield 'boolean string variants missing' => [
|
|
[
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'disabled' => [
|
|
'true' => 'disable',
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary'],
|
|
'text-primary',
|
|
];
|
|
|
|
yield 'boolean list variants true' => [
|
|
[
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'disabled' => [
|
|
'true' => ['disable', 'opacity-50'],
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'disabled' => true],
|
|
'text-primary disable opacity-50',
|
|
];
|
|
|
|
yield 'boolean list variants false' => [
|
|
[
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'disabled' => [
|
|
'true' => ['disable', 'opacity-50'],
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary', 'disabled' => false],
|
|
'text-primary',
|
|
];
|
|
|
|
yield 'boolean list variants missing' => [
|
|
[
|
|
'variants' => [
|
|
'colors' => [
|
|
'primary' => 'text-primary',
|
|
'secondary' => 'text-secondary',
|
|
],
|
|
'disabled' => [
|
|
'true' => ['disable', 'opacity-50'],
|
|
],
|
|
],
|
|
],
|
|
['colors' => 'primary'],
|
|
'text-primary',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideAdditionalClassesCases
|
|
*/
|
|
public function testAdditionalClasses(string|array $base, array|string $additionals, string $expected): void
|
|
{
|
|
$cva = new Cva($base);
|
|
if (!$additionals) {
|
|
$this->assertEquals($expected, $cva->apply([]));
|
|
} else {
|
|
$this->assertEquals($expected, $cva->apply([], ...(array) $additionals));
|
|
}
|
|
}
|
|
|
|
public static function provideAdditionalClassesCases(): iterable
|
|
{
|
|
yield 'additionals_are_optional' => [
|
|
'',
|
|
'foo',
|
|
'foo',
|
|
];
|
|
|
|
yield 'additional_are_used' => [
|
|
'',
|
|
'foo',
|
|
'foo',
|
|
];
|
|
|
|
yield 'additionals_are_used' => [
|
|
'',
|
|
['foo', 'bar'],
|
|
'foo bar',
|
|
];
|
|
|
|
yield 'additionals_preserve_order' => [
|
|
['foo'],
|
|
['bar', 'foo'],
|
|
'foo bar',
|
|
];
|
|
|
|
yield 'additional_are_deduplicated' => [
|
|
'',
|
|
['bar', 'bar'],
|
|
'bar',
|
|
];
|
|
}
|
|
}
|