Skip to content

Commit 9e4b703

Browse files
authored
Merge pull request php-webdriver#1032 from OndraM/feature/reuse-session-capabilities
Improve session reusing, require capabilities to be set
2 parents e6a956c + f998f32 commit 9e4b703

File tree

8 files changed

+123
-14
lines changed

8 files changed

+123
-14
lines changed

.github/workflows/tests.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ jobs:
162162
CHROMEDRIVER_PATH: "${{ (matrix.browser == 'chrome' && !matrix.selenium-server) && '/usr/local/share/chrome_driver/chromedriver' || '' }}"
163163
GECKODRIVER_PATH: "${{ (matrix.browser == 'firefox' && !matrix.selenium-server) && '/usr/local/share/gecko_driver/geckodriver' || '' }}"
164164
DISABLE_W3C_PROTOCOL: "${{ matrix.w3c && '0' || '1' }}"
165+
SELENIUM_SERVER: "${{ matrix.selenium-server && '1' || '0' }}"
165166
run: |
166167
if [ "$BROWSER_NAME" = "chrome" ]; then EXCLUDE_GROUP+="exclude-chrome,"; fi
167168
if [ "$BROWSER_NAME" = "firefox" ]; then EXCLUDE_GROUP+="exclude-firefox,"; fi

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/).
77

88
### Changed
99
- Require PHP ^7.3.
10+
- Capabilities must be either explicitly provided or retrievable from Selenium Grid when resuing session with `createBySessionID()`.
1011
- Throw `UnexpectedResponseException` instead of `UnknownErrorException` in `findElement()` and `findElements()` methods.
1112
- Throw custom php-webdriver exceptions instead of native PHP SPL exceptions.
12-
- Do not mix internal non-W3C WebDriver exceptions, separate them into own namespace.
13+
- Do not mix internal non-W3C WebDriver exceptions, separate them into own namespaces.
1314

1415
## 1.13.0 - 2022-10-03
1516
### Added

lib/Exception/Internal/UnexpectedResponseException.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,16 @@ public static function forJsonDecodingError(int $jsonLastError, string $rawResul
2626
)
2727
);
2828
}
29+
30+
public static function forCapabilitiesRetrievalError(\Exception $previousException): self
31+
{
32+
return new self(
33+
sprintf(
34+
'Existing Capabilities were not provided, and they also cannot be read from Selenium Grid'
35+
. ' (error: "%s"). You are probably not using Selenium Grid, so to reuse the previous session,'
36+
. ' Capabilities must be explicitly provided to createBySessionID() method.',
37+
$previousException->getMessage()
38+
)
39+
);
40+
}
2941
}

lib/Remote/RemoteWebDriver.php

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,19 @@ class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInpu
5656
/**
5757
* @param HttpCommandExecutor $commandExecutor
5858
* @param string $sessionId
59-
* @param WebDriverCapabilities|null $capabilities
59+
* @param WebDriverCapabilities $capabilities
6060
* @param bool $isW3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec
6161
*/
6262
protected function __construct(
6363
HttpCommandExecutor $commandExecutor,
6464
$sessionId,
65-
WebDriverCapabilities $capabilities = null,
65+
WebDriverCapabilities $capabilities,
6666
$isW3cCompliant = false
6767
) {
6868
$this->executor = $commandExecutor;
6969
$this->sessionID = $sessionId;
7070
$this->isW3cCompliant = $isW3cCompliant;
71-
72-
if ($capabilities !== null) {
73-
$this->capabilities = $capabilities;
74-
}
71+
$this->capabilities = $capabilities;
7572
}
7673

