Skip to content

Commit 3bbeb45

Browse files
Fix could not find node with given id (#620)
* fix could not find node with given id * style-ci * fix: code style * fix: styleci --------- Co-authored-by: Graham Campbell <GrahamCampbell@users.noreply.github.com>
1 parent 5849ef8 commit 3bbeb45

File tree

11 files changed

+178
-17
lines changed

11 files changed

+178
-17
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
],
1818
"require": {
1919
"php": "^7.4.15 || ^8.0.2",
20-
"chrome-php/wrench": "^1.6",
20+
"chrome-php/wrench": "^1.7",
2121
"evenement/evenement": "^3.0.1",
2222
"monolog/monolog": "^1.27.1 || ^2.8 || ^3.2",
2323
"psr/log": "^1.1 || ^2.0 || ^3.0",

src/Communication/Connection.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
use Evenement\EventEmitter;
1515
use HeadlessChromium\Communication\Socket\SocketInterface;
16+
use HeadlessChromium\Communication\Socket\WaitForDataInterface;
1617
use HeadlessChromium\Communication\Socket\Wrench;
1718
use HeadlessChromium\Exception\CommunicationException;
1819
use HeadlessChromium\Exception\CommunicationException\CannotReadResponse;
20+
use HeadlessChromium\Exception\CommunicationException\CantSyncEventsException;
1921
use HeadlessChromium\Exception\CommunicationException\InvalidResponse;
2022
use HeadlessChromium\Exception\OperationTimedOut;
2123
use HeadlessChromium\Exception\TargetDestroyed;
@@ -346,6 +348,19 @@ public function readLine()
346348
return false;
347349
}
348350

351+
public function processAllEvents(): void
352+
{
353+
if (false === $this->wsClient instanceof WaitForDataInterface) {
354+
throw new CantSyncEventsException();
355+
}
356+
357+
$hasData = $this->wsClient->waitForData(0);
358+
359+
if ($hasData) {
360+
$this->receiveData();
361+
}
362+
}
363+
349364
/**
350365
* Dispatches the message and either stores the response or emits an event.
351366
*
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace HeadlessChromium\Communication\Socket;
4+
5+
interface WaitForDataInterface
6+
{
7+
public function waitForData(float $maxSeconds): bool;
8+
}

src/Communication/Socket/Wrench.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use Wrench\Client as WrenchClient;
1919
use Wrench\Payload\Payload;
2020

21-
class Wrench implements SocketInterface, LoggerAwareInterface
21+
class Wrench implements SocketInterface, LoggerAwareInterface, WaitForDataInterface
2222
{
2323
use LoggerAwareTrait;
2424

@@ -137,4 +137,9 @@ public function disconnect($reason = 1000)
137137

138138
return $disconnected;
139139
}
140+
141+
public function waitForData(float $maxSeconds): bool
142+
{
143+
return $this->client->waitForData($maxSeconds);
144+
}
140145
}

src/Dom/Dom.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ class Dom extends Node
1111
{
1212
public function __construct(Page $page)
1313
{
14-
$message = new Message('DOM.getDocument');
15-
$response = $page->getSession()->sendMessageSync($message);
16-
17-
$rootNodeId = $response->getResultData('root')['nodeId'];
14+
$rootNodeId = $this->getRootNodeId($page);
1815

1916
parent::__construct($page, $rootNodeId);
2017
}
@@ -24,6 +21,8 @@ public function __construct(Page $page)
2421
*/
2522
public function search(string $selector): array
2623
{
24+
$this->prepareForRequest();
25+
2726
$message = new Message('DOM.performSearch', [
2827
'query' => $selector,
2928
]);
@@ -55,4 +54,23 @@ public function search(string $selector): array
5554

5655
return $nodes;
5756
}
57+
58+
public function prepareForRequest(bool $throw = true): void
59+
{
60+
$this->page->assertNotClosed();
61+
62+
$this->page->getSession()->getConnection()->processAllEvents();
63+
64+
if ($this->isStale) {
65+
$this->nodeId = $this->getRootNodeId($this->page);
66+
}
67+
}
68+
69+
public function getRootNodeId(Page $page)
70+
{
71+
$message = new Message('DOM.getDocument');
72+
$response = $page->getSession()->sendMessageSync($message);
73+
74+
return $response->getResultData('root')['nodeId'];
75+
}
5876
}

