xref: /template/strap/vendor/php-webdriver/webdriver/lib/Remote/RemoteWebElement.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
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