7774
/**
@@ -139,14 +136,22 @@ public static function create(
139136
/**
140137
* [Experimental] Construct the RemoteWebDriver by an existing session.
141138
*
142-
* This constructor can boost the performance a lot by reusing the same browser for the whole test suite.
143-
* You cannot pass the desired capabilities because the session was created before.
139+
* This constructor can boost the performance by reusing the same browser for the whole test suite. On the other
140+
* hand, because the browser is not pristine, this may lead to flaky and dependent tests. So carefully
141+
* consider the tradeoffs.
142+
*
143+
* To create the instance, we need to know Capabilities of the previously created session. You can either
144+
* pass them in $existingCapabilities parameter, or we will attempt to receive them from the Selenium Grid server.
145+
* However, if Capabilities were not provided and the attempt to get them was not successful,
146+
* exception will be thrown.
144147
*
145148
* @param string $session_id The existing session id
146149
* @param string $selenium_server_url The url of the remote Selenium WebDriver server
147150
* @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server
148151
* @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server
149152
* @param bool $isW3cCompliant True to use W3C WebDriver (default), false to use the legacy JsonWire protocol
153+
* @param WebDriverCapabilities|null $existingCapabilities Provide capabilities of the existing previously created
154+
* session. If not provided, we will attempt to read them, but this will only work when using Selenium Grid.
150155
* @return static
151156
*/
152157
public static function createBySessionID(
@@ -157,6 +162,7 @@ public static function createBySessionID(
157162
) {
158163
// BC layer to not break the method signature
159164
$isW3cCompliant = func_num_args() > 4 ? func_get_arg(4) : true;
165+
$existingCapabilities = func_num_args() > 5 ? func_get_arg(5) : null;
160166

161167
$executor = new HttpCommandExecutor($selenium_server_url, null, null);
162168
if ($connection_timeout_in_ms !== null) {
@@ -170,7 +176,12 @@ public static function createBySessionID(
170176
$executor->disableW3cCompliance();
171177
}
172178

173-
return new static($executor, $session_id, null, $isW3cCompliant);
179+
// if capabilities were not provided, attempt to read them from the Selenium Grid API
180+
if ($existingCapabilities === null) {
181+
$existingCapabilities = self::readExistingCapabilitiesFromSeleniumGrid($session_id, $executor);
182+
}
183+
184+
return new static($executor, $session_id, $existingCapabilities, $isW3cCompliant);
174185
}
175186

176187
/**
@@ -727,4 +738,26 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities
727738

728739
return $desired_capabilities;
729740
}
741+
742+
protected static function readExistingCapabilitiesFromSeleniumGrid(
743+
string $session_id,
744+
HttpCommandExecutor $executor
745+
): DesiredCapabilities {
746+
$getCapabilitiesCommand = new CustomWebDriverCommand($session_id, '/se/grid/session/:sessionId', 'GET', []);
747+
748+
try {
749+
$capabilitiesResponse = $executor->execute($getCapabilitiesCommand);
750+
751+
$existingCapabilities = DesiredCapabilities::createFromW3cCapabilities(
752+
$capabilitiesResponse->getValue()['capabilities']
753+
);
754+
if ($existingCapabilities === null) {
755+
throw UnexpectedResponseException::forError('Empty capabilities received');
756+
}
757+
} catch (\Exception $e) {
758+
throw UnexpectedResponseException::forCapabilitiesRetrievalError($e);
759+
}
760+
761+
return $existingCapabilities;
762+
}
730763
}

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ parameters:
2323
# Parameter is intentionally not part of signature to not break BC
2424
- message: '#PHPDoc tag \@param references unknown parameter: \$isW3cCompliant#'
2525
path: 'lib/Remote/RemoteWebDriver.php'
26+
- message: '#PHPDoc tag \@param references unknown parameter: \$existingCapabilities#'
27+
path: 'lib/Remote/RemoteWebDriver.php'
2628

2729
inferPrivatePropertyTypeFromConstructor: true

tests/functional/RemoteWebDriverCreateTest.php

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace Facebook\WebDriver;
44

5+
use Facebook\WebDriver\Exception\Internal\UnexpectedResponseException;
56
use Facebook\WebDriver\Remote\DesiredCapabilities;
67
use Facebook\WebDriver\Remote\HttpCommandExecutor;
78
use Facebook\WebDriver\Remote\RemoteWebDriver;
89
use Facebook\WebDriver\Remote\WebDriverBrowserType;
910

1011
/**
12+
* @covers \Facebook\WebDriver\Exception\Internal\UnexpectedResponseException
1113
* @covers \Facebook\WebDriver\Remote\HttpCommandExecutor
1214
* @covers \Facebook\WebDriver\Remote\RemoteWebDriver
1315
*/
@@ -111,12 +113,41 @@ public function testShouldCreateInstanceFromExistingSessionId()
111113
$originalDriver->get($this->getTestPageUrl(TestPage::INDEX));
112114
$this->compatAssertStringContainsString('/index.html', $originalDriver->getCurrentURL());
113115

114-
// Store session ID
116+
// Store session attributes
115117
$sessionId = $originalDriver->getSessionID();
116118
$isW3cCompliant = $originalDriver->isW3cCompliant();
119+
$originalCapabilities = $originalDriver->getCapabilities();
120+
121+
$capabilitiesForSessionReuse = $originalCapabilities;
122+
if ($this->isSeleniumServerUsed()) {
123+
// do not provide capabilities when selenium server is used, to test they are read from selenium server
124+
$capabilitiesForSessionReuse = null;
125+
}
117126

118127
// Create new RemoteWebDriver instance based on the session ID
119-
$this->driver = RemoteWebDriver::createBySessionID($sessionId, $this->serverUrl, null, null, $isW3cCompliant);
128+
$this->driver = RemoteWebDriver::createBySessionID(
129+
$sessionId,
130+
$this->serverUrl,
131+
null,
132+
null,
133+
$isW3cCompliant,
134+
$capabilitiesForSessionReuse
135+
);
136+
137+
// Capabilities should be retrieved and be set to the driver instance
138+
$returnedCapabilities = $this->driver->getCapabilities();
139+
$this->assertInstanceOf(WebDriverCapabilities::class, $returnedCapabilities);
140+
141+
$expectedBrowserName = $this->desiredCapabilities->getBrowserName();
142+
143+
if ($this->isSauceLabsBuild() && $expectedBrowserName === 'MicrosoftEdge') {
144+
$expectedBrowserName = 'msedge'; // SauceLabs for some reason reports MicrosoftEdge as msedge
145+
}
146+
$this->assertEqualsIgnoringCase(
147+
$expectedBrowserName,
148+
$returnedCapabilities->getBrowserName()
149+
);
150+
$this->assertEqualsCanonicalizing($originalCapabilities, $this->driver->getCapabilities());
120151

121152
// Check we reused the previous instance (window) and it has the same URL
122153
$this->compatAssertStringContainsString('/index.html', $this->driver->getCurrentURL());
@@ -125,6 +156,22 @@ public function testShouldCreateInstanceFromExistingSessionId()
125156
$this->assertNotEmpty($this->driver->findElement(WebDriverBy::id('id_test'))->getText());
126157
}
127158

159+
public function testShouldRequireCapabilitiesToBeSetToReuseExistingSession()
160+
{
161+
$this->expectException(UnexpectedResponseException::class);
162+
$this->expectExceptionMessage(
163+
'Existing Capabilities were not provided, and they also cannot be read from Selenium Grid'
164+
);
165+
166+
// Do not provide capabilities, they also cannot be retrieved from the Selenium Grid
167+
RemoteWebDriver::createBySessionID(
168+
'sessionId',
169+
'http://localhost:332', // nothing should be running there
170+
null,
171+
null
172+
);
173+
}
174+
128175
protected function createWebDriver()
129176
{
130177
}

tests/functional/WebDriverTestCase.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ public static function isW3cProtocolBuild()
110110
return getenv('DISABLE_W3C_PROTOCOL') !== '1';
111111
}
112112