src/Dom/Node.php

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use HeadlessChromium\Communication\Message;
99
use HeadlessChromium\Communication\Response;
1010
use HeadlessChromium\Exception\DomException;
11+
use HeadlessChromium\Exception\StaleElementException;
1112
use HeadlessChromium\Page;
1213

1314
class Node
@@ -22,16 +23,37 @@ class Node
2223
*/
2324
protected $nodeId;
2425

26+
/**
27+
* @var bool
28+
*/
29+
protected bool $isStale = false;
30+
2531
public function __construct(Page $page, int $nodeId)
2632
{
2733
$this->page = $page;
2834
$this->nodeId = $nodeId;
35+
36+
$page->getSession()->on('method:DOM.documentUpdated', function (...$event): void {
37+
$this->isStale = true;
38+
});
39+
}
40+
41+
public function getNodeId(): int
42+
{
43+
return $this->nodeId;
44+
}
45+
46+
public function getNodeIdForRequest(): int
47+
{
48+
$this->prepareForRequest();
49+
50+
return $this->getNodeId();
2951
}
3052

3153
public function getAttributes(): NodeAttributes
3254
{
3355
$message = new Message('DOM.getAttributes', [
34-
'nodeId' => $this->nodeId,
56+
'nodeId' => $this->getNodeIdForRequest(),
3557
]);
3658
$response = $this->page->getSession()->sendMessageSync($message);
3759

@@ -45,7 +67,7 @@ public function getAttributes(): NodeAttributes
4567
public function setAttributeValue(string $name, string $value): void
4668
{
4769
$message = new Message('DOM.setAttributeValue', [
48-
'nodeId' => $this->nodeId,
70+
'nodeId' => $this->getNodeIdForRequest(),
4971
'name' => $name,
5072
'value' => $value,
5173
]);
@@ -57,7 +79,7 @@ public function setAttributeValue(string $name, string $value): void
5779
public function querySelector(string $selector): ?self
5880
{
5981
$message = new Message('DOM.querySelector', [
60-
'nodeId' => $this->nodeId,
82+
'nodeId' => $this->getNodeIdForRequest(),
6183
'selector' => $selector,
6284
]);
6385
$response = $this->page->getSession()->sendMessageSync($message);
@@ -75,7 +97,7 @@ public function querySelector(string $selector): ?self
7597
public function querySelectorAll(string $selector): array
7698
{
7799
$message = new Message('DOM.querySelectorAll', [
78-
'nodeId' => $this->nodeId,
100+
'nodeId' => $this->getNodeIdForRequest(),
79101
'selector' => $selector,
80102
]);
81103
$response = $this->page->getSession()->sendMessageSync($message);
@@ -94,7 +116,7 @@ public function querySelectorAll(string $selector): array
94116
public function focus(): void
95117
{
96118
$message = new Message('DOM.focus', [
97-
'nodeId' => $this->nodeId,
119+
'nodeId' => $this->getNodeIdForRequest(),
98120
]);
99121
$response = $this->page->getSession()->sendMessageSync($message);
100122

@@ -109,7 +131,7 @@ public function getAttribute(string $name): ?string
109131
public function getPosition(): ?NodePosition
110132
{
111133
$message = new Message('DOM.getBoxModel', [
112-
'nodeId' => $this->nodeId,
134+
'nodeId' => $this->getNodeIdForRequest(),
113135
]);
114136
$response = $this->page->getSession()->sendMessageSync($message);
115137

@@ -132,7 +154,7 @@ public function hasPosition(): bool
132154
public function getHTML(): string
133155
{
134156
$message = new Message('DOM.getOuterHTML', [
135-
'nodeId' => $this->nodeId,
157+
'nodeId' => $this->getNodeIdForRequest(),
136158
]);
137159
$response = $this->page->getSession()->sendMessageSync($message);
138160

@@ -144,7 +166,7 @@ public function getHTML(): string
144166
public function setHTML(string $outerHTML): void
145167
{
146168
$message = new Message('DOM.setOuterHTML', [
147-
'nodeId' => $this->nodeId,
169+
'nodeId' => $this->getNodeIdForRequest(),
148170
'outerHTML' => $outerHTML,
149171
]);
150172
$response = $this->page->getSession()->sendMessageSync($message);
@@ -160,7 +182,7 @@ public function getText(): string
160182
public function scrollIntoView(): void
161183
{
162184
$message = new Message('DOM.scrollIntoViewIfNeeded', [
163-
'nodeId' => $this->nodeId,
185+
'nodeId' => $this->getNodeIdForRequest(),
164186
]);
165187
$response = $this->page->getSession()->sendMessageSync($message);
166188

@@ -199,7 +221,7 @@ public function sendFiles(array $filePaths): void
199221
{
200222
$message = new Message('DOM.setFileInputFiles', [
201223
'files' => $filePaths,
202-
'nodeId' => $this->nodeId,
224+
'nodeId' => $this->getNodeIdForRequest(),
203225
]);
204226
$response = $this->page->getSession()->sendMessageSync($message);
205227

@@ -231,4 +253,15 @@ public function getClip(): ?Clip
231253
$position->getHeight(),
232254
);
233255
}
256+
257+
protected function prepareForRequest(): void
258+
{
259+
$this->page->assertNotClosed();
260+
261+
$this->page->getSession()->getConnection()->processAllEvents();
262+
263+
if ($this->isStale) {
264+
throw new StaleElementException();
265+
}
266+
}
234267
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace HeadlessChromium\Exception\CommunicationException;
4+
5+
use HeadlessChromium\Exception\CommunicationException;
6+
7+
class CantSyncEventsException extends CommunicationException
8+
{
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace HeadlessChromium\Exception;
4+
5+
class StaleElementException extends DomException
6+
{
7+
}

src/Page.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ class Page
6969
*/
7070
protected $keyboard;
7171

72+
/**
73+
* @var Dom|null
74+
*/
75+
protected $dom = null;
76+
7277
/**
7378
* Page constructor.
7479
*
@@ -815,7 +820,13 @@ public function keyboard()
815820

816821
public function dom(): Dom
817822
{
818-
return new Dom($this);
823+
$this->assertNotClosed();
824+
825+
if (null === $this->dom) {
826+
$this->dom = new Dom($this);
827+
}
828+
829+
return $this->dom;
819830
}
820831

821832
/**

tests/DomTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use HeadlessChromium\Browser;
66
use HeadlessChromium\BrowserFactory;
7+
use HeadlessChromium\Exception\StaleElementException;
78

89
/**
910
* @covers \HeadlessChromium\Dom\Dom
@@ -187,4 +188,56 @@ public function testSetHTML(): void
187188

188189
self::assertEquals('<span id="span">hello</span>', $value);
189190
}
191+
192+
public function testDomDoesReturnsTheSameObject(): void
193+
{
194+
$page = $this->openSitePage('domForm.html');
195+
196+
$firstDom = $page->dom();
197+
198+
$element = $firstDom->querySelector('#myinput');
199+
200+
$secondDom = $page->dom();
201+
202+
$element->focus();
203+
204+
$this->assertEquals($firstDom, $secondDom);
205+
}
206+
207+
public function testRootNodeIdIsUpdatedAfterReload(): void
208+
{
209+
$page = $this->openSitePage('domForm.html');
210+
211+
$dom = $page->dom();
212+
213+
$nodeId = $dom->getNodeId();
214+
215+
$reloadBtn = $dom->querySelector('#reload-btn');
216+
$reloadBtn->click();
217+
218+
$page->waitForReload();
219+
220+
$reloadBtn = $dom->querySelector('#reload-btn');
221+
$this->assertNotNull($reloadBtn);
222+
223+
$this->assertNotEquals($nodeId, $page->dom()->getNodeId());
224+
}
225+
226+
public function testRegularNodeIsMarkedAsStaleAfterReload(): void
227+
{
228+
$page = $this->openSitePage('domForm.html');
229+
230+
$dom = $page->dom();
231+
232+
$inputNode = $dom->querySelector('#myinput');
233+
234+
$reloadBtn = $dom->querySelector('#reload-btn');
235+
$reloadBtn->click();
236+
237+
$page->waitForReload();
238+
239+
$this->expectException(StaleElementException::class);
240+
241+
$inputNode->sendKeys('test');
242+
}
190243
}

0 commit comments

Comments
 (0)