Skip to content

Commit 08662cf

Browse files
committed
getScreenshot method implementation (W3C client), with a saveScreenshot sugar (webdriver scope)
1 parent 6c20460 commit 08662cf

5 files changed

Lines changed: 161 additions & 44 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"react/http": "^1.1",
2727
"react/promise": "^2.8",
2828
"react/promise-timer": "^1.6",
29+
"react/stream": "^1.1",
2930
"symfony/options-resolver": "^5.1"
3031
},
3132
"require-dev": {

src/Client/W3CClient.php

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,8 @@ public function getTabIdentifiers(string $sessionIdentifier): PromiseInterface
212212
$responsePromise->then(
213213
function (ResponseInterface $response) use ($tabLookupDeferred) {
214214
try {
215-
$responseBody = (string) $response->getBody();
216-
$bodyDeserialized = json_decode($responseBody, true);
215+
$tabIdentifiers = $this->deserializeResponse($response);
217216

218-
if (!array_key_exists('value', $bodyDeserialized) || !is_array($bodyDeserialized['value'])) {
219-
// todo: locate an error message or set it as "undefined error"
220-
throw new RuntimeException('Unable to locate tab identifiers in the response.');
221-
}
222-
223-
$tabIdentifiers = $bodyDeserialized['value'];
224217
$tabLookupDeferred->resolve($tabIdentifiers);
225218
} catch (Throwable $exception) {
226219
$reason = new RuntimeException(
@@ -295,7 +288,7 @@ function (ResponseInterface $response) {
295288
->then(
296289
null,
297290
function (Throwable $rejectionReason) {
298-
throw new RuntimeException('Unable to open an URI (request).', 0, $rejectionReason);
291+
throw new RuntimeException('Unable to open an URI.', 0, $rejectionReason);
299292
}
300293
)
301294
;
@@ -366,7 +359,7 @@ function (ResponseInterface $response) {
366359
->then(
367360
null,
368361
function (Throwable $rejectionReason) {
369-
throw new RuntimeException('Unable to get an element identifier (request).', 0, $rejectionReason);
362+
throw new RuntimeException('Unable to get an element identifier.', 0, $rejectionReason);
370363
}
371364
)
372365
;
@@ -456,7 +449,7 @@ function (ResponseInterface $response) {
456449
->then(
457450
null,
458451
function (Throwable $rejectionReason) {
459-
throw new RuntimeException('Unable to confirm mouse move action (request).', 0, $rejectionReason);
452+
throw new RuntimeException('Unable to confirm mouse move action.', 0, $rejectionReason);
460453
}
461454
)
462455
;
@@ -493,7 +486,7 @@ function (ResponseInterface $response) {
493486
->then(
494487
null,
495488
function (Throwable $rejectionReason) {
496-
throw new RuntimeException('Unable to confirm mouse click action (request).', 0, $rejectionReason);
489+
throw new RuntimeException('Unable to confirm mouse click action.', 0, $rejectionReason);
497490
}
498491
)
499492
;
@@ -506,9 +499,37 @@ function (Throwable $rejectionReason) {
506499
*/
507500
public function getScreenshot(string $sessionIdentifier): PromiseInterface
508501
{
509-
// TODO: Implement getScreenshot() method.
502+
$requestUri = sprintf(
503+
'http://%s:%d/wd/hub/session/%s/screenshot',
504+
$this->_options['server']['host'],
505+
$this->_options['server']['port'],
506+
$sessionIdentifier
507+
);
510508

511-
return reject(new RuntimeException('Not implemented.'));
509+
$requestHeaders = [
510+
'Content-Type' => 'application/json; charset=UTF-8',
511+
];
512+
513+
$responsePromise = $this->httpClient->get($requestUri, $requestHeaders);
514+
515+
$imageContentsPromise = $responsePromise
516+
->then(
517+
function (ResponseInterface $response) {
518+
$imageContentsEncoded = $this->deserializeResponse($response);
519+
$imageContents = base64_decode($imageContentsEncoded);
520+
521+
return $imageContents;
522+
}
523+
)
524+
->then(
525+
null,
526+
function (Throwable $rejectionReason) {
527+
throw new RuntimeException('Unable to get a screenshot.', 0, $rejectionReason);
528+
}
529+
)
530+
;
531+
532+
return $imageContentsPromise;
512533
}
513534

514535
/**
@@ -556,23 +577,43 @@ private function requestMouseActions(string $sessionIdentifier, array $mouseActi
556577
/**
557578
* Ensures that a related action is properly executed (confirmed) by the remote server, triggers an error otherwise.
558579
*
559-
* Is used when no specific context to check is required (some methods will use more advanced confirmation checks
560-
* instead this "default").
580+
* It is used when no specific context is required to confirm successful command execution (some methods will use
581+
* more advanced confirmation checks instead of this "default").
561582
*
562583
* @param ResponseInterface $response PSR-7 response message from the Selenium hub with action results
563584
* @param string $errorMessage Will be used if an error is registered during confirmation check
564585
*
565586
* @return void
566587
*
567-
* @throws RuntimeException Whenever an error has occurred during confirmation check for a remote action
588+
* @throws RuntimeException Whenever an error has been occurred during confirmation check for a remote action
568589
*/
569590
private function onCommandConfirmation(ResponseInterface $response, string $errorMessage): void
570591
{
571-
$responseBody = (string) $response->getBody();
592+
$responseValueNode = $this->deserializeResponse($response);
572593

573594
// todo: locate an error message or set it as "undefined error"
574-
if ('{"value":null}' !== $responseBody) {
595+
if (null !== $responseValueNode) {
575596
throw new RuntimeException($errorMessage);
576597
}
577598
}
599+
600+
/**
601+
* Returns a "value" node contents, which will be extracted from the PSR-7 response message
602+
*
603+
* @param ResponseInterface $response PSR-7 response message from the Selenium hub with action results
604+
*
605+
* @return mixed
606+
*/
607+
private function deserializeResponse(ResponseInterface $response)
608+
{
609+
$responseBody = (string) $response->getBody();
610+
$bodyDeserialized = json_decode($responseBody, true);
611+
612+
if (!array_key_exists('value', $bodyDeserialized)) {
613+
// todo: locate an error message or set it as "undefined error"
614+
throw new RuntimeException('Unable to locate "value" node (response deserialization).');
615+
}
616+
617+
return $bodyDeserialized['value'];
618+
}
578619
}

src/ClientInterface.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public function setActiveTab(string $sessionIdentifier, string $tabIdentifier):
110110

111111
/**
112112
* Returns a promise that will be resolved when the remote WebDriver service confirms URI navigation within
113-
* currently active (focused) tab ({@link getActiveTabIdentifier()})
113+
* currently active (focused) tab ({@link getActiveTabIdentifier()}).
114114
*
115115
* @param string $sessionIdentifier Session identifier for Selenium Grid server (hub)
116116
* @param string $uri Website URL or any other resource identifier, to open in the active (focused)
@@ -193,8 +193,10 @@ public function getElementVisibility(string $sessionIdentifier, array $elementId
193193
* Returns a promise that will be resolved when the remote WebDriver service confirms element click operation
194194
* against active browser tab.
195195
*
196-
* Note: if target element is a link, it may trigger an implicit tab(s) creation. In such case you need to take into
197-
* consideration potentially new list of window handles for the current session (see {@link getTabIdentifiers()}).
196+
* Note: if target element is a link, it may trigger an implicit tab(s) creation. In such case you need to take
197+
* into consideration potentially new list of window handles for the current session (see {@link
198+
* getTabIdentifiers()}). The remote webdriver service WILL NOT automatically perform tab switch operation if
199+
* a command implicitly triggers new tab.
198200
*
199201
* @param string $sessionIdentifier Session identifier for Selenium Grid server (hub)
200202
* @param array $elementIdentifier An internal WebDriver handle that refers to the element on the page

src/SeleniumHubDriver.php

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717

1818
use Itnelo\React\WebDriver\Timeout\Interceptor as TimeoutInterceptor;
1919
use React\EventLoop\LoopInterface;
20+
use React\Promise\Deferred;
2021
use React\Promise\PromiseInterface;
22+
use React\Stream\WritableResourceStream;
2123
use RuntimeException;
2224
use Symfony\Component\OptionsResolver\Exception\ExceptionInterface as ConfigurationExceptionInterface;
2325
use Symfony\Component\OptionsResolver\OptionsResolver;
26+
use Throwable;
2427
use function React\Promise\reject;
2528
use function React\Promise\Timer\resolve;
2629

@@ -82,26 +85,6 @@ public function __construct(
8285
$this->timeoutInterceptor = $timeoutInterceptor;
8386
}
8487

85-
/**
86-
* {@inheritDoc}
87-
*/
88-
public function wait(float $time): PromiseInterface
89-
{
90-
$idlePromise = resolve($time, $this->loop);
91-
92-
return $idlePromise;
93-
}
94-
95-
/**
96-
* {@inheritDoc}
97-
*/
98-
public function waitUntil(float $time, callable $conditionMetCallback): PromiseInterface
99-
{
100-
// TODO: Implement waitUntil() method.
101-
102-
return reject(new RuntimeException('Not implemented.'));
103-
}
104-
10588
/**
10689
* {@inheritDoc}
10790
*/
@@ -294,10 +277,89 @@ public function mouseLeftClick(string $sessionIdentifier): PromiseInterface
294277
/**
295278
* {@inheritDoc}
296279
*/
297-
public function getScreenshot(string $sessionIdentifier): PromiseInterface
280+
public function wait(float $time): PromiseInterface
281+
{
282+
$idlePromise = resolve($time, $this->loop);
283+
284+
return $idlePromise;
285+
}
286+
287+
/**
288+
* {@inheritDoc}
289+
*/
290+
public function waitUntil(float $time, callable $conditionMetCallback): PromiseInterface
298291
{
299-
// TODO: Implement getScreenshot() method.
292+
// TODO: Implement waitUntil() method.
300293

301294
return reject(new RuntimeException('Not implemented.'));
302295
}
296+
297+
/**
298+
* {@inheritDoc}
299+
*/
300+
public function getScreenshot(string $sessionIdentifier): PromiseInterface
301+
{
302+
$screenshotPromise = $this->hubClient->getScreenshot($sessionIdentifier);
303+
304+
return $this->timeoutInterceptor->applyTimeout(
305+
$screenshotPromise,
306+
'Unable to complete a get screenshot command.'
307+
);
308+
}
309+
310+
/**
311+
* {@inheritDoc}
312+
*/
313+
public function saveScreenshot(string $sessionIdentifier, string $filePath): PromiseInterface
314+
{
315+
$savingDeferred = new Deferred();
316+
317+
$screenshotPromise = $this->hubClient->getScreenshot($sessionIdentifier);
318+
319+
$screenshotPromise
320+
->then(
321+
function (string $imageContents) use ($savingDeferred, $filePath) {
322+
$fileResource = fopen($filePath, 'w');
323+
$writeStream = new WritableResourceStream($fileResource, $this->loop);
324+
325+
$writeStream->end($imageContents);
326+
327+
$writeStream->on(
328+
'drain',
329+
function () use ($savingDeferred, $writeStream) {
330+
// explicitly removing all listeners, because we don't have a contract with guarantees that
331+
// this handler will be triggered once, see https://github.com/reactphp/stream#drain-event.
332+
$writeStream->removeAllListeners('drain');
333+
334+
$savingDeferred->resolve(null);
335+
}
336+
);
337+
338+
$writeStream->on(
339+
'error',
340+
function (Throwable $exception) use ($savingDeferred) {
341+
$reason = new RuntimeException('Unable to save a screenshot (stream).', 0, $exception);
342+
343+
$savingDeferred->reject($reason);
344+
}
345+
);
346+
}
347+
)
348+
->then(
349+
null,
350+
function (Throwable $rejectionReason) use ($savingDeferred) {
351+
$reason = new RuntimeException('Unable to save a screenshot.', 0, $rejectionReason);
352+
353+
$savingDeferred->reject($reason);
354+
}
355+
)
356+
;
357+
358+
$saveConfirmationPromise = $savingDeferred->promise();
359+
360+
return $this->timeoutInterceptor->applyTimeout(
361+
$saveConfirmationPromise,
362+
'Unable to complete a save screenshot command.'
363+
);
364+
}
303365
}

src/WebDriverInterface.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,15 @@ public function wait(float $time): PromiseInterface;
109109
* @return PromiseInterface<null>
110110
*/
111111
public function waitUntil(float $time, callable $conditionMetCallback): PromiseInterface;
112+
113+
/**
114+
* Returns a promise that will be resolved if a screenshot is successfully received and saved using the specified
115+
* {filePath}, rejection reason with error message will be provided otherwise.
116+
*
117+
* @param string $sessionIdentifier Session identifier for Selenium Grid server (hub)
118+
* @param string $filePath Path where a screenshot image will be saved
119+
*
120+
* @return PromiseInterface<null>
121+
*/
122+
public function saveScreenshot(string $sessionIdentifier, string $filePath): PromiseInterface;
112123
}

0 commit comments

Comments
 (0)