1*04fd306cSNickeau<?php 2*04fd306cSNickeau 3*04fd306cSNickeaunamespace Facebook\WebDriver\Remote; 4*04fd306cSNickeau 5*04fd306cSNickeauuse Facebook\WebDriver\Exception\ElementNotInteractableException; 6*04fd306cSNickeauuse Facebook\WebDriver\Exception\UnsupportedOperationException; 7*04fd306cSNickeauuse Facebook\WebDriver\Exception\WebDriverException; 8*04fd306cSNickeauuse Facebook\WebDriver\Interactions\Internal\WebDriverCoordinates; 9*04fd306cSNickeauuse Facebook\WebDriver\Internal\WebDriverLocatable; 10*04fd306cSNickeauuse Facebook\WebDriver\WebDriverBy; 11*04fd306cSNickeauuse Facebook\WebDriver\WebDriverDimension; 12*04fd306cSNickeauuse Facebook\WebDriver\WebDriverElement; 13*04fd306cSNickeauuse Facebook\WebDriver\WebDriverKeys; 14*04fd306cSNickeauuse Facebook\WebDriver\WebDriverPoint; 15*04fd306cSNickeauuse ZipArchive; 16*04fd306cSNickeau 17*04fd306cSNickeau/** 18*04fd306cSNickeau * Represents an HTML element. 19*04fd306cSNickeau */ 20*04fd306cSNickeauclass RemoteWebElement implements WebDriverElement, WebDriverLocatable 21*04fd306cSNickeau{ 22*04fd306cSNickeau /** 23*04fd306cSNickeau * @var RemoteExecuteMethod 24*04fd306cSNickeau */ 25*04fd306cSNickeau protected $executor; 26*04fd306cSNickeau /** 27*04fd306cSNickeau * @var string 28*04fd306cSNickeau */ 29*04fd306cSNickeau protected $id; 30*04fd306cSNickeau /** 31*04fd306cSNickeau * @var FileDetector 32*04fd306cSNickeau */ 33*04fd306cSNickeau protected $fileDetector; 34*04fd306cSNickeau /** 35*04fd306cSNickeau * @var bool 36*04fd306cSNickeau */ 37*04fd306cSNickeau protected $isW3cCompliant; 38*04fd306cSNickeau 39*04fd306cSNickeau /** 40*04fd306cSNickeau * @param RemoteExecuteMethod $executor 41*04fd306cSNickeau * @param string $id 42*04fd306cSNickeau * @param bool $isW3cCompliant 43*04fd306cSNickeau */ 44*04fd306cSNickeau public function __construct(RemoteExecuteMethod $executor, $id, $isW3cCompliant = false) 45*04fd306cSNickeau { 46*04fd306cSNickeau $this->executor = $executor; 47*04fd306cSNickeau $this->id = $id; 48*04fd306cSNickeau $this->fileDetector = new UselessFileDetector(); 49*04fd306cSNickeau $this->isW3cCompliant = $isW3cCompliant; 50*04fd306cSNickeau } 51*04fd306cSNickeau 52*04fd306cSNickeau /** 53*04fd306cSNickeau * Clear content editable or resettable element 54*04fd306cSNickeau * 55*04fd306cSNickeau * @return RemoteWebElement The current instance. 56*04fd306cSNickeau */ 57*04fd306cSNickeau public function clear() 58*04fd306cSNickeau { 59*04fd306cSNickeau $this->executor->execute( 60*04fd306cSNickeau DriverCommand::CLEAR_ELEMENT, 61*04fd306cSNickeau [':id' => $this->id] 62*04fd306cSNickeau ); 63*04fd306cSNickeau 64*04fd306cSNickeau return $this; 65*04fd306cSNickeau } 66*04fd306cSNickeau 67*04fd306cSNickeau /** 68*04fd306cSNickeau * Click this element. 69*04fd306cSNickeau * 70*04fd306cSNickeau * @return RemoteWebElement The current instance. 71*04fd306cSNickeau */ 72*04fd306cSNickeau public function click() 73*04fd306cSNickeau { 74*04fd306cSNickeau try { 75*04fd306cSNickeau $this->executor->execute( 76*04fd306cSNickeau DriverCommand::CLICK_ELEMENT, 77*04fd306cSNickeau [':id' => $this->id] 78*04fd306cSNickeau ); 79*04fd306cSNickeau } catch (ElementNotInteractableException $e) { 80*04fd306cSNickeau // An issue with geckodriver (https://github.com/mozilla/geckodriver/issues/653) prevents clicking on a link 81*04fd306cSNickeau // if the first child is a block-level element. 82*04fd306cSNickeau // The workaround in this case is to click on a child element. 83*04fd306cSNickeau $this->clickChildElement($e); 84*04fd306cSNickeau } 85*04fd306cSNickeau 86*04fd306cSNickeau return $this; 87*04fd306cSNickeau } 88*04fd306cSNickeau 89*04fd306cSNickeau /** 90*04fd306cSNickeau * Find the first WebDriverElement within this element using the given mechanism. 91*04fd306cSNickeau * 92*04fd306cSNickeau * When using xpath be aware that webdriver follows standard conventions: a search prefixed with "//" will 93*04fd306cSNickeau * search the entire document from the root, not just the children (relative context) of this current node. 94*04fd306cSNickeau * Use ".//" to limit your search to the children of this element. 95*04fd306cSNickeau * 96*04fd306cSNickeau * @param WebDriverBy $by 97*04fd306cSNickeau * @return RemoteWebElement NoSuchElementException is thrown in HttpCommandExecutor if no element is found. 98*04fd306cSNickeau * @see WebDriverBy 99*04fd306cSNickeau */ 100*04fd306cSNickeau public function findElement(WebDriverBy $by) 101*04fd306cSNickeau { 102*04fd306cSNickeau $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); 103*04fd306cSNickeau $params[':id'] = $this->id; 104*04fd306cSNickeau 105*04fd306cSNickeau $raw_element = $this->executor->execute( 106*04fd306cSNickeau DriverCommand::FIND_CHILD_ELEMENT, 107*04fd306cSNickeau $params 108*04fd306cSNickeau ); 109*04fd306cSNickeau 110*04fd306cSNickeau return $this->newElement(JsonWireCompat::getElement($raw_element)); 111*04fd306cSNickeau } 112*04fd306cSNickeau 113*04fd306cSNickeau /** 114*04fd306cSNickeau * Find all WebDriverElements within this element using the given mechanism. 115*04fd306cSNickeau * 116*04fd306cSNickeau * When using xpath be aware that webdriver follows standard conventions: a search prefixed with "//" will 117*04fd306cSNickeau * search the entire document from the root, not just the children (relative context) of this current node. 118*04fd306cSNickeau * Use ".//" to limit your search to the children of this element. 119*04fd306cSNickeau * 120*04fd306cSNickeau * @param WebDriverBy $by 121*04fd306cSNickeau * @return RemoteWebElement[] A list of all WebDriverElements, or an empty 122*04fd306cSNickeau * array if nothing matches 123*04fd306cSNickeau * @see WebDriverBy 124*04fd306cSNickeau */ 125*04fd306cSNickeau public function findElements(WebDriverBy $by) 126*04fd306cSNickeau { 127*04fd306cSNickeau $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); 128*04fd306cSNickeau $params[':id'] = $this->id; 129*04fd306cSNickeau $raw_elements = $this->executor->execute( 130*04fd306cSNickeau DriverCommand::FIND_CHILD_ELEMENTS, 131*04fd306cSNickeau $params 132*04fd306cSNickeau ); 133*04fd306cSNickeau 134*04fd306cSNickeau $elements = []; 135*04fd306cSNickeau foreach ($raw_elements as $raw_element) { 136*04fd306cSNickeau $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); 137*04fd306cSNickeau } 138*04fd306cSNickeau 139*04fd306cSNickeau return $elements; 140*04fd306cSNickeau } 141*04fd306cSNickeau 142*04fd306cSNickeau /** 143*04fd306cSNickeau * Get the value of the given attribute of the element. 144*04fd306cSNickeau * Attribute is meant what is declared in the HTML markup of the element. 145*04fd306cSNickeau * To read a value of a IDL "JavaScript" property (like `innerHTML`), use `getDomProperty()` method. 146*04fd306cSNickeau * 147*04fd306cSNickeau * @param string $attribute_name The name of the attribute. 148*04fd306cSNickeau * @return string|true|null The value of the attribute. If this is boolean attribute, return true if the element 149*04fd306cSNickeau * has it, otherwise return null. 150*04fd306cSNickeau */ 151*04fd306cSNickeau public function getAttribute($attribute_name) 152*04fd306cSNickeau { 153*04fd306cSNickeau $params = [ 154*04fd306cSNickeau ':name' => $attribute_name, 155*04fd306cSNickeau ':id' => $this->id, 156*04fd306cSNickeau ]; 157*04fd306cSNickeau 158*04fd306cSNickeau if ($this->isW3cCompliant && ($attribute_name === 'value' || $attribute_name === 'index')) { 159*04fd306cSNickeau $value = $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); 160*04fd306cSNickeau 161*04fd306cSNickeau if ($value === true) { 162*04fd306cSNickeau return 'true'; 163*04fd306cSNickeau } 164*04fd306cSNickeau 165*04fd306cSNickeau if ($value === false) { 166*04fd306cSNickeau return 'false'; 167*04fd306cSNickeau } 168*04fd306cSNickeau 169*04fd306cSNickeau if ($value !== null) { 170*04fd306cSNickeau return (string) $value; 171*04fd306cSNickeau } 172*04fd306cSNickeau } 173*04fd306cSNickeau 174*04fd306cSNickeau return $this->executor->execute(DriverCommand::GET_ELEMENT_ATTRIBUTE, $params); 175*04fd306cSNickeau } 176*04fd306cSNickeau 177*04fd306cSNickeau /** 178*04fd306cSNickeau * Gets the value of a IDL JavaScript property of this element (for example `innerHTML`, `tagName` etc.). 179*04fd306cSNickeau * 180*04fd306cSNickeau * @see https://developer.mozilla.org/en-US/docs/Glossary/IDL 181*04fd306cSNickeau * @see https://developer.mozilla.org/en-US/docs/Web/API/Element#properties 182*04fd306cSNickeau * @param string $propertyName 183*04fd306cSNickeau * @return mixed|null The property's current value or null if the value is not set or the property does not exist. 184*04fd306cSNickeau */ 185*04fd306cSNickeau public function getDomProperty($propertyName) 186*04fd306cSNickeau { 187*04fd306cSNickeau if (!$this->isW3cCompliant) { 188*04fd306cSNickeau throw new UnsupportedOperationException('This method is only supported in W3C mode'); 189*04fd306cSNickeau } 190*04fd306cSNickeau 191*04fd306cSNickeau $params = [ 192*04fd306cSNickeau ':name' => $propertyName, 193*04fd306cSNickeau ':id' => $this->id, 194*04fd306cSNickeau ]; 195*04fd306cSNickeau 196*04fd306cSNickeau return $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); 197*04fd306cSNickeau } 198*04fd306cSNickeau 199*04fd306cSNickeau /** 200*04fd306cSNickeau * Get the value of a given CSS property. 201*04fd306cSNickeau * 202*04fd306cSNickeau * @param string $css_property_name The name of the CSS property. 203*04fd306cSNickeau * @return string The value of the CSS property. 204*04fd306cSNickeau */ 205*04fd306cSNickeau public function getCSSValue($css_property_name) 206*04fd306cSNickeau { 207*04fd306cSNickeau $params = [ 208*04fd306cSNickeau ':propertyName' => $css_property_name, 209*04fd306cSNickeau ':id' => $this->id, 210*04fd306cSNickeau ]; 211*04fd306cSNickeau 212*04fd306cSNickeau return $this->executor->execute( 213*04fd306cSNickeau DriverCommand::GET_ELEMENT_VALUE_OF_CSS_PROPERTY, 214*04fd306cSNickeau $params 215*04fd306cSNickeau ); 216*04fd306cSNickeau } 217*04fd306cSNickeau 218*04fd306cSNickeau /** 219*04fd306cSNickeau * Get the location of element relative to the top-left corner of the page. 220*04fd306cSNickeau * 221*04fd306cSNickeau * @return WebDriverPoint The location of the element. 222*04fd306cSNickeau */ 223*04fd306cSNickeau public function getLocation() 224*04fd306cSNickeau { 225*04fd306cSNickeau $location = $this->executor->execute( 226*04fd306cSNickeau DriverCommand::GET_ELEMENT_LOCATION, 227*04fd306cSNickeau [':id' => $this->id] 228*04fd306cSNickeau ); 229*04fd306cSNickeau 230*04fd306cSNickeau return new WebDriverPoint($location['x'], $location['y']); 231*04fd306cSNickeau } 232*04fd306cSNickeau 233*04fd306cSNickeau /** 234*04fd306cSNickeau * Try scrolling the element into the view port and return the location of 235*04fd306cSNickeau * element relative to the top-left corner of the page afterwards. 236*04fd306cSNickeau * 237*04fd306cSNickeau * @return WebDriverPoint The location of the element. 238*04fd306cSNickeau */ 239*04fd306cSNickeau public function getLocationOnScreenOnceScrolledIntoView() 240*04fd306cSNickeau { 241*04fd306cSNickeau if ($this->isW3cCompliant) { 242*04fd306cSNickeau $script = <<<JS 243*04fd306cSNickeauvar e = arguments[0]; 244*04fd306cSNickeaue.scrollIntoView({ behavior: 'instant', block: 'end', inline: 'nearest' }); 245*04fd306cSNickeauvar rect = e.getBoundingClientRect(); 246*04fd306cSNickeaureturn {'x': rect.left, 'y': rect.top}; 247*04fd306cSNickeauJS; 248*04fd306cSNickeau 249*04fd306cSNickeau $result = $this->executor->execute(DriverCommand::EXECUTE_SCRIPT, [ 250*04fd306cSNickeau 'script' => $script, 251*04fd306cSNickeau 'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]], 252*04fd306cSNickeau ]); 253*04fd306cSNickeau $location = ['x' => $result['x'], 'y' => $result['y']]; 254*04fd306cSNickeau } else { 255*04fd306cSNickeau $location = $this->executor->execute( 256*04fd306cSNickeau DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW, 257*04fd306cSNickeau [':id' => $this->id] 258*04fd306cSNickeau ); 259*04fd306cSNickeau } 260*04fd306cSNickeau 261*04fd306cSNickeau return new WebDriverPoint($location['x'], $location['y']); 262*04fd306cSNickeau } 263*04fd306cSNickeau 264*04fd306cSNickeau /** 265*04fd306cSNickeau * @return WebDriverCoordinates 266*04fd306cSNickeau */ 267*04fd306cSNickeau public function getCoordinates() 268*04fd306cSNickeau { 269*04fd306cSNickeau $element = $this; 270*04fd306cSNickeau 271*04fd306cSNickeau $on_screen = null; // planned but not yet implemented 272*04fd306cSNickeau $in_view_port = static function () use ($element) { 273*04fd306cSNickeau return $element->getLocationOnScreenOnceScrolledIntoView(); 274*04fd306cSNickeau }; 275*04fd306cSNickeau $on_page = static function () use ($element) { 276*04fd306cSNickeau return $element->getLocation(); 277*04fd306cSNickeau }; 278*04fd306cSNickeau $auxiliary = $this->getID(); 279*04fd306cSNickeau 280*04fd306cSNickeau return new WebDriverCoordinates( 281*04fd306cSNickeau $on_screen, 282*04fd306cSNickeau $in_view_port, 283*04fd306cSNickeau $on_page, 284*04fd306cSNickeau $auxiliary 285*04fd306cSNickeau ); 286*04fd306cSNickeau } 287*04fd306cSNickeau 288*04fd306cSNickeau /** 289*04fd306cSNickeau * Get the size of element. 290*04fd306cSNickeau * 291*04fd306cSNickeau * @return WebDriverDimension The dimension of the element. 292*04fd306cSNickeau */ 293*04fd306cSNickeau public function getSize() 294*04fd306cSNickeau { 295*04fd306cSNickeau $size = $this->executor->execute( 296*04fd306cSNickeau DriverCommand::GET_ELEMENT_SIZE, 297*04fd306cSNickeau [':id' => $this->id] 298*04fd306cSNickeau ); 299*04fd306cSNickeau 300*04fd306cSNickeau return new WebDriverDimension($size['width'], $size['height']); 301*04fd306cSNickeau } 302*04fd306cSNickeau 303*04fd306cSNickeau /** 304*04fd306cSNickeau * Get the (lowercase) tag name of this element. 305*04fd306cSNickeau * 306*04fd306cSNickeau * @return string The tag name. 307*04fd306cSNickeau */ 308*04fd306cSNickeau public function getTagName() 309*04fd306cSNickeau { 310*04fd306cSNickeau // Force tag name to be lowercase as expected by JsonWire protocol for Opera driver 311*04fd306cSNickeau // until this issue is not resolved : 312*04fd306cSNickeau // https://github.com/operasoftware/operadriver/issues/102 313*04fd306cSNickeau // Remove it when fixed to be consistent with the protocol. 314*04fd306cSNickeau return mb_strtolower($this->executor->execute( 315*04fd306cSNickeau DriverCommand::GET_ELEMENT_TAG_NAME, 316*04fd306cSNickeau [':id' => $this->id] 317*04fd306cSNickeau )); 318*04fd306cSNickeau } 319*04fd306cSNickeau 320*04fd306cSNickeau /** 321*04fd306cSNickeau * Get the visible (i.e. not hidden by CSS) innerText of this element, 322*04fd306cSNickeau * including sub-elements, without any leading or trailing whitespace. 323*04fd306cSNickeau * 324*04fd306cSNickeau * @return string The visible innerText of this element. 325*04fd306cSNickeau */ 326*04fd306cSNickeau public function getText() 327*04fd306cSNickeau { 328*04fd306cSNickeau return $this->executor->execute( 329*04fd306cSNickeau DriverCommand::GET_ELEMENT_TEXT, 330*04fd306cSNickeau [':id' => $this->id] 331*04fd306cSNickeau ); 332*04fd306cSNickeau } 333*04fd306cSNickeau 334*04fd306cSNickeau /** 335*04fd306cSNickeau * Is this element displayed or not? This method avoids the problem of having 336*04fd306cSNickeau * to parse an element's "style" attribute. 337*04fd306cSNickeau * 338*04fd306cSNickeau * @return bool 339*04fd306cSNickeau */ 340*04fd306cSNickeau public function isDisplayed() 341*04fd306cSNickeau { 342*04fd306cSNickeau return $this->executor->execute( 343*04fd306cSNickeau DriverCommand::IS_ELEMENT_DISPLAYED, 344*04fd306cSNickeau [':id' => $this->id] 345*04fd306cSNickeau ); 346*04fd306cSNickeau } 347*04fd306cSNickeau 348*04fd306cSNickeau /** 349*04fd306cSNickeau * Is the element currently enabled or not? This will generally return true 350*04fd306cSNickeau * for everything but disabled input elements. 351*04fd306cSNickeau * 352*04fd306cSNickeau * @return bool 353*04fd306cSNickeau */ 354*04fd306cSNickeau public function isEnabled() 355*04fd306cSNickeau { 356*04fd306cSNickeau return $this->executor->execute( 357*04fd306cSNickeau DriverCommand::IS_ELEMENT_ENABLED, 358*04fd306cSNickeau [':id' => $this->id] 359*04fd306cSNickeau ); 360*04fd306cSNickeau } 361*04fd306cSNickeau 362*04fd306cSNickeau /** 363*04fd306cSNickeau * Determine whether this element is selected or not. 364*04fd306cSNickeau * 365*04fd306cSNickeau * @return bool 366*04fd306cSNickeau */ 367*04fd306cSNickeau public function isSelected() 368*04fd306cSNickeau { 369*04fd306cSNickeau return $this->executor->execute( 370*04fd306cSNickeau DriverCommand::IS_ELEMENT_SELECTED, 371*04fd306cSNickeau [':id' => $this->id] 372*04fd306cSNickeau ); 373*04fd306cSNickeau } 374*04fd306cSNickeau 375*04fd306cSNickeau /** 376*04fd306cSNickeau * Simulate typing into an element, which may set its value. 377*04fd306cSNickeau * 378*04fd306cSNickeau * @param mixed $value The data to be typed. 379*04fd306cSNickeau * @return RemoteWebElement The current instance. 380*04fd306cSNickeau */ 381*04fd306cSNickeau public function sendKeys($value) 382*04fd306cSNickeau { 383*04fd306cSNickeau $local_file = $this->fileDetector->getLocalFile($value); 384*04fd306cSNickeau 385*04fd306cSNickeau $params = []; 386*04fd306cSNickeau if ($local_file === null) { 387*04fd306cSNickeau if ($this->isW3cCompliant) { 388*04fd306cSNickeau // Work around the Geckodriver NULL issue by splitting on NULL and calling sendKeys multiple times. 389*04fd306cSNickeau // See https://bugzilla.mozilla.org/show_bug.cgi?id=1494661. 390*04fd306cSNickeau $encodedValues = explode(WebDriverKeys::NULL, WebDriverKeys::encode($value, true)); 391*04fd306cSNickeau foreach ($encodedValues as $encodedValue) { 392*04fd306cSNickeau $params[] = [ 393*04fd306cSNickeau 'text' => $encodedValue, 394*04fd306cSNickeau ':id' => $this->id, 395*04fd306cSNickeau ]; 396*04fd306cSNickeau } 397*04fd306cSNickeau } else { 398*04fd306cSNickeau $params[] = [ 399*04fd306cSNickeau 'value' => WebDriverKeys::encode($value), 400*04fd306cSNickeau ':id' => $this->id, 401*04fd306cSNickeau ]; 402*04fd306cSNickeau } 403*04fd306cSNickeau } else { 404*04fd306cSNickeau if ($this->isW3cCompliant) { 405*04fd306cSNickeau try { 406*04fd306cSNickeau // Attempt to upload the file to the remote browser. 407*04fd306cSNickeau // This is so far non-W3C compliant method, so it may fail - if so, we just ignore the exception. 408*04fd306cSNickeau // @see https://github.com/w3c/webdriver/issues/1355 409*04fd306cSNickeau $fileName = $this->upload($local_file); 410*04fd306cSNickeau } catch (WebDriverException $e) { 411*04fd306cSNickeau $fileName = $local_file; 412*04fd306cSNickeau } 413*04fd306cSNickeau 414*04fd306cSNickeau $params[] = [ 415*04fd306cSNickeau 'text' => $fileName, 416*04fd306cSNickeau ':id' => $this->id, 417*04fd306cSNickeau ]; 418*04fd306cSNickeau } else { 419*04fd306cSNickeau $params[] = [ 420*04fd306cSNickeau 'value' => WebDriverKeys::encode($this->upload($local_file)), 421*04fd306cSNickeau ':id' => $this->id, 422*04fd306cSNickeau ]; 423*04fd306cSNickeau } 424*04fd306cSNickeau } 425*04fd306cSNickeau 426*04fd306cSNickeau foreach ($params as $param) { 427*04fd306cSNickeau $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $param); 428*04fd306cSNickeau } 429*04fd306cSNickeau 430*04fd306cSNickeau return $this; 431*04fd306cSNickeau } 432*04fd306cSNickeau 433*04fd306cSNickeau /** 434*04fd306cSNickeau * Set the fileDetector in order to let the RemoteWebElement to know that you are going to upload a file. 435*04fd306cSNickeau * 436*04fd306cSNickeau * Basically, if you want WebDriver trying to send a file, set the fileDetector 437*04fd306cSNickeau * to be LocalFileDetector. Otherwise, keep it UselessFileDetector. 438*04fd306cSNickeau * 439*04fd306cSNickeau * eg. `$element->setFileDetector(new LocalFileDetector);` 440*04fd306cSNickeau * 441*04fd306cSNickeau * @param FileDetector $detector 442*04fd306cSNickeau * @return RemoteWebElement 443*04fd306cSNickeau * @see FileDetector 444*04fd306cSNickeau * @see LocalFileDetector 445*04fd306cSNickeau * @see UselessFileDetector 446*04fd306cSNickeau */ 447*04fd306cSNickeau public function setFileDetector(FileDetector $detector) 448*04fd306cSNickeau { 449*04fd306cSNickeau $this->fileDetector = $detector; 450*04fd306cSNickeau 451*04fd306cSNickeau return $this; 452*04fd306cSNickeau } 453*04fd306cSNickeau 454*04fd306cSNickeau /** 455*04fd306cSNickeau * If this current element is a form, or an element within a form, then this will be submitted to the remote server. 456*04fd306cSNickeau * 457*04fd306cSNickeau * @return RemoteWebElement The current instance. 458*04fd306cSNickeau */ 459*04fd306cSNickeau public function submit() 460*04fd306cSNickeau { 461*04fd306cSNickeau if ($this->isW3cCompliant) { 462*04fd306cSNickeau // Submit method cannot be called directly in case an input of this form is named "submit". 463*04fd306cSNickeau // We use this polyfill to trigger 'submit' event using form.dispatchEvent(). 464*04fd306cSNickeau $submitPolyfill = $script = <<<HTXT 465*04fd306cSNickeau var form = arguments[0]; 466*04fd306cSNickeau while (form.nodeName !== "FORM" && form.parentNode) { // find the parent form of this element 467*04fd306cSNickeau form = form.parentNode; 468*04fd306cSNickeau } 469*04fd306cSNickeau if (!form) { 470*04fd306cSNickeau throw Error('Unable to find containing form element'); 471*04fd306cSNickeau } 472*04fd306cSNickeau var event = new Event('submit', {bubbles: true, cancelable: true}); 473*04fd306cSNickeau if (form.dispatchEvent(event)) { 474*04fd306cSNickeau HTMLFormElement.prototype.submit.call(form); 475*04fd306cSNickeau } 476*04fd306cSNickeauHTXT; 477*04fd306cSNickeau $this->executor->execute(DriverCommand::EXECUTE_SCRIPT, [ 478*04fd306cSNickeau 'script' => $submitPolyfill, 479*04fd306cSNickeau 'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]], 480*04fd306cSNickeau ]); 481*04fd306cSNickeau 482*04fd306cSNickeau return $this; 483*04fd306cSNickeau } 484*04fd306cSNickeau 485*04fd306cSNickeau $this->executor->execute( 486*04fd306cSNickeau DriverCommand::SUBMIT_ELEMENT, 487*04fd306cSNickeau [':id' => $this->id] 488*04fd306cSNickeau ); 489*04fd306cSNickeau 490*04fd306cSNickeau return $this; 491*04fd306cSNickeau } 492*04fd306cSNickeau 493*04fd306cSNickeau /** 494*04fd306cSNickeau * Get the opaque ID of the element. 495*04fd306cSNickeau * 496*04fd306cSNickeau * @return string The opaque ID. 497*04fd306cSNickeau */ 498*04fd306cSNickeau public function getID() 499*04fd306cSNickeau { 500*04fd306cSNickeau return $this->id; 501*04fd306cSNickeau } 502*04fd306cSNickeau 503*04fd306cSNickeau /** 504*04fd306cSNickeau * Take a screenshot of a specific element. 505*04fd306cSNickeau * 506*04fd306cSNickeau * @param string $save_as The path of the screenshot to be saved. 507*04fd306cSNickeau * @return string The screenshot in PNG format. 508*04fd306cSNickeau */ 509*04fd306cSNickeau public function takeElementScreenshot($save_as = null) 510*04fd306cSNickeau { 511*04fd306cSNickeau $screenshot = base64_decode( 512*04fd306cSNickeau $this->executor->execute( 513*04fd306cSNickeau DriverCommand::TAKE_ELEMENT_SCREENSHOT, 514*04fd306cSNickeau [':id' => $this->id] 515*04fd306cSNickeau ), 516*04fd306cSNickeau true 517*04fd306cSNickeau ); 518*04fd306cSNickeau 519*04fd306cSNickeau if ($save_as !== null) { 520*04fd306cSNickeau $directoryPath = dirname($save_as); 521*04fd306cSNickeau if (!file_exists($directoryPath)) { 522*04fd306cSNickeau mkdir($directoryPath, 0777, true); 523*04fd306cSNickeau } 524*04fd306cSNickeau 525*04fd306cSNickeau file_put_contents($save_as, $screenshot); 526*04fd306cSNickeau } 527*04fd306cSNickeau 528*04fd306cSNickeau return $screenshot; 529*04fd306cSNickeau } 530*04fd306cSNickeau 531*04fd306cSNickeau /** 532*04fd306cSNickeau * Test if two elements IDs refer to the same DOM element. 533*04fd306cSNickeau * 534*04fd306cSNickeau * @param WebDriverElement $other 535*04fd306cSNickeau * @return bool 536*04fd306cSNickeau */ 537*04fd306cSNickeau public function equals(WebDriverElement $other) 538*04fd306cSNickeau { 539*04fd306cSNickeau if ($this->isW3cCompliant) { 540*04fd306cSNickeau return $this->getID() === $other->getID(); 541*04fd306cSNickeau } 542*04fd306cSNickeau 543*04fd306cSNickeau return $this->executor->execute(DriverCommand::ELEMENT_EQUALS, [ 544*04fd306cSNickeau ':id' => $this->id, 545*04fd306cSNickeau ':other' => $other->getID(), 546*04fd306cSNickeau ]); 547*04fd306cSNickeau } 548*04fd306cSNickeau 549*04fd306cSNickeau /** 550*04fd306cSNickeau * Attempt to click on a child level element. 551*04fd306cSNickeau * 552*04fd306cSNickeau * This provides a workaround for geckodriver bug 653 whereby a link whose first element is a block-level element 553*04fd306cSNickeau * throws an ElementNotInteractableException could not scroll into view exception. 554*04fd306cSNickeau * 555*04fd306cSNickeau * The workaround provided here attempts to click on a child node of the element. 556*04fd306cSNickeau * In case the first child is hidden, other elements are processed until we run out of elements. 557*04fd306cSNickeau * 558*04fd306cSNickeau * @param ElementNotInteractableException $originalException The exception to throw if unable to click on any child 559*04fd306cSNickeau * @see https://github.com/mozilla/geckodriver/issues/653 560*04fd306cSNickeau * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1374283 561*04fd306cSNickeau */ 562*04fd306cSNickeau protected function clickChildElement(ElementNotInteractableException $originalException) 563*04fd306cSNickeau { 564*04fd306cSNickeau $children = $this->findElements(WebDriverBy::xpath('./*')); 565*04fd306cSNickeau foreach ($children as $child) { 566*04fd306cSNickeau try { 567*04fd306cSNickeau // Note: This does not use $child->click() as this would cause recursion into all children. 568*04fd306cSNickeau // Where the element is hidden, all children will also be hidden. 569*04fd306cSNickeau $this->executor->execute( 570*04fd306cSNickeau DriverCommand::CLICK_ELEMENT, 571*04fd306cSNickeau [':id' => $child->id] 572*04fd306cSNickeau ); 573*04fd306cSNickeau 574*04fd306cSNickeau return; 575*04fd306cSNickeau } catch (ElementNotInteractableException $e) { 576*04fd306cSNickeau // Ignore the ElementNotInteractableException exception on this node. Try the next child instead. 577*04fd306cSNickeau } 578*04fd306cSNickeau } 579*04fd306cSNickeau 580*04fd306cSNickeau throw $originalException; 581*04fd306cSNickeau } 582*04fd306cSNickeau 583*04fd306cSNickeau /** 584*04fd306cSNickeau * Return the WebDriverElement with $id 585*04fd306cSNickeau * 586*04fd306cSNickeau * @param string $id 587*04fd306cSNickeau * 588*04fd306cSNickeau * @return static 589*04fd306cSNickeau */ 590*04fd306cSNickeau protected function newElement($id) 591*04fd306cSNickeau { 592*04fd306cSNickeau return new static($this->executor, $id, $this->isW3cCompliant); 593*04fd306cSNickeau } 594*04fd306cSNickeau 595*04fd306cSNickeau /** 596*04fd306cSNickeau * Upload a local file to the server 597*04fd306cSNickeau * 598*04fd306cSNickeau * @param string $local_file 599*04fd306cSNickeau * 600*04fd306cSNickeau * @throws WebDriverException 601*04fd306cSNickeau * @return string The remote path of the file. 602*04fd306cSNickeau */ 603*04fd306cSNickeau protected function upload($local_file) 604*04fd306cSNickeau { 605*04fd306cSNickeau if (!is_file($local_file)) { 606*04fd306cSNickeau throw new WebDriverException('You may only upload files: ' . $local_file); 607*04fd306cSNickeau } 608*04fd306cSNickeau 609*04fd306cSNickeau $temp_zip_path = $this->createTemporaryZipArchive($local_file); 610*04fd306cSNickeau 611*04fd306cSNickeau $remote_path = $this->executor->execute( 612*04fd306cSNickeau DriverCommand::UPLOAD_FILE, 613*04fd306cSNickeau ['file' => base64_encode(file_get_contents($temp_zip_path))] 614*04fd306cSNickeau ); 615*04fd306cSNickeau 616*04fd306cSNickeau unlink($temp_zip_path); 617*04fd306cSNickeau 618*04fd306cSNickeau return $remote_path; 619*04fd306cSNickeau } 620*04fd306cSNickeau 621*04fd306cSNickeau /** 622*04fd306cSNickeau * @param string $fileToZip 623*04fd306cSNickeau * @return string 624*04fd306cSNickeau */ 625*04fd306cSNickeau protected function createTemporaryZipArchive($fileToZip) 626*04fd306cSNickeau { 627*04fd306cSNickeau // Create a temporary file in the system temp directory. 628*04fd306cSNickeau // Intentionally do not use `tempnam()`, as it creates empty file which zip extension may not handle. 629*04fd306cSNickeau $tempZipPath = sys_get_temp_dir() . '/' . uniqid('WebDriverZip', false); 630*04fd306cSNickeau 631*04fd306cSNickeau $zip = new ZipArchive(); 632*04fd306cSNickeau if (($errorCode = $zip->open($tempZipPath, ZipArchive::CREATE)) !== true) { 633*04fd306cSNickeau throw new WebDriverException(sprintf('Error creating zip archive: %s', $errorCode)); 634*04fd306cSNickeau } 635*04fd306cSNickeau 636*04fd306cSNickeau $info = pathinfo($fileToZip); 637*04fd306cSNickeau $file_name = $info['basename']; 638*04fd306cSNickeau $zip->addFile($fileToZip, $file_name); 639*04fd306cSNickeau $zip->close(); 640*04fd306cSNickeau 641*04fd306cSNickeau return $tempZipPath; 642*04fd306cSNickeau } 643*04fd306cSNickeau} 644