Initial commit

This commit is contained in:
2026-08-20 11:41:17 +00:00
commit 6949695eac
2334 changed files with 646393 additions and 0 deletions
@@ -0,0 +1,235 @@
<?php
/**
* SpecificityTest.php
*
* @since 2002-08-03
* @category Library
* @package Pdf
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf
*
* This file is part of tc-lib-pdf software library.
*/
namespace Test;
use Com\Tecnick\Pdf\CSS\Specificity;
use PHPUnit\Framework\Attributes\DataProvider;
class SpecificityTest extends TestUtil
{
public function testConstructorInitializesValues(): void
{
$spec = new Specificity(1, 2, 3);
$this->assertSame(1, $spec->idCount);
$this->assertSame(2, $spec->classCount);
$this->assertSame(3, $spec->typeCount);
}
public function testConstructorNormalizesNegativeValues(): void
{
$spec = new Specificity(-1, -2, -3);
$this->assertSame(0, $spec->idCount);
$this->assertSame(0, $spec->classCount);
$this->assertSame(0, $spec->typeCount);
}
#[DataProvider('selectorSpecificityProvider')]
public function testFromSelectorCalculatesCorrectSpecificity(
string $selector,
int $expectedA,
int $expectedB,
int $expectedC,
): void {
$spec = Specificity::fromSelector($selector);
$this->assertSame($expectedA, $spec->idCount, "ID count mismatch for: {$selector}");
$this->assertSame(
$expectedB,
$spec->classCount,
"Class/attribute/pseudo-class count mismatch for: {$selector}",
);
$this->assertSame($expectedC, $spec->typeCount, "Type/pseudo-element count mismatch for: {$selector}");
}
/** @return array<string, array{0: string, 1: int, 2: int, 3: int}> */
public static function selectorSpecificityProvider(): array
{
return [
'universal selector' => ['*', 0, 0, 0],
'single type' => ['div', 0, 0, 1],
'single class' => ['.highlight', 0, 1, 0],
'single id' => ['#main', 1, 0, 0],
'type with class' => ['div.highlight', 0, 1, 1],
'type with id' => ['div#main', 1, 0, 1],
'type with attribute' => ['div[role="button"]', 0, 1, 1],
'type with pseudo-class' => ['div:hover', 0, 1, 1],
'descendant combinator' => ['div p', 0, 0, 2],
'child combinator' => ['div > p', 0, 0, 2],
'adjacent sibling' => ['h1 + p', 0, 0, 2],
'general sibling' => ['h1 ~ p', 0, 0, 2],
'multiple ids' => ['#main #article', 2, 0, 0],
'multiple classes' => ['.header.highlight.active', 0, 3, 0],
'complex selector 1' => ['.header nav li a:hover', 0, 2, 3],
'complex selector 2' => ['#nav .menu li a:visited', 1, 2, 2],
'multiple pseudo-classes' => ['a:link:visited:hover', 0, 3, 1],
'pseudo-element before' => ['p::before', 0, 0, 2],
'pseudo-element after' => ['p::after', 0, 0, 2],
'type and pseudo-element' => ['div::before', 0, 0, 2],
'multiple attributes' => ['div[data-foo][data-bar]', 0, 2, 1],
];
}
#[DataProvider('specificityComparisonProvider')]
public function testComparisonMethods(
int $idCount1,
int $classCount1,
int $typeCount1,
int $idCount2,
int $classCount2,
int $typeCount2,
int $expectedComparison,
): void {
$spec1 = new Specificity($idCount1, $classCount1, $typeCount1);
$spec2 = new Specificity($idCount2, $classCount2, $typeCount2);
$result = $spec1->compareTo($spec2);
$this->assertSame($expectedComparison, $result);
if ($expectedComparison < 0) {
$this->assertTrue($spec1->isLessThan($spec2));
$this->assertFalse($spec1->isGreaterThan($spec2));
$this->assertFalse($spec1->equals($spec2));
} elseif ($expectedComparison > 0) {
$this->assertTrue($spec1->isGreaterThan($spec2));
$this->assertFalse($spec1->isLessThan($spec2));
$this->assertFalse($spec1->equals($spec2));
} else {
$this->assertTrue($spec1->equals($spec2));
$this->assertFalse($spec1->isLessThan($spec2));
$this->assertFalse($spec1->isGreaterThan($spec2));
}
}
/**
* @return array<string, array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int}>
*/
public static function specificityComparisonProvider(): array
{
return [
'equal' => [0, 0, 0, 0, 0, 0, 0],
'a determines precedence' => [1, 0, 0, 0, 9, 9, 1],
'b determines when a equal' => [0, 1, 0, 0, 0, 9, 1],
'c determines when a and b equal' => [0, 0, 1, 0, 0, 0, 1],
'less: all components' => [0, 0, 0, 1, 1, 1, -1],
'greater: all components' => [1, 1, 1, 0, 0, 0, 1],
'edge case: high b vs low a' => [0, 99, 99, 1, 0, 0, -1],
'edge case: high c vs low a' => [0, 0, 99, 1, 0, 0, -1],
'edge case: all high vs single a' => [0, 99, 99, 1, 0, 0, -1],
];
}
public function testSortKeyFormatting(): void
{
$spec = new Specificity(1, 2, 3);
$key = $spec->toSortKey(0);
$this->assertStringMatchesFormat('%s_%s', $key);
$this->assertStringContainsString('0001', $key);
$this->assertStringContainsString('0002', $key);
$this->assertStringContainsString('0003', $key);
$this->assertStringContainsString('000000', $key);
}
public function testSortKeyWithSourceOrder(): void
{
$spec = new Specificity(1, 2, 3);
$key1 = $spec->toSortKey(0);
$key2 = $spec->toSortKey(1);
// Same specificity, different source order - should be ordered by index
$this->assertLessThan(0, \strcmp($key1, $key2));
}
public function testSortKeyOrderingBySpecificity(): void
{
$low = new Specificity(0, 0, 1);
$medium = new Specificity(0, 1, 0);
$high = new Specificity(1, 0, 0);
$keys = [
$high->toSortKey(0),
$low->toSortKey(0),
$medium->toSortKey(0),
];
$sorted = $keys;
\sort($sorted);
// After string sort, low < medium < high
$this->assertSame($low->toSortKey(0), $sorted[0]);
assert(isset($sorted[1]), "\$sorted[1] must be set");
$this->assertSame($medium->toSortKey(0), $sorted[1]);
assert(isset($sorted[2]), "\$sorted[2] must be set");
$this->assertSame($high->toSortKey(0), $sorted[2]);
}
public function testToStringRepresentation(): void
{
$spec = new Specificity(1, 2, 3);
$str = $spec->toString();
$this->assertSame('(1,2,3)', $str);
}
public function testLegacyStringConversion(): void
{
$spec = new Specificity(1, 2, 3);
$legacy = $spec->toLegacyString(0);
$this->assertSame('0123', $legacy);
$legacyInline = $spec->toLegacyString(1);
$this->assertSame('1123', $legacyInline);
}
public function testLegacyStringParsing(): void
{
$spec = Specificity::fromLegacyString('0123');
$this->assertSame(1, $spec->idCount);
$this->assertSame(2, $spec->classCount);
$this->assertSame(3, $spec->typeCount);
}
public function testLegacyStringRoundTrip(): void
{
$original = new Specificity(2, 3, 4);
$legacy = $original->toLegacyString(0);
$restored = Specificity::fromLegacyString($legacy);
$this->assertTrue($original->equals($restored));
}
public function testRealWorldSelectorComparisons(): void
{
// From CSS spec examples
$headingOne = Specificity::fromSelector('h1');
$h1_foo = Specificity::fromSelector('h1.foo');
$h1_foo_bar = Specificity::fromSelector('h1.foo.bar');
$div_p_a = Specificity::fromSelector('div p a');
$foo_bar_baz = Specificity::fromSelector('.foo .bar .baz');
$id_selector = Specificity::fromSelector('#main');
// h1 (0,0,1) < h1.foo (0,1,1)
$this->assertTrue($headingOne->isLessThan($h1_foo));
// h1.foo (0,1,1) < h1.foo.bar (0,2,1)
$this->assertTrue($h1_foo->isLessThan($h1_foo_bar));
// div p a (0,0,3) < .foo .bar .baz (0,3,0)
$this->assertTrue($div_p_a->isLessThan($foo_bar_baz));
// All < #main (1,0,0)
$this->assertTrue($foo_bar_baz->isLessThan($id_selector));
$this->assertTrue($h1_foo_bar->isLessThan($id_selector));
}
}