From df56311f1f4ce966a0210d74276e8c5e99ffde1c Mon Sep 17 00:00:00 2001 From: Seriyyy95 Date: Tue, 23 Apr 2024 21:19:27 +0300 Subject: [PATCH 1/4] fix could not find node with given id --- .gitignore | 2 + composer.json | 4 +- src/Communication/Connection.php | 9 ++++ src/Communication/Socket/MockSocket.php | 7 +++ src/Communication/Socket/SocketInterface.php | 2 + src/Communication/Socket/Wrench.php | 5 ++ src/Dom/Dom.php | 26 ++++++++-- src/Dom/Node.php | 53 ++++++++++++++++---- src/Exception/StaleElementException.php | 8 +++ src/Page.php | 13 ++++- tests/DomTest.php | 53 ++++++++++++++++++++ tests/resources/static-web/domForm.html | 2 + 12 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 src/Exception/StaleElementException.php diff --git a/.gitignore b/.gitignore index 04626b2f..8be5d942 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ composer.lock phpstan.neon phpunit.xml vendor +.idea + diff --git a/composer.json b/composer.json index c1df8f2f..587c81bd 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^7.4.15 || ^8.0.2", - "chrome-php/wrench": "^1.6", + "chrome-php/wrench": "^1.7", "evenement/evenement": "^3.0.1", "monolog/monolog": "^1.27.1 || ^2.8 || ^3.2", "psr/log": "^1.1 || ^2.0 || ^3.0", @@ -46,6 +46,8 @@ }, "preferred-install": "dist" }, + "minimum-stability": "dev", + "prefer-stable": true, "extra": { "bamarni-bin": { "bin-links": true, diff --git a/src/Communication/Connection.php b/src/Communication/Connection.php index 0df55167..c60467f4 100644 --- a/src/Communication/Connection.php +++ b/src/Communication/Connection.php @@ -346,6 +346,15 @@ public function readLine() return false; } + public function processAllEvents(): void + { + $hasData = $this->wsClient->waitForData(0); + + if ($hasData) { + $this->receiveData(); + } + } + /** * Dispatches the message and either stores the response or emits an event. * diff --git a/src/Communication/Socket/MockSocket.php b/src/Communication/Socket/MockSocket.php index 66a450b5..0c9da21b 100644 --- a/src/Communication/Socket/MockSocket.php +++ b/src/Communication/Socket/MockSocket.php @@ -124,4 +124,11 @@ public function disconnect($reason = 1000) return true; } + + public function waitForData(float $maxSeconds): bool + { + // TODO: Implement wait if needed. + + return !empty($this->receivedData); + } } diff --git a/src/Communication/Socket/SocketInterface.php b/src/Communication/Socket/SocketInterface.php index d151617c..8900aeb2 100644 --- a/src/Communication/Socket/SocketInterface.php +++ b/src/Communication/Socket/SocketInterface.php @@ -52,4 +52,6 @@ public function isConnected(); * @return bool */ public function disconnect($reason = 1000); + + public function waitForData(float $maxSeconds): bool; } diff --git a/src/Communication/Socket/Wrench.php b/src/Communication/Socket/Wrench.php index fa3ffa38..659075ed 100644 --- a/src/Communication/Socket/Wrench.php +++ b/src/Communication/Socket/Wrench.php @@ -137,4 +137,9 @@ public function disconnect($reason = 1000) return $disconnected; } + + public function waitForData(float $maxSeconds): bool + { + return $this->client->waitForData($maxSeconds); + } } diff --git a/src/Dom/Dom.php b/src/Dom/Dom.php index c685f4df..96f491a7 100644 --- a/src/Dom/Dom.php +++ b/src/Dom/Dom.php @@ -11,10 +11,7 @@ class Dom extends Node { public function __construct(Page $page) { - $message = new Message('DOM.getDocument'); - $response = $page->getSession()->sendMessageSync($message); - - $rootNodeId = $response->getResultData('root')['nodeId']; + $rootNodeId = $this->getRootNodeId($page); parent::__construct($page, $rootNodeId); } @@ -24,6 +21,8 @@ public function __construct(Page $page) */ public function search(string $selector): array { + $this->prepareForRequest(); + $message = new Message('DOM.performSearch', [ 'query' => $selector, ]); @@ -55,4 +54,23 @@ public function search(string $selector): array return $nodes; } + + public function prepareForRequest(bool $throw = true) + { + $this->page->assertNotClosed(); + + $this->page->getSession()->getConnection()->processAllEvents(); + + if ($this->isStale) { + $this->nodeId = $this->getRootNodeId($this->page); + } + } + + public function getRootNodeId(Page $page) + { + $message = new Message('DOM.getDocument'); + $response = $page->getSession()->sendMessageSync($message); + + return $response->getResultData('root')['nodeId']; + } } diff --git a/src/Dom/Node.php b/src/Dom/Node.php index bf8df5c5..aa8044b5 100644 --- a/src/Dom/Node.php +++ b/src/Dom/Node.php @@ -7,6 +7,7 @@ use HeadlessChromium\Communication\Message; use HeadlessChromium\Communication\Response; use HeadlessChromium\Exception\DomException; +use HeadlessChromium\Exception\StaleElementException; use HeadlessChromium\Page; class Node @@ -21,16 +22,37 @@ class Node */ protected $nodeId; + /** + * @var bool + */ + protected bool $isStale = false; + public function __construct(Page $page, int $nodeId) { $this->page = $page; $this->nodeId = $nodeId; + + $page->getSession()->on('method:DOM.documentUpdated', function (...$event) { + $this->isStale = true; + }); + } + + public function getNodeId(): int + { + return $this->nodeId; + } + + public function getNodeIdForRequest(): int + { + $this->prepareForRequest(); + + return $this->getNodeId(); } public function getAttributes(): NodeAttributes { $message = new Message('DOM.getAttributes', [ - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), ]); $response = $this->page->getSession()->sendMessageSync($message); @@ -44,7 +66,7 @@ public function getAttributes(): NodeAttributes public function setAttributeValue(string $name, string $value): void { $message = new Message('DOM.setAttributeValue', [ - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), 'name' => $name, 'value' => $value, ]); @@ -56,7 +78,7 @@ public function setAttributeValue(string $name, string $value): void public function querySelector(string $selector): ?self { $message = new Message('DOM.querySelector', [ - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), 'selector' => $selector, ]); $response = $this->page->getSession()->sendMessageSync($message); @@ -74,7 +96,7 @@ public function querySelector(string $selector): ?self public function querySelectorAll(string $selector): array { $message = new Message('DOM.querySelectorAll', [ - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), 'selector' => $selector, ]); $response = $this->page->getSession()->sendMessageSync($message); @@ -93,7 +115,7 @@ public function querySelectorAll(string $selector): array public function focus(): void { $message = new Message('DOM.focus', [ - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), ]); $response = $this->page->getSession()->sendMessageSync($message); @@ -108,7 +130,7 @@ public function getAttribute(string $name): ?string public function getPosition(): ?NodePosition { $message = new Message('DOM.getBoxModel', [ - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), ]); $response = $this->page->getSession()->sendMessageSync($message); @@ -131,7 +153,7 @@ public function hasPosition(): bool public function getHTML(): string { $message = new Message('DOM.getOuterHTML', [ - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), ]); $response = $this->page->getSession()->sendMessageSync($message); @@ -143,7 +165,7 @@ public function getHTML(): string public function setHTML(string $outerHTML): void { $message = new Message('DOM.setOuterHTML', [ - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), 'outerHTML' => $outerHTML, ]); $response = $this->page->getSession()->sendMessageSync($message); @@ -159,7 +181,7 @@ public function getText(): string public function scrollIntoView(): void { $message = new Message('DOM.scrollIntoViewIfNeeded', [ - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), ]); $response = $this->page->getSession()->sendMessageSync($message); @@ -198,7 +220,7 @@ public function sendFiles(array $filePaths): void { $message = new Message('DOM.setFileInputFiles', [ 'files' => $filePaths, - 'nodeId' => $this->nodeId, + 'nodeId' => $this->getNodeIdForRequest(), ]); $response = $this->page->getSession()->sendMessageSync($message); @@ -214,4 +236,15 @@ public function assertNotError(Response $response): void throw new DOMException($response->getErrorMessage()); } } + + protected function prepareForRequest() + { + $this->page->assertNotClosed(); + + $this->page->getSession()->getConnection()->processAllEvents(); + + if ($this->isStale) { + throw new StaleElementException(); + } + } } diff --git a/src/Exception/StaleElementException.php b/src/Exception/StaleElementException.php new file mode 100644 index 00000000..f3bb3477 --- /dev/null +++ b/src/Exception/StaleElementException.php @@ -0,0 +1,8 @@ +assertNotClosed(); + + if ($this->dom === null) { + $this->dom = new Dom($this); + } + + return $this->dom; } /** diff --git a/tests/DomTest.php b/tests/DomTest.php index 8204e54b..3a5a5fdd 100644 --- a/tests/DomTest.php +++ b/tests/DomTest.php @@ -4,6 +4,7 @@ use HeadlessChromium\Browser; use HeadlessChromium\BrowserFactory; +use HeadlessChromium\Exception\StaleElementException; /** * @covers \HeadlessChromium\Dom\Dom @@ -187,4 +188,56 @@ public function testSetHTML(): void self::assertEquals('hello', $value); } + + public function testDomDoesReturnsTheSameObject(): void + { + $page = $this->openSitePage('domForm.html'); + + $firstDom = $page->dom(); + + $element = $firstDom->querySelector('#myinput'); + + $secondDom = $page->dom(); + + $element->focus(); + + $this->assertEquals($firstDom, $secondDom); + } + + public function testRootNodeIdIsUpdatedAfterReload(): void + { + $page = $this->openSitePage('domForm.html'); + + $dom = $page->dom(); + + $nodeId = $dom->getNodeId(); + + $reloadBtn = $dom->querySelector('#reload-btn'); + $reloadBtn->click(); + + $page->waitForReload(); + + $reloadBtn = $dom->querySelector('#reload-btn'); + $this->assertNotNull($reloadBtn); + + $this->assertNotEquals($nodeId, $page->dom()->getNodeId()); + } + + public function testRegularNodeIsMarkedAsStaleAfterReload(): void + { + $page = $this->openSitePage('domForm.html'); + + $dom = $page->dom(); + + $inputNode = $dom->querySelector('#myinput'); + + $reloadBtn = $dom->querySelector('#reload-btn'); + $reloadBtn->click(); + + $page->waitForReload(); + + $this->expectException(StaleElementException::class); + + $inputNode->sendKeys('test'); + } } diff --git a/tests/resources/static-web/domForm.html b/tests/resources/static-web/domForm.html index 7eff87f1..5590519a 100644 --- a/tests/resources/static-web/domForm.html +++ b/tests/resources/static-web/domForm.html @@ -17,5 +17,7 @@