113+
public static function isSeleniumServerUsed(): bool
114+
{
115+
return getenv('SELENIUM_SERVER') === '1';
116+
}
117+
113118
public static function skipForW3cProtocol($message = 'Not supported by W3C specification')
114119
{
115120
if (static::isW3cProtocolBuild()) {

tests/unit/Remote/RemoteWebDriverTest.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use PHPUnit\Framework\TestCase;
1212

1313
/**
14-
* Unit part of RemoteWebDriver tests. Ie. tests for behavior which do not interact with the real remote server.
14+
* Unit part of RemoteWebDriver tests. I.e. tests for behavior which do not interact with the real remote server.
1515
*
1616
* @coversDefaultClass \Facebook\WebDriver\Remote\RemoteWebDriver
1717
*/
@@ -22,7 +22,15 @@ class RemoteWebDriverTest extends TestCase
2222

2323
protected function setUp(): void
2424
{
25-
$this->driver = RemoteWebDriver::createBySessionID('session-id', 'http://foo.bar:4444');
25+
// `createBySessionID()` is used because it is the simplest way to instantiate real RemoteWebDriver
26+
$this->driver = RemoteWebDriver::createBySessionID(
27+
'session-id',
28+
'http://foo.bar:4444',
29+
null,
30+
null,
31+
true,
32+
new DesiredCapabilities([])
33+
);
2634
}
2735

2836
/**

0 commit comments

Comments
 (0)