Skip to content

Commit aa0f655

Browse files
committed
tests(class-variance): added new testsuite
1 parent 911e6b2 commit aa0f655

4 files changed

Lines changed: 513 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\ClassVariance\Tests;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use PHPUnit\Framework\TestCase;
9+
10+
use function Tempest\ClassVariance\tv;
11+
12+
final class ClassNamesBooleanTest extends TestCase
13+
{
14+
#[Test]
15+
public function class_prop_with_boolean_value_ignored(): void
16+
{
17+
$component = tv(
18+
base: ['base' => ['component']],
19+
variants: [
20+
'variant' => [
21+
'primary' => ['base' => 'bg-blue-500'],
22+
],
23+
],
24+
defaultVariants: ['variant' => 'primary'],
25+
);
26+
27+
$this->assertSame('component bg-blue-500', $component(['class' => true], 'base'));
28+
$this->assertSame('component bg-blue-500', $component(['class' => false], 'base'));
29+
$this->assertSame('component bg-blue-500', $component(['className' => true], 'base'));
30+
$this->assertSame('component bg-blue-500', $component(['className' => false], 'base'));
31+
}
32+
33+
#[Test]
34+
public function class_prop_normal_strings_still_work(): void
35+
{
36+
$component = tv(
37+
base: ['base' => ['component']],
38+
variants: [
39+
'variant' => [
40+
'primary' => ['base' => 'bg-blue-500'],
41+
],
42+
],
43+
defaultVariants: ['variant' => 'primary'],
44+
);
45+
46+
$this->assertSame(
47+
'component bg-blue-500 custom-class',
48+
$component(['class' => 'custom-class'], 'base'),
49+
);
50+
51+
$this->assertSame(
52+
'component bg-blue-500 another-class',
53+
$component(['className' => 'another-class'], 'base'),
54+
);
55+
56+
// Two unknown classes with the same prefix: tv() keeps both (unknown classes are never deduplicated)
57+
$this->assertSame(
58+
'component bg-blue-500 custom-1 custom-2',
59+
$component(['class' => 'custom-1 custom-2'], 'base'),
60+
);
61+
}
62+
63+
#[Test]
64+
public function slot_based_class_with_boolean_in_array(): void
65+
{
66+
$component = tv(
67+
base: [
68+
'base' => ['component-base'],
69+
'label' => ['component-label'],
70+
],
71+
variants: [
72+
'variant' => [
73+
'primary' => [
74+
'base' => ['bg-blue-500'],
75+
'label' => ['text-white'],
76+
],
77+
],
78+
],
79+
defaultVariants: ['variant' => 'primary'],
80+
);
81+
82+
// Boolean in slot class array ignored; only the matching slot's value applied
83+
$this->assertSame(
84+
'component-base bg-blue-500',
85+
$component(['class' => ['base' => true, 'label' => 'extra']], 'base'),
86+
);
87+
88+
$this->assertSame(
89+
'component-label text-white extra',
90+
$component(['class' => ['base' => true, 'label' => 'extra']], 'label'),
91+
);
92+
}
93+
94+
#[Test]
95+
public function empty_string_and_whitespace_still_filtered(): void
96+
{
97+
$component = tv(base: ['base' => ['component']]);
98+
99+
$this->assertSame('component', $component(['class' => ''], 'base'));
100+
$this->assertSame('component', $component(['class' => ' '], 'base'));
101+
$this->assertSame('component', $component(['class' => false], 'base'));
102+
}
103+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\ClassVariance\Tests;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use PHPUnit\Framework\TestCase;
9+
10+
use function Tempest\ClassVariance\tv;
11+
12+
final class CompoundVariantsAdvancedTest extends TestCase
13+
{
14+
#[Test]
15+
public function compound_variant_with_array_conditions(): void
16+
{
17+
$component = tv(
18+
base: [
19+
'base' => ['component'],
20+
'content' => ['content-base'],
21+
'leading' => ['leading-base'],
22+
],
23+
variants: [
24+
'variant' => [
25+
'solid' => ['base' => ['bg-solid']],
26+
'outline' => ['base' => ['border-outline']],
27+
'soft' => ['base' => ['bg-soft']],
28+
'subtle' => ['base' => ['bg-subtle']],
29+
],
30+
'compact' => [
31+
'true' => ['content' => ['p-2']],
32+
'false' => ['content' => ['p-4']],
33+
],
34+
],
35+
compoundVariants: [
36+
[
37+
'variant' => ['solid', 'outline', 'soft', 'subtle'],
38+
'compact' => 'false',
39+
'class' => [
40+
'content' => 'px-4 py-3 rounded-lg min-h-12',
41+
'leading' => 'mt-2',
42+
],
43+
],
44+
],
45+
defaultVariants: [
46+
'variant' => 'solid',
47+
'compact' => 'false',
48+
],
49+
);
50+
51+
// Default: variant=solid, compact=false — compound matches
52+
$this->assertSame(
53+
'content-base p-4 px-4 py-3 rounded-lg min-h-12',
54+
$component(slot: 'content'),
55+
);
56+
57+
$this->assertSame(
58+
'leading-base mt-2',
59+
$component(slot: 'leading'),
60+
);
61+
62+
// variant=outline, compact=false — compound still matches
63+
$this->assertSame(
64+
'content-base p-4 px-4 py-3 rounded-lg min-h-12',
65+
$component(['variant' => 'outline', 'compact' => 'false'], 'content'),
66+
);
67+
68+
// variant=solid, compact=true — compound does NOT match
69+
$this->assertSame(
70+
'content-base p-2',
71+
$component(['variant' => 'solid', 'compact' => 'true'], 'content'),
72+
);
73+
}
74+
75+
#[Test]
76+
public function compound_variant_with_multiple_array_conditions(): void
77+
{
78+
$component = tv(
79+
base: ['base' => ['component']],
80+
variants: [
81+
'color' => [
82+
'neutral' => ['base' => 'text-neutral'],
83+
'primary' => ['base' => 'text-primary'],
84+
],
85+
'variant' => [
86+
'outline' => ['base' => 'border'],
87+
'subtle' => ['base' => 'bg-subtle'],
88+
],
89+
'multiple' => [
90+
'true' => ['base' => 'multiple'],
91+
'false' => ['base' => 'single'],
92+
],
93+
],
94+
compoundVariants: [
95+
[
96+
'color' => 'neutral',
97+
'multiple' => 'true',
98+
'variant' => ['outline', 'subtle'],
99+
'class' => ['base' => 'has-focus-visible:ring-2 has-focus-visible:ring-inverted'],
100+
],
101+
],
102+
defaultVariants: [
103+
'color' => 'neutral',
104+
'variant' => 'outline',
105+
'multiple' => 'false',
106+
],
107+
);
108+
109+
// Default: multiple=false — compound does NOT match
110+
$this->assertSame(
111+
'component text-neutral border single',
112+
$component(slot: 'base'),
113+
);
114+
115+
// color=neutral, variant=outline, multiple=true — compound matches
116+
$this->assertSame(
117+
'component text-neutral border multiple has-focus-visible:ring-2 has-focus-visible:ring-inverted',
118+
$component(['color' => 'neutral', 'variant' => 'outline', 'multiple' => 'true'], 'base'),
119+
);
120+
121+
// color=neutral, variant=subtle, multiple=true — compound matches
122+
$this->assertSame(
123+
'component text-neutral bg-subtle multiple has-focus-visible:ring-2 has-focus-visible:ring-inverted',
124+
$component(['color' => 'neutral', 'variant' => 'subtle', 'multiple' => 'true'], 'base'),
125+
);
126+
127+
// color=primary — compound does NOT match
128+
$this->assertSame(
129+
'component text-primary border multiple',
130+
$component(['color' => 'primary', 'variant' => 'outline', 'multiple' => 'true'], 'base'),
131+
);
132+
}
133+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\ClassVariance\Tests;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use PHPUnit\Framework\TestCase;
9+
10+
use function Tempest\ClassVariance\cv;
11+
use function Tempest\ClassVariance\tv;
12+
13+
final class CvTest extends TestCase
14+
{
15+
#[Test]
16+
public function tv_resolves_defaults_and_compound_variants(): void
17+
{
18+
$button = tv(
19+
base: ['font-semibold', 'border', 'rounded'],
20+
variants: [
21+
'variant' => [
22+
'primary' => ['bg-blue-500', 'text-white', 'border-transparent', 'hover:bg-blue-600'],
23+
'secondary' => ['bg-white', 'text-gray-800', 'border-gray-400', 'hover:bg-gray-100'],
24+
],
25+
'size' => [
26+
'small' => ['text-sm', 'py-1', 'px-2'],
27+
'medium' => ['text-base', 'py-2', 'px-4'],
28+
],
29+
],
30+
compoundVariants: [
31+
[
32+
'variant' => 'primary',
33+
'size' => 'medium',
34+
'class' => 'uppercase',
35+
],
36+
],
37+
defaultVariants: [
38+
'variant' => 'primary',
39+
'size' => 'medium',
40+
],
41+
);
42+
43+
$this->assertSame(
44+
'font-semibold border rounded bg-blue-500 text-white border-transparent hover:bg-blue-600 text-base py-2 px-4 uppercase',
45+
$button(),
46+
);
47+
48+
$this->assertSame(
49+
'font-semibold border rounded bg-white text-gray-800 border-gray-400 hover:bg-gray-100 text-sm py-1 px-2',
50+
$button(['variant' => 'secondary', 'size' => 'small']),
51+
);
52+
}
53+
54+
#[Test]
55+
public function class_prop_appended_and_takes_priority_over_classname(): void
56+
{
57+
$button = tv(
58+
base: ['font-semibold', 'border', 'rounded'],
59+
variants: [
60+
'variant' => [
61+
'secondary' => ['bg-white', 'text-gray-800', 'border-gray-400', 'hover:bg-gray-100'],
62+
],
63+
'size' => [
64+
'small' => ['text-sm', 'py-1', 'px-2'],
65+
],
66+
],
67+
defaultVariants: ['variant' => 'secondary', 'size' => 'small'],
68+
);
69+
70+
// When both class and className are provided, class takes priority (className ignored)
71+
$this->assertSame(
72+
'font-semibold border rounded bg-white text-gray-800 border-gray-400 hover:bg-gray-100 text-sm py-1 px-2 focus:ring-2',
73+
$button(['class' => 'focus:ring-2', 'className' => 'focus:ring-4', 'variant' => 'secondary', 'size' => 'small']),
74+
);
75+
76+
// className used when class is absent
77+
$this->assertSame(
78+
'font-semibold border rounded bg-white text-gray-800 border-gray-400 hover:bg-gray-100 text-sm py-1 px-2 focus:ring-2',
79+
$button(['className' => 'focus:ring-2', 'variant' => 'secondary', 'size' => 'small']),
80+
);
81+
}
82+
83+
#[Test]
84+
public function cv_separator_deduplication(): void
85+
{
86+
// cv() uses separator heuristic: classes sharing a prefix-before-'-' conflict.
87+
// Last class in the group wins — this is the intentional cv() behaviour.
88+
$component = cv(
89+
base: 'btn',
90+
variants: [
91+
'size' => [
92+
'sm' => 'size-sm',
93+
'lg' => 'size-lg',
94+
],
95+
'intent' => [
96+
'primary' => 'intent-primary',
97+
'danger' => 'intent-danger',
98+
],
99+
],
100+
defaultVariants: ['size' => 'sm', 'intent' => 'primary'],
101+
);
102+
103+
$this->assertSame('btn size-sm intent-primary', $component());
104+
$this->assertSame('btn size-lg intent-danger', $component(['size' => 'lg', 'intent' => 'danger']));
105+
106+
// Passing class= with a class that shares prefix with a base class: last wins
107+
$this->assertSame('btn size-lg intent-primary', $component(['class' => 'size-lg']));
108+
}
109+
}

0 commit comments

Comments
 (0)