Thanks to visit codestin.com
Credit goes to github.com

Skip to content
13 changes: 9 additions & 4 deletions src/Locator/Options/GetByRoleOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Playwright\Locator\Options;

use Playwright\Exception\RuntimeException;
use Playwright\Regex;

/**
* @phpstan-type GetByRoleOptionsArray array{
Expand All @@ -23,7 +24,7 @@
* expanded?: bool,
* includeHidden?: bool,
* level?: int,
* name?: string,
* name?: string|Regex,
* pressed?: bool,
* selected?: bool
* }
Expand All @@ -39,7 +40,7 @@ public function __construct(
public ?bool $expanded = null,
public ?bool $includeHidden = null,
public ?int $level = null,
public ?string $name = null,
public string|Regex|null $name = null,
public bool|string|null $pressed = null,
public ?bool $selected = null,
public LocatorOptions $locatorOptions = new LocatorOptions(),
Expand Down Expand Up @@ -165,7 +166,7 @@ private static function extractPressed(array $options): bool|string|null
/**
* @param array<string, mixed> $options
*/
private static function extractName(array $options): ?string
private static function extractName(array $options): string|Regex|null
{
if (!array_key_exists('name', $options)) {
return null;
Expand All @@ -176,11 +177,15 @@ private static function extractName(array $options): ?string
return null;
}

if ($value instanceof Regex) {
return $value;
}

if (is_scalar($value) || $value instanceof \Stringable) {
return (string) $value;
}

throw new RuntimeException('getByRole option "name" must be stringable.');
throw new RuntimeException('getByRole option "name" must be a string or Regex instance.');
}

