From 6efda918b215394f79502325e86739b4b1145575 Mon Sep 17 00:00:00 2001 From: Abderrahman Daif Date: Sun, 8 Feb 2026 01:30:40 +0100 Subject: [PATCH 1/2] Fix filter() to use keys and allow extension capabilities The filter() method had three bugs: 1. array_filter callback received values instead of keys, causing TypeError when capability values are arrays (e.g. goog:chromeOptions) 2. Extension capabilities (keys containing ':') were stripped, but W3C spec requires them to be allowed through 3. array_values() destroyed the key-value mapping, turning {"browserName": "chrome"} into ["chrome"] Fix by using ARRAY_FILTER_USE_KEY, allowing extension capabilities per W3C spec, and removing array_values() wrapper. References: - https://www.w3.org/TR/webdriver2/#dfn-extension-capability - https://www.php.net/array_filter --- lib/WebDriver/WebDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/WebDriver/WebDriver.php b/lib/WebDriver/WebDriver.php index b97eb87..0e204f4 100644 --- a/lib/WebDriver/WebDriver.php +++ b/lib/WebDriver/WebDriver.php @@ -166,7 +166,7 @@ public function sessions() */ private function filter($capabilities) { - return $capabilities ? array_values(array_filter($capabilities, function ($capability) { return self::$w3cCapabilities[$capability] ?? 0; })) : null; + return $capabilities ? array_filter($capabilities, function ($key) { return isset(self::$w3cCapabilities[$key]) || str_contains($key, ':'); }, ARRAY_FILTER_USE_KEY) : null; } /** From 9d89e626c071a0820edbd8fb27cae9e3dd018c5d Mon Sep 17 00:00:00 2001 From: Abderrahman Daif Date: Tue, 10 Feb 2026 14:33:28 +0100 Subject: [PATCH 2/2] test: add unit tests for W3C capability filtering Add WebDriverFilterTest covering 8 test cases: - Non-W3C capabilities are stripped - All W3C standard capabilities pass through - Vendor extension capabilities (with ':') are preserved - Falsy values (false, 0) are NOT stripped (addresses review concern) - Null capabilities handled gracefully - requiredCapabilities (alwaysMatch) also filtered - Legacy capability names remapped to W3C equivalents - Key-value mapping preserved (not reindexed numerically) --- test/Test/WebDriver/WebDriverFilterTest.php | 278 ++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 test/Test/WebDriver/WebDriverFilterTest.php diff --git a/test/Test/WebDriver/WebDriverFilterTest.php b/test/Test/WebDriver/WebDriverFilterTest.php new file mode 100644 index 0000000..aa05ba2 --- /dev/null +++ b/test/Test/WebDriver/WebDriverFilterTest.php @@ -0,0 +1,278 @@ + + */ + +namespace Test\WebDriver; + +use PHPUnit\Framework\TestCase; +use WebDriver\Browser; +use WebDriver\Capability; +use WebDriver\Service\CurlService; +use WebDriver\ServiceFactory; +use WebDriver\WebDriver; + +/** + * Test WebDriver capability filtering in session creation + * + * @package WebDriver + * + * @group Unit + */ +class WebDriverFilterTest extends TestCase +{ + /** + * @var array|null + */ + private $capturedParameters; + + /** + * Create a WebDriver instance with mocked CurlService that captures request parameters + * + * @return WebDriver + */ + private function createDriverWithMock() + { + $mockCurlService = $this->createMock(CurlService::class); + $mockCurlService->expects($this->any()) + ->method('execute') + ->will($this->returnCallback(function ($requestMethod, $url, $parameters) { + $this->capturedParameters = $parameters; + + $info = [ + 'url' => $url, + 'request_method' => $requestMethod, + 'http_code' => 200, + ]; + + $result = json_encode([ + 'value' => [ + 'sessionId' => 'mock-session-id', + 'capabilities' => [ + 'browserName' => 'chrome', + ], + ], + ]); + + return [$result, $info]; + })); + + ServiceFactory::getInstance()->setService('service.curl', $mockCurlService); + + return new WebDriver('http://localhost:4444/wd/hub'); + } + + /** + * Non-W3C capabilities should be stripped from desiredCapabilities + */ + public function testSessionFiltersNonW3cCapabilities() + { + $driver = $this->createDriverWithMock(); + + $driver->session(Browser::CHROME, [ + Capability::BROWSER_NAME => 'chrome', + 'unknownCapability' => 'value', + 'anotherInvalidCap' => true, + ]); + + $firstMatch = $this->capturedParameters['capabilities']['firstMatch']; + $filtered = $firstMatch[0]; + + $this->assertArrayHasKey(Capability::BROWSER_NAME, $filtered); + $this->assertArrayNotHasKey('unknownCapability', $filtered); + $this->assertArrayNotHasKey('anotherInvalidCap', $filtered); + } + + /** + * All W3C standard capabilities should pass through the filter + */ + public function testSessionPreservesW3cCapabilities() + { + $driver = $this->createDriverWithMock(); + + $capabilities = [ + Capability::BROWSER_NAME => 'firefox', + Capability::BROWSER_VERSION => '120.0', + Capability::PLATFORM_NAME => 'linux', + Capability::ACCEPT_INSECURE_CERTS => true, + Capability::PAGE_LOAD_STRATEGY => 'normal', + Capability::SET_WINDOW_RECT => true, + Capability::STRICT_FILE_INTERACTABILITY => false, + Capability::TIMEOUTS => ['implicit' => 5000], + Capability::UNHANDLED_PROMPT_BEHAVIOR => 'dismiss', + ]; + + $driver->session(Browser::FIREFOX, $capabilities); + + $firstMatch = $this->capturedParameters['capabilities']['firstMatch']; + $filtered = $firstMatch[0]; + + foreach ($capabilities as $key => $value) { + $this->assertArrayHasKey($key, $filtered, "W3C capability '$key' should be preserved"); + $this->assertSame($value, $filtered[$key], "W3C capability '$key' value should be unchanged"); + } + } + + /** + * Vendor extension capabilities (containing ':') should pass through the filter + */ + public function testSessionPreservesExtensionCapabilities() + { + $driver = $this->createDriverWithMock(); + + $chromeOptions = ['args' => ['--headless', '--no-sandbox']]; + $firefoxOptions = ['prefs' => ['dom.webnotifications.enabled' => false]]; + + $driver->session(Browser::CHROME, [ + Capability::BROWSER_NAME => 'chrome', + 'goog:chromeOptions' => $chromeOptions, + 'moz:firefoxOptions' => $firefoxOptions, + 'ms:edgeOptions' => ['args' => ['--start-maximized']], + ]); + + $firstMatch = $this->capturedParameters['capabilities']['firstMatch']; + $filtered = $firstMatch[0]; + + $this->assertArrayHasKey('goog:chromeOptions', $filtered); + $this->assertSame($chromeOptions, $filtered['goog:chromeOptions']); + $this->assertArrayHasKey('moz:firefoxOptions', $filtered); + $this->assertSame($firefoxOptions, $filtered['moz:firefoxOptions']); + $this->assertArrayHasKey('ms:edgeOptions', $filtered); + } + + /** + * Capabilities with falsy values (false, empty string, empty array) must NOT be stripped. + * + * This is critical: a user may explicitly set acceptInsecureCerts => false to disable + * a capability that is enabled by default. The filter must preserve user intent. + */ + public function testSessionPreservesFalsyCapabilityValues() + { + $driver = $this->createDriverWithMock(); + + $driver->session(Browser::CHROME, [ + Capability::ACCEPT_INSECURE_CERTS => false, + Capability::SET_WINDOW_RECT => false, + Capability::STRICT_FILE_INTERACTABILITY => false, + ]); + + $firstMatch = $this->capturedParameters['capabilities']['firstMatch']; + $filtered = $firstMatch[0]; + + $this->assertArrayHasKey(Capability::ACCEPT_INSECURE_CERTS, $filtered, 'acceptInsecureCerts => false must not be stripped'); + $this->assertFalse($filtered[Capability::ACCEPT_INSECURE_CERTS]); + + $this->assertArrayHasKey(Capability::SET_WINDOW_RECT, $filtered, 'setWindowRect => false must not be stripped'); + $this->assertFalse($filtered[Capability::SET_WINDOW_RECT]); + + $this->assertArrayHasKey(Capability::STRICT_FILE_INTERACTABILITY, $filtered, 'strictFileInteractability => false must not be stripped'); + $this->assertFalse($filtered[Capability::STRICT_FILE_INTERACTABILITY]); + } + + /** + * Null desiredCapabilities should not cause errors + */ + public function testSessionWithNullCapabilities() + { + $driver = $this->createDriverWithMock(); + + $session = $driver->session(Browser::CHROME, null); + + $firstMatch = $this->capturedParameters['capabilities']['firstMatch']; + + // Without capabilities, firstMatch should only contain the default chrome entry + $this->assertCount(1, $firstMatch); + $this->assertEquals([Capability::BROWSER_NAME => Browser::CHROME], $firstMatch[0]); + } + + /** + * requiredCapabilities (alwaysMatch) should also be filtered + */ + public function testSessionFiltersRequiredCapabilities() + { + $driver = $this->createDriverWithMock(); + + $driver->session(Browser::CHROME, null, [ + Capability::PLATFORM_NAME => 'linux', + 'unknownCap' => 'value', + 'goog:chromeOptions' => ['args' => ['--headless']], + ]); + + $alwaysMatch = $this->capturedParameters['capabilities']['alwaysMatch']; + + $this->assertArrayHasKey(Capability::PLATFORM_NAME, $alwaysMatch); + $this->assertArrayNotHasKey('unknownCap', $alwaysMatch); + $this->assertArrayHasKey('goog:chromeOptions', $alwaysMatch); + } + + /** + * Legacy JSON Wire Protocol capability names should be remapped to W3C names + */ + public function testSessionRemapsLegacyCapabilities() + { + $driver = $this->createDriverWithMock(); + + $driver->session(Browser::CHROME, [ + Capability::PLATFORM => 'LINUX', + Capability::VERSION => '120.0', + Capability::ACCEPT_SSL_CERTS => true, + ]); + + $firstMatch = $this->capturedParameters['capabilities']['firstMatch']; + $filtered = $firstMatch[0]; + + // Legacy keys should be remapped to W3C equivalents + $this->assertArrayHasKey(Capability::PLATFORM_NAME, $filtered); + $this->assertEquals('LINUX', $filtered[Capability::PLATFORM_NAME]); + + $this->assertArrayHasKey(Capability::BROWSER_VERSION, $filtered); + $this->assertEquals('120.0', $filtered[Capability::BROWSER_VERSION]); + + $this->assertArrayHasKey(Capability::ACCEPT_INSECURE_CERTS, $filtered); + $this->assertTrue($filtered[Capability::ACCEPT_INSECURE_CERTS]); + + // Original legacy keys should not remain + $this->assertArrayNotHasKey(Capability::PLATFORM, $filtered); + $this->assertArrayNotHasKey(Capability::VERSION, $filtered); + $this->assertArrayNotHasKey(Capability::ACCEPT_SSL_CERTS, $filtered); + } + + /** + * Key-value mapping must be preserved (not reindexed numerically) + */ + public function testSessionPreservesKeyValueMapping() + { + $driver = $this->createDriverWithMock(); + + $driver->session(Browser::CHROME, [ + Capability::BROWSER_NAME => 'chrome', + Capability::PLATFORM_NAME => 'linux', + Capability::ACCEPT_INSECURE_CERTS => true, + ]); + + $firstMatch = $this->capturedParameters['capabilities']['firstMatch']; + $filtered = $firstMatch[0]; + + // Keys must be string capability names, not numeric indices + $this->assertIsString(array_key_first($filtered)); + $this->assertSame('chrome', $filtered[Capability::BROWSER_NAME]); + $this->assertSame('linux', $filtered[Capability::PLATFORM_NAME]); + } +}