From df56311f1f4ce966a0210d74276e8c5e99ffde1c Mon Sep 17 00:00:00 2001 From: Seriyyy95 Date: Tue, 23 Apr 2024 21:19:27 +0300 Subject: [PATCH] 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
+ +