/**
Expand Down
97 changes: 12 additions & 85 deletions src/Locator/RoleSelectorBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

namespace Playwright\Locator;

use Playwright\Regex;

/**
* Helper for generating Playwright role selectors with accessibility focused options.
*
Expand All @@ -24,7 +26,6 @@ final class RoleSelectorBuilder
/** @var array<int, string> */
private const ROLE_SPECIFIC_KEYS = [
'name',
'nameRegex',
'exact',
'checked',
'disabled',
Expand All @@ -43,15 +44,11 @@ public static function buildSelector(string $role, array $options = []): string
$normalizedRole = self::normalizeRole($role);
$selector = 'internal:role='.$normalizedRole;

$nameFragment = self::buildNameAttribute($options);
$nameFragment = self::buildNameAttribute($options, !empty($options['exact']));
if (null !== $nameFragment) {
$selector .= $nameFragment;
}

if (!empty($options['exact'])) {
$selector .= '[exact]';
}

$selector .= self::buildBooleanAttribute('checked', $options['checked'] ?? null);
$selector .= self::buildBooleanAttribute('disabled', $options['disabled'] ?? null);
$selector .= self::buildBooleanAttribute('expanded', $options['expanded'] ?? null);
Expand Down Expand Up @@ -93,27 +90,20 @@ private static function normalizeRole(string $role): string
/**
* @param array<string, mixed> $options
*/
private static function buildNameAttribute(array $options): ?string
private static function buildNameAttribute(array $options, bool $exact = false): ?string
{
if (array_key_exists('nameRegex', $options)) {
$regexFragment = self::formatRegexAttribute('name', $options['nameRegex']);
if (null !== $regexFragment) {
return $regexFragment;
}
}

if (!array_key_exists('name', $options)) {
return null;
}

$nameOption = $options['name'];

if (is_array($nameOption) && array_key_exists('regex', $nameOption)) {
return self::formatRegexAttribute('name', $nameOption);
if ($nameOption instanceof Regex) {
return '[name='.$nameOption->pattern.']';
}

if ($nameOption instanceof \Stringable) {
return '[name="'.self::escapeAttributeValue((string) $nameOption).'"]';
$nameOption = (string) $nameOption;
}

if (is_string($nameOption)) {
Expand All @@ -122,7 +112,11 @@ private static function buildNameAttribute(array $options): ?string
return null;
}

return '[name="'.self::escapeAttributeValue($nameOption).'"]';
if ($exact) {
return '[name="'.self::escapeAttributeValue($nameOption).'"]';
}

return '[name=/'.preg_quote($nameOption, '/').'/i]';
}

return null;
Expand Down Expand Up @@ -160,71 +154,4 @@ private static function escapeAttributeValue(string $value): string
{
return addcslashes($value, '\\"');
}

private static function escapeRegexPattern(string $pattern): string
{
return addcslashes($pattern, '/');
}

private static function formatRegexAttribute(string $attribute, mixed $value): ?string
{
$pattern = null;
$flags = '';

if (is_string($value) || $value instanceof \Stringable) {
$pattern = (string) $value;
} elseif (is_array($value)) {
$patternValue = $value['pattern'] ?? $value['regex'] ?? null;
if (is_string($patternValue) || $patternValue instanceof \Stringable) {
$pattern = (string) $patternValue;
}

$flagsValue = $value['flags'] ?? null;
if (is_string($flagsValue)) {
$flags = $flagsValue;
}

$ignoreCase = $value['ignoreCase'] ?? $value['ignore_case'] ?? null;
if (true === $ignoreCase && !str_contains($flags, 'i')) {
$flags .= 'i';
}
}

if (null === $pattern) {
return null;
}

$pattern = trim($pattern);
if ('' === $pattern) {
return null;
}

if ('/' !== $pattern[0]) {
$pattern = '/'.self::escapeRegexPattern($pattern).'/';
}

if ('' !== $flags) {
$pattern .= self::sanitizeRegexFlags($flags);
}

return '['.$attribute.'='.$pattern.']';
}

private static function sanitizeRegexFlags(string $flags): string
{
$valid = ['d', 'g', 'i', 'm', 's', 'u', 'y'];
$unique = [];

foreach (str_split($flags) as $flag) {
if (!in_array($flag, $valid, true)) {
continue;
}
if (in_array($flag, $unique, true)) {
continue;
}
$unique[] = $flag;
}

return implode('', $unique);
}
}
109 changes: 88 additions & 21 deletions src/Page/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Playwright\Locator\LocatorInterface;
use Playwright\Locator\Options\GetByRoleOptions;
use Playwright\Locator\Options\LocatorOptions;
use Playwright\Locator\RoleSelectorBuilder;
use Playwright\Network\Request;
use Playwright\Network\Response;
use Playwright\Network\ResponseInterface;
Expand All @@ -61,6 +62,7 @@
use Playwright\Page\Options\WaitForResponseOptions;
use Playwright\Page\Options\WaitForSelectorOptions;
use Playwright\Page\Options\WaitForUrlOptions;
use Playwright\Regex;
use Playwright\Screenshot\ScreenshotHelper;
use Playwright\Transport\TransportInterface;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -214,33 +216,50 @@ public function locator(string $selector, array|LocatorOptions $options = []): L
/**
* @param array<string, mixed>|LocatorOptions $options
*/
public function getByAltText(string $text, array|LocatorOptions $options = []): LocatorInterface
public function getByAltText(string|Regex $text, array|LocatorOptions $options = []): LocatorInterface
{
return $this->locator(\sprintf('[alt="%s"]', $text), $this->normalizeLocatorOptions($options));
$opts = $this->normalizeLocatorOptions($options);
$exact = self::extractExact($opts);
$selector = self::buildAttrSelector('alt', $text, $exact);

return $this->locator($selector, $opts);
}

/**
* @param array<string, mixed>|LocatorOptions $options
*/
public function getByLabel(string $text, array|LocatorOptions $options = []): LocatorInterface
public function getByLabel(string|Regex $text, array|LocatorOptions $options = []): LocatorInterface
{
return $this->locator(\sprintf('label:text-is("%s") >> nth=0', $text), $this->normalizeLocatorOptions($options));
$opts = $this->normalizeLocatorOptions($options);
$exact = self::extractExact($opts);
$selector = self::buildLabelSelector($text, $exact);

return $this->locator($selector, $opts);
}

/**
* @param array<string, mixed>|LocatorOptions $options
*/
public function getByPlaceholder(string $text, array|LocatorOptions $options = []): LocatorInterface
public function getByPlaceholder(string|Regex $text, array|LocatorOptions $options = []): LocatorInterface
{
return $this->locator(\sprintf('[placeholder="%s"]', $text), $this->normalizeLocatorOptions($options));
$opts = $this->normalizeLocatorOptions($options);
$exact = self::extractExact($opts);
$selector = self::buildAttrSelector('placeholder', $text, $exact);

return $this->locator($selector, $opts);
}

/**
* @param array<string, mixed>|GetByRoleOptions $options
*/
public function getByRole(string $role, array|GetByRoleOptions $options = []): LocatorInterface
{
return $this->locator($role, $this->normalizeGetByRoleOptions($options));
$options = GetByRoleOptions::from($options);
$optionsArray = $options->toArray();
$selector = RoleSelectorBuilder::buildSelector($role, $optionsArray);
$locatorOptions = RoleSelectorBuilder::filterLocatorOptions($optionsArray);

return $this->locator($selector, $locatorOptions);
}

/**
Expand All @@ -254,17 +273,25 @@ public function getByTestId(string $testId, array|LocatorOptions $options = []):
/**
* @param array<string, mixed>|LocatorOptions $options
*/
public function getByText(string $text, array|LocatorOptions $options = []): LocatorInterface
public function getByText(string|Regex $text, array|LocatorOptions $options = []): LocatorInterface
{
return $this->locator(\sprintf('text="%s"', $text), $this->normalizeLocatorOptions($options));
$opts = $this->normalizeLocatorOptions($options);
$exact = self::extractExact($opts);
$selector = self::buildTextSelector($text, $exact);

return $this->locator($selector, $opts);
}

/**
* @param array<string, mixed>|LocatorOptions $options
*/
public function getByTitle(string $text, array|LocatorOptions $options = []): LocatorInterface
public function getByTitle(string|Regex $text, array|LocatorOptions $options = []): LocatorInterface
{
return $this->locator(\sprintf('[title="%s"]', $text), $this->normalizeLocatorOptions($options));
$opts = $this->normalizeLocatorOptions($options);
$exact = self::extractExact($opts);
$selector = self::buildAttrSelector('title', $text, $exact);

return $this->locator($selector, $opts);
}

/**
Expand Down Expand Up @@ -589,16 +616,6 @@ private function normalizeLocatorOptions(array|LocatorOptions $options): array
return LocatorOptions::from($options)->toArray();
}

/**
* @param array<string, mixed>|GetByRoleOptions $options
*
* @return array<string, mixed>
*/
private function normalizeGetByRoleOptions(array|GetByRoleOptions $options): array
{
return GetByRoleOptions::from($options)->toArray();
}

/**
* @param array<string, mixed> $params
*
Expand Down Expand Up @@ -1082,6 +1099,56 @@ private static function normalizeForPage(string $expression): string
return $expression;
}

/**
* @param array<string, mixed> $options
*/
private static function extractExact(array &$options): bool
{
$exact = (bool) ($options['exact'] ?? false);
unset($options['exact']);

return $exact;
}

private static function buildTextSelector(string|Regex $text, bool $exact): string
{
if ($text instanceof Regex) {
return \sprintf('internal:text=%s', $text->pattern);
}

if ($exact) {
return \sprintf('internal:text="%s"', addcslashes($text, '\\"'));
}

return \sprintf('internal:text=/%s/i', preg_quote($text, '/'));
}

private static function buildAttrSelector(string $attr, string|Regex $text, bool $exact): string
{
if ($text instanceof Regex) {
return \sprintf('internal:attr=[%s=%s]', $attr, $text->pattern);
}

if ($exact) {
return \sprintf('internal:attr=[%s="%s"]', $attr, addcslashes($text, '\\"'));
}

return \sprintf('internal:attr=[%s=/%s/i]', $attr, preg_quote($text, '/'));
}

private static function buildLabelSelector(string|Regex $text, bool $exact): string
{
if ($text instanceof Regex) {
return \sprintf('internal:label=%s', $text->pattern);
}

if ($exact) {
return \sprintf('internal:label="%s"', addcslashes($text, '\\"'));
}

return \sprintf('internal:label=/%s/i', preg_quote($text, '/'));
}

private static function isFunctionLike(string $s): bool
{
return (bool) preg_match('/^((async\s+)?function\b|\([^)]*\)\s*=>|[A-Za-z_$][A-Za-z0-9_$]*\s*=>|async\s*\([^)]*\)\s*=>)/', $s);
Expand Down
Loading