Skip to content

Commit 3e7f010

Browse files
committed
feat: add Selector class with various methods for generating selectors and escaping text
1 parent 09a7833 commit 3e7f010

2 files changed

Lines changed: 380 additions & 0 deletions

File tree

src/Support/Selector.php

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pest\Browser\Support;
6+
7+
final class Selector
8+
{
9+
public const string SELECTOR_PREFIX = 'internal:';
10+
11+
/**
12+
* Get selector by attribute text.
13+
*/
14+
public static function getByAttributeTextSelector(string $attrName, string $text, bool $exact = false): string
15+
{
16+
return self::SELECTOR_PREFIX."attr=[{$attrName}=".self::escapeForAttributeSelectorOrRegex($text, $exact).']';
17+
}
18+
19+
/**
20+
* Get selector by test ID.
21+
*/
22+
public static function getByTestIdSelector(string $testIdAttributeName, string $testId): string
23+
{
24+
return self::SELECTOR_PREFIX."testid=[{$testIdAttributeName}=".self::escapeForAttributeSelectorOrRegex($testId, true).']';
25+
}
26+
27+
/**
28+
* Get selector by label.
29+
*/
30+
public static function getByLabelSelector(string $text, bool $exact): string
31+
{
32+
return self::SELECTOR_PREFIX.'label='.self::escapeForTextSelector($text, $exact);
33+
}
34+
35+
/**
36+
* Get selector by alt text.
37+
*/
38+
public static function getByAltTextSelector(string $text, bool $exact): string
39+
{
40+
return self::getByAttributeTextSelector('alt', $text, $exact);
41+
}
42+
43+
/**
44+
* Get selector by title.
45+
*/
46+
public static function getByTitleSelector(string $text, bool $exact): string
47+
{
48+
return self::getByAttributeTextSelector('title', $text, $exact);
49+
}
50+
51+
/**
52+
* Get selector by placeholder.
53+
*/
54+
public static function getByPlaceholderSelector(string $text, bool $exact): string
55+
{
56+
return self::getByAttributeTextSelector('placeholder', $text, $exact);
57+
}
58+
59+
/**
60+
* Get selector by text.
61+
*/
62+
public static function getByTextSelector(string $text, bool $exact): string
63+
{
64+
return self::SELECTOR_PREFIX.'text='.self::escapeForTextSelector($text, $exact);
65+
}
66+
67+
/**
68+
* Escape text for regex.
69+
*/
70+
public static function escapeForRegex(string $text): string
71+
{
72+
return preg_quote($text, '/');
73+
}
74+
75+
/**
76+
* Escape for text selector.
77+
*/
78+
public static function escapeForTextSelector(string $text, bool $exact = false): string
79+
{
80+
if ($exact) {
81+
return json_encode($text).'s';
82+
}
83+
84+
return json_encode($text).'i';
85+
}
86+
87+
/**
88+
* Escape for attribute selector or regex.
89+
*/
90+
public static function escapeForAttributeSelectorOrRegex(string $text, bool $exact = false): string
91+
{
92+
return self::escapeForAttributeSelector($text, $exact);
93+
}
94+
95+
/**
96+
* Escape for attribute selector.
97+
*/
98+
public static function escapeForAttributeSelector(string $text, bool $exact = false): string
99+
{
100+
$escapedText = str_replace('\\', '\\\\', $text);
101+
$escapedText = str_replace('"', '\\"', $escapedText);
102+
103+
if ($exact) {
104+
return "\"{$escapedText}\"";
105+
}
106+
107+
return "\"{$escapedText}\"i";
108+
109+
}
110+
111+
public static function getByRoleSelector(string $role, array $options = []): string
112+
{
113+
$props = [];
114+
115+
if (isset($options['checked'])) {
116+
$props['checked'] = $options['checked'] ? 'true' : 'false';
117+
}
118+
if (isset($options['disabled'])) {
119+
$props['disabled'] = $options['disabled'] ? 'true' : 'false';
120+
}
121+
if (isset($options['selected'])) {
122+
$props['selected'] = $options['selected'] ? 'true' : 'false';
123+
}
124+
if (isset($options['expanded'])) {
125+
$props['expanded'] = $options['expanded'] ? 'true' : 'false';
126+
}
127+
if (isset($options['includeHidden'])) {
128+
$props['include-hidden'] = $options['includeHidden'] ? 'true' : 'false';
129+
}
130+
if (isset($options['level'])) {
131+
$props['level'] = (string) $options['level'];
132+
}
133+
if (isset($options['name'])) {
134+
$exact = $options['exact'] ?? false;
135+
$props['name'] = self::escapeForAttributeSelector($options['name'], $exact);
136+
}
137+
if (isset($options['pressed'])) {
138+
$props['pressed'] = $options['pressed'] ? 'true' : 'false';
139+
}
140+
141+
$propsStr = '';
142+
foreach ($props as $k => $v) {
143+
$propsStr .= '['.$k.'='.$v.']';
144+
}
145+
146+
return self::SELECTOR_PREFIX."role={$role}{$propsStr}";
147+
}
148+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Pest\Browser\Support\Selector;
6+
7+
describe('getByAttributeTextSelector', function () {
8+
it('returns correct selector without exact match', function () {
9+
$selector = Selector::getByAttributeTextSelector('data-test', 'example', false);
10+
11+
expect($selector)->toBe('attr=[data-test="example"i]');
12+
});
13+
14+
it('returns correct selector with exact match', function () {
15+
$selector = Selector::getByAttributeTextSelector('data-test', 'example', true);
16+
17+
expect($selector)->toBe('attr=[data-test="example"]');
18+
});
19+
20+
it('escapes special characters in attribute values', function () {
21+
$selector = Selector::getByAttributeTextSelector('data-test', 'example "quoted" text', false);
22+
23+
expect($selector)->toBe('attr=[data-test="example \"quoted\" text"i]');
24+
});
25+
26+
it('escapes backslashes in attribute values', function () {
27+
$selector = Selector::getByAttributeTextSelector('data-test', 'example\\path', false);
28+
29+
expect($selector)->toBe('attr=[data-test="example\\\\path"i]');
30+
});
31+
});
32+
33+
describe('getByTestIdSelector', function () {
34+
it('returns correct selector for test ID', function () {
35+
$selector = Selector::getByTestIdSelector('data-testid', 'login-button');
36+
37+
expect($selector)->toBe('testid=[data-testid="login-button"]');
38+
});
39+
40+
it('escapes special characters in test ID', function () {
41+
$selector = Selector::getByTestIdSelector('data-testid', 'button"with"quotes');
42+
43+
expect($selector)->toBe('testid=[data-testid="button\"with\"quotes"]');
44+
});
45+
});
46+
47+
describe('getByLabelSelector', function () {
48+
it('returns correct selector without exact match', function () {
49+
$selector = Selector::getByLabelSelector('Email address', false);
50+
51+
expect($selector)->toBe('label="Email address"i');
52+
});
53+
54+
it('returns correct selector with exact match', function () {
55+
$selector = Selector::getByLabelSelector('Email address', true);
56+
57+
expect($selector)->toBe('label="Email address"s');
58+
});
59+
});
60+
61+
describe('getByAltTextSelector', function () {
62+
it('returns correct selector without exact match', function () {
63+
$selector = Selector::getByAltTextSelector('Logo image', false);
64+
65+
expect($selector)->toBe('attr=[alt="Logo image"i]');
66+
});
67+
68+
it('returns correct selector with exact match', function () {
69+
$selector = Selector::getByAltTextSelector('Logo image', true);
70+
71+
expect($selector)->toBe('attr=[alt="Logo image"]');
72+
});
73+
});
74+
75+
describe('getByTitleSelector', function () {
76+
it('returns correct selector without exact match', function () {
77+
$selector = Selector::getByTitleSelector('Information', false);
78+
79+
expect($selector)->toBe('attr=[title="Information"i]');
80+
});
81+
82+
it('returns correct selector with exact match', function () {
83+
$selector = Selector::getByTitleSelector('Information', true);
84+
85+
expect($selector)->toBe('attr=[title="Information"]');
86+
});
87+
});
88+
89+
describe('getByPlaceholderSelector', function () {
90+
it('returns correct selector without exact match', function () {
91+
$selector = Selector::getByPlaceholderSelector('Search...', false);
92+
93+
expect($selector)->toBe('attr=[placeholder="Search..."i]');
94+
});
95+
96+
it('returns correct selector with exact match', function () {
97+
$selector = Selector::getByPlaceholderSelector('Search...', true);
98+
99+
expect($selector)->toBe('attr=[placeholder="Search..."]');
100+
});
101+
});
102+
103+
describe('getByTextSelector', function () {
104+
it('returns correct selector without exact match', function () {
105+
$selector = Selector::getByTextSelector('Submit form', false);
106+
107+
expect($selector)->toBe('text="Submit form"i');
108+
});
109+
110+
it('returns correct selector with exact match', function () {
111+
$selector = Selector::getByTextSelector('Submit form', true);
112+
113+
expect($selector)->toBe('text="Submit form"s');
114+
});
115+
});
116+
117+
describe('escapeForRegex', function () {
118+
it('escapes special regex characters', function () {
119+
$escaped = Selector::escapeForRegex('text+with(special)[regex]{chars}.*$^');
120+
121+
expect($escaped)->toBe('text\+with\(special\)\[regex\]\{chars\}\.\*\$\^');
122+
});
123+
});
124+
125+
describe('escapeForTextSelector', function () {
126+
it('returns JSON-encoded string with "i" suffix for non-exact match', function () {
127+
$escaped = Selector::escapeForTextSelector('Search text', false);
128+
129+
expect($escaped)->toBe('"Search text"i');
130+
});
131+
132+
it('returns JSON-encoded string with "s" suffix for exact match', function () {
133+
$escaped = Selector::escapeForTextSelector('Search text', true);
134+
135+
expect($escaped)->toBe('"Search text"s');
136+
});
137+
138+
it('properly escapes quotes in the text', function () {
139+
$escaped = Selector::escapeForTextSelector('Text with "quotes"', false);
140+
141+
expect($escaped)->toBe('"Text with \"quotes\""i');
142+
});
143+
});
144+
145+
describe('escapeForAttributeSelector', function () {
146+
it('handles simple strings', function () {
147+
$escaped = Selector::escapeForAttributeSelector('simple');
148+
149+
expect($escaped)->toBe('"simple"i');
150+
});
151+
152+
it('escapes backslashes', function () {
153+
$escaped = Selector::escapeForAttributeSelector('text\\with\\backslashes');
154+
155+
expect($escaped)->toBe('"text\\\\with\\\\backslashes"i');
156+
});
157+
158+
it('escapes quotes', function () {
159+
$escaped = Selector::escapeForAttributeSelector('text"with"quotes');
160+
161+
expect($escaped)->toBe('"text\"with\"quotes"i');
162+
});
163+
164+
it('uses "i" suffix for non-exact match', function () {
165+
$escaped = Selector::escapeForAttributeSelector('text', false);
166+
167+
expect($escaped)->toBe('"text"i');
168+
});
169+
170+
it('omits "i" suffix for exact match', function () {
171+
$escaped = Selector::escapeForAttributeSelector('text', true);
172+
173+
expect($escaped)->toBe('"text"');
174+
});
175+
});
176+
177+
describe('getByRoleSelector', function () {
178+
it('returns basic role selector without options', function () {
179+
$selector = Selector::getByRoleSelector('button');
180+
181+
expect($selector)->toBe('role=button');
182+
});
183+
184+
it('handles boolean options correctly', function () {
185+
$selector = Selector::getByRoleSelector('checkbox', [
186+
'checked' => true,
187+
'disabled' => false
188+
]);
189+
190+
expect($selector)->toBe('role=checkbox[checked=true][disabled=false]');
191+
});
192+
193+
it('handles name option without exact flag', function () {
194+
$selector = Selector::getByRoleSelector('button', [
195+
'name' => 'Submit form'
196+
]);
197+
198+
expect($selector)->toBe('role=button[name="Submit form"i]');
199+
});
200+
201+
it('handles name option with exact flag', function () {
202+
$selector = Selector::getByRoleSelector('button', [
203+
'name' => 'Submit form',
204+
'exact' => true
205+
]);
206+
207+
expect($selector)->toBe('role=button[name="Submit form"]');
208+
});
209+
210+
it('handles all possible options', function () {
211+
$selector = Selector::getByRoleSelector('combobox', [
212+
'checked' => true,
213+
'disabled' => false,
214+
'selected' => true,
215+
'expanded' => true,
216+
'includeHidden' => false,
217+
'level' => 2,
218+
'name' => 'Country selector',
219+
'pressed' => false,
220+
]);
221+
222+
expect($selector)->toBe('role=combobox[checked=true][disabled=false][selected=true][expanded=true][include-hidden=false][level=2][name="Country selector"i][pressed=false]');
223+
});
224+
225+
it('escapes special characters in name option', function () {
226+
$selector = Selector::getByRoleSelector('button', [
227+
'name' => 'Button "with" quotes\\backslashes'
228+
]);
229+
230+
expect($selector)->toBe('role=button[name="Button \"with\" quotes\\\\backslashes"i]');
231+
});
232+
});

0 commit comments

Comments
 (0)