packages/requirements/tests/Unit/Source/TextFragmentTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Source;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Small;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use Requirements\Input\InvalidInputException;
13use Requirements\Source\TextFragment;
14use Requirements\Source\Unit;
15
16#[CoversClass(TextFragment::class)]
17#[UsesClass(Unit::class)]
18#[Small]
19final class TextFragmentTest extends TestCase
20{
21 #[DataProvider('providerIsFragment')]
22 public function testIsFragment(string $selector, bool $expected): void
23 {
24 self::assertSame($expected, TextFragment::isFragment($selector));
25 }
26
27 /**
28 * @return array<string, array{string, bool}>
29 */
30 public static function providerIsFragment(): array
31 {
32 return [
33 'directive' => ['#:~:text=Names', true],
34 'element fragment before directive' => ['#example:~:text=Names', true],
35 'malformed directive' => ['#:~:', true],
36 'css id' => ['#a', false],
37 'css selector' => ['main p', false],
38 'directive without hash' => [':~:text=Names', false],
39 'hash later' => ['main #:~:text=Names', false],
40 ];
41 }
42
43 public function testCreateDirectiveEncodingPreservesSyntaxCharactersAndUnicode(): void
44 {
45 $text = 'UTF-8, A&B + "日本語"';
46 $fragment = TextFragment::create($text);
47
48 self::assertSame('#:~:text=UTF%2D8%2C%20A%26B%20%2B%20%22%E6%97%A5%E6%9C%AC%E8%AA%9E%22', $fragment);
49 self::assertSame($text, TextFragment::text($fragment));
50 self::assertSame($text, TextFragment::text(str_replace('#:', '#example:', $fragment)));
51 }
52
53 public function testCreateNormalizesTheText(): void
54 {
55 self::assertSame('#:~:text=Names%20start.', TextFragment::create(" Names\n start. "));
56 }
57
58 #[DataProvider('providerTextDecodes')]
59 public function testTextDecodesAndNormalizes(string $fragment, string $expected): void
60 {
61 self::assertSame($expected, TextFragment::text($fragment));
62 }
63
64 /**
65 * @return array<string, array{string, string}>
66 */
67 public static function providerTextDecodes(): array
68 {
69 return [
70 'plain' => ['#:~:text=Names', 'Names'],
71 'encoded hyphen' => ['#:~:text=a%2Db', 'a-b'],
72 'lowercase escape' => ['#:~:text=a%2db', 'a-b'],
73 'whitespace normalized' => ['#:~:text=%20Names%20%20start%0A', 'Names start'],
74 'raw unicode' => ['#:~:text=日本語', '日本語'],
75 'plus kept' => ['#:~:text=a+b', 'a+b'],
76 ];
77 }
78
79 #[DataProvider('providerTextUnsupported')]
80 public function testTextUnsupportedOrMalformedDirectivesFailExplicitly(string $fragment): void
81 {
82 $this->expectException(InvalidInputException::class);
83 TextFragment::text($fragment);
84 }
85
86 /**
87 * @return list<array{string}>
88 */
89 public static function providerTextUnsupported(): array
90 {
91 return [
92 ['#:~:text='],
93 ['#:~:text=start,end'],
94 ['#:~:text=prefix-,start'],
95 ['#:~:text=start,-suffix'],
96 ['#:~:text=first&text=second'],
97 ['#:~:text=bad%xx'],
98 ['#:~:text=%FF'],
99 ['#:~:text=%20'],
100 ['#text=missing-directive-marker'],
101 ];
102 }
103
104 #[DataProvider('providerTextNotOneExactDirective')]
105 public function testTextRejectsAnythingButOneExactDirective(string $fragment): void
106 {
107 $this->expectException(InvalidInputException::class);
108 $this->expectExceptionMessage('Use one exact Text Fragment (#:~:text=percent-encoded-text); ranges, context terms and multiple directives are unsupported.');
109 TextFragment::text($fragment);
110 }
111
112 /**
113 * @return array<string, array{string}>
114 */
115 public static function providerTextNotOneExactDirective(): array
116 {
117 return [
118 'empty text' => ['#:~:text='],
119 'range' => ['#:~:text=start,end'],
120 'prefix' => ['#:~:text=prefix-,start'],
121 'several directives' => ['#:~:text=first&text=second'],
122 'bad escape' => ['#:~:text=bad%xx'],
123 'truncated escape' => ['#:~:text=bad%2'],
124 'second hash' => ['#a#:~:text=Names'],
125 'text before hash' => ['main#:~:text=Names'],
126 'other directive' => ['#:~:note=Names'],
127 'raw invalid UTF-8' => ["#:~:text=\xFF"],
128 ];
129 }
130
131 #[DataProvider('providerTextEmptyOrInvalid')]
132 public function testTextRejectsEmptyOrInvalidUtf8(string $fragment): void
133 {
134 $this->expectException(InvalidInputException::class);
135 $this->expectExceptionMessage('Text Fragment text must be nonempty UTF-8.');
136 TextFragment::text($fragment);
137 }
138
139 /**
140 * @return array<string, array{string}>
141 */
142 public static function providerTextEmptyOrInvalid(): array
143 {
144 return [
145 'invalid UTF-8' => ['#:~:text=%FF'],
146 'blank' => ['#:~:text=%20'],
147 'no-break space' => ['#:~:text=%C2%A0'],
148 ];
149 }
150}
151