Form

bar
+ + From c77bf8cfce7d2c29d3f3995c874559ece2365f89 Mon Sep 17 00:00:00 2001 From: Seriyyy95 Date: Tue, 23 Apr 2024 21:44:58 +0300 Subject: [PATCH 2/4] style-ci --- src/Dom/Dom.php | 2 +- src/Dom/Node.php | 6 +++--- src/Exception/StaleElementException.php | 1 - src/Page.php | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Dom/Dom.php b/src/Dom/Dom.php index 96f491a7..ac73759a 100644 --- a/src/Dom/Dom.php +++ b/src/Dom/Dom.php @@ -55,7 +55,7 @@ public function search(string $selector): array return $nodes; } - public function prepareForRequest(bool $throw = true) + public function prepareForRequest(bool $throw = true): void { $this->page->assertNotClosed(); diff --git a/src/Dom/Node.php b/src/Dom/Node.php index aa8044b5..c55d5ed5 100644 --- a/src/Dom/Node.php +++ b/src/Dom/Node.php @@ -32,14 +32,14 @@ public function __construct(Page $page, int $nodeId) $this->page = $page; $this->nodeId = $nodeId; - $page->getSession()->on('method:DOM.documentUpdated', function (...$event) { + $page->getSession()->on('method:DOM.documentUpdated', function (...$event): void { $this->isStale = true; }); } public function getNodeId(): int { - return $this->nodeId; + return $this->nodeId; } public function getNodeIdForRequest(): int @@ -237,7 +237,7 @@ public function assertNotError(Response $response): void } } - protected function prepareForRequest() + protected function prepareForRequest(): void { $this->page->assertNotClosed(); diff --git a/src/Exception/StaleElementException.php b/src/Exception/StaleElementException.php index f3bb3477..0fb744cf 100644 --- a/src/Exception/StaleElementException.php +++ b/src/Exception/StaleElementException.php @@ -4,5 +4,4 @@ class StaleElementException extends DomException { - } diff --git a/src/Page.php b/src/Page.php index b664d125..10fd2d98 100644 --- a/src/Page.php +++ b/src/Page.php @@ -814,7 +814,7 @@ public function dom(): Dom { $this->assertNotClosed(); - if ($this->dom === null) { + if (null === $this->dom) { $this->dom = new Dom($this); } From 9c20a931c94b1172a57ecbab6d5503fd9efa655c Mon Sep 17 00:00:00 2001 From: Seriyyy95 Date: Thu, 25 Apr 2024 21:12:49 +0300 Subject: [PATCH 3/4] fix: code style --- .gitignore | 1 - composer.json | 2 -- src/Communication/Connection.php | 6 ++++++ src/Communication/Socket/MockSocket.php | 7 ------- src/Communication/Socket/SocketInterface.php | 2 -- src/Communication/Socket/WaitForDataInterface.php | 8 ++++++++ src/Communication/Socket/Wrench.php | 2 +- .../CommunicationException/CantSyncEventsException.php | 10 ++++++++++ 8 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 src/Communication/Socket/WaitForDataInterface.php create mode 100644 src/Exception/CommunicationException/CantSyncEventsException.php diff --git a/.gitignore b/.gitignore index 8be5d942..f207df2e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ composer.lock phpstan.neon phpunit.xml vendor -.idea diff --git a/composer.json b/composer.json index 587c81bd..206d0a40 100644 --- a/composer.json +++ b/composer.json @@ -46,8 +46,6 @@ }, "preferred-install": "dist" }, - "minimum-stability": "dev", - "prefer-stable": true, "extra": { "bamarni-bin": { "bin-links": true, diff --git a/src/Communication/Connection.php b/src/Communication/Connection.php index c60467f4..14b1be08 100644 --- a/src/Communication/Connection.php +++ b/src/Communication/Connection.php @@ -12,9 +12,11 @@ namespace HeadlessChromium\Communication; use Evenement\EventEmitter; +use HeadlessChromium\Communication\Socket\WaitForDataInterface; use HeadlessChromium\Communication\Socket\SocketInterface; use HeadlessChromium\Communication\Socket\Wrench; use HeadlessChromium\Exception\CommunicationException; +use HeadlessChromium\Exception\CommunicationException\CantSyncEventsException; use HeadlessChromium\Exception\CommunicationException\CannotReadResponse; use HeadlessChromium\Exception\CommunicationException\InvalidResponse; use HeadlessChromium\Exception\OperationTimedOut; @@ -348,6 +350,10 @@ public function readLine() public function processAllEvents(): void { + if($this->wsClient instanceof WaitForDataInterface === false){ + throw new CantSyncEventsException(); + } + $hasData = $this->wsClient->waitForData(0); if ($hasData) { diff --git a/src/Communication/Socket/MockSocket.php b/src/Communication/Socket/MockSocket.php index 0c9da21b..66a450b5 100644 --- a/src/Communication/Socket/MockSocket.php +++ b/src/Communication/Socket/MockSocket.php @@ -124,11 +124,4 @@ public function disconnect($reason = 1000) return true; } - - public function waitForData(float $maxSeconds): bool - { - // TODO: Implement wait if needed. - - return !empty($this->receivedData); - } } diff --git a/src/Communication/Socket/SocketInterface.php b/src/Communication/Socket/SocketInterface.php index 8900aeb2..d151617c 100644 --- a/src/Communication/Socket/SocketInterface.php +++ b/src/Communication/Socket/SocketInterface.php @@ -52,6 +52,4 @@ public function isConnected(); * @return bool */ public function disconnect($reason = 1000); - - public function waitForData(float $maxSeconds): bool; } diff --git a/src/Communication/Socket/WaitForDataInterface.php b/src/Communication/Socket/WaitForDataInterface.php new file mode 100644 index 00000000..a7793439 --- /dev/null +++ b/src/Communication/Socket/WaitForDataInterface.php @@ -0,0 +1,8 @@ + Date: Thu, 25 Apr 2024 21:19:29 +0300 Subject: [PATCH 4/4] fix: styleci --- .gitignore | 1 - src/Communication/Connection.php | 6 +++--- .../CommunicationException/CantSyncEventsException.php | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index f207df2e..04626b2f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ composer.lock phpstan.neon phpunit.xml vendor - diff --git a/src/Communication/Connection.php b/src/Communication/Connection.php index 14b1be08..ec61b0c8 100644 --- a/src/Communication/Connection.php +++ b/src/Communication/Connection.php @@ -12,12 +12,12 @@ namespace HeadlessChromium\Communication; use Evenement\EventEmitter; -use HeadlessChromium\Communication\Socket\WaitForDataInterface; use HeadlessChromium\Communication\Socket\SocketInterface; +use HeadlessChromium\Communication\Socket\WaitForDataInterface; use HeadlessChromium\Communication\Socket\Wrench; use HeadlessChromium\Exception\CommunicationException; -use HeadlessChromium\Exception\CommunicationException\CantSyncEventsException; use HeadlessChromium\Exception\CommunicationException\CannotReadResponse; +use HeadlessChromium\Exception\CommunicationException\CantSyncEventsException; use HeadlessChromium\Exception\CommunicationException\InvalidResponse; use HeadlessChromium\Exception\OperationTimedOut; use HeadlessChromium\Exception\TargetDestroyed; @@ -350,7 +350,7 @@ public function readLine() public function processAllEvents(): void { - if($this->wsClient instanceof WaitForDataInterface === false){ + if (false === $this->wsClient instanceof WaitForDataInterface) { throw new CantSyncEventsException(); } diff --git a/src/Exception/CommunicationException/CantSyncEventsException.php b/src/Exception/CommunicationException/CantSyncEventsException.php index ecca0095..68253169 100644 --- a/src/Exception/CommunicationException/CantSyncEventsException.php +++ b/src/Exception/CommunicationException/CantSyncEventsException.php @@ -6,5 +6,4 @@ class CantSyncEventsException extends CommunicationException { - }