diff --git a/.github/workflows/Nightly.yaml b/.github/workflows/Nightly.yaml new file mode 100644 index 0000000..c1c6542 --- /dev/null +++ b/.github/workflows/Nightly.yaml @@ -0,0 +1,65 @@ +name: "Nightly" + +on: + schedule: + - cron: "27 3 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + descriptors: + name: "Device descriptors" + runs-on: ubuntu-latest + steps: + - name: "Git: checkout" + uses: actions/checkout@v4 + + - name: "PHP: setup 8.4" + uses: shivammathur/setup-php@v2 + with: + php-version: "8.4" + coverage: none + + - name: "Node: setup" + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: "Playwright: resolve latest stable" + id: resolve + run: | + version=$(npm view playwright version) + case "$version" in + [0-9]*.[0-9]*.[0-9]*) ;; + *) echo "::error::could not resolve the latest Playwright version"; exit 1 ;; + esac + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Latest stable Playwright is $version" + + - name: "Devices: verify latest stable" + env: + PLAYWRIGHT_VERSION: ${{ steps.resolve.outputs.version }} + run: php bin/check-devices.php --playwright-version="$PLAYWRIGHT_VERSION" | tee device-descriptors-report.md + + - name: "Summary" + if: always() + env: + VERSION: ${{ steps.resolve.outputs.version }} + STATUS: ${{ job.status }} + run: | + if [ -f device-descriptors-report.md ]; then + cat device-descriptors-report.md >> "$GITHUB_STEP_SUMMARY" + echo >> "$GITHUB_STEP_SUMMARY" + fi + { + echo "| | |" + echo "|---|---|" + echo "| Latest Playwright | \`${VERSION:-unresolved}\` |" + echo "| Result | $STATUS |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/update.yaml b/.github/workflows/update.yaml index 7d48d0c..decdd2e 100644 --- a/.github/workflows/update.yaml +++ b/.github/workflows/update.yaml @@ -1,9 +1,7 @@ name: "Update devices" on: - # schedule: - # - cron: '0 2 * * *' # Every night at 02:00 UTC - workflow_dispatch: {} + workflow_dispatch: jobs: update: @@ -24,7 +22,31 @@ jobs: run: composer update --no-interaction --prefer-dist - name: Update device descriptors - run: php bin/update-devices.php + run: php bin/update-devices.php | tee device-descriptors-report.md - name: Update markdown files run: php bin/update-docs.php + + - name: Format generated files + run: vendor/bin/php-cs-fixer fix + + - name: Validate generated changes + run: git diff --check + + - name: Write summary + if: always() + run: | + if [ -f device-descriptors-report.md ]; then + cat device-descriptors-report.md >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Create update patch + run: git diff --binary > device-descriptors.patch + + - name: Upload update patch + uses: actions/upload-artifact@v4 + with: + name: device-descriptors-update + path: device-descriptors.patch + if-no-files-found: error + retention-days: 7 diff --git a/.gitignore b/.gitignore index d8135fb..f62abd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.phpunit.cache/ /vendor/ +/data/deviceDescriptorsSource.json /.php-cs-fixer.cache /composer.lock /phpstan.neon diff --git a/README.md b/README.md index b7f0c60..1e1866e 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ Use `DeviceRegistry::has()` to check a name and `DeviceRegistry::all()` to retri The generated [device catalog](docs/DEVICES.md) lists every available descriptor and its browser, screen, viewport, scale, mobile, and touch values. +Each catalog is generated from an explicit stable Playwright release. The source version is recorded in [`data/playwright-version.txt`](data/playwright-version.txt); generation never follows Playwright's moving `main` branch. + +This package follows SemVer independently of Playwright. A catalog update is released as a new minor version because it changes the public descriptor data; matching Playwright and package version numbers is not implied. + Device descriptors emulate browser-visible properties. They do not reproduce physical hardware, operating-system UI, network conditions, or device performance. ## Documentation @@ -95,6 +99,24 @@ vendor/bin/phpstan analyse vendor/bin/phpunit ``` +To refresh the catalog for a stable Playwright release: + +```bash +php bin/update-devices.php +php bin/update-docs.php +vendor/bin/php-cs-fixer fix +``` + +Verify the committed catalog without modifying it: + +```bash +php bin/check-devices.php +``` + +Both commands resolve the latest stable Playwright version from npm. Pass `--playwright-version=1.63.0` only when reproducing a specific historical snapshot. + +The nightly workflow runs the check automatically. It fails when the recorded source version is behind or when the committed catalog does not match its recorded release. + ## License Playwright PHP Devices is released under the [MIT License](LICENSE). diff --git a/bin/check-devices.php b/bin/check-devices.php new file mode 100644 index 0000000..3cf45ad --- /dev/null +++ b/bin/check-devices.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +require_once __DIR__.'/update-devices-functions.php'; + +use function Playwright\Device\Update\commandArguments; +use function Playwright\Device\Update\compareDeviceCatalogs; +use function Playwright\Device\Update\downloadDescriptors; +use function Playwright\Device\Update\formatDeviceCatalogDiff; +use function Playwright\Device\Update\latestPlaywrightVersion; +use function Playwright\Device\Update\loadDeviceCatalog; +use function Playwright\Device\Update\normalizeDevices; +use function Playwright\Device\Update\parsePlaywrightVersion; +use function Playwright\Device\Update\readRecordedPlaywrightVersion; +use function Playwright\Device\Update\sourceUrl; + +$rootDir = dirname(__DIR__); +$devicesPath = $rootDir.'/data/devices.php'; +$versionPath = $rootDir.'/data/playwright-version.txt'; + +try { + $arguments = commandArguments($_SERVER['argv'] ?? null); + $recordedVersion = readRecordedPlaywrightVersion($versionPath); + $expectedVersion = [] === $arguments ? latestPlaywrightVersion() : parsePlaywrightVersion($arguments); + $currentDevices = loadDeviceCatalog($devicesPath); + + $json = downloadDescriptors(sourceUrl($expectedVersion)); + $decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + if (!is_array($decoded)) { + throw new UnexpectedValueException('Playwright descriptors must decode to an object.'); + } + + $expectedDevices = normalizeDevices($decoded); + $diff = compareDeviceCatalogs($currentDevices, $expectedDevices); + fwrite(STDOUT, formatDeviceCatalogDiff($diff, $recordedVersion, $expectedVersion)); + + if ($recordedVersion !== $expectedVersion || $currentDevices !== $expectedDevices) { + $updateCommand = [] === $arguments + ? 'php bin/update-devices.php' + : sprintf('php bin/update-devices.php --playwright-version=%s', $expectedVersion); + + throw new RuntimeException('Device descriptors are out of date. Run: '.$updateCommand); + } + + fwrite(STDOUT, sprintf('Device descriptors are up to date with Playwright v%s.'.PHP_EOL, $expectedVersion)); +} catch (Throwable $e) { + fwrite(STDERR, 'Error: '.$e->getMessage().PHP_EOL); + exit(1); +} diff --git a/bin/update-devices-functions.php b/bin/update-devices-functions.php new file mode 100644 index 0000000..0e64c79 --- /dev/null +++ b/bin/update-devices-functions.php @@ -0,0 +1,557 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Playwright\Device\Update; + +const SOURCE_URL_TEMPLATE = 'https://raw.githubusercontent.com/microsoft/playwright/v%s/packages/isomorphic/deviceDescriptorsSource.json'; +const LATEST_VERSION_URL = 'https://registry.npmjs.org/playwright/latest'; + +/** + * @return list + */ +function commandArguments(mixed $arguments): array +{ + if (!is_array($arguments)) { + throw new \InvalidArgumentException('Command arguments are unavailable.'); + } + + $result = []; + foreach (array_slice($arguments, 1) as $argument) { + if (!is_string($argument)) { + throw new \InvalidArgumentException('Command arguments must be strings.'); + } + + $result[] = $argument; + } + + return $result; +} + +/** + * @param list $arguments + */ +function parsePlaywrightVersion(array $arguments): string +{ + if (1 === count($arguments) && str_starts_with($arguments[0], '--playwright-version=')) { + return normalizePlaywrightVersion(substr($arguments[0], strlen('--playwright-version='))); + } + + if (2 === count($arguments) && '--playwright-version' === $arguments[0]) { + return normalizePlaywrightVersion($arguments[1]); + } + + throw new \InvalidArgumentException('The --playwright-version option is required.'); +} + +function normalizePlaywrightVersion(string $version): string +{ + $version = ltrim(trim($version), 'v'); + + if (1 !== preg_match('/^\d+\.\d+\.\d+$/D', $version)) { + throw new \InvalidArgumentException(sprintf('Invalid Playwright version "%s".', $version)); + } + + return $version; +} + +function sourceUrl(string $version): string +{ + return sprintf(SOURCE_URL_TEMPLATE, normalizePlaywrightVersion($version)); +} + +function latestPlaywrightVersion(): string +{ + fwrite(STDERR, 'Resolving the latest stable Playwright version'.PHP_EOL); + + $decoded = json_decode(download(LATEST_VERSION_URL), true, 512, JSON_THROW_ON_ERROR); + if (!is_array($decoded) || !isset($decoded['version']) || !is_string($decoded['version'])) { + throw new \UnexpectedValueException('The npm registry returned no valid Playwright version.'); + } + + return normalizePlaywrightVersion($decoded['version']); +} + +function downloadDescriptors(string $url): string +{ + fwrite(STDERR, 'Downloading descriptors from '.$url.PHP_EOL); + + return download($url); +} + +function download(string $url): string +{ + if (function_exists('curl_init')) { + $handle = curl_init($url); + if (false === $handle) { + throw new \RuntimeException('Failed to initialize cURL.'); + } + + curl_setopt_array($handle, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 15, + CURLOPT_TIMEOUT => 60, + CURLOPT_USERAGENT => 'playwright-php/devices-updater', + ]); + + $response = curl_exec($handle); + $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE); + $error = curl_error($handle); + + if (false === $response || $httpCode >= 400) { + throw new \RuntimeException(sprintf('cURL download failed (HTTP %s): %s', $httpCode ?: 'n/a', $error ?: 'unknown error')); + } + + return (string) $response; + } + + $context = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'timeout' => 60, + 'header' => ['User-Agent: playwright-php/devices-updater'], + ], + 'https' => [ + 'method' => 'GET', + 'timeout' => 60, + 'header' => ['User-Agent: playwright-php/devices-updater'], + ], + ]); + + $response = @file_get_contents($url, false, $context); + if (false === $response) { + throw new \RuntimeException('Failed to download descriptors using file_get_contents'); + } + + return (string) $response; +} + +/** + * @param array $source + * + * @return array> + */ +function normalizeDevices(array $source): array +{ + $normalized = []; + + foreach ($source as $name => $descriptor) { + if (!is_string($name) || !is_array($descriptor)) { + throw new \UnexpectedValueException('Every Playwright device descriptor must be a named object.'); + } + + $isLandscape = str_ends_with($name, ' landscape'); + $baseName = $isLandscape ? substr($name, 0, -10) : $name; + + if (!isset($normalized[$baseName])) { + $normalized[$baseName] = []; + } + + if ($isLandscape) { + $normalized[$baseName]['vp_l'] = descriptorDimensions($descriptor, 'viewport', $name); + continue; + } + + $normalized[$baseName]['ua'] = descriptorString($descriptor, 'userAgent', $name); + $normalized[$baseName]['dbt'] = descriptorString($descriptor, 'defaultBrowserType', $name); + $normalized[$baseName]['sf'] = descriptorFloat($descriptor, 'deviceScaleFactor', $name); + $normalized[$baseName]['vp'] = descriptorDimensions($descriptor, 'viewport', $name); + + if (isset($descriptor['screen'])) { + $normalized[$baseName]['sc'] = descriptorDimensions($descriptor, 'screen', $name); + } else { + unset($normalized[$baseName]['sc']); + } + + $isMobile = $descriptor['isMobile'] ?? null; + if (false === $isMobile) { + $normalized[$baseName]['m'] = false; + } else { + unset($normalized[$baseName]['m']); + } + + $hasTouch = $descriptor['hasTouch'] ?? null; + if (false === $hasTouch) { + $normalized[$baseName]['t'] = false; + } else { + unset($normalized[$baseName]['t']); + } + } + + ksort($normalized, SORT_NATURAL | SORT_FLAG_CASE); + + return $normalized; +} + +/** + * @return array> + */ +function loadDeviceCatalog(string $path): array +{ + if (!is_file($path)) { + throw new \RuntimeException(sprintf('Device catalog "%s" does not exist.', $path)); + } + + $loaded = require $path; + if (!is_array($loaded)) { + throw new \UnexpectedValueException(sprintf('Device catalog "%s" must return an array.', $path)); + } + + $catalog = []; + foreach ($loaded as $name => $descriptor) { + if (!is_string($name) || !is_array($descriptor)) { + throw new \UnexpectedValueException(sprintf('Device catalog "%s" contains an invalid descriptor.', $path)); + } + + $normalizedDescriptor = []; + foreach ($descriptor as $property => $value) { + if (!is_string($property)) { + throw new \UnexpectedValueException(sprintf('Device catalog "%s" contains an invalid descriptor property.', $path)); + } + + $normalizedDescriptor[$property] = $value; + } + + $catalog[$name] = $normalizedDescriptor; + } + + return $catalog; +} + +function readRecordedPlaywrightVersion(string $path): string +{ + if (!is_file($path)) { + throw new \RuntimeException(sprintf('Playwright version file "%s" does not exist.', $path)); + } + + return normalizePlaywrightVersion((string) file_get_contents($path)); +} + +/** + * @param array> $before + * @param array> $after + * + * @return array{ + * beforeCount: int, + * afterCount: int, + * added: list, + * removed: list, + * changedCount: int, + * properties: array, + * userAgentReplacements: list, + * otherUserAgentChanges: int + * } + */ +function compareDeviceCatalogs(array $before, array $after): array +{ + $added = array_keys(array_diff_key($after, $before)); + $removed = array_keys(array_diff_key($before, $after)); + sort($added, SORT_NATURAL | SORT_FLAG_CASE); + sort($removed, SORT_NATURAL | SORT_FLAG_CASE); + + $properties = array_fill_keys(['ua', 'vp', 'vp_l', 'sc', 'sf', 'm', 't', 'dbt'], 0); + $changedCount = 0; + $replacementCounts = []; + $otherUserAgentChanges = 0; + + foreach (array_intersect_key($after, $before) as $name => $afterDescriptor) { + $beforeDescriptor = $before[$name]; + $deviceChanged = false; + + foreach (array_unique(array_merge(array_keys($beforeDescriptor), array_keys($afterDescriptor))) as $property) { + $beforeHasProperty = array_key_exists($property, $beforeDescriptor); + $afterHasProperty = array_key_exists($property, $afterDescriptor); + if ($beforeHasProperty === $afterHasProperty && (!$beforeHasProperty || $beforeDescriptor[$property] === $afterDescriptor[$property])) { + continue; + } + + $deviceChanged = true; + $properties[$property] = ($properties[$property] ?? 0) + 1; + } + + if (!$deviceChanged) { + continue; + } + + ++$changedCount; + if (($beforeDescriptor['ua'] ?? null) === ($afterDescriptor['ua'] ?? null)) { + continue; + } + + $beforeUserAgent = $beforeDescriptor['ua'] ?? null; + $afterUserAgent = $afterDescriptor['ua'] ?? null; + $engine = $afterDescriptor['dbt'] ?? $beforeDescriptor['dbt'] ?? null; + if (!is_string($beforeUserAgent) || !is_string($afterUserAgent) || !is_string($engine)) { + ++$otherUserAgentChanges; + continue; + } + + $versionChanges = describeUserAgentVersionChanges($beforeUserAgent, $afterUserAgent, $engine); + if (null === $versionChanges) { + ++$otherUserAgentChanges; + continue; + } + + $key = implode("\0", [$engine, $versionChanges]); + if (!isset($replacementCounts[$key])) { + $replacementCounts[$key] = [ + 'engine' => $engine, + 'changes' => $versionChanges, + 'count' => 0, + ]; + } + + ++$replacementCounts[$key]['count']; + } + + $userAgentReplacements = array_values($replacementCounts); + usort($userAgentReplacements, static fn (array $left, array $right): int => [$left['engine'], $left['changes']] <=> [$right['engine'], $right['changes']]); + + return [ + 'beforeCount' => count($before), + 'afterCount' => count($after), + 'added' => $added, + 'removed' => $removed, + 'changedCount' => $changedCount, + 'properties' => $properties, + 'userAgentReplacements' => $userAgentReplacements, + 'otherUserAgentChanges' => $otherUserAgentChanges, + ]; +} + +function describeUserAgentVersionChanges(string $beforeUserAgent, string $afterUserAgent, string $engine): ?string +{ + $versionTokens = match ($engine) { + 'chromium' => [ + ['token' => 'Chrome', 'separator' => '/', 'pattern' => '/\bChrome\/([0-9.]+)/'], + ['token' => 'Edg', 'separator' => '/', 'pattern' => '/\bEdg\/([0-9.]+)/'], + ], + 'firefox' => [ + ['token' => 'Firefox', 'separator' => '/', 'pattern' => '/\bFirefox\/([0-9.]+)/'], + ['token' => 'rv', 'separator' => ':', 'pattern' => '/\brv:([0-9.]+)/'], + ], + 'webkit' => [ + ['token' => 'Version', 'separator' => '/', 'pattern' => '/\bVersion\/([0-9.]+)/'], + ], + default => [], + }; + + $updatedUserAgent = $beforeUserAgent; + $changes = []; + foreach ($versionTokens as $versionToken) { + if (1 !== preg_match($versionToken['pattern'], $beforeUserAgent, $beforeMatches) + || 1 !== preg_match($versionToken['pattern'], $afterUserAgent, $afterMatches) + || $beforeMatches[1] === $afterMatches[1]) { + continue; + } + + $updatedUserAgent = str_replace( + $versionToken['token'].$versionToken['separator'].$beforeMatches[1], + $versionToken['token'].$versionToken['separator'].$afterMatches[1], + $updatedUserAgent, + $replacementCount, + ); + if (1 !== $replacementCount) { + return null; + } + + $changes[] = sprintf('%s %s -> %s', $versionToken['token'], $beforeMatches[1], $afterMatches[1]); + } + + if ([] === $changes || $updatedUserAgent !== $afterUserAgent) { + return null; + } + + return implode('; ', $changes); +} + +/** + * @param array{ + * beforeCount: int, + * afterCount: int, + * added: list, + * removed: list, + * changedCount: int, + * properties: array, + * userAgentReplacements: list, + * otherUserAgentChanges: int + * } $diff + */ +function formatDeviceCatalogDiff(array $diff, ?string $beforeVersion, string $afterVersion): string +{ + $engineLabels = [ + 'chromium' => 'Chromium', + 'firefox' => 'Firefox', + 'webkit' => 'WebKit', + ]; + $propertyLabels = [ + 'ua' => 'userAgent', + 'vp' => 'viewport', + 'vp_l' => 'landscapeViewport', + 'sc' => 'screen', + 'sf' => 'deviceScaleFactor', + 'm' => 'mobile', + 't' => 'touch', + 'dbt' => 'browserType', + ]; + + $lines = [ + '## Device descriptor changes', + '', + sprintf('Source: %s -> Playwright v%s', null === $beforeVersion ? 'previous catalog' : 'Playwright v'.$beforeVersion, $afterVersion), + '', + sprintf('Devices: %d -> %d', $diff['beforeCount'], $diff['afterCount']), + sprintf('Added: %d', count($diff['added'])), + sprintf('Removed: %d', count($diff['removed'])), + sprintf('Changed: %d', $diff['changedCount']), + ]; + + if ([] !== $diff['added']) { + $lines[] = 'Added devices: '.implode(', ', $diff['added']); + } + if ([] !== $diff['removed']) { + $lines[] = 'Removed devices: '.implode(', ', $diff['removed']); + } + + $lines[] = ''; + $lines[] = 'Changed properties:'; + foreach ($propertyLabels as $property => $label) { + $lines[] = sprintf('- %s: %d', $label, $diff['properties'][$property] ?? 0); + } + + if ([] !== $diff['userAgentReplacements'] || $diff['otherUserAgentChanges'] > 0) { + $lines[] = ''; + $lines[] = 'User-agent replacements:'; + foreach ($diff['userAgentReplacements'] as $replacement) { + $lines[] = sprintf( + '- %s, %d device%s: %s', + $engineLabels[$replacement['engine']] ?? $replacement['engine'], + $replacement['count'], + 1 === $replacement['count'] ? '' : 's', + $replacement['changes'], + ); + } + if ($diff['otherUserAgentChanges'] > 0) { + $lines[] = sprintf('- Other user-agent changes: %d', $diff['otherUserAgentChanges']); + } + } + + return implode(PHP_EOL, $lines).PHP_EOL; +} + +/** + * @param array $descriptor + * + * @return array{0:int,1:int} + */ +function descriptorDimensions(array $descriptor, string $key, string $deviceName): array +{ + $dimensions = $descriptor[$key] ?? null; + if (!is_array($dimensions)) { + throw new \UnexpectedValueException(sprintf('Descriptor "%s" has no valid %s dimensions.', $deviceName, $key)); + } + + $width = $dimensions['width'] ?? null; + $height = $dimensions['height'] ?? null; + if (!is_int($width) || !is_int($height)) { + throw new \UnexpectedValueException(sprintf('Descriptor "%s" has invalid %s dimensions.', $deviceName, $key)); + } + + return [$width, $height]; +} + +/** + * @param array $descriptor + */ +function descriptorString(array $descriptor, string $key, string $deviceName): string +{ + $value = $descriptor[$key] ?? null; + if (!is_string($value)) { + throw new \UnexpectedValueException(sprintf('Descriptor "%s" has no valid %s value.', $deviceName, $key)); + } + + return $value; +} + +/** + * @param array $descriptor + */ +function descriptorFloat(array $descriptor, string $key, string $deviceName): float +{ + $value = $descriptor[$key] ?? null; + if (!is_int($value) && !is_float($value)) { + throw new \UnexpectedValueException(sprintf('Descriptor "%s" has no valid %s value.', $deviceName, $key)); + } + + return (float) $value; +} + +/** + * @param array> $devices + */ +function writeDevicesFile(string $path, array $devices, string $playwrightVersion): void +{ + ensureDirectory(dirname($path)); + + $export = var_export($devices, true); + $export = str_replace(' ', ' ', $export); + + $header = sprintf(<<<'PHP' + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// Generated from Microsoft Playwright v%s device descriptors. +PHP, normalizePlaywrightVersion($playwrightVersion)); + + file_put_contents($path, $header.PHP_EOL.PHP_EOL.'return '.$export.';'.PHP_EOL); +} + +function writeJsonFile(string $path, string $contents): void +{ + ensureDirectory(dirname($path)); + file_put_contents($path, json_encode(json_decode($contents, true, 512, JSON_THROW_ON_ERROR), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL); +} + +function writeVersionFile(string $path, string $playwrightVersion): void +{ + ensureDirectory(dirname($path)); + file_put_contents($path, normalizePlaywrightVersion($playwrightVersion).PHP_EOL); +} + +function ensureDirectory(string $path): void +{ + if (is_dir($path)) { + return; + } + + if (!mkdir($path, 0775, true) && !is_dir($path)) { + throw new \RuntimeException('Unable to create directory: '.$path); + } +} + +function relativePath(string $path): string +{ + return ltrim(str_replace(dirname(__DIR__), '', $path), '/'); +} diff --git a/bin/update-devices.php b/bin/update-devices.php index b63036f..ed43171 100644 --- a/bin/update-devices.php +++ b/bin/update-devices.php @@ -12,192 +12,56 @@ * file that was distributed with this source code. */ -const SOURCE_URL = 'https://raw.githubusercontent.com/microsoft/playwright/main/packages/isomorphic/deviceDescriptorsSource.json'; +require_once __DIR__.'/update-devices-functions.php'; + +use function Playwright\Device\Update\commandArguments; +use function Playwright\Device\Update\compareDeviceCatalogs; +use function Playwright\Device\Update\downloadDescriptors; +use function Playwright\Device\Update\formatDeviceCatalogDiff; +use function Playwright\Device\Update\latestPlaywrightVersion; +use function Playwright\Device\Update\loadDeviceCatalog; +use function Playwright\Device\Update\normalizeDevices; +use function Playwright\Device\Update\parsePlaywrightVersion; +use function Playwright\Device\Update\readRecordedPlaywrightVersion; +use function Playwright\Device\Update\relativePath; +use function Playwright\Device\Update\sourceUrl; +use function Playwright\Device\Update\writeDevicesFile; +use function Playwright\Device\Update\writeJsonFile; +use function Playwright\Device\Update\writeVersionFile; $rootDir = dirname(__DIR__); $jsonPath = $rootDir.'/data/deviceDescriptorsSource.json'; $devicesPath = $rootDir.'/data/devices.php'; +$versionPath = $rootDir.'/data/playwright-version.txt'; try { - $json = downloadDescriptors(SOURCE_URL); - writeJsonFile($jsonPath, $json); + $arguments = commandArguments($_SERVER['argv'] ?? null); + $playwrightVersion = [] === $arguments ? latestPlaywrightVersion() : parsePlaywrightVersion($arguments); + $currentDevices = loadDeviceCatalog($devicesPath); + $currentVersion = is_file($versionPath) ? readRecordedPlaywrightVersion($versionPath) : null; + $json = downloadDescriptors(sourceUrl($playwrightVersion)); $decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR); - $normalized = normalizeDevices($decoded); + if (!is_array($decoded)) { + throw new UnexpectedValueException('Playwright descriptors must decode to an object.'); + } - writeDevicesFile($devicesPath, $normalized); + $normalized = normalizeDevices($decoded); + fwrite(STDOUT, formatDeviceCatalogDiff(compareDeviceCatalogs($currentDevices, $normalized), $currentVersion, $playwrightVersion)); - info(sprintf('Updated %s and %s', relativePath($jsonPath), relativePath($devicesPath))); + writeJsonFile($jsonPath, $json); + writeDevicesFile($devicesPath, $normalized, $playwrightVersion); + writeVersionFile($versionPath, $playwrightVersion); + + fwrite(STDOUT, sprintf( + "Updated %s, %s and %s from Playwright v%s\n", + relativePath($jsonPath), + relativePath($devicesPath), + relativePath($versionPath), + $playwrightVersion, + )); } catch (Throwable $e) { fwrite(STDERR, 'Error: '.$e->getMessage().PHP_EOL); + fwrite(STDERR, 'Usage: php bin/update-devices.php [--playwright-version=]'.PHP_EOL); exit(1); } - -function downloadDescriptors(string $url): string -{ - info('Downloading descriptors from '.$url); - - if (function_exists('curl_init')) { - $handle = curl_init($url); - curl_setopt_array($handle, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_CONNECTTIMEOUT => 15, - CURLOPT_TIMEOUT => 60, - CURLOPT_USERAGENT => 'playwright-php/devices-updater', - ]); - - $response = curl_exec($handle); - $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE); - $error = curl_error($handle); - - if (false === $response || $httpCode >= 400) { - throw new RuntimeException(sprintf('cURL download failed (HTTP %s): %s', $httpCode ?: 'n/a', $error ?: 'unknown error')); - } - - return (string) $response; - } - - $context = stream_context_create([ - 'http' => [ - 'method' => 'GET', - 'timeout' => 60, - 'header' => ['User-Agent: playwright-php/devices-updater'], - ], - 'https' => [ - 'method' => 'GET', - 'timeout' => 60, - 'header' => ['User-Agent: playwright-php/devices-updater'], - ], - ]); - - $response = @file_get_contents($url, false, $context); - if (false === $response) { - throw new RuntimeException('Failed to download descriptors using file_get_contents'); - } - - return (string) $response; -} - -/** - * @param array> $source - * - * @return array> - */ -function normalizeDevices(array $source): array -{ - $normalized = []; - - foreach ($source as $name => $descriptor) { - $isLandscape = str_ends_with($name, ' landscape'); - $baseName = $isLandscape ? substr($name, 0, -10) : $name; - - if (!isset($normalized[$baseName])) { - $normalized[$baseName] = []; - } - - if ($isLandscape) { - $normalized[$baseName]['vp_l'] = flattenDimensions($descriptor['viewport']); - continue; - } - - $normalized[$baseName]['ua'] = (string) $descriptor['userAgent']; - $normalized[$baseName]['dbt'] = (string) $descriptor['defaultBrowserType']; - $normalized[$baseName]['sf'] = (float) $descriptor['deviceScaleFactor']; - $normalized[$baseName]['vp'] = flattenDimensions($descriptor['viewport']); - - if (isset($descriptor['screen'])) { - $normalized[$baseName]['sc'] = flattenDimensions($descriptor['screen']); - } else { - unset($normalized[$baseName]['sc']); - } - - $isMobile = $descriptor['isMobile'] ?? null; - if (false === $isMobile) { - $normalized[$baseName]['m'] = false; - } else { - unset($normalized[$baseName]['m']); - } - - $hasTouch = $descriptor['hasTouch'] ?? null; - if (false === $hasTouch) { - $normalized[$baseName]['t'] = false; - } else { - unset($normalized[$baseName]['t']); - } - } - - ksort($normalized, SORT_NATURAL | SORT_FLAG_CASE); - - return $normalized; -} - -/** - * @param array{width:int,height:int} $dimensions - * - * @return array{0:int,1:int} - */ -function flattenDimensions(array $dimensions): array -{ - return [(int) $dimensions['width'], (int) $dimensions['height']]; -} - -/** - * @param array> $devices - */ -function writeDevicesFile(string $path, array $devices): void -{ - ensureDirectory(dirname($path)); - - $export = var_export($devices, true); - $export = str_replace(' ', ' ', $export); - - $header = <<<'PHP' - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -return -PHP; - - $contents = $header.$export.';'.PHP_EOL; - - file_put_contents($path, $contents); -} - -function writeJsonFile(string $path, string $contents): void -{ - ensureDirectory(dirname($path)); - file_put_contents($path, json_encode(json_decode($contents, true, 512, JSON_THROW_ON_ERROR), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL); -} - -function ensureDirectory(string $path): void -{ - if (is_dir($path)) { - return; - } - - if (!mkdir($path, 0775, true) && !is_dir($path)) { - throw new RuntimeException('Unable to create directory: '.$path); - } -} - -function relativePath(string $path): string -{ - return ltrim(str_replace(dirname(__DIR__), '', $path), '/'); -} - -function info(string $message): void -{ - fwrite(STDOUT, $message.PHP_EOL); -} diff --git a/bin/update-docs.php b/bin/update-docs.php index eb13fd8..251c536 100644 --- a/bin/update-docs.php +++ b/bin/update-docs.php @@ -19,6 +19,16 @@ $rootDir = dirname(__DIR__); $outputPath = $rootDir.'/docs/DEVICES.md'; +$versionPath = $rootDir.'/data/playwright-version.txt'; + +if (!is_file($versionPath)) { + throw new RuntimeException(sprintf('Playwright version file "%s" does not exist. Run bin/update-devices.php first.', $versionPath)); +} + +$playwrightVersion = trim((string) file_get_contents($versionPath)); +if (1 !== preg_match('/^\d+\.\d+\.\d+$/D', $playwrightVersion)) { + throw new RuntimeException(sprintf('Invalid Playwright version "%s" in %s.', $playwrightVersion, $versionPath)); +} $registry = new DeviceRegistry(); $devices = $registry->all(); @@ -30,7 +40,7 @@ $lines = []; $lines[] = '# Device Catalogue'; $lines[] = ''; -$lines[] = 'Generated from Playwright\'s upstream device descriptors ('.date('Y-m-d').').'; +$lines[] = sprintf('Generated from Playwright v%s device descriptors.', $playwrightVersion); $lines[] = ''; $lines[] = '| Device | Browser | Screen | Scale | Viewport | Landscape | Mobile | Touch |'; $lines[] = '| --- | --- | --- | --- | --- | --- | --- | --- |'; @@ -69,6 +79,9 @@ echo 'Updated '.relativePath($outputPath).PHP_EOL; +/** + * @param array{width: int, height: int}|null $dimensions + */ function formatDimensions(?array $dimensions, string $emptyValue = 'n/a'): string { if (null === $dimensions) { diff --git a/data/devices.php b/data/devices.php index 878b70e..d186d03 100644 --- a/data/devices.php +++ b/data/devices.php @@ -12,9 +12,11 @@ * file that was distributed with this source code. */ +// Generated from Microsoft Playwright v1.63.0 device descriptors. + return [ 'Blackberry PlayBook' => [ - 'ua' => 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/26.5 Safari/536.2+', + 'ua' => 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/26.6 Safari/536.2+', 'dbt' => 'webkit', 'sf' => 1.0, 'vp' => [ @@ -27,7 +29,7 @@ ], ], 'BlackBerry Z30' => [ - 'ua' => 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/26.5 Mobile Safari/537.10+', + 'ua' => 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/26.6 Mobile Safari/537.10+', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -40,7 +42,7 @@ ], ], 'Desktop Chrome' => [ - 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Safari/537.36', + 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36', 'dbt' => 'chromium', 'sf' => 1.0, 'vp' => [ @@ -55,7 +57,7 @@ 't' => false, ], 'Desktop Chrome HiDPI' => [ - 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Safari/537.36', + 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -70,7 +72,7 @@ 't' => false, ], 'Desktop Edge' => [ - 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Safari/537.36 Edg/152.0.7977.54', + 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36 Edg/153.0.8010.12', 'dbt' => 'chromium', 'sf' => 1.0, 'vp' => [ @@ -85,7 +87,7 @@ 't' => false, ], 'Desktop Edge HiDPI' => [ - 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Safari/537.36 Edg/152.0.7977.54', + 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36 Edg/153.0.8010.12', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -100,7 +102,7 @@ 't' => false, ], 'Desktop Firefox' => [ - 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0', + 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0', 'dbt' => 'firefox', 'sf' => 1.0, 'vp' => [ @@ -115,7 +117,7 @@ 't' => false, ], 'Desktop Firefox HiDPI' => [ - 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0', + 'ua' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:155.0) Gecko/20100101 Firefox/155.0', 'dbt' => 'firefox', 'sf' => 2.0, 'vp' => [ @@ -130,7 +132,7 @@ 't' => false, ], 'Desktop Safari' => [ - 'ua' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15', + 'ua' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Safari/605.1.15', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -145,7 +147,7 @@ 't' => false, ], 'Galaxy A55' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.25, 'vp' => [ @@ -158,7 +160,7 @@ ], ], 'Galaxy Note 3' => [ - 'ua' => 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/26.5 Mobile Safari/534.30', + 'ua' => 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/26.6 Mobile Safari/534.30', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -171,7 +173,7 @@ ], ], 'Galaxy Note II' => [ - 'ua' => 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/26.5 Mobile Safari/534.30', + 'ua' => 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/26.6 Mobile Safari/534.30', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -184,7 +186,7 @@ ], ], 'Galaxy S5' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -197,7 +199,7 @@ ], ], 'Galaxy S8' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -210,7 +212,7 @@ ], ], 'Galaxy S9+' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 4.5, 'vp' => [ @@ -223,7 +225,7 @@ ], ], 'Galaxy S24' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -236,7 +238,7 @@ ], ], 'Galaxy S III' => [ - 'ua' => 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/26.5 Mobile Safari/534.30', + 'ua' => 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/26.6 Mobile Safari/534.30', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -249,7 +251,7 @@ ], ], 'Galaxy Tab S4' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.25, 'vp' => [ @@ -262,7 +264,7 @@ ], ], 'Galaxy Tab S9' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.5, 'vp' => [ @@ -275,7 +277,7 @@ ], ], 'Galaxy Z Flip 6' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F741U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F741U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -292,7 +294,7 @@ ], ], 'Galaxy Z Flip 6 Cover' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F741U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F741U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -309,7 +311,7 @@ ], ], 'Galaxy Z Flip 7' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F761U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F761U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -326,7 +328,7 @@ ], ], 'Galaxy Z Flip 7 Cover' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F761U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F761U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -343,7 +345,7 @@ ], ], 'Galaxy Z Fold 6' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F956U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F956U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -360,7 +362,7 @@ ], ], 'Galaxy Z Fold 6 Cover' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F956U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F956U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -377,7 +379,7 @@ ], ], 'Galaxy Z Fold 7' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F966U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F966U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -394,7 +396,7 @@ ], ], 'Galaxy Z Fold 7 Cover' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F966U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 10; SM-F966U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -411,7 +413,7 @@ ], ], 'iPad (gen 5)' => [ - 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -424,7 +426,7 @@ ], ], 'iPad (gen 6)' => [ - 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -437,7 +439,7 @@ ], ], 'iPad (gen 7)' => [ - 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -450,7 +452,7 @@ ], ], 'iPad (gen 11)' => [ - 'ua' => 'Mozilla/5.0 (iPad; CPU OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/19E241 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPad; CPU OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/19E241 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.5, 'vp' => [ @@ -463,7 +465,7 @@ ], ], 'iPad Mini' => [ - 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -476,7 +478,7 @@ ], ], 'iPad Pro 11' => [ - 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -489,7 +491,7 @@ ], ], 'iPhone 6' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.5 Mobile/15A372 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.6 Mobile/15A372 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -502,7 +504,7 @@ ], ], 'iPhone 6 Plus' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.5 Mobile/15A372 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.6 Mobile/15A372 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -515,7 +517,7 @@ ], ], 'iPhone 7' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.5 Mobile/15A372 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.6 Mobile/15A372 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -528,7 +530,7 @@ ], ], 'iPhone 7 Plus' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.5 Mobile/15A372 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.6 Mobile/15A372 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -541,7 +543,7 @@ ], ], 'iPhone 8' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.5 Mobile/15A372 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.6 Mobile/15A372 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -554,7 +556,7 @@ ], ], 'iPhone 8 Plus' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.5 Mobile/15A372 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.6 Mobile/15A372 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -567,7 +569,7 @@ ], ], 'iPhone 11' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -584,7 +586,7 @@ ], ], 'iPhone 11 Pro' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -601,7 +603,7 @@ ], ], 'iPhone 11 Pro Max' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -618,7 +620,7 @@ ], ], 'iPhone 12' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -635,7 +637,7 @@ ], ], 'iPhone 12 Mini' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -652,7 +654,7 @@ ], ], 'iPhone 12 Pro' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -669,7 +671,7 @@ ], ], 'iPhone 12 Pro Max' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -686,7 +688,7 @@ ], ], 'iPhone 13' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -703,7 +705,7 @@ ], ], 'iPhone 13 Mini' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -720,7 +722,7 @@ ], ], 'iPhone 13 Pro' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -737,7 +739,7 @@ ], ], 'iPhone 13 Pro Max' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -754,7 +756,7 @@ ], ], 'iPhone 14' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -771,7 +773,7 @@ ], ], 'iPhone 14 Plus' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -788,7 +790,7 @@ ], ], 'iPhone 14 Pro' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -805,7 +807,7 @@ ], ], 'iPhone 14 Pro Max' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -822,7 +824,7 @@ ], ], 'iPhone 15' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -839,7 +841,7 @@ ], ], 'iPhone 15 Plus' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -856,7 +858,7 @@ ], ], 'iPhone 15 Pro' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -873,7 +875,7 @@ ], ], 'iPhone 15 Pro Max' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -890,7 +892,7 @@ ], ], 'iPhone 16' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -907,7 +909,7 @@ ], ], 'iPhone 16 Plus' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -924,7 +926,7 @@ ], ], 'iPhone 16 Pro' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -941,7 +943,7 @@ ], ], 'iPhone 16 Pro Max' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -958,7 +960,7 @@ ], ], 'iPhone 16e' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -975,7 +977,7 @@ ], ], 'iPhone 17' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -992,7 +994,7 @@ ], ], 'iPhone 17 Pro' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -1009,7 +1011,7 @@ ], ], 'iPhone 17 Pro Max' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -1026,7 +1028,7 @@ ], ], 'iPhone 17e' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -1043,7 +1045,7 @@ ], ], 'iPhone Air' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -1060,7 +1062,7 @@ ], ], 'iPhone SE' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/26.5 Mobile/14E304 Safari/602.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/26.6 Mobile/14E304 Safari/602.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -1073,7 +1075,7 @@ ], ], 'iPhone SE (3rd gen)' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/26.5 Mobile/19E241 Safari/602.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/26.6 Mobile/19E241 Safari/602.1', 'dbt' => 'webkit', 'sf' => 2.0, 'vp' => [ @@ -1086,7 +1088,7 @@ ], ], 'iPhone X' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.5 Mobile/15A372 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/26.6 Mobile/15A372 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -1099,7 +1101,7 @@ ], ], 'iPhone XR' => [ - 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1', + 'ua' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1', 'dbt' => 'webkit', 'sf' => 3.0, 'vp' => [ @@ -1125,7 +1127,7 @@ ], ], 'LG Optimus L70' => [ - 'ua' => 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 1.25, 'vp' => [ @@ -1138,7 +1140,7 @@ ], ], 'Microsoft Lumia 550' => [ - 'ua' => 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36 Edge/14.14263', + 'ua' => 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36 Edge/14.14263', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -1151,7 +1153,7 @@ ], ], 'Microsoft Lumia 950' => [ - 'ua' => 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36 Edge/14.14263', + 'ua' => 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36 Edge/14.14263', 'dbt' => 'chromium', 'sf' => 4.0, 'vp' => [ @@ -1164,7 +1166,7 @@ ], ], 'Moto G4' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -1177,7 +1179,7 @@ ], ], 'Nexus 4' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -1190,7 +1192,7 @@ ], ], 'Nexus 5' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -1203,7 +1205,7 @@ ], ], 'Nexus 5X' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.625, 'vp' => [ @@ -1216,7 +1218,7 @@ ], ], 'Nexus 6' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.5, 'vp' => [ @@ -1229,7 +1231,7 @@ ], ], 'Nexus 6P' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.5, 'vp' => [ @@ -1242,7 +1244,7 @@ ], ], 'Nexus 7' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -1255,7 +1257,7 @@ ], ], 'Nexus 10' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.0, 'vp' => [ @@ -1294,7 +1296,7 @@ ], ], 'Pixel 2' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.625, 'vp' => [ @@ -1307,7 +1309,7 @@ ], ], 'Pixel 2 XL' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.5, 'vp' => [ @@ -1320,7 +1322,7 @@ ], ], 'Pixel 3' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.75, 'vp' => [ @@ -1333,7 +1335,7 @@ ], ], 'Pixel 4' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -1346,7 +1348,7 @@ ], ], 'Pixel 4a (5G)' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.63, 'vp' => [ @@ -1363,7 +1365,7 @@ ], ], 'Pixel 5' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.75, 'vp' => [ @@ -1380,7 +1382,7 @@ ], ], 'Pixel 6' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.625, 'vp' => [ @@ -1397,7 +1399,7 @@ ], ], 'Pixel 6 Pro' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 12; Pixel 6 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 12; Pixel 6 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.5, 'vp' => [ @@ -1414,7 +1416,7 @@ ], ], 'Pixel 6a' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 12; Pixel 6a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 12; Pixel 6a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.625, 'vp' => [ @@ -1431,7 +1433,7 @@ ], ], 'Pixel 7' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.625, 'vp' => [ @@ -1448,7 +1450,7 @@ ], ], 'Pixel 7 Pro' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 13; Pixel 7 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 13; Pixel 7 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.5, 'vp' => [ @@ -1465,7 +1467,7 @@ ], ], 'Pixel 7a' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 13; Pixel 7a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 13; Pixel 7a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.625, 'vp' => [ @@ -1482,7 +1484,7 @@ ], ], 'Pixel 8' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.625, 'vp' => [ @@ -1499,7 +1501,7 @@ ], ], 'Pixel 8 Pro' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -1516,7 +1518,7 @@ ], ], 'Pixel 8a' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 8a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 8a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 2.625, 'vp' => [ @@ -1533,7 +1535,7 @@ ], ], 'Pixel 9' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -1550,7 +1552,7 @@ ], ], 'Pixel 9 Pro' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 9 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 9 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -1567,7 +1569,7 @@ ], ], 'Pixel 9 Pro XL' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 9 Pro XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 14; Pixel 9 Pro XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -1584,7 +1586,7 @@ ], ], 'Pixel 10' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 16; Pixel 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 16; Pixel 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -1601,7 +1603,7 @@ ], ], 'Pixel 10 Pro' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 16; Pixel 10 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 16; Pixel 10 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ @@ -1618,7 +1620,7 @@ ], ], 'Pixel 10 Pro XL' => [ - 'ua' => 'Mozilla/5.0 (Linux; Android 16; Pixel 10 Pro XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.54 Mobile Safari/537.36', + 'ua' => 'Mozilla/5.0 (Linux; Android 16; Pixel 10 Pro XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36', 'dbt' => 'chromium', 'sf' => 3.0, 'vp' => [ diff --git a/data/playwright-version.txt b/data/playwright-version.txt new file mode 100644 index 0000000..af92bdd --- /dev/null +++ b/data/playwright-version.txt @@ -0,0 +1 @@ +1.63.0 diff --git a/docs/DEVICES.md b/docs/DEVICES.md index ff5ddf9..364df30 100644 --- a/docs/DEVICES.md +++ b/docs/DEVICES.md @@ -1,6 +1,6 @@ # Device Catalogue -Generated from Playwright's upstream device descriptors (2026-08-24). +Generated from Playwright v1.63.0 device descriptors. | Device | Browser | Screen | Scale | Viewport | Landscape | Mobile | Touch | | --- | --- | --- | --- | --- | --- | --- | --- | diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 402eff2..a56337f 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,6 +1,7 @@ parameters: level: 10 paths: + - bin/ - src/ - tests/ treatPhpDocTypesAsCertain: false diff --git a/tests/UpdateDevicesTest.php b/tests/UpdateDevicesTest.php new file mode 100644 index 0000000..ef2025b --- /dev/null +++ b/tests/UpdateDevicesTest.php @@ -0,0 +1,190 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Playwright\Device\Tests; + +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\TestCase; + +use function Playwright\Device\Update\compareDeviceCatalogs; +use function Playwright\Device\Update\formatDeviceCatalogDiff; +use function Playwright\Device\Update\normalizePlaywrightVersion; +use function Playwright\Device\Update\parsePlaywrightVersion; +use function Playwright\Device\Update\sourceUrl; +use function Playwright\Device\Update\writeDevicesFile; + +require_once __DIR__.'/../bin/update-devices-functions.php'; + +#[CoversNothing] +class UpdateDevicesTest extends TestCase +{ + public function testParsesPlaywrightVersionOption(): void + { + $this->assertSame('1.63.0', parsePlaywrightVersion(['--playwright-version=1.63.0'])); + $this->assertSame('1.63.0', parsePlaywrightVersion(['--playwright-version', 'v1.63.0'])); + } + + public function testRejectsMissingPlaywrightVersion(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The --playwright-version option is required.'); + + parsePlaywrightVersion([]); + } + + public function testRejectsInvalidPlaywrightVersion(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Playwright version'); + + normalizePlaywrightVersion('main'); + } + + public function testBuildsUrlForImmutablePlaywrightTag(): void + { + $url = sourceUrl('1.63.0'); + + $this->assertSame( + 'https://raw.githubusercontent.com/microsoft/playwright/v1.63.0/packages/isomorphic/deviceDescriptorsSource.json', + $url, + ); + $this->assertStringNotContainsString('/main/', $url); + } + + public function testWritesLoadableDevicesFileWithSourceVersion(): void + { + $path = tempnam(sys_get_temp_dir(), 'playwright-devices-'); + $this->assertNotFalse($path); + + $devices = [ + 'Test Phone' => [ + 'ua' => 'Test-UA', + 'dbt' => 'chromium', + 'sf' => 2.0, + 'vp' => [390, 790], + ], + ]; + + try { + writeDevicesFile($path, $devices, '1.63.0'); + + $this->assertSame($devices, require $path); + $this->assertStringContainsString( + 'Generated from Microsoft Playwright v1.63.0 device descriptors.', + (string) file_get_contents($path), + ); + } finally { + unlink($path); + } + } + + public function testFormatsSemanticCatalogDiff(): void + { + $before = [ + 'Desktop Edge' => [ + 'ua' => 'Mozilla/5.0 Chrome/152.0.1 Safari/537.36 Edg/152.0.1', + 'dbt' => 'chromium', + 'vp' => [1280, 720], + ], + 'Phone A' => [ + 'ua' => 'Mozilla/5.0 Chrome/152.0.1 Safari/537.36', + 'dbt' => 'chromium', + 'vp' => [390, 790], + ], + 'Phone B' => [ + 'ua' => 'Mozilla/5.0 Chrome/152.0.1 Safari/537.36', + 'dbt' => 'chromium', + 'vp' => [400, 800], + ], + 'Phone C' => [ + 'ua' => 'Mozilla/5.0 Android 14 Firefox/153.0', + 'dbt' => 'firefox', + 'vp' => [420, 840], + ], + 'Tablet A' => [ + 'ua' => 'Mozilla/5.0 Version/26.5 Mobile Safari/605.1.15', + 'dbt' => 'webkit', + 'vp' => [820, 1180], + ], + 'Removed Phone' => [ + 'ua' => 'Legacy', + 'dbt' => 'webkit', + 'vp' => [320, 480], + ], + ]; + $after = [ + 'Desktop Edge' => [ + 'ua' => 'Mozilla/5.0 Chrome/153.0.2 Safari/537.36 Edg/153.0.2', + 'dbt' => 'chromium', + 'vp' => [1280, 720], + ], + 'Phone A' => [ + 'ua' => 'Mozilla/5.0 Chrome/153.0.2 Safari/537.36', + 'dbt' => 'chromium', + 'vp' => [390, 790], + ], + 'Phone B' => [ + 'ua' => 'Mozilla/5.0 Chrome/153.0.2 Safari/537.36', + 'dbt' => 'chromium', + 'vp' => [410, 810], + ], + 'Phone C' => [ + 'ua' => 'Mozilla/5.0 Android 15 Firefox/155.0', + 'dbt' => 'firefox', + 'vp' => [420, 840], + ], + 'Tablet A' => [ + 'ua' => 'Mozilla/5.0 Version/26.6 Mobile Safari/605.1.15', + 'dbt' => 'webkit', + 'vp' => [820, 1180], + ], + 'Added Phone' => [ + 'ua' => 'New', + 'dbt' => 'webkit', + 'vp' => [430, 850], + ], + ]; + + $report = formatDeviceCatalogDiff(compareDeviceCatalogs($before, $after), '1.62.0', '1.63.0'); + + $this->assertSame(<<<'REPORT' +## Device descriptor changes + +Source: Playwright v1.62.0 -> Playwright v1.63.0 + +Devices: 6 -> 6 +Added: 1 +Removed: 1 +Changed: 5 +Added devices: Added Phone +Removed devices: Removed Phone + +Changed properties: +- userAgent: 5 +- viewport: 1 +- landscapeViewport: 0 +- screen: 0 +- deviceScaleFactor: 0 +- mobile: 0 +- touch: 0 +- browserType: 0 + +User-agent replacements: +- Chromium, 2 devices: Chrome 152.0.1 -> 153.0.2 +- Chromium, 1 device: Chrome 152.0.1 -> 153.0.2; Edg 152.0.1 -> 153.0.2 +- WebKit, 1 device: Version 26.5 -> 26.6 +- Other user-agent changes: 1 +REPORT.PHP_EOL, $report); + } +}