WebDriver::submitForm()   F
last analyzed

Complexity

Conditions 27
Paths 588

Size

Total Lines 78

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 27
nc 588
nop 3
dl 0
loc 78
rs 0.5722
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Codeception\Module;
4
5
use Codeception\Coverage\Subscriber\LocalServer;
6
use Codeception\Exception\ConnectionException;
7
use Codeception\Exception\ElementNotFound;
8
use Codeception\Exception\MalformedLocatorException;
9
use Codeception\Exception\ModuleConfigException as ModuleConfigException;
10
use Codeception\Exception\ModuleException;
11
use Codeception\Exception\TestRuntimeException;
12
use Codeception\Lib\Interfaces\ConflictsWithModule;
13
use Codeception\Lib\Interfaces\ElementLocator;
14
use Codeception\Lib\Interfaces\MultiSession as MultiSessionInterface;
15
use Codeception\Lib\Interfaces\PageSourceSaver;
16
use Codeception\Lib\Interfaces\Remote as RemoteInterface;
17
use Codeception\Lib\Interfaces\RequiresPackage;
18
use Codeception\Lib\Interfaces\ScreenshotSaver;
19
use Codeception\Lib\Interfaces\SessionSnapshot;
20
use Codeception\Lib\Interfaces\Web as WebInterface;
21
use Codeception\Module as CodeceptionModule;
22
use Codeception\PHPUnit\Constraint\Page as PageConstraint;
23
use Codeception\PHPUnit\Constraint\WebDriver as WebDriverConstraint;
24
use Codeception\PHPUnit\Constraint\WebDriverNot as WebDriverConstraintNot;
25
use Codeception\Test\Descriptor;
26
use Codeception\Test\Interfaces\ScenarioDriven;
27
use Codeception\TestInterface;
28
use Codeception\Util\ActionSequence;
29
use Codeception\Util\Debug;
30
use Codeception\Util\Locator;
31
use Codeception\Util\Uri;
32
use Facebook\WebDriver\Cookie;
33
use Facebook\WebDriver\Exception\InvalidElementStateException;
34
use Facebook\WebDriver\Exception\InvalidSelectorException;
35
use Facebook\WebDriver\Exception\NoSuchElementException;
36
use Facebook\WebDriver\Exception\UnknownServerException;
37
use Facebook\WebDriver\Exception\WebDriverCurlException;
38
use Facebook\WebDriver\Interactions\WebDriverActions;
39
use Facebook\WebDriver\Remote\LocalFileDetector;
40
use Facebook\WebDriver\Remote\RemoteWebDriver;
41
use Facebook\WebDriver\Remote\RemoteWebElement;
42
use Facebook\WebDriver\Remote\UselessFileDetector;
43
use Facebook\WebDriver\Remote\WebDriverCapabilityType;
44
use Facebook\WebDriver\WebDriverBy;
45
use Facebook\WebDriver\WebDriverDimension;
46
use Facebook\WebDriver\WebDriverElement;
47
use Facebook\WebDriver\WebDriverExpectedCondition;
48
use Facebook\WebDriver\WebDriverKeys;
49
use Facebook\WebDriver\WebDriverSelect;
50
use GuzzleHttp\Cookie\SetCookie;
51
use Symfony\Component\DomCrawler\Crawler;
52
53
/**
54
 * New generation Selenium WebDriver module.
55
 *
56
 * ## Local Testing
57
 *
58
 * ### Selenium
59
 *
60
 * To run Selenium Server you need [Java](https://www.java.com/) as well as Chrome or Firefox browser installed.
61
 *
62
 * 1. Download [Selenium Standalone Server](http://docs.seleniumhq.org/download/)
63
 * 2. To use Chrome, install [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/getting-started). To use Firefox, install [GeckoDriver](https://github.com/mozilla/geckodriver).
64
 * 3. Launch the Selenium Server: `java -jar selenium-server-standalone-3.xx.xxx.jar`. To locate Chromedriver binary use `-Dwebdriver.chrome.driver=./chromedriver` option. For Geckodriver use `-Dwebdriver.gecko.driver=./geckodriver`.
65
 * 4. Configure this module (in `acceptance.suite.yml`) by setting `url` and `browser`:
66
 *
67
 * ```yaml
68
 *     modules:
69
 *        enabled:
70
 *           - WebDriver:
71
 *              url: 'http://localhost/'
72
 *              browser: chrome # 'chrome' or 'firefox'
73
 * ```
74
 *
75
 * Launch Selenium Server before executing tests.
76
 *
77
 * ```
78
 * java -jar "/path/to/selenium-server-standalone-xxx.jar"
79
 * ```
80
 *
81
 * ### ChromeDriver
82
 *
83
 * To run tests in Chrome browser you may connect to ChromeDriver directly, without using Selenium Server.
84
 *
85
 * 1. Install [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/getting-started).
86
 * 2. Launch ChromeDriver: `chromedriver --url-base=/wd/hub`
87
 * 3. Configure this module to use ChromeDriver port:
88
 *
89
 * ```yaml
90
 *     modules:
91
 *        enabled:
92
 *           - WebDriver:
93
 *              url: 'http://localhost/'
94
 *              window_size: false # disabled in ChromeDriver
95
 *              port: 9515
96
 *              browser: chrome
97
 *              capabilities:
98
 *                  chromeOptions: # additional chrome options
99
 * ```
100
 *
101
 * Additional [Chrome options](https://sites.google.com/a/chromium.org/chromedriver/capabilities) can be set in `chromeOptions` capabilities.
102
 *
103
 *
104
 * ### PhantomJS
105
 *
106
 * PhantomJS is a [headless browser](https://en.wikipedia.org/wiki/Headless_browser) alternative to Selenium Server that implements
107
 * [the WebDriver protocol](https://code.google.com/p/selenium/wiki/JsonWireProtocol).
108
 * It allows you to run Selenium tests on a server without a GUI installed.
109
 *
110
 * 1. Download [PhantomJS](http://phantomjs.org/download.html)
111
 * 2. Run PhantomJS in WebDriver mode: `phantomjs --webdriver=4444`
112
 * 3. Configure this module (in `acceptance.suite.yml`) by setting url and `phantomjs` as browser:
113
 *
114
 * ```yaml
115
 *     modules:
116
 *        enabled:
117
 *           - WebDriver:
118
 *              url: 'http://localhost/'
119
 *              browser: phantomjs
120
 * ```
121
 *
122
 * Since PhantomJS doesn't give you any visual feedback, it's probably a good idea to install [Codeception\Extension\Recorder](http://codeception.com/extensions#CodeceptionExtensionRecorder) which gives you screenshots of how PhantomJS "sees" your pages.
123
 *
124
 * ### Headless Selenium in Docker
125
 *
126
 * Docker can ship Selenium Server with all its dependencies and browsers inside a single container.
127
 * Running tests inside Docker is as easy as pulling [official selenium image](https://github.com/SeleniumHQ/docker-selenium) and starting a container with Chrome:
128
 *
129
 * ```
130
 * docker run --net=host selenium/standalone-chrome
131
 * ```
132
 *
133
 * By using `--net=host` we allow selenium to access local websites.
134
 *
135
 * ## Cloud Testing
136
 *
137
 * Cloud Testing services can run your WebDriver tests in the cloud.
138
 * In case you want to test a local site or site behind a firewall
139
 * you should use a tunnel application provided by a service.
140
 *
141
 * ### SauceLabs
142
 *
143
 * 1. Create an account at [SauceLabs.com](http://SauceLabs.com) to get your username and access key
144
 * 2. In the module configuration use the format `username`:`access_key`@ondemand.saucelabs.com' for `host`
145
 * 3. Configure `platform` under `capabilities` to define the [Operating System](https://docs.saucelabs.com/reference/platforms-configurator/#/)
146
 * 4. run a tunnel app if your site can't be accessed from Internet
147
 *
148
 * ```yaml
149
 *     modules:
150
 *        enabled:
151
 *           - WebDriver:
152
 *              url: http://mysite.com
153
 *              host: '<username>:<access key>@ondemand.saucelabs.com'
154
 *              port: 80
155
 *              browser: chrome
156
 *              capabilities:
157
 *                  platform: 'Windows 10'
158
 * ```
159
 *
160
 * ### BrowserStack
161
 *
162
 * 1. Create an account at [BrowserStack](https://www.browserstack.com/) to get your username and access key
163
 * 2. In the module configuration use the format `username`:`access_key`@hub.browserstack.com' for `host`
164
 * 3. Configure `os` and `os_version` under `capabilities` to define the operating System
165
 * 4. If your site is available only locally or via VPN you should use a tunnel app. In this case add `browserstack.local` capability and set it to true.
166
 *
167
 * ```yaml
168
 *     modules:
169
 *        enabled:
170
 *           - WebDriver:
171
 *              url: http://mysite.com
172
 *              host: '<username>:<access key>@hub.browserstack.com'
173
 *              port: 80
174
 *              browser: chrome
175
 *              capabilities:
176
 *                  os: Windows
177
 *                  os_version: 10
178
 *                  browserstack.local: true # for local testing
179
 * ```
180
 * ### TestingBot
181
 *
182
 * 1. Create an account at [TestingBot](https://testingbot.com/) to get your key and secret
183
 * 2. In the module configuration use the format `key`:`secret`@hub.testingbot.com' for `host`
184
 * 3. Configure `platform` under `capabilities` to define the [Operating System](https://testingbot.com/support/getting-started/browsers.html)
185
 * 4. Run [TestingBot Tunnel](https://testingbot.com/support/other/tunnel) if your site can't be accessed from Internet
186
 *
187
 * ```yaml
188
 *     modules:
189
 *        enabled:
190
 *           - WebDriver:
191
 *              url: http://mysite.com
192
 *              host: '<key>:<secret>@hub.testingbot.com'
193
 *              port: 80
194
 *              browser: chrome
195
 *              capabilities:
196
 *                  platform: Windows 10
197
 * ```
198
 *
199
 * ## Configuration
200
 *
201
 * * `url` *required* - Starting URL for your app.
202
 * * `browser` *required* - Browser to launch.
203
 * * `host` - Selenium server host (127.0.0.1 by default).
204
 * * `port` - Selenium server port (4444 by default).
205
 * * `restart` - Set to `false` (default) to use the same browser window for all tests, or set to `true` to create a new window for each test. In any case, when all tests are finished the browser window is closed.
206
 * * `start` - Autostart a browser for tests. Can be disabled if browser session is started with `_initializeSession` inside a Helper.
207
 * * `window_size` - Initial window size. Set to `maximize` or a dimension in the format `640x480`.
208
 * * `clear_cookies` - Set to false to keep cookies, or set to true (default) to delete all cookies between tests.
209
 * * `wait` (default: 0 seconds) - Whenever element is required and is not on page, wait for n seconds to find it before fail.
210
 * * `capabilities` - Sets Selenium [desired capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities). Should be a key-value array.
211
 * * `connection_timeout` - timeout for opening a connection to remote selenium server (30 seconds by default).
212
 * * `request_timeout` - timeout for a request to return something from remote selenium server (30 seconds by default).
213
 * * `pageload_timeout` - amount of time to wait for a page load to complete before throwing an error (default 0 seconds).
214
 * * `http_proxy` - sets http proxy server url for testing a remote server.
215
 * * `http_proxy_port` - sets http proxy server port
216
 * * `debug_log_entries` - how many selenium entries to print with `debugWebDriverLogs` or on fail (15 by default).
217
 * * `log_js_errors` - Set to true to include possible JavaScript to HTML report, or set to false (default) to deactivate.
218
 *
219
 * Example (`acceptance.suite.yml`)
220
 *
221
 * ```yaml
222
 *     modules:
223
 *        enabled:
224
 *           - WebDriver:
225
 *              url: 'http://localhost/'
226
 *              browser: firefox
227
 *              window_size: 1024x768
228
 *              capabilities:
229
 *                  unexpectedAlertBehaviour: 'accept'
230
 *                  firefox_profile: '~/firefox-profiles/codeception-profile.zip.b64'
231
 * ```
232
 *
233
 * ## Usage
234
 *
235
 * ### Locating Elements
236
 *
237
 * Most methods in this module that operate on a DOM element (e.g. `click`) accept a locator as the first argument,
238
 * which can be either a string or an array.
239
 *
240
 * If the locator is an array, it should have a single element,
241
 * with the key signifying the locator type (`id`, `name`, `css`, `xpath`, `link`, or `class`)
242
 * and the value being the locator itself.
243
 * This is called a "strict" locator.
244
 * Examples:
245
 *
246
 * * `['id' => 'foo']` matches `<div id="foo">`
247
 * * `['name' => 'foo']` matches `<div name="foo">`
248
 * * `['css' => 'input[type=input][value=foo]']` matches `<input type="input" value="foo">`
249
 * * `['xpath' => "//input[@type='submit'][contains(@value, 'foo')]"]` matches `<input type="submit" value="foobar">`
250
 * * `['link' => 'Click here']` matches `<a href="google.com">Click here</a>`
251
 * * `['class' => 'foo']` matches `<div class="foo">`
252
 *
253
 * Writing good locators can be tricky.
254
 * The Mozilla team has written an excellent guide titled [Writing reliable locators for Selenium and WebDriver tests](https://blog.mozilla.org/webqa/2013/09/26/writing-reliable-locators-for-selenium-and-webdriver-tests/).
255
 *
256
 * If you prefer, you may also pass a string for the locator. This is called a "fuzzy" locator.
257
 * In this case, Codeception uses a a variety of heuristics (depending on the exact method called) to determine what element you're referring to.
258
 * For example, here's the heuristic used for the `submitForm` method:
259
 *
260
 * 1. Does the locator look like an ID selector (e.g. "#foo")? If so, try to find a form matching that ID.
261
 * 2. If nothing found, check if locator looks like a CSS selector. If so, run it.
262
 * 3. If nothing found, check if locator looks like an XPath expression. If so, run it.
263
 * 4. Throw an `ElementNotFound` exception.
264
 *
265
 * Be warned that fuzzy locators can be significantly slower than strict locators.
266
 * Especially if you use Selenium WebDriver with `wait` (aka implicit wait) option.
267
 * In the example above if you set `wait` to 5 seconds and use XPath string as fuzzy locator,
268
 * `submitForm` method will wait for 5 seconds at each step.
269
 * That means 5 seconds finding the form by ID, another 5 seconds finding by CSS
270
 * until it finally tries to find the form by XPath).
271
 * If speed is a concern, it's recommended you stick with explicitly specifying the locator type via the array syntax.
272
 *
273
 * ## Public Properties
274
 *
275
 * * `webDriver` - instance of `\Facebook\WebDriver\Remote\RemoteWebDriver`. Can be accessed from Helper classes for complex WebDriver interactions.
276
 *
277
 * ```php
278
 * // inside Helper class
279
 * $this->getModule('WebDriver')->webDriver->getKeyboard()->sendKeys('hello, webdriver');
280
 * ```
281
 *
282
 */
283
class WebDriver extends CodeceptionModule implements
284
    WebInterface,
285
    RemoteInterface,
286
    MultiSessionInterface,
287
    SessionSnapshot,
288
    ScreenshotSaver,
289
    PageSourceSaver,
290
    ElementLocator,
291
    ConflictsWithModule,
292
    RequiresPackage
293
{
294
    protected $requiredFields = ['browser', 'url'];
295
    protected $config = [
296
        'protocol'           => 'http',
297
        'host'               => '127.0.0.1',
298
        'port'               => '4444',
299
        'path'               => '/wd/hub',
300
        'start'              => true,
301
        'restart'            => false,
302
        'wait'               => 0,
303
        'clear_cookies'      => true,
304
        'window_size'        => false,
305
        'capabilities'       => [],
306
        'connection_timeout' => null,
307
        'request_timeout'    => null,
308
        'pageload_timeout'   => null,
309
        'http_proxy'         => null,
310
        'http_proxy_port'    => null,
311
        'ssl_proxy'          => null,
312
        'ssl_proxy_port'     => null,
313
        'debug_log_entries'  => 15,
314
        'log_js_errors'      => false
315
    ];
316
317
    protected $wdHost;
318
    protected $capabilities;
319
    protected $connectionTimeoutInMs;
320
    protected $requestTimeoutInMs;
321
    protected $test;
322
    protected $sessions = [];
323
    protected $sessionSnapshots = [];
324
    protected $httpProxy;
325
    protected $httpProxyPort;
326
    protected $sslProxy;
327
    protected $sslProxyPort;
328
329
    /**
330
     * @var RemoteWebDriver
331
     */
332
    public $webDriver;
333
334
    /**
335
     * @var RemoteWebElement
336
     */
337
    protected $baseElement;
338
339
    public function _requires()
340
    {
341
        return ['Facebook\WebDriver\Remote\RemoteWebDriver' => '"facebook/webdriver": "^1.0.1"'];
342
    }
343
344
    /**
345
     * @return RemoteWebElement
346
     * @throws ModuleException
347
     */
348
    protected function getBaseElement()
349
    {
350
        if (!$this->baseElement) {
351
            throw new ModuleException($this, "Page not loaded. Use `\$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it");
352
        }
353
        return $this->baseElement;
354
    }
355
356
    public function _initialize()
357
    {
358
        $this->wdHost = sprintf('%s://%s:%s%s', $this->config['protocol'], $this->config['host'], $this->config['port'], $this->config['path']);
359
        $this->capabilities = $this->config['capabilities'];
360
        $this->capabilities[WebDriverCapabilityType::BROWSER_NAME] = $this->config['browser'];
361
        if ($proxy = $this->getProxy()) {
362
            $this->capabilities[WebDriverCapabilityType::PROXY] = $proxy;
363
        }
364
        $this->connectionTimeoutInMs = $this->config['connection_timeout'] * 1000;
365
        $this->requestTimeoutInMs = $this->config['request_timeout'] * 1000;
366
        $this->loadFirefoxProfile();
367
    }
368
369
    /**
370
     * Change capabilities of WebDriver. Should be executed before starting a new browser session.
371
     * This method expects a function to be passed which returns array or [WebDriver Desired Capabilities](https://github.com/facebook/php-webdriver/blob/community/lib/Remote/DesiredCapabilities.php) object.
372
     * Additional [Chrome options](https://github.com/facebook/php-webdriver/wiki/ChromeOptions) (like adding extensions) can be passed as well.
373
     *
374
     * ```php
375
     * <?php // in helper
376
     * public function _before(TestInterface $test)
377
     * {
378
     *     $this->getModule('WebDriver')->_capabilities(function($currentCapabilities) {
379
     *         // or new \Facebook\WebDriver\Remote\DesiredCapabilities();
380
     *         return \Facebook\WebDriver\Remote\DesiredCapabilities::firefox();
381
     *     });
382
     * }
383
     * ```
384
     *
385
     * to make this work load `\Helper\Acceptance` before `WebDriver` in `acceptance.suite.yml`:
386
     *
387
     * ```yaml
388
     * modules:
389
     *     enabled:
390
     *         - \Helper\Acceptance
391
     *         - WebDriver
392
     * ```
393
     *
394
     * For instance, [**BrowserStack** cloud service](https://www.browserstack.com/automate/capabilities) may require a test name to be set in capabilities.
395
     * This is how it can be done via `_capabilities` method from `Helper\Acceptance`:
396
     *
397
     * ```php
398
     * <?php // inside Helper\Acceptance
399
     * public function _before(TestInterface $test)
400
     * {
401
     *      $name = $test->getMetadata()->getName();
402
     *      $this->getModule('WebDriver')->_capabilities(function($currentCapabilities) use ($name) {
403
     *          $currentCapabilities['name'] = $name;
404
     *          return $currentCapabilities;
405
     *      });
406
     * }
407
     * ```
408
     * In this case, please ensure that `\Helper\Acceptance` is loaded before WebDriver so new capabilities could be applied.
409
     *
410
     * @api
411
     * @param \Closure $capabilityFunction
412
     */
413
    public function _capabilities(\Closure $capabilityFunction)
414
    {
415
        $this->capabilities = $capabilityFunction($this->capabilities);
416
    }
417
418
    public function _conflicts()
419
    {
420
        return 'Codeception\Lib\Interfaces\Web';
421
    }
422
423
    public function _before(TestInterface $test)
424
    {
425
        if (!isset($this->webDriver) && $this->config['start']) {
426
            $this->_initializeSession();
427
        }
428
        $this->setBaseElement();
429
430
        if (method_exists($this->webDriver, 'getCapabilities')) {
431
            $browser = $this->webDriver->getCapabilities()->getBrowserName();
432
            $capabilities = $this->webDriver->getCapabilities()->toArray();
433
        } else {
434
            //Used with facebook/php-webdriver <1.3.0 (usually on PHP 5.4)
435
            $browser = $this->config['browser'];
436
            $capabilities = $this->config['capabilities'];
437
        }
438
        $test->getMetadata()->setCurrent(
439
            [
440
                'browser'      => $browser,
441
                'capabilities' => $capabilities,
442
            ]
443
        );
444
    }
445
446
    /**
447
     * Restarts a web browser.
448
     * Can be used with `_reconfigure` to open browser with different configuration
449
     *
450
     * ```php
451
     * <?php
452
     * // inside a Helper
453
     * $this->getModule('WebDriver')->_restart(); // just restart
454
     * $this->getModule('WebDriver')->_restart(['browser' => $browser]); // reconfigure + restart
455
     * ```
456
     *
457
     * @param array $config
458
     * @api
459
     */
460
    public function _restart($config = [])
461
    {
462
        $this->webDriver->quit();
463
        if (!empty($config)) {
464
            $this->_reconfigure($config);
465
        }
466
        $this->_initializeSession();
467
    }
468
469
    protected function onReconfigure()
470
    {
471
        $this->_initialize();
472
    }
473
474
    protected function loadFirefoxProfile()
475
    {
476
        if (!array_key_exists('firefox_profile', $this->config['capabilities'])) {
477
            return;
478
        }
479
480
        $firefox_profile = $this->config['capabilities']['firefox_profile'];
481
        if (file_exists($firefox_profile) === false) {
482
            throw new ModuleConfigException(
483
                __CLASS__,
484
                "Firefox profile does not exist under given path " . $firefox_profile
485
            );
486
        }
487
        // Set firefox profile as capability
488
        $this->capabilities['firefox_profile'] = file_get_contents($firefox_profile);
489
    }
490
491
    protected function initialWindowSize()
492
    {
493
        if ($this->config['window_size'] == 'maximize') {
494
            $this->maximizeWindow();
495
            return;
496
        }
497
        $size = explode('x', $this->config['window_size']);
498
        if (count($size) == 2) {
499
            $this->resizeWindow(intval($size[0]), intval($size[1]));
500
        }
501
    }
502
503
    public function _after(TestInterface $test)
504
    {
505
        if ($this->config['restart']) {
506
            $this->stopAllSessions();
507
            return;
508
        }
509
        if ($this->config['clear_cookies'] && isset($this->webDriver)) {
510
            try {
511
                $this->webDriver->manage()->deleteAllCookies();
512
            } catch (\Exception $e) {
513
                // may cause fatal errors when not handled
514
                $this->debug("Error, can't clean cookies after a test: " . $e->getMessage());
515
            }
516
        }
517
    }
518
519
    public function _failed(TestInterface $test, $fail)
520
    {
521
        $this->debugWebDriverLogs($test);
522
        $filename = preg_replace('~\W~', '.', Descriptor::getTestSignatureUnique($test));
0 ignored issues
show
Documentation introduced by
$test is of type object<Codeception\TestInterface>, but the function expects a object<PHPUnit\Framework\SelfDescribing>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
523
        $outputDir = codecept_output_dir();
524
        $this->_saveScreenshot($report = $outputDir . mb_strcut($filename, 0, 245, 'utf-8') . '.fail.png');
525
        $test->getMetadata()->addReport('png', $report);
526
        $this->_savePageSource($report = $outputDir . mb_strcut($filename, 0, 244, 'utf-8') . '.fail.html');
527
        $test->getMetadata()->addReport('html', $report);
528
        $this->debug("Screenshot and page source were saved into '$outputDir' dir");
529
    }
530
531
    /**
532
     * Print out latest Selenium Logs in debug mode
533
     *
534
     * @param TestInterface $test
535
     */
536
    public function debugWebDriverLogs(TestInterface $test = null)
537
    {
538
        if (!isset($this->webDriver)) {
539
            $this->debug('WebDriver::debugWebDriverLogs method has been called when webDriver is not set');
540
            return;
541
        }
542
        try {
543
            // Dump out latest Selenium logs
544
            $logs = $this->webDriver->manage()->getAvailableLogTypes();
545
            foreach ($logs as $logType) {
0 ignored issues
show
Bug introduced by
The expression $logs of type object<Facebook\WebDrive...mote\WebDriverResponse> is not traversable.
Loading history...
546
                $logEntries = array_slice(
547
                    $this->webDriver->manage()->getLog($logType),
548
                    -$this->config['debug_log_entries']
549
                );
550
551
                if (empty($logEntries)) {
552
                    $this->debugSection("Selenium {$logType} Logs", " EMPTY ");
553
                    continue;
554
                }
555
                $this->debugSection("Selenium {$logType} Logs", "\n" . $this->formatLogEntries($logEntries));
556
557
                if ($logType === 'browser' && $this->config['log_js_errors']
558
                    && ($test instanceof ScenarioDriven)
559
                ) {
560
                    $this->logJSErrors($test, $logEntries);
561
                }
562
            }
563
        } catch (\Exception $e) {
564
            $this->debug('Unable to retrieve Selenium logs : ' . $e->getMessage());
565
        }
566
    }
567
568
    /**
569
     * Turns an array of log entries into a human-readable string.
570
     * Each log entry is an array with the keys "timestamp", "level", and "message".
571
     * See https://code.google.com/p/selenium/wiki/JsonWireProtocol#Log_Entry_JSON_Object
572
     *
573
     * @param array $logEntries
574
     * @return string
575
     */
576
    protected function formatLogEntries(array $logEntries)
577
    {
578
        $formattedLogs = '';
579
580
        foreach ($logEntries as $logEntry) {
581
            // Timestamp is in milliseconds, but date() requires seconds.
582
            $time = date('H:i:s', $logEntry['timestamp'] / 1000) .
583
                // Append the milliseconds to the end of the time string
584
                '.' . ($logEntry['timestamp'] % 1000);
585
            $formattedLogs .= "{$time} {$logEntry['level']} - {$logEntry['message']}\n";
586
        }
587
        return $formattedLogs;
588
    }
589
590
    /**
591
     * Logs JavaScript errors as comments.
592
     *
593
     * @param ScenarioDriven $test
594
     * @param array $browserLogEntries
595
     */
596
    protected function logJSErrors(ScenarioDriven $test, array $browserLogEntries)
597
    {
598
        foreach ($browserLogEntries as $logEntry) {
599
            if (true === isset($logEntry['level'])
600
                && true === isset($logEntry['message'])
601
                && $this->isJSError($logEntry['level'], $logEntry['message'])
602
            ) {
603
                // Timestamp is in milliseconds, but date() requires seconds.
604
                $time = date('H:i:s', $logEntry['timestamp'] / 1000) .
605
                    // Append the milliseconds to the end of the time string
606
                    '.' . ($logEntry['timestamp'] % 1000);
607
                $test->getScenario()->comment("{$time} {$logEntry['level']} - {$logEntry['message']}");
608
            }
609
        }
610
    }
611
612
    /**
613
     * Determines if the log entry is an error.
614
     * The decision is made depending on browser and log-level.
615
     *
616
     * @param string $logEntryLevel
617
     * @param string $message
618
     * @return bool
619
     */
620
    protected function isJSError($logEntryLevel, $message)
621
    {
622
        return
623
            (
624
                ($this->isPhantom() && $logEntryLevel != 'INFO')          // phantomjs logs errors as "WARNING"
625
                || $logEntryLevel === 'SEVERE'                            // other browsers log errors as "SEVERE"
626
            )
627
            && strpos($message, 'ERR_PROXY_CONNECTION_FAILED') === false;  // ignore blackhole proxy
628
    }
629
630
    public function _afterSuite()
631
    {
632
        // this is just to make sure webDriver is cleared after suite
633
        $this->stopAllSessions();
634
    }
635
636
    protected function stopAllSessions()
637
    {
638
        foreach ($this->sessions as $session) {
639
            $this->_closeSession($session);
640
        }
641
        $this->webDriver = null;
642
        $this->baseElement = null;
643
    }
644
645 View Code Duplication
    public function amOnSubdomain($subdomain)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
646
    {
647
        $url = $this->config['url'];
648
        $url = preg_replace('~(https?:\/\/)(.*\.)(.*\.)~', "$1$3", $url); // removing current subdomain
649
        $url = preg_replace('~(https?:\/\/)(.*)~', "$1$subdomain.$2", $url); // inserting new
650
        $this->_reconfigure(['url' => $url]);
651
    }
652
653
    /**
654
     * Returns URL of a host.
655
     *
656
     * @api
657
     * @return mixed
658
     * @throws ModuleConfigException
659
     */
660
    public function _getUrl()
661
    {
662
        if (!isset($this->config['url'])) {
663
            throw new ModuleConfigException(
664
                __CLASS__,
665
                "Module connection failure. The URL for client can't bre retrieved"
666
            );
667
        }
668
        return $this->config['url'];
669
    }
670
671
    protected function getProxy()
672
    {
673
        $proxyConfig = [];
674
        if ($this->config['http_proxy']) {
675
            $proxyConfig['httpProxy'] = $this->config['http_proxy'];
676
            if ($this->config['http_proxy_port']) {
677
                $proxyConfig['httpProxy'] .= ':' . $this->config['http_proxy_port'];
678
            }
679
        }
680
        if ($this->config['ssl_proxy']) {
681
            $proxyConfig['sslProxy'] = $this->config['ssl_proxy'];
682
            if ($this->config['ssl_proxy_port']) {
683
                $proxyConfig['sslProxy'] .= ':' . $this->config['ssl_proxy_port'];
684
            }
685
        }
686
        if (!empty($proxyConfig)) {
687
            $proxyConfig['proxyType'] = 'manual';
688
            return $proxyConfig;
689
        }
690
        return null;
691
    }
692
693
    /**
694
     * Uri of currently opened page.
695
     * @return string
696
     * @api
697
     * @throws ModuleException
698
     */
699
    public function _getCurrentUri()
700
    {
701
        $url = $this->webDriver->getCurrentURL();
702
        if ($url == 'about:blank' || strpos($url, 'data:') === 0) {
703
            throw new ModuleException($this, 'Current url is blank, no page was opened');
704
        }
705
        return Uri::retrieveUri($url);
706
    }
707
708 View Code Duplication
    public function _saveScreenshot($filename)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
709
    {
710
        if (!isset($this->webDriver)) {
711
            $this->debug('WebDriver::_saveScreenshot method has been called when webDriver is not set');
712
            return;
713
        }
714
        try {
715
            $this->webDriver->takeScreenshot($filename);
716
        } catch (\Exception $e) {
717
            $this->debug('Unable to retrieve screenshot from Selenium : ' . $e->getMessage());
718
        }
719
    }
720
721
    public function _findElements($locator)
722
    {
723
        return $this->match($this->webDriver, $locator);
724
    }
725
726
    /**
727
     * Saves HTML source of a page to a file
728
     * @param $filename
729
     */
730 View Code Duplication
    public function _savePageSource($filename)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
731
    {
732
        if (!isset($this->webDriver)) {
733
            $this->debug('WebDriver::_savePageSource method has been called when webDriver is not set');
734
            return;
735
        }
736
        try {
737
            file_put_contents($filename, $this->webDriver->getPageSource());
738
        } catch (\Exception $e) {
739
            $this->debug('Unable to retrieve source page from Selenium : ' . $e->getMessage());
740
        }
741
    }
742
743
    /**
744
     * Takes a screenshot of the current window and saves it to `tests/_output/debug`.
745
     *
746
     * ``` php
747
     * <?php
748
     * $I->amOnPage('/user/edit');
749
     * $I->makeScreenshot('edit_page');
750
     * // saved to: tests/_output/debug/edit_page.png
751
     * $I->makeScreenshot();
752
     * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.png
753
     * ```
754
     *
755
     * @param $name
756
     */
757
    public function makeScreenshot($name = null)
758
    {
759
        if (empty($name)) {
760
            $name = uniqid(date("Y-m-d_H-i-s_"));
761
        }
762
        $debugDir = codecept_log_dir() . 'debug';
763
        if (!is_dir($debugDir)) {
764
            mkdir($debugDir, 0777);
765
        }
766
        $screenName = $debugDir . DIRECTORY_SEPARATOR . $name . '.png';
767
        $this->_saveScreenshot($screenName);
768
        $this->debug("Screenshot saved to $screenName");
769
    }
770
771
    /**
772
     * Resize the current window.
773
     *
774
     * ``` php
775
     * <?php
776
     * $I->resizeWindow(800, 600);
777
     *
778
     * ```
779
     *
780
     * @param int $width
781
     * @param int $height
782
     */
783
    public function resizeWindow($width, $height)
784
    {
785
        $this->webDriver->manage()->window()->setSize(new WebDriverDimension($width, $height));
786
    }
787
788 View Code Duplication
    public function seeCookie($cookie, array $params = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
789
    {
790
        $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params);
791
        $cookies = array_map(
792
            function ($c) {
793
                return $c['name'];
794
            },
795
            $cookies
796
        );
797
        $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies()));
798
        $this->assertContains($cookie, $cookies);
799
    }
800
801 View Code Duplication
    public function dontSeeCookie($cookie, array $params = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
802
    {
803
        $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params);
804
        $cookies = array_map(
805
            function ($c) {
806
                return $c['name'];
807
            },
808
            $cookies
809
        );
810
        $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies()));
811
        $this->assertNotContains($cookie, $cookies);
812
    }
813
814
    public function setCookie($cookie, $value, array $params = [])
815
    {
816
        $params['name'] = $cookie;
817
        $params['value'] = $value;
818
        if (isset($params['expires'])) { // PhpBrowser compatibility
819
            $params['expiry'] = $params['expires'];
820
        }
821
        if (!isset($params['domain'])) {
822
            $urlParts = parse_url($this->config['url']);
823
            if (isset($urlParts['host'])) {
824
                $params['domain'] = $urlParts['host'];
825
            }
826
        }
827
        $this->webDriver->manage()->addCookie($params);
828
        $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies()));
829
    }
830
831
    public function resetCookie($cookie, array $params = [])
832
    {
833
        $this->webDriver->manage()->deleteCookieNamed($cookie);
834
        $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies()));
835
    }
836
837
    public function grabCookie($cookie, array $params = [])
838
    {
839
        $params['name'] = $cookie;
840
        $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params);
841
        if (empty($cookies)) {
842
            return null;
843
        }
844
        $cookie = reset($cookies);
845
        return $cookie['value'];
846
    }
847
848
    /**
849
     * Grabs current page source code.
850
     *
851
     * @throws ModuleException if no page was opened.
852
     *
853
     * @return string Current page source code.
854
     */
855
    public function grabPageSource()
856
    {
857
        // Make sure that some page was opened.
858
        $this->_getCurrentUri();
859
860
        return $this->webDriver->getPageSource();
861
    }
862
863
    protected function filterCookies($cookies, $params = [])
864
    {
865
        foreach (['domain', 'path', 'name'] as $filter) {
866
            if (!isset($params[$filter])) {
867
                continue;
868
            }
869
            $cookies = array_filter(
870
                $cookies,
871
                function ($item) use ($filter, $params) {
872
                    return $item[$filter] == $params[$filter];
873
                }
874
            );
875
        }
876
        return $cookies;
877
    }
878
879
    public function amOnUrl($url)
880
    {
881
        $host = Uri::retrieveHost($url);
882
        $this->_reconfigure(['url' => $host]);
883
        $this->debugSection('Host', $host);
884
        $this->webDriver->get($url);
885
    }
886
887
    public function amOnPage($page)
888
    {
889
        $url = Uri::appendPath($this->config['url'], $page);
890
        $this->debugSection('GET', $url);
891
        $this->webDriver->get($url);
892
    }
893
894
    public function see($text, $selector = null)
895
    {
896
        if (!$selector) {
897
            return $this->assertPageContains($text);
898
        }
899
        $this->enableImplicitWait();
900
        $nodes = $this->matchVisible($selector);
901
        $this->disableImplicitWait();
902
        $this->assertNodesContain($text, $nodes, $selector);
903
    }
904
905
    public function dontSee($text, $selector = null)
906
    {
907
        if (!$selector) {
908
            return $this->assertPageNotContains($text);
909
        }
910
        $nodes = $this->matchVisible($selector);
911
        $this->assertNodesNotContain($text, $nodes, $selector);
912
    }
913
914
    public function seeInSource($raw)
915
    {
916
        $this->assertPageSourceContains($raw);
917
    }
918
919
    public function dontSeeInSource($raw)
920
    {
921
        $this->assertPageSourceNotContains($raw);
922
    }
923
924
    /**
925
     * Checks that the page source contains the given string.
926
     *
927
     * ```php
928
     * <?php
929
     * $I->seeInPageSource('<link rel="apple-touch-icon"');
930
     * ```
931
     *
932
     * @param $text
933
     */
934
    public function seeInPageSource($text)
935
    {
936
        $this->assertThat(
937
            $this->webDriver->getPageSource(),
938
            new PageConstraint($text, $this->_getCurrentUri()),
939
            ''
940
        );
941
    }
942
943
    /**
944
     * Checks that the page source doesn't contain the given string.
945
     *
946
     * @param $text
947
     */
948
    public function dontSeeInPageSource($text)
949
    {
950
        $this->assertThatItsNot(
951
            $this->webDriver->getPageSource(),
952
            new PageConstraint($text, $this->_getCurrentUri()),
953
            ''
954
        );
955
    }
956
957
    public function click($link, $context = null)
958
    {
959
        $page = $this->webDriver;
960
        if ($context) {
961
            $page = $this->matchFirstOrFail($this->webDriver, $context);
962
        }
963
        $el = $this->_findClickable($page, $link);
0 ignored issues
show
Bug introduced by
It seems like $page defined by $this->matchFirstOrFail(...s->webDriver, $context) on line 961 can also be of type object<Facebook\WebDriver\WebDriverElement>; however, Codeception\Module\WebDriver::_findClickable() does only seem to accept object<Facebook\WebDriver\Remote\RemoteWebDriver>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
964
        if (!$el) { // check one more time if this was a CSS selector we didn't match
965
            try {
966
                $els = $this->match($page, $link);
967
            } catch (MalformedLocatorException $e) {
968
                throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button");
969
            }
970
            $el = reset($els);
971
        }
972
        if (!$el) {
973
            throw new ElementNotFound($link, 'Link or Button or CSS or XPath');
974
        }
975
        $el->click();
976
    }
977
978
    /**
979
     * Locates a clickable element.
980
     *
981
     * Use it in Helpers or GroupObject or Extension classes:
982
     *
983
     * ```php
984
     * <?php
985
     * $module = $this->getModule('WebDriver');
986
     * $page = $module->webDriver;
987
     *
988
     * // search a link or button on a page
989
     * $el = $module->_findClickable($page, 'Click Me');
990
     *
991
     * // search a link or button within an element
992
     * $topBar = $module->_findElements('.top-bar')[0];
993
     * $el = $module->_findClickable($topBar, 'Click Me');
994
     *
995
     * ```
996
     * @api
997
     * @param RemoteWebDriver $page WebDriver instance or an element to search within
998
     * @param $link a link text or locator to click
999
     * @return WebDriverElement
1000
     */
1001
    public function _findClickable($page, $link)
1002
    {
1003
        if (is_array($link) or ($link instanceof WebDriverBy)) {
1004
            return $this->matchFirstOrFail($page, $link);
1005
        }
1006
1007
        // try to match by strict locators, CSS Ids or XPath
1008
        if (Locator::isPrecise($link)) {
1009
            return $this->matchFirstOrFail($page, $link);
1010
        }
1011
1012
        $locator = Crawler::xpathLiteral(trim($link));
1013
1014
        // narrow
1015
        $xpath = Locator::combine(
1016
            ".//a[normalize-space(.)=$locator]",
1017
            ".//button[normalize-space(.)=$locator]",
1018
            ".//a/img[normalize-space(@alt)=$locator]/ancestor::a",
1019
            ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][normalize-space(@value)=$locator]"
1020
        );
1021
1022
        $els = $page->findElements(WebDriverBy::xpath($xpath));
1023
        if (count($els)) {
1024
            return reset($els);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression reset($els); of type Facebook\WebDriver\Remote\RemoteWebElement|false adds false to the return on line 1024 which is incompatible with the return type documented by Codeception\Module\WebDriver::_findClickable of type Facebook\WebDriver\WebDriverElement|null. It seems like you forgot to handle an error condition.
Loading history...
1025
        }
1026
1027
        // wide
1028
        $xpath = Locator::combine(
1029
            ".//a[./@href][((contains(normalize-space(string(.)), $locator)) or contains(./@title, $locator) or .//img[contains(./@alt, $locator)])]",
1030
            ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][contains(./@value, $locator)]",
1031
            ".//input[./@type = 'image'][contains(./@alt, $locator)]",
1032
            ".//button[contains(normalize-space(string(.)), $locator)]",
1033
            ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][./@name = $locator or ./@title = $locator]",
1034
            ".//button[./@name = $locator or ./@title = $locator]"
1035
        );
1036
1037
        $els = $page->findElements(WebDriverBy::xpath($xpath));
1038
        if (count($els)) {
1039
            return reset($els);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression reset($els); of type Facebook\WebDriver\Remote\RemoteWebElement|false adds false to the return on line 1039 which is incompatible with the return type documented by Codeception\Module\WebDriver::_findClickable of type Facebook\WebDriver\WebDriverElement|null. It seems like you forgot to handle an error condition.
Loading history...
1040
        }
1041
1042
        return null;
1043
    }
1044
1045
    /**
1046
     * @param $selector
1047
     * @return WebDriverElement[]
1048
     * @throws \Codeception\Exception\ElementNotFound
1049
     */
1050
    protected function findFields($selector)
1051
    {
1052
        if ($selector instanceof WebDriverElement) {
1053
            return [$selector];
1054
        }
1055
        if (is_array($selector) || ($selector instanceof WebDriverBy)) {
1056
            $fields = $this->match($this->webDriver, $selector);
1057
1058
            if (empty($fields)) {
1059
                throw new ElementNotFound($selector);
1060
            }
1061
            return $fields;
1062
        }
1063
1064
        $locator = Crawler::xpathLiteral(trim($selector));
1065
        // by text or label
1066
        $xpath = Locator::combine(
1067
        // @codingStandardsIgnoreStart
1068
            ".//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = $locator) or ./@id = //label[contains(normalize-space(string(.)), $locator)]/@for) or ./@placeholder = $locator)]",
1069
            ".//label[contains(normalize-space(string(.)), $locator)]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]"
1070
        // @codingStandardsIgnoreEnd
1071
        );
1072
        $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath));
1073
        if (!empty($fields)) {
1074
            return $fields;
1075
        }
1076
1077
        // by name
1078
        $xpath = ".//*[self::input | self::textarea | self::select][@name = $locator]";
1079
        $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath));
1080
        if (!empty($fields)) {
1081
            return $fields;
1082
        }
1083
1084
        // try to match by CSS or XPath
1085
        $fields = $this->match($this->webDriver, $selector, false);
1086
        if (!empty($fields)) {
1087
            return $fields;
1088
        }
1089
1090
        throw new ElementNotFound($selector, "Field by name, label, CSS or XPath");
1091
    }
1092
1093
    /**
1094
     * @param $selector
1095
     * @return WebDriverElement
1096
     * @throws \Codeception\Exception\ElementNotFound
1097
     */
1098
    protected function findField($selector)
1099
    {
1100
        $arr = $this->findFields($selector);
1101
        return reset($arr);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression reset($arr); of type Facebook\WebDriver\WebDriverElement|false adds false to the return on line 1101 which is incompatible with the return type documented by Codeception\Module\WebDriver::findField of type Facebook\WebDriver\WebDriverElement. It seems like you forgot to handle an error condition.
Loading history...
1102
    }
1103
1104
    public function seeLink($text, $url = null)
1105
    {
1106
        $this->enableImplicitWait();
1107
        $nodes = $this->getBaseElement()->findElements(WebDriverBy::partialLinkText($text));
1108
        $this->disableImplicitWait();
1109
        $currentUri = $this->_getCurrentUri();
1110
1111
        if (empty($nodes)) {
1112
            $this->fail("No links containing text '$text' were found in page $currentUri");
1113
        }
1114
        if ($url) {
1115
            $nodes = $this->filterNodesByHref($url, $nodes);
1116
        }
1117
        $this->assertNotEmpty($nodes, "No links containing text '$text' and URL '$url' were found in page $currentUri");
1118
    }
1119
1120
    public function dontSeeLink($text, $url = null)
1121
    {
1122
        $nodes = $this->getBaseElement()->findElements(WebDriverBy::partialLinkText($text));
1123
        $currentUri = $this->_getCurrentUri();
1124
        if (!$url) {
1125
            $this->assertEmpty($nodes, "Link containing text '$text' was found in page $currentUri");
1126
        } else {
1127
            $nodes = $this->filterNodesByHref($url, $nodes);
1128
            $this->assertEmpty($nodes, "Link containing text '$text' and URL '$url' was found in page $currentUri");
1129
        }
1130
    }
1131
1132
    /**
1133
     * @param string $url
1134
     * @param $nodes
1135
     * @return array
1136
     */
1137
    private function filterNodesByHref($url, $nodes)
1138
    {
1139
        //current uri can be relative, merging it with configured base url gives absolute url
1140
        $absoluteCurrentUrl = Uri::mergeUrls($this->_getUrl(), $this->_getCurrentUri());
1141
        $expectedUrl = Uri::mergeUrls($absoluteCurrentUrl, $url);
1142
1143
        $nodes = array_filter(
1144
            $nodes,
1145
            function (WebDriverElement $e) use ($expectedUrl, $absoluteCurrentUrl) {
1146
                $elementHref = Uri::mergeUrls($absoluteCurrentUrl, $e->getAttribute('href'));
1147
                return $elementHref === $expectedUrl;
1148
            }
1149
        );
1150
        return $nodes;
1151
    }
1152
1153
    public function seeInCurrentUrl($uri)
1154
    {
1155
        $this->assertContains($uri, $this->_getCurrentUri());
1156
    }
1157
1158
    public function seeCurrentUrlEquals($uri)
1159
    {
1160
        $this->assertEquals($uri, $this->_getCurrentUri());
1161
    }
1162
1163
    public function seeCurrentUrlMatches($uri)
1164
    {
1165
        $this->assertRegExp($uri, $this->_getCurrentUri());
1166
    }
1167
1168
    public function dontSeeInCurrentUrl($uri)
1169
    {
1170
        $this->assertNotContains($uri, $this->_getCurrentUri());
1171
    }
1172
1173
    public function dontSeeCurrentUrlEquals($uri)
1174
    {
1175
        $this->assertNotEquals($uri, $this->_getCurrentUri());
1176
    }
1177
1178
    public function dontSeeCurrentUrlMatches($uri)
1179
    {
1180
        $this->assertNotRegExp($uri, $this->_getCurrentUri());
1181
    }
1182
1183 View Code Duplication
    public function grabFromCurrentUrl($uri = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1184
    {
1185
        if (!$uri) {
1186
            return $this->_getCurrentUri();
1187
        }
1188
        $matches = [];
1189
        $res = preg_match($uri, $this->_getCurrentUri(), $matches);
1190
        if (!$res) {
1191
            $this->fail("Couldn't match $uri in " . $this->_getCurrentUri());
1192
        }
1193
        if (!isset($matches[1])) {
1194
            $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'");
1195
        }
1196
        return $matches[1];
1197
    }
1198
1199
    public function seeCheckboxIsChecked($checkbox)
1200
    {
1201
        $this->assertTrue($this->findField($checkbox)->isSelected());
1202
    }
1203
1204
    public function dontSeeCheckboxIsChecked($checkbox)
1205
    {
1206
        $this->assertFalse($this->findField($checkbox)->isSelected());
1207
    }
1208
1209
    public function seeInField($field, $value)
1210
    {
1211
        $els = $this->findFields($field);
1212
        $this->assert($this->proceedSeeInField($els, $value));
0 ignored issues
show
Documentation introduced by
$els is of type array<integer,object<Fac...iver\WebDriverElement>>, but the function expects a array<integer,object<Fac...mote\RemoteWebElement>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1213
    }
1214
1215
    public function dontSeeInField($field, $value)
1216
    {
1217
        $els = $this->findFields($field);
1218
        $this->assertNot($this->proceedSeeInField($els, $value));
0 ignored issues
show
Documentation introduced by
$els is of type array<integer,object<Fac...iver\WebDriverElement>>, but the function expects a array<integer,object<Fac...mote\RemoteWebElement>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1219
    }
1220
1221
    public function seeInFormFields($formSelector, array $params)
1222
    {
1223
        $this->proceedSeeInFormFields($formSelector, $params, false);
1224
    }
1225
1226
    public function dontSeeInFormFields($formSelector, array $params)
1227
    {
1228
        $this->proceedSeeInFormFields($formSelector, $params, true);
1229
    }
1230
1231
    protected function proceedSeeInFormFields($formSelector, array $params, $assertNot)
1232
    {
1233
        $form = $this->match($this->getBaseElement(), $formSelector);
1234
        if (empty($form)) {
1235
            throw new ElementNotFound($formSelector, "Form via CSS or XPath");
1236
        }
1237
        $form = reset($form);
1238
1239
        $els = [];
1240
        foreach ($params as $name => $values) {
1241
            $this->pushFormField($els, $form, $name, $values);
1242
        }
1243
1244 View Code Duplication
        foreach ($els as $arrayElement) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1245
            list($el, $values) = $arrayElement;
1246
1247
            if (!is_array($values)) {
1248
                $values = [$values];
1249
            }
1250
1251
            foreach ($values as $value) {
1252
                $ret = $this->proceedSeeInField($el, $value);
1253
                if ($assertNot) {
1254
                    $this->assertNot($ret);
1255
                } else {
1256
                    $this->assert($ret);
1257
                }
1258
            }
1259
        }
1260
    }
1261
1262
    /**
1263
     * Map an array element passed to seeInFormFields to its corresponding WebDriver element,
1264
     * recursing through array values if the field is not found.
1265
     *
1266
     * @param array $els The previously found elements.
1267
     * @param RemoteWebElement $form The form in which to search for fields.
1268
     * @param string $name The field's name.
1269
     * @param mixed $values
1270
     * @return void
1271
     */
1272
    protected function pushFormField(&$els, $form, $name, $values)
1273
    {
1274
        $el = $form->findElements(WebDriverBy::name($name));
1275
1276
        if ($el) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $el of type Facebook\WebDriver\Remote\RemoteWebElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1277
            $els[] = [$el, $values];
1278 View Code Duplication
        } elseif (is_array($values)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1279
            foreach ($values as $key => $value) {
1280
                $this->pushFormField($els, $form, "{$name}[$key]", $value);
1281
            }
1282
        } else {
1283
            throw new ElementNotFound($name);
1284
        }
1285
    }
1286
1287
    /**
1288
     * @param RemoteWebElement[] $elements
1289
     * @param $value
1290
     * @return array
1291
     */
1292
    protected function proceedSeeInField(array $elements, $value)
1293
    {
1294
        $strField = reset($elements)->getAttribute('name');
1295
        if (reset($elements)->getTagName() === 'select') {
1296
            $el = reset($elements);
1297
            $elements = $el->findElements(WebDriverBy::xpath('.//option'));
1298
            if (empty($value) && empty($elements)) {
1299
                return ['True', true];
1300
            }
1301
        }
1302
1303
        $currentValues = [];
1304
        if (is_bool($value)) {
1305
            $currentValues = [false];
1306
        }
1307
        foreach ($elements as $el) {
1308
            switch ($el->getTagName()) {
1309
                case 'input':
1310
                    if ($el->getAttribute('type') === 'radio' || $el->getAttribute('type') === 'checkbox') {
1311
                        if ($el->getAttribute('checked')) {
1312
                            if (is_bool($value)) {
1313
                                $currentValues = [true];
1314
                                break;
1315
                            } else {
1316
                                $currentValues[] = $el->getAttribute('value');
1317
                            }
1318
                        }
1319
                    } else {
1320
                        $currentValues[] = $el->getAttribute('value');
1321
                    }
1322
                    break;
1323
                case 'option':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
1324
                    // no break we need the trim text and the value also
1325
                    if (!$el->isSelected()) {
1326
                        break;
1327
                    }
1328
                    $currentValues[] = $el->getText();
1329
                case 'textarea':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
1330
                    // we include trimmed and real value of textarea for check
1331
                    $currentValues[] = trim($el->getText());
1332
                default:
1333
                    $currentValues[] = $el->getAttribute('value'); // raw value
1334
                    break;
1335
            }
1336
        }
1337
1338
        return [
1339
            'Contains',
1340
            $value,
1341
            $currentValues,
1342
            "Failed testing for '$value' in $strField's value: '" . implode("', '", $currentValues) . "'"
1343
        ];
1344
    }
1345
1346
    public function selectOption($select, $option)
1347
    {
1348
        $el = $this->findField($select);
1349
        if ($el->getTagName() != 'select') {
1350
            $els = $this->matchCheckables($select);
1351
            $radio = null;
1352
            foreach ($els as $el) {
1353
                $radio = $this->findCheckable($el, $option, true);
1354
                if ($radio) {
1355
                    break;
1356
                }
1357
            }
1358
            if (!$radio) {
1359
                throw new ElementNotFound($select, "Radiobutton with value or name '$option in");
1360
            }
1361
            $radio->click();
1362
            return;
1363
        }
1364
1365
        $wdSelect = new WebDriverSelect($el);
1366
        if ($wdSelect->isMultiple()) {
1367
            $wdSelect->deselectAll();
1368
        }
1369
        if (!is_array($option)) {
1370
            $option = [$option];
1371
        }
1372
1373
        $matched = false;
1374
1375
        if (key($option) !== 'value') {
1376
            foreach ($option as $opt) {
1377
                try {
1378
                    $wdSelect->selectByVisibleText($opt);
1379
                    $matched = true;
1380
                } catch (NoSuchElementException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1381
                }
1382
            }
1383
        }
1384
1385
        if ($matched) {
1386
            return;
1387
        }
1388
1389
        if (key($option) !== 'text') {
1390
            foreach ($option as $opt) {
1391
                try {
1392
                    $wdSelect->selectByValue($opt);
1393
                    $matched = true;
1394
                } catch (NoSuchElementException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1395
                }
1396
            }
1397
        }
1398
1399
        if ($matched) {
1400
            return;
1401
        }
1402
1403
        // partially matching
1404
        foreach ($option as $opt) {
1405
            try {
1406
                $optElement = $el->findElement(WebDriverBy::xpath('.//option [contains (., "' . $opt . '")]'));
1407
                $matched = true;
1408
                if (!$optElement->isSelected()) {
1409
                    $optElement->click();
1410
                }
1411
            } catch (NoSuchElementException $e) {
1412
                // exception treated at the end
1413
            }
1414
        }
1415
        if ($matched) {
1416
            return;
1417
        }
1418
        throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value");
1419
    }
1420
1421
    /**
1422
     * Manually starts a new browser session.
1423
     *
1424
     * ```php
1425
     * <?php
1426
     * $this->getModule('WebDriver')->_initializeSession();
1427
     * ```
1428
     *
1429
     * @api
1430
     */
1431
    public function _initializeSession()
1432
    {
1433
        try {
1434
            $this->sessions[] = $this->webDriver;
1435
            $this->webDriver = RemoteWebDriver::create(
1436
                $this->wdHost,
1437
                $this->capabilities,
1438
                $this->connectionTimeoutInMs,
1439
                $this->requestTimeoutInMs,
1440
                $this->httpProxy,
1441
                $this->httpProxyPort
1442
            );
1443
            if (!is_null($this->config['pageload_timeout'])) {
1444
                $this->webDriver->manage()->timeouts()->pageLoadTimeout($this->config['pageload_timeout']);
1445
            }
1446
            $this->setBaseElement();
1447
            $this->initialWindowSize();
1448
        } catch (WebDriverCurlException $e) {
1449
            throw new ConnectionException("Can't connect to Webdriver at {$this->wdHost}. Please make sure that Selenium Server or PhantomJS is running.");
1450
        }
1451
    }
1452
1453
    /**
1454
     * Loads current RemoteWebDriver instance as a session
1455
     *
1456
     * @api
1457
     * @param RemoteWebDriver $session
1458
     */
1459
    public function _loadSession($session)
1460
    {
1461
        $this->webDriver = $session;
1462
        $this->setBaseElement();
1463
    }
1464
1465
    /**
1466
     * Returns current WebDriver session for saving
1467
     *
1468
     * @api
1469
     * @return RemoteWebDriver
1470
     */
1471
    public function _backupSession()
1472
    {
1473
        return $this->webDriver;
1474
    }
1475
1476
    /**
1477
     * Manually closes current WebDriver session.
1478
     *
1479
     * ```php
1480
     * <?php
1481
     * $this->getModule('WebDriver')->_closeSession();
1482
     *
1483
     * // close a specific session
1484
     * $webDriver = $this->getModule('WebDriver')->webDriver;
1485
     * $this->getModule('WebDriver')->_closeSession($webDriver);
1486
     * ```
1487
     *
1488
     * @api
1489
     * @param $webDriver (optional) a specific webdriver session instance
1490
     */
1491
    public function _closeSession($webDriver = null)
1492
    {
1493
        if (!$webDriver and $this->webDriver) {
1494
            $webDriver = $this->webDriver;
1495
        }
1496
        if (!$webDriver) {
1497
            return;
1498
        }
1499
        try {
1500
            $webDriver->quit();
1501
            unset($webDriver);
1502
        } catch (UnknownServerException $e) {
1503
            // Session already closed so nothing to do
1504
        }
1505
    }
1506
1507
    /**
1508
     * Unselect an option in the given select box.
1509
     *
1510
     * @param $select
1511
     * @param $option
1512
     */
1513
    public function unselectOption($select, $option)
1514
    {
1515
        $el = $this->findField($select);
1516
1517
        $wdSelect = new WebDriverSelect($el);
1518
1519
        if (!is_array($option)) {
1520
            $option = [$option];
1521
        }
1522
1523
        $matched = false;
1524
1525
        foreach ($option as $opt) {
1526
            try {
1527
                $wdSelect->deselectByVisibleText($opt);
1528
                $matched = true;
1529
            } catch (NoSuchElementException $e) {
1530
                // exception treated at the end
1531
            }
1532
1533
            try {
1534
                $wdSelect->deselectByValue($opt);
1535
                $matched = true;
1536
            } catch (NoSuchElementException $e) {
1537
                // exception treated at the end
1538
            }
1539
        }
1540
1541
        if ($matched) {
1542
            return;
1543
        }
1544
        throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value");
1545
    }
1546
1547
    /**
1548
     * @param $context
1549
     * @param $radioOrCheckbox
1550
     * @param bool $byValue
1551
     * @return mixed|null
1552
     */
1553
    protected function findCheckable($context, $radioOrCheckbox, $byValue = false)
1554
    {
1555
        if ($radioOrCheckbox instanceof WebDriverElement) {
1556
            return $radioOrCheckbox;
1557
        }
1558
1559
        if (is_array($radioOrCheckbox) or ($radioOrCheckbox instanceof WebDriverBy)) {
1560
            return $this->matchFirstOrFail($this->getBaseElement(), $radioOrCheckbox);
1561
        }
1562
1563
        $locator = Crawler::xpathLiteral($radioOrCheckbox);
1564
        if ($context instanceof WebDriverElement && $context->getTagName() === 'input') {
1565
            $contextType = $context->getAttribute('type');
1566
            if (!in_array($contextType, ['checkbox', 'radio'], true)) {
1567
                return null;
1568
            }
1569
            $nameLiteral = Crawler::xPathLiteral($context->getAttribute('name'));
1570
            $typeLiteral = Crawler::xPathLiteral($contextType);
1571
            $inputLocatorFragment = "input[@type = $typeLiteral][@name = $nameLiteral]";
1572
            $xpath = Locator::combine(
1573
            // @codingStandardsIgnoreStart
1574
                "ancestor::form//{$inputLocatorFragment}[(@id = ancestor::form//label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator]",
1575
                // @codingStandardsIgnoreEnd
1576
                "ancestor::form//label[contains(normalize-space(string(.)), $locator)]//{$inputLocatorFragment}"
1577
            );
1578
            if ($byValue) {
1579
                $xpath = Locator::combine($xpath, "ancestor::form//{$inputLocatorFragment}[@value = $locator]");
1580
            }
1581
        } else {
1582
            $xpath = Locator::combine(
1583
            // @codingStandardsIgnoreStart
1584
                "//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator or @name = $locator]",
1585
                // @codingStandardsIgnoreEnd
1586
                "//label[contains(normalize-space(string(.)), $locator)]//input[@type = 'radio' or @type = 'checkbox']"
1587
            );
1588
            if ($byValue) {
1589
                $xpath = Locator::combine($xpath, "//input[@type = 'checkbox' or @type = 'radio'][@value = $locator]");
1590
            }
1591
        }
1592
        $els = $context->findElements(WebDriverBy::xpath($xpath));
1593
        if (count($els)) {
1594
            return reset($els);
1595
        }
1596
        $els = $context->findElements(WebDriverBy::xpath(str_replace('ancestor::form', '', $xpath)));
1597
        if (count($els)) {
1598
            return reset($els);
1599
        }
1600
        $els = $this->match($context, $radioOrCheckbox);
1601
        if (count($els)) {
1602
            return reset($els);
1603
        }
1604
        return null;
1605
    }
1606
1607
    protected function matchCheckables($selector)
1608
    {
1609
        $els = $this->match($this->webDriver, $selector);
1610
        if (!count($els)) {
1611
            throw new ElementNotFound($selector, "Element containing radio by CSS or XPath");
1612
        }
1613
        return $els;
1614
    }
1615
1616 View Code Duplication
    public function checkOption($option)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1617
    {
1618
        $field = $this->findCheckable($this->webDriver, $option);
1619
        if (!$field) {
1620
            throw new ElementNotFound($option, "Checkbox or Radio by Label or CSS or XPath");
1621
        }
1622
        if ($field->isSelected()) {
1623
            return;
1624
        }
1625
        $field->click();
1626
    }
1627
1628 View Code Duplication
    public function uncheckOption($option)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1629
    {
1630
        $field = $this->findCheckable($this->getBaseElement(), $option);
1631
        if (!$field) {
1632
            throw new ElementNotFound($option, "Checkbox by Label or CSS or XPath");
1633
        }
1634
        if (!$field->isSelected()) {
1635
            return;
1636
        }
1637
        $field->click();
1638
    }
1639
1640
    public function fillField($field, $value)
1641
    {
1642
        $el = $this->findField($field);
1643
        $el->clear();
1644
        $el->sendKeys((string)$value);
1645
    }
1646
1647
    /**
1648
     * Clears given field which isn't empty.
1649
     *
1650
     * ``` php
1651
     * <?php
1652
     * $I->clearField('#username');
1653
     * ```
1654
     *
1655
     * @param $field
1656
     */
1657
    public function clearField($field)
1658
    {
1659
        $el = $this->findField($field);
1660
        $el->clear();
1661
    }
1662
1663
    public function attachFile($field, $filename)
1664
    {
1665
        $el = $this->findField($field);
1666
        // in order to be compatible on different OS
1667
        $filePath = codecept_data_dir() . $filename;
1668
        if (!file_exists($filePath)) {
1669
            throw new \InvalidArgumentException("File does not exist: $filePath");
1670
        }
1671
        if (!is_readable($filePath)) {
1672
            throw new \InvalidArgumentException("File is not readable: $filePath");
1673
        }
1674
        // in order for remote upload to be enabled
1675
        $el->setFileDetector(new LocalFileDetector());
1676
1677
        // skip file detector for phantomjs
1678
        if ($this->isPhantom()) {
1679
            $el->setFileDetector(new UselessFileDetector());
1680
        }
1681
        $el->sendKeys(realpath($filePath));
1682
    }
1683
1684
    /**
1685
     * Grabs all visible text from the current page.
1686
     *
1687
     * @return string
1688
     */
1689
    protected function getVisibleText()
1690
    {
1691
        if ($this->getBaseElement() instanceof RemoteWebElement) {
1692
            return $this->getBaseElement()->getText();
1693
        }
1694
        $els = $this->getBaseElement()->findElements(WebDriverBy::cssSelector('body'));
1695
        if (isset($els[0])) {
1696
            return $els[0]->getText();
1697
        }
1698
        return '';
1699
    }
1700
1701
    public function grabTextFrom($cssOrXPathOrRegex)
1702
    {
1703
        $els = $this->match($this->getBaseElement(), $cssOrXPathOrRegex, false);
1704
        if (count($els)) {
1705
            return $els[0]->getText();
1706
        }
1707
        if (@preg_match($cssOrXPathOrRegex, $this->webDriver->getPageSource(), $matches)) {
1708
            return $matches[1];
1709
        }
1710
        throw new ElementNotFound($cssOrXPathOrRegex, 'CSS or XPath or Regex');
1711
    }
1712
1713
    public function grabAttributeFrom($cssOrXpath, $attribute)
1714
    {
1715
        $el = $this->matchFirstOrFail($this->getBaseElement(), $cssOrXpath);
1716
        return $el->getAttribute($attribute);
1717
    }
1718
1719
    public function grabValueFrom($field)
1720
    {
1721
        $el = $this->findField($field);
1722
        // value of multiple select is the value of the first selected option
1723
        if ($el->getTagName() == 'select') {
1724
            $select = new WebDriverSelect($el);
1725
            return $select->getFirstSelectedOption()->getAttribute('value');
1726
        }
1727
        return $el->getAttribute('value');
1728
    }
1729
1730
    public function grabMultiple($cssOrXpath, $attribute = null)
1731
    {
1732
        $els = $this->match($this->getBaseElement(), $cssOrXpath);
1733
        return array_map(
1734
            function (WebDriverElement $e) use ($attribute) {
1735
                if ($attribute) {
1736
                    return $e->getAttribute($attribute);
1737
                }
1738
                return $e->getText();
1739
            },
1740
            $els
1741
        );
1742
    }
1743
1744
1745
    protected function filterByAttributes($els, array $attributes)
1746
    {
1747
        foreach ($attributes as $attr => $value) {
1748
            $els = array_filter(
1749
                $els,
1750
                function (WebDriverElement $el) use ($attr, $value) {
1751
                    return $el->getAttribute($attr) == $value;
1752
                }
1753
            );
1754
        }
1755
        return $els;
1756
    }
1757
1758
    public function seeElement($selector, $attributes = [])
1759
    {
1760
        $this->enableImplicitWait();
1761
        $els = $this->matchVisible($selector);
1762
        $this->disableImplicitWait();
1763
        $els = $this->filterByAttributes($els, $attributes);
1764
        $this->assertNotEmpty($els);
1765
    }
1766
1767
    public function dontSeeElement($selector, $attributes = [])
1768
    {
1769
        $els = $this->matchVisible($selector);
1770
        $els = $this->filterByAttributes($els, $attributes);
1771
        $this->assertEmpty($els);
1772
    }
1773
1774
    /**
1775
     * Checks that the given element exists on the page, even it is invisible.
1776
     *
1777
     * ``` php
1778
     * <?php
1779
     * $I->seeElementInDOM('//form/input[type=hidden]');
1780
     * ?>
1781
     * ```
1782
     *
1783
     * @param $selector
1784
     * @param array $attributes
1785
     */
1786
    public function seeElementInDOM($selector, $attributes = [])
1787
    {
1788
        $this->enableImplicitWait();
1789
        $els = $this->match($this->getBaseElement(), $selector);
1790
        $els = $this->filterByAttributes($els, $attributes);
1791
        $this->disableImplicitWait();
1792
        $this->assertNotEmpty($els);
1793
    }
1794
1795
1796
    /**
1797
     * Opposite of `seeElementInDOM`.
1798
     *
1799
     * @param $selector
1800
     * @param array $attributes
1801
     */
1802
    public function dontSeeElementInDOM($selector, $attributes = [])
1803
    {
1804
        $els = $this->match($this->getBaseElement(), $selector);
1805
        $els = $this->filterByAttributes($els, $attributes);
1806
        $this->assertEmpty($els);
1807
    }
1808
1809 View Code Duplication
    public function seeNumberOfElements($selector, $expected)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1810
    {
1811
        $counted = count($this->matchVisible($selector));
1812
        if (is_array($expected)) {
1813
            list($floor, $ceil) = $expected;
1814
            $this->assertTrue(
1815
                $floor <= $counted && $ceil >= $counted,
1816
                'Number of elements counted differs from expected range'
1817
            );
1818
        } else {
1819
            $this->assertEquals(
1820
                $expected,
1821
                $counted,
1822
                'Number of elements counted differs from expected number'
1823
            );
1824
        }
1825
    }
1826
1827 View Code Duplication
    public function seeNumberOfElementsInDOM($selector, $expected)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1828
    {
1829
        $counted = count($this->match($this->getBaseElement(), $selector));
1830
        if (is_array($expected)) {
1831
            list($floor, $ceil) = $expected;
1832
            $this->assertTrue(
1833
                $floor <= $counted && $ceil >= $counted,
1834
                'Number of elements counted differs from expected range'
1835
            );
1836
        } else {
1837
            $this->assertEquals(
1838
                $expected,
1839
                $counted,
1840
                'Number of elements counted differs from expected number'
1841
            );
1842
        }
1843
    }
1844
1845 View Code Duplication
    public function seeOptionIsSelected($selector, $optionText)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1846
    {
1847
        $el = $this->findField($selector);
1848
        if ($el->getTagName() !== 'select') {
1849
            $els = $this->matchCheckables($selector);
1850
            foreach ($els as $k => $el) {
1851
                $els[$k] = $this->findCheckable($el, $optionText, true);
1852
            }
1853
            $this->assertNotEmpty(
1854
                array_filter(
1855
                    $els,
1856
                    function ($e) {
1857
                        return $e && $e->isSelected();
1858
                    }
1859
                )
1860
            );
1861
            return;
1862
        }
1863
        $select = new WebDriverSelect($el);
1864
        $this->assertNodesContain($optionText, $select->getAllSelectedOptions(), 'option');
1865
    }
1866
1867 View Code Duplication
    public function dontSeeOptionIsSelected($selector, $optionText)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1868
    {
1869
        $el = $this->findField($selector);
1870
        if ($el->getTagName() !== 'select') {
1871
            $els = $this->matchCheckables($selector);
1872
            foreach ($els as $k => $el) {
1873
                $els[$k] = $this->findCheckable($el, $optionText, true);
1874
            }
1875
            $this->assertEmpty(
1876
                array_filter(
1877
                    $els,
1878
                    function ($e) {
1879
                        return $e && $e->isSelected();
1880
                    }
1881
                )
1882
            );
1883
            return;
1884
        }
1885
        $select = new WebDriverSelect($el);
1886
        $this->assertNodesNotContain($optionText, $select->getAllSelectedOptions(), 'option');
1887
    }
1888
1889
    public function seeInTitle($title)
1890
    {
1891
        $this->assertContains($title, $this->webDriver->getTitle());
1892
    }
1893
1894
    public function dontSeeInTitle($title)
1895
    {
1896
        $this->assertNotContains($title, $this->webDriver->getTitle());
1897
    }
1898
1899
    /**
1900
     * Accepts the active JavaScript native popup window, as created by `window.alert`|`window.confirm`|`window.prompt`.
1901
     * Don't confuse popups with modal windows,
1902
     * as created by [various libraries](http://jster.net/category/windows-modals-popups).
1903
     */
1904
    public function acceptPopup()
1905
    {
1906
        if ($this->isPhantom()) {
1907
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1908
        }
1909
        $this->webDriver->switchTo()->alert()->accept();
1910
    }
1911
1912
    /**
1913
     * Dismisses the active JavaScript popup, as created by `window.alert`, `window.confirm`, or `window.prompt`.
1914
     */
1915
    public function cancelPopup()
1916
    {
1917
        if ($this->isPhantom()) {
1918
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1919
        }
1920
        $this->webDriver->switchTo()->alert()->dismiss();
1921
    }
1922
1923
    /**
1924
     * Checks that the active JavaScript popup,
1925
     * as created by `window.alert`|`window.confirm`|`window.prompt`, contains the given string.
1926
     *
1927
     * @param $text
1928
     *
1929
     * @throws \Codeception\Exception\ModuleException
1930
     */
1931 View Code Duplication
    public function seeInPopup($text)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1932
    {
1933
        if ($this->isPhantom()) {
1934
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1935
        }
1936
        $alert = $this->webDriver->switchTo()->alert();
1937
        try {
1938
            $this->assertContains($text, $alert->getText());
1939
        } catch (\PHPUnit\Framework\AssertionFailedError $e) {
1940
            $alert->dismiss();
1941
            throw $e;
1942
        }
1943
    }
1944
1945
    /**
1946
     * Checks that the active JavaScript popup,
1947
     * as created by `window.alert`|`window.confirm`|`window.prompt`, does NOT contain the given string.
1948
     *
1949
     * @param $text
1950
     *
1951
     * @throws \Codeception\Exception\ModuleException
1952
     */
1953 View Code Duplication
    public function dontSeeInPopup($text)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1954
    {
1955
        if ($this->isPhantom()) {
1956
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1957
        }
1958
        $alert = $this->webDriver->switchTo()->alert();
1959
        try {
1960
            $this->assertNotContains($text, $alert->getText());
1961
        } catch (\PHPUnit\Framework\AssertionFailedError $e) {
1962
            $alert->dismiss();
1963
            throw $e;
1964
        }
1965
    }
1966
1967
    /**
1968
     * Enters text into a native JavaScript prompt popup, as created by `window.prompt`.
1969
     *
1970
     * @param $keys
1971
     *
1972
     * @throws \Codeception\Exception\ModuleException
1973
     */
1974
    public function typeInPopup($keys)
1975
    {
1976
        if ($this->isPhantom()) {
1977
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1978
        }
1979
        $this->webDriver->switchTo()->alert()->sendKeys($keys);
1980
    }
1981
1982
    /**
1983
     * Reloads the current page.
1984
     */
1985
    public function reloadPage()
1986
    {
1987
        $this->webDriver->navigate()->refresh();
1988
    }
1989
1990
    /**
1991
     * Moves back in history.
1992
     */
1993
    public function moveBack()
1994
    {
1995
        $this->webDriver->navigate()->back();
1996
        $this->debug($this->_getCurrentUri());
1997
    }
1998
1999
    /**
2000
     * Moves forward in history.
2001
     */
2002
    public function moveForward()
2003
    {
2004
        $this->webDriver->navigate()->forward();
2005
        $this->debug($this->_getCurrentUri());
2006
    }
2007
2008 View Code Duplication
    protected function getSubmissionFormFieldName($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2009
    {
2010
        if (substr($name, -2) === '[]') {
2011
            return substr($name, 0, -2);
2012
        }
2013
        return $name;
2014
    }
2015
2016
    /**
2017
     * Submits the given form on the page, optionally with the given form
2018
     * values.  Give the form fields values as an array. Note that hidden fields
2019
     * can't be accessed.
2020
     *
2021
     * Skipped fields will be filled by their values from the page.
2022
     * You don't need to click the 'Submit' button afterwards.
2023
     * This command itself triggers the request to form's action.
2024
     *
2025
     * You can optionally specify what button's value to include
2026
     * in the request with the last parameter as an alternative to
2027
     * explicitly setting its value in the second parameter, as
2028
     * button values are not otherwise included in the request.
2029
     *
2030
     * Examples:
2031
     *
2032
     * ``` php
2033
     * <?php
2034
     * $I->submitForm('#login', [
2035
     *     'login' => 'davert',
2036
     *     'password' => '123456'
2037
     * ]);
2038
     * // or
2039
     * $I->submitForm('#login', [
2040
     *     'login' => 'davert',
2041
     *     'password' => '123456'
2042
     * ], 'submitButtonName');
2043
     *
2044
     * ```
2045
     *
2046
     * For example, given this sample "Sign Up" form:
2047
     *
2048
     * ``` html
2049
     * <form action="/sign_up">
2050
     *     Login:
2051
     *     <input type="text" name="user[login]" /><br/>
2052
     *     Password:
2053
     *     <input type="password" name="user[password]" /><br/>
2054
     *     Do you agree to our terms?
2055
     *     <input type="checkbox" name="user[agree]" /><br/>
2056
     *     Select pricing plan:
2057
     *     <select name="plan">
2058
     *         <option value="1">Free</option>
2059
     *         <option value="2" selected="selected">Paid</option>
2060
     *     </select>
2061
     *     <input type="submit" name="submitButton" value="Submit" />
2062
     * </form>
2063
     * ```
2064
     *
2065
     * You could write the following to submit it:
2066
     *
2067
     * ``` php
2068
     * <?php
2069
     * $I->submitForm(
2070
     *     '#userForm',
2071
     *     [
2072
     *         'user[login]' => 'Davert',
2073
     *         'user[password]' => '123456',
2074
     *         'user[agree]' => true
2075
     *     ],
2076
     *     'submitButton'
2077
     * );
2078
     * ```
2079
     * Note that "2" will be the submitted value for the "plan" field, as it is
2080
     * the selected option.
2081
     *
2082
     * Also note that this differs from PhpBrowser, in that
2083
     * ```'user' => [ 'login' => 'Davert' ]``` is not supported at the moment.
2084
     * Named array keys *must* be included in the name as above.
2085
     *
2086
     * Pair this with seeInFormFields for quick testing magic.
2087
     *
2088
     * ``` php
2089
     * <?php
2090
     * $form = [
2091
     *      'field1' => 'value',
2092
     *      'field2' => 'another value',
2093
     *      'checkbox1' => true,
2094
     *      // ...
2095
     * ];
2096
     * $I->submitForm('//form[@id=my-form]', $form, 'submitButton');
2097
     * // $I->amOnPage('/path/to/form-page') may be needed
2098
     * $I->seeInFormFields('//form[@id=my-form]', $form);
2099
     * ?>
2100
     * ```
2101
     *
2102
     * Parameter values must be set to arrays for multiple input fields
2103
     * of the same name, or multi-select combo boxes.  For checkboxes,
2104
     * either the string value can be used, or boolean values which will
2105
     * be replaced by the checkbox's value in the DOM.
2106
     *
2107
     * ``` php
2108
     * <?php
2109
     * $I->submitForm('#my-form', [
2110
     *      'field1' => 'value',
2111
     *      'checkbox' => [
2112
     *          'value of first checkbox',
2113
     *          'value of second checkbox,
2114
     *      ],
2115
     *      'otherCheckboxes' => [
2116
     *          true,
2117
     *          false,
2118
     *          false
2119
     *      ],
2120
     *      'multiselect' => [
2121
     *          'first option value',
2122
     *          'second option value'
2123
     *      ]
2124
     * ]);
2125
     * ?>
2126
     * ```
2127
     *
2128
     * Mixing string and boolean values for a checkbox's value is not supported
2129
     * and may produce unexpected results.
2130
     *
2131
     * Field names ending in "[]" must be passed without the trailing square
2132
     * bracket characters, and must contain an array for its value.  This allows
2133
     * submitting multiple values with the same name, consider:
2134
     *
2135
     * ```php
2136
     * $I->submitForm('#my-form', [
2137
     *     'field[]' => 'value',
2138
     *     'field[]' => 'another value', // 'field[]' is already a defined key
2139
     * ]);
2140
     * ```
2141
     *
2142
     * The solution is to pass an array value:
2143
     *
2144
     * ```php
2145
     * // this way both values are submitted
2146
     * $I->submitForm('#my-form', [
2147
     *     'field' => [
2148
     *         'value',
2149
     *         'another value',
2150
     *     ]
2151
     * ]);
2152
     * ```
2153
     *
2154
     * The `$button` parameter can be either a string, an array or an instance
2155
     * of Facebook\WebDriver\WebDriverBy. When it is a string, the
2156
     * button will be found by its "name" attribute. If $button is an
2157
     * array then it will be treated as a strict selector and a WebDriverBy
2158
     * will be used verbatim.
2159
     *
2160
     * For example, given the following HTML:
2161
     *
2162
     * ``` html
2163
     * <input type="submit" name="submitButton" value="Submit" />
2164
     * ```
2165
     *
2166
     * `$button` could be any one of the following:
2167
     *   - 'submitButton'
2168
     *   - ['name' => 'submitButton']
2169
     *   - WebDriverBy::name('submitButton')
2170
     *
2171
     * @param $selector
2172
     * @param $params
2173
     * @param $button
2174
     */
2175
    public function submitForm($selector, array $params, $button = null)
2176
    {
2177
        $form = $this->matchFirstOrFail($this->getBaseElement(), $selector);
2178
2179
        $fields = $form->findElements(
2180
            WebDriverBy::cssSelector('input:enabled,textarea:enabled,select:enabled,input[type=hidden]')
2181
        );
2182
        foreach ($fields as $field) {
2183
            $fieldName = $this->getSubmissionFormFieldName($field->getAttribute('name'));
2184
            if (!isset($params[$fieldName])) {
2185
                continue;
2186
            }
2187
            $value = $params[$fieldName];
2188
            if (is_array($value) && $field->getTagName() !== 'select') {
2189
                if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') {
2190
                    $found = false;
2191
                    foreach ($value as $index => $val) {
2192
                        if (!is_bool($val) && $val === $field->getAttribute('value')) {
2193
                            array_splice($params[$fieldName], $index, 1);
2194
                            $value = $val;
2195
                            $found = true;
2196
                            break;
2197
                        }
2198
                    }
2199
                    if (!$found && !empty($value) && is_bool(reset($value))) {
2200
                        $value = array_pop($params[$fieldName]);
2201
                    }
2202
                } else {
2203
                    $value = array_pop($params[$fieldName]);
2204
                }
2205
            }
2206
2207
            if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') {
2208
                if ($value === true || $value === $field->getAttribute('value')) {
2209
                    $this->checkOption($field);
2210
                } else {
2211
                    $this->uncheckOption($field);
2212
                }
2213
            } elseif ($field->getAttribute('type') === 'button' || $field->getAttribute('type') === 'submit') {
2214
                continue;
2215
            } elseif ($field->getTagName() === 'select') {
2216
                $this->selectOption($field, $value);
2217
            } else {
2218
                $this->fillField($field, $value);
2219
            }
2220
        }
2221
2222
        $this->debugSection(
2223
            'Uri',
2224
            $form->getAttribute('action') ? $form->getAttribute('action') : $this->_getCurrentUri()
2225
        );
2226
        $this->debugSection('Method', $form->getAttribute('method') ? $form->getAttribute('method') : 'GET');
2227
        $this->debugSection('Parameters', json_encode($params));
2228
2229
        $submitted = false;
2230
        if (!empty($button)) {
2231
            if (is_array($button)) {
2232
                $buttonSelector = $this->getStrictLocator($button);
2233
            } elseif ($button instanceof WebDriverBy) {
2234
                $buttonSelector = $button;
2235
            } else {
2236
                $buttonSelector = WebDriverBy::name($button);
2237
            }
2238
2239
            $els = $form->findElements($buttonSelector);
2240
2241
            if (!empty($els)) {
2242
                $el = reset($els);
2243
                $el->click();
2244
                $submitted = true;
2245
            }
2246
        }
2247
2248
        if (!$submitted) {
2249
            $form->submit();
2250
        }
2251
        $this->debugSection('Page', $this->_getCurrentUri());
2252
    }
2253
2254
    /**
2255
     * Waits up to $timeout seconds for the given element to change.
2256
     * Element "change" is determined by a callback function which is called repeatedly
2257
     * until the return value evaluates to true.
2258
     *
2259
     * ``` php
2260
     * <?php
2261
     * use \Facebook\WebDriver\WebDriverElement
2262
     * $I->waitForElementChange('#menu', function(WebDriverElement $el) {
2263
     *     return $el->isDisplayed();
2264
     * }, 100);
2265
     * ?>
2266
     * ```
2267
     *
2268
     * @param $element
2269
     * @param \Closure $callback
2270
     * @param int $timeout seconds
2271
     * @throws \Codeception\Exception\ElementNotFound
2272
     */
2273
    public function waitForElementChange($element, \Closure $callback, $timeout = 30)
2274
    {
2275
        $el = $this->matchFirstOrFail($this->getBaseElement(), $element);
2276
        $checker = function () use ($el, $callback) {
2277
            return $callback($el);
2278
        };
2279
        $this->webDriver->wait($timeout)->until($checker);
2280
    }
2281
2282
    /**
2283
     * Waits up to $timeout seconds for an element to appear on the page.
2284
     * If the element doesn't appear, a timeout exception is thrown.
2285
     *
2286
     * ``` php
2287
     * <?php
2288
     * $I->waitForElement('#agree_button', 30); // secs
2289
     * $I->click('#agree_button');
2290
     * ?>
2291
     * ```
2292
     *
2293
     * @param $element
2294
     * @param int $timeout seconds
2295
     * @throws \Exception
2296
     */
2297
    public function waitForElement($element, $timeout = 10)
2298
    {
2299
        $condition = WebDriverExpectedCondition::presenceOfElementLocated($this->getLocator($element));
2300
        $this->webDriver->wait($timeout)->until($condition);
0 ignored issues
show
Documentation introduced by
$condition is of type object<Facebook\WebDrive...riverExpectedCondition>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2301
    }
2302
2303
    /**
2304
     * Waits up to $timeout seconds for the given element to be visible on the page.
2305
     * If element doesn't appear, a timeout exception is thrown.
2306
     *
2307
     * ``` php
2308
     * <?php
2309
     * $I->waitForElementVisible('#agree_button', 30); // secs
2310
     * $I->click('#agree_button');
2311
     * ?>
2312
     * ```
2313
     *
2314
     * @param $element
2315
     * @param int $timeout seconds
2316
     * @throws \Exception
2317
     */
2318
    public function waitForElementVisible($element, $timeout = 10)
2319
    {
2320
        $condition = WebDriverExpectedCondition::visibilityOfElementLocated($this->getLocator($element));
2321
        $this->webDriver->wait($timeout)->until($condition);
0 ignored issues
show
Documentation introduced by
$condition is of type object<Facebook\WebDrive...riverExpectedCondition>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2322
    }
2323
2324
    /**
2325
     * Waits up to $timeout seconds for the given element to become invisible.
2326
     * If element stays visible, a timeout exception is thrown.
2327
     *
2328
     * ``` php
2329
     * <?php
2330
     * $I->waitForElementNotVisible('#agree_button', 30); // secs
2331
     * ?>
2332
     * ```
2333
     *
2334
     * @param $element
2335
     * @param int $timeout seconds
2336
     * @throws \Exception
2337
     */
2338
    public function waitForElementNotVisible($element, $timeout = 10)
2339
    {
2340
        $condition = WebDriverExpectedCondition::invisibilityOfElementLocated($this->getLocator($element));
2341
        $this->webDriver->wait($timeout)->until($condition);
0 ignored issues
show
Documentation introduced by
$condition is of type object<Facebook\WebDrive...riverExpectedCondition>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2342
    }
2343
2344
    /**
2345
     * Waits up to $timeout seconds for the given string to appear on the page.
2346
     *
2347
     * Can also be passed a selector to search in, be as specific as possible when using selectors.
2348
     * waitForText() will only watch the first instance of the matching selector / text provided.
2349
     * If the given text doesn't appear, a timeout exception is thrown.
2350
     *
2351
     * ``` php
2352
     * <?php
2353
     * $I->waitForText('foo', 30); // secs
2354
     * $I->waitForText('foo', 30, '.title'); // secs
2355
     * ?>
2356
     * ```
2357
     *
2358
     * @param string $text
2359
     * @param int $timeout seconds
2360
     * @param string $selector optional
2361
     * @throws \Exception
2362
     */
2363
    public function waitForText($text, $timeout = 10, $selector = null)
2364
    {
2365
        $message = sprintf(
2366
            'Waited for %d secs but text %s still not found',
2367
            $timeout,
2368
            Locator::humanReadableString($text)
2369
        );
2370
        if (!$selector) {
2371
            $condition = WebDriverExpectedCondition::textToBePresentInElement(WebDriverBy::xpath('//body'), $text);
0 ignored issues
show
Deprecated Code introduced by
The method Facebook\WebDriver\WebDr...tToBePresentInElement() has been deprecated with message: Use WebDriverExpectedCondition::elementTextContains() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2372
            $this->webDriver->wait($timeout)->until($condition, $message);
0 ignored issues
show
Documentation introduced by
$condition is of type object<Facebook\WebDrive...riverExpectedCondition>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2373
            return;
2374
        }
2375
2376
        $condition = WebDriverExpectedCondition::textToBePresentInElement($this->getLocator($selector), $text);
0 ignored issues
show
Deprecated Code introduced by
The method Facebook\WebDriver\WebDr...tToBePresentInElement() has been deprecated with message: Use WebDriverExpectedCondition::elementTextContains() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2377
        $this->webDriver->wait($timeout)->until($condition, $message);
0 ignored issues
show
Documentation introduced by
$condition is of type object<Facebook\WebDrive...riverExpectedCondition>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2378
    }
2379
2380
    /**
2381
     * Wait for $timeout seconds.
2382
     *
2383
     * @param int|float $timeout secs
2384
     * @throws \Codeception\Exception\TestRuntimeException
2385
     */
2386
    public function wait($timeout)
2387
    {
2388
        if ($timeout >= 1000) {
2389
            throw new TestRuntimeException(
2390
                "
2391
                Waiting for more then 1000 seconds: 16.6667 mins\n
2392
                Please note that wait method accepts number of seconds as parameter."
2393
            );
2394
        }
2395
        usleep($timeout * 1000000);
2396
    }
2397
2398
    /**
2399
     * Low-level API method.
2400
     * If Codeception commands are not enough, this allows you to use Selenium WebDriver methods directly:
2401
     *
2402
     * ``` php
2403
     * $I->executeInSelenium(function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) {
2404
     *   $webdriver->get('http://google.com');
2405
     * });
2406
     * ```
2407
     *
2408
     * This runs in the context of the
2409
     * [RemoteWebDriver class](https://github.com/facebook/php-webdriver/blob/master/lib/remote/RemoteWebDriver.php).
2410
     * Try not to use this command on a regular basis.
2411
     * If Codeception lacks a feature you need, please implement it and submit a patch.
2412
     *
2413
     * @param callable $function
2414
     */
2415
    public function executeInSelenium(\Closure $function)
2416
    {
2417
        return $function($this->webDriver);
2418
    }
2419
2420
    /**
2421
     * Switch to another window identified by name.
2422
     *
2423
     * The window can only be identified by name. If the $name parameter is blank, the parent window will be used.
2424
     *
2425
     * Example:
2426
     * ``` html
2427
     * <input type="button" value="Open window" onclick="window.open('http://example.com', 'another_window')">
2428
     * ```
2429
     *
2430
     * ``` php
2431
     * <?php
2432
     * $I->click("Open window");
2433
     * # switch to another window
2434
     * $I->switchToWindow("another_window");
2435
     * # switch to parent window
2436
     * $I->switchToWindow();
2437
     * ?>
2438
     * ```
2439
     *
2440
     * If the window has no name, match it by switching to next active tab using `switchToNextTab` method.
2441
     *
2442
     * Or use native Selenium functions to get access to all opened windows:
2443
     *
2444
     * ``` php
2445
     * <?php
2446
     * $I->executeInSelenium(function (\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) {
2447
     *      $handles=$webdriver->getWindowHandles();
2448
     *      $last_window = end($handles);
2449
     *      $webdriver->switchTo()->window($last_window);
2450
     * });
2451
     * ?>
2452
     * ```
2453
     *
2454
     * @param string|null $name
2455
     */
2456
    public function switchToWindow($name = null)
2457
    {
2458
        $this->webDriver->switchTo()->window($name);
2459
    }
2460
2461
    /**
2462
     * Switch to another frame on the page.
2463
     *
2464
     * Example:
2465
     * ``` html
2466
     * <iframe name="another_frame" src="http://example.com">
2467
     *
2468
     * ```
2469
     *
2470
     * ``` php
2471
     * <?php
2472
     * # switch to iframe
2473
     * $I->switchToIFrame("another_frame");
2474
     * # switch to parent page
2475
     * $I->switchToIFrame();
2476
     *
2477
     * ```
2478
     *
2479
     * @param string|null $name
2480
     */
2481
    public function switchToIFrame($name = null)
2482
    {
2483
        if (is_null($name)) {
2484
            $this->webDriver->switchTo()->defaultContent();
2485
            return;
2486
        }
2487
        $this->webDriver->switchTo()->frame($name);
2488
    }
2489
2490
    /**
2491
     * Executes JavaScript and waits up to $timeout seconds for it to return true.
2492
     *
2493
     * In this example we will wait up to 60 seconds for all jQuery AJAX requests to finish.
2494
     *
2495
     * ``` php
2496
     * <?php
2497
     * $I->waitForJS("return $.active == 0;", 60);
2498
     * ?>
2499
     * ```
2500
     *
2501
     * @param string $script
2502
     * @param int $timeout seconds
2503
     */
2504
    public function waitForJS($script, $timeout = 5)
2505
    {
2506
        $condition = function ($wd) use ($script) {
2507
            return $wd->executeScript($script);
2508
        };
2509
        $message = sprintf(
2510
            'Waited for %d secs but script %s still not executed',
2511
            $timeout,
2512
            Locator::humanReadableString($script)
2513
        );
2514
        $this->webDriver->wait($timeout)->until($condition, $message);
2515
    }
2516
2517
    /**
2518
     * Executes custom JavaScript.
2519
     *
2520
     * This example uses jQuery to get a value and assigns that value to a PHP variable:
2521
     *
2522
     * ```php
2523
     * <?php
2524
     * $myVar = $I->executeJS('return $("#myField").val()');
2525
     *
2526
     * // additional arguments can be passed as array
2527
     * // Example shows `Hello World` alert:
2528
     * $I->executeJS("window.alert(arguments[0])", ['Hello world']);
2529
     * ```
2530
     *
2531
     * @param $script
2532
     * @param array $arguments
2533
     * @return mixed
2534
     */
2535
    public function executeJS($script, array $arguments = [])
2536
    {
2537
        return $this->webDriver->executeScript($script, $arguments);
2538
    }
2539
2540
    /**
2541
     * Executes asynchronous JavaScript.
2542
     * A callback should be executed by JavaScript to exit from a script.
2543
     * Callback is passed as a last element in `arguments` array.
2544
     * Additional arguments can be passed as array in second parameter.
2545
     *
2546
     * ```js
2547
     * // wait for 1200 milliseconds my running `setTimeout`
2548
     * * $I->executeAsyncJS('setTimeout(arguments[0], 1200)');
2549
     *
2550
     * $seconds = 1200; // or seconds are passed as argument
2551
     * $I->executeAsyncJS('setTimeout(arguments[1], arguments[0])', [$seconds]);
2552
     * ```
2553
     *
2554
     * @param $script
2555
     * @param array $arguments
2556
     * @return mixed
2557
     */
2558
    public function executeAsyncJS($script, array $arguments = [])
2559
    {
2560
        return $this->webDriver->executeAsyncScript($script, $arguments);
2561
    }
2562
2563
    /**
2564
     * Maximizes the current window.
2565
     */
2566
    public function maximizeWindow()
2567
    {
2568
        $this->webDriver->manage()->window()->maximize();
2569
    }
2570
2571
    /**
2572
     * Performs a simple mouse drag-and-drop operation.
2573
     *
2574
     * ``` php
2575
     * <?php
2576
     * $I->dragAndDrop('#drag', '#drop');
2577
     * ?>
2578
     * ```
2579
     *
2580
     * @param string $source (CSS ID or XPath)
2581
     * @param string $target (CSS ID or XPath)
2582
     */
2583
    public function dragAndDrop($source, $target)
2584
    {
2585
        $snodes = $this->matchFirstOrFail($this->getBaseElement(), $source);
2586
        $tnodes = $this->matchFirstOrFail($this->getBaseElement(), $target);
2587
2588
        $action = new WebDriverActions($this->webDriver);
2589
        $action->dragAndDrop($snodes, $tnodes)->perform();
2590
    }
2591
2592
    /**
2593
     * Move mouse over the first element matched by the given locator.
2594
     * If the first parameter null then the page is used.
2595
     * If the second and third parameters are given,
2596
     * then the mouse is moved to an offset of the element's top-left corner.
2597
     * Otherwise, the mouse is moved to the center of the element.
2598
     *
2599
     * ``` php
2600
     * <?php
2601
     * $I->moveMouseOver(['css' => '.checkout']);
2602
     * $I->moveMouseOver(null, 20, 50);
2603
     * $I->moveMouseOver(['css' => '.checkout'], 20, 50);
2604
     * ?>
2605
     * ```
2606
     *
2607
     * @param string $cssOrXPath css or xpath of the web element
2608
     * @param int $offsetX
2609
     * @param int $offsetY
2610
     *
2611
     * @throws \Codeception\Exception\ElementNotFound
2612
     */
2613
    public function moveMouseOver($cssOrXPath = null, $offsetX = null, $offsetY = null)
2614
    {
2615
        $where = null;
2616
        if (null !== $cssOrXPath) {
2617
            $el = $this->matchFirstOrFail($this->getBaseElement(), $cssOrXPath);
2618
            $where = $el->getCoordinates();
2619
        }
2620
2621
        $this->webDriver->getMouse()->mouseMove($where, $offsetX, $offsetY);
2622
    }
2623
2624
    /**
2625
     * Performs click with the left mouse button on an element.
2626
     * If the first parameter `null` then the offset is relative to the actual mouse position.
2627
     * If the second and third parameters are given,
2628
     * then the mouse is moved to an offset of the element's top-left corner.
2629
     * Otherwise, the mouse is moved to the center of the element.
2630
     *
2631
     * ``` php
2632
     * <?php
2633
     * $I->clickWithLeftButton(['css' => '.checkout']);
2634
     * $I->clickWithLeftButton(null, 20, 50);
2635
     * $I->clickWithLeftButton(['css' => '.checkout'], 20, 50);
2636
     * ?>
2637
     * ```
2638
     *
2639
     * @param string $cssOrXPath css or xpath of the web element (body by default).
2640
     * @param int $offsetX
2641
     * @param int $offsetY
2642
     *
2643
     * @throws \Codeception\Exception\ElementNotFound
2644
     */
2645
    public function clickWithLeftButton($cssOrXPath = null, $offsetX = null, $offsetY = null)
2646
    {
2647
        $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY);
2648
        $this->webDriver->getMouse()->click();
2649
    }
2650
2651
    /**
2652
     * Performs contextual click with the right mouse button on an element.
2653
     * If the first parameter `null` then the offset is relative to the actual mouse position.
2654
     * If the second and third parameters are given,
2655
     * then the mouse is moved to an offset of the element's top-left corner.
2656
     * Otherwise, the mouse is moved to the center of the element.
2657
     *
2658
     * ``` php
2659
     * <?php
2660
     * $I->clickWithRightButton(['css' => '.checkout']);
2661
     * $I->clickWithRightButton(null, 20, 50);
2662
     * $I->clickWithRightButton(['css' => '.checkout'], 20, 50);
2663
     * ?>
2664
     * ```
2665
     *
2666
     * @param string $cssOrXPath css or xpath of the web element (body by default).
2667
     * @param int $offsetX
2668
     * @param int $offsetY
2669
     *
2670
     * @throws \Codeception\Exception\ElementNotFound
2671
     */
2672
    public function clickWithRightButton($cssOrXPath = null, $offsetX = null, $offsetY = null)
2673
    {
2674
        $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY);
2675
        $this->webDriver->getMouse()->contextClick();
2676
    }
2677
2678
    /**
2679
     * Pauses test execution in debug mode.
2680
     * To proceed test press "ENTER" in console.
2681
     *
2682
     * This method is useful while writing tests,
2683
     * since it allows you to inspect the current page in the middle of a test case.
2684
     */
2685
    public function pauseExecution()
2686
    {
2687
        Debug::pause();
2688
    }
2689
2690
    /**
2691
     * Performs a double-click on an element matched by CSS or XPath.
2692
     *
2693
     * @param $cssOrXPath
2694
     * @throws \Codeception\Exception\ElementNotFound
2695
     */
2696
    public function doubleClick($cssOrXPath)
2697
    {
2698
        $el = $this->matchFirstOrFail($this->getBaseElement(), $cssOrXPath);
2699
        $this->webDriver->getMouse()->doubleClick($el->getCoordinates());
2700
    }
2701
2702
    /**
2703
     * @param $page
2704
     * @param $selector
2705
     * @param bool $throwMalformed
2706
     * @return array
2707
     */
2708
    protected function match($page, $selector, $throwMalformed = true)
2709
    {
2710
        if (is_array($selector)) {
2711
            try {
2712
                return $page->findElements($this->getStrictLocator($selector));
2713
            } catch (InvalidSelectorException $e) {
2714
                throw new MalformedLocatorException(key($selector) . ' => ' . reset($selector), "Strict locator");
2715
            } catch (InvalidElementStateException $e) {
2716
                if ($this->isPhantom() and $e->getResults()['status'] == 12) {
2717
                    throw new MalformedLocatorException(
2718
                        key($selector) . ' => ' . reset($selector),
2719
                        "Strict locator " . $e->getCode()
2720
                    );
2721
                }
2722
            }
2723
        }
2724
        if ($selector instanceof WebDriverBy) {
2725
            try {
2726
                return $page->findElements($selector);
2727
            } catch (InvalidSelectorException $e) {
2728
                throw new MalformedLocatorException(
2729
                    sprintf(
2730
                        "WebDriverBy::%s('%s')",
2731
                        $selector->getMechanism(),
2732
                        $selector->getValue()
2733
                    ),
2734
                    'WebDriver'
2735
                );
2736
            }
2737
        }
2738
        $isValidLocator = false;
2739
        $nodes = [];
2740
        try {
2741
            if (Locator::isID($selector)) {
2742
                $isValidLocator = true;
2743
                $nodes = $page->findElements(WebDriverBy::id(substr($selector, 1)));
2744
            }
2745
            if (Locator::isClass($selector)) {
2746
                $isValidLocator = true;
2747
                $nodes = $page->findElements(WebDriverBy::className(substr($selector, 1)));
2748
            }
2749
            if (empty($nodes) and Locator::isCSS($selector)) {
2750
                $isValidLocator = true;
2751
                try {
2752
                    $nodes = $page->findElements(WebDriverBy::cssSelector($selector));
2753
                } catch (InvalidElementStateException $e) {
2754
                    $nodes = $page->findElements(WebDriverBy::linkText($selector));
2755
                }
2756
            }
2757
            if (empty($nodes) and Locator::isXPath($selector)) {
2758
                $isValidLocator = true;
2759
                $nodes = $page->findElements(WebDriverBy::xpath($selector));
2760
            }
2761
        } catch (InvalidSelectorException $e) {
2762
            throw new MalformedLocatorException($selector);
2763
        }
2764
        if (!$isValidLocator and $throwMalformed) {
2765
            throw new MalformedLocatorException($selector);
2766
        }
2767
        return $nodes;
2768
    }
2769
2770
    /**
2771
     * @param array $by
2772
     * @return WebDriverBy
2773
     */
2774
    protected function getStrictLocator(array $by)
2775
    {
2776
        $type = key($by);
2777
        $locator = $by[$type];
2778
        switch ($type) {
2779
            case 'id':
2780
                return WebDriverBy::id($locator);
2781
            case 'name':
2782
                return WebDriverBy::name($locator);
2783
            case 'css':
2784
                return WebDriverBy::cssSelector($locator);
2785
            case 'xpath':
2786
                return WebDriverBy::xpath($locator);
2787
            case 'link':
2788
                return WebDriverBy::linkText($locator);
2789
            case 'class':
2790
                return WebDriverBy::className($locator);
2791
            default:
2792
                throw new MalformedLocatorException(
2793
                    "$by => $locator",
2794
                    "Strict locator can be either xpath, css, id, link, class, name: "
2795
                );
2796
        }
2797
    }
2798
2799
    /**
2800
     * @param $page
2801
     * @param $selector
2802
     * @return WebDriverElement
2803
     * @throws \Codeception\Exception\ElementNotFound
2804
     */
2805
    protected function matchFirstOrFail($page, $selector)
2806
    {
2807
        $this->enableImplicitWait();
2808
        $els = $this->match($page, $selector);
2809
        $this->disableImplicitWait();
2810
        if (!count($els)) {
2811
            throw new ElementNotFound($selector, "CSS or XPath");
2812
        }
2813
        return reset($els);
2814
    }
2815
2816
    /**
2817
     * Presses the given key on the given element.
2818
     * To specify a character and modifier (e.g. ctrl, alt, shift, meta), pass an array for $char with
2819
     * the modifier as the first element and the character as the second.
2820
     * For special keys use key constants from WebDriverKeys class.
2821
     *
2822
     * ``` php
2823
     * <?php
2824
     * // <input id="page" value="old" />
2825
     * $I->pressKey('#page','a'); // => olda
2826
     * $I->pressKey('#page',array('ctrl','a'),'new'); //=> new
2827
     * $I->pressKey('#page',array('shift','111'),'1','x'); //=> old!!!1x
2828
     * $I->pressKey('descendant-or-self::*[@id='page']','u'); //=> oldu
2829
     * $I->pressKey('#name', array('ctrl', 'a'), \Facebook\WebDriver\WebDriverKeys::DELETE); //=>''
2830
     * ?>
2831
     * ```
2832
     *
2833
     * @param $element
2834
     * @param $char string|array Can be char or array with modifier. You can provide several chars.
2835
     * @throws \Codeception\Exception\ElementNotFound
2836
     */
2837
    public function pressKey($element, $char)
0 ignored issues
show
Unused Code introduced by
The parameter $char is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2838
    {
2839
        $el = $this->matchFirstOrFail($this->getBaseElement(), $element);
2840
        $args = func_get_args();
2841
        array_shift($args);
2842
        $keys = [];
2843
        foreach ($args as $key) {
2844
            $keys[] = $this->convertKeyModifier($key);
2845
        }
2846
        $el->sendKeys($keys);
2847
    }
2848
2849
    protected function convertKeyModifier($keys)
2850
    {
2851
        if (!is_array($keys)) {
2852
            return $keys;
2853
        }
2854
        if (!isset($keys[1])) {
2855
            return $keys;
2856
        }
2857
        list($modifier, $key) = $keys;
2858
2859
        switch ($modifier) {
2860
            case 'ctrl':
2861
            case 'control':
2862
                return [WebDriverKeys::CONTROL, $key];
2863
            case 'alt':
2864
                return [WebDriverKeys::ALT, $key];
2865
            case 'shift':
2866
                return [WebDriverKeys::SHIFT, $key];
2867
            case 'meta':
2868
                return [WebDriverKeys::META, $key];
2869
        }
2870
        return $keys;
2871
    }
2872
2873
    protected function assertNodesContain($text, $nodes, $selector = null)
2874
    {
2875
        $this->assertThat($nodes, new WebDriverConstraint($text, $this->_getCurrentUri()), $selector);
2876
    }
2877
2878
    protected function assertNodesNotContain($text, $nodes, $selector = null)
2879
    {
2880
        $this->assertThat($nodes, new WebDriverConstraintNot($text, $this->_getCurrentUri()), $selector);
2881
    }
2882
2883
    protected function assertPageContains($needle, $message = '')
2884
    {
2885
        $this->assertThat(
2886
            htmlspecialchars_decode($this->getVisibleText()),
2887
            new PageConstraint($needle, $this->_getCurrentUri()),
2888
            $message
2889
        );
2890
    }
2891
2892
    protected function assertPageNotContains($needle, $message = '')
2893
    {
2894
        $this->assertThatItsNot(
2895
            htmlspecialchars_decode($this->getVisibleText()),
2896
            new PageConstraint($needle, $this->_getCurrentUri()),
2897
            $message
2898
        );
2899
    }
2900
2901
    protected function assertPageSourceContains($needle, $message = '')
2902
    {
2903
        $this->assertThat(
2904
            $this->webDriver->getPageSource(),
2905
            new PageConstraint($needle, $this->_getCurrentUri()),
2906
            $message
2907
        );
2908
    }
2909
2910
    protected function assertPageSourceNotContains($needle, $message = '')
2911
    {
2912
        $this->assertThatItsNot(
2913
            $this->webDriver->getPageSource(),
2914
            new PageConstraint($needle, $this->_getCurrentUri()),
2915
            $message
2916
        );
2917
    }
2918
2919
    /**
2920
     * Append the given text to the given element.
2921
     * Can also add a selection to a select box.
2922
     *
2923
     * ``` php
2924
     * <?php
2925
     * $I->appendField('#mySelectbox', 'SelectValue');
2926
     * $I->appendField('#myTextField', 'appended');
2927
     * ?>
2928
     * ```
2929
     *
2930
     * @param string $field
2931
     * @param string $value
2932
     * @throws \Codeception\Exception\ElementNotFound
2933
     */
2934
    public function appendField($field, $value)
2935
    {
2936
        $el = $this->findField($field);
2937
2938
        switch ($el->getTagName()) {
2939
            //Multiple select
2940
            case "select":
2941
                $matched = false;
2942
                $wdSelect = new WebDriverSelect($el);
2943
                try {
2944
                    $wdSelect->selectByVisibleText($value);
2945
                    $matched = true;
2946
                } catch (NoSuchElementException $e) {
2947
                    // exception treated at the end
2948
                }
2949
2950
                try {
2951
                    $wdSelect->selectByValue($value);
2952
                    $matched = true;
2953
                } catch (NoSuchElementException $e) {
2954
                    // exception treated at the end
2955
                }
2956
                if ($matched) {
2957
                    return;
2958
                }
2959
2960
                throw new ElementNotFound(json_encode($value), "Option inside $field matched by name or value");
2961
            case "textarea":
2962
                $el->sendKeys($value);
2963
                return;
2964
            case "div": //allows for content editable divs
2965
                $el->sendKeys(WebDriverKeys::END);
2966
                $el->sendKeys($value);
2967
                return;
2968
            //Text, Checkbox, Radio
2969
            case "input":
2970
                $type = $el->getAttribute('type');
2971
2972
                if ($type == 'checkbox') {
2973
                    //Find by value or css,id,xpath
2974
                    $field = $this->findCheckable($this->getBaseElement(), $value, true);
2975
                    if (!$field) {
2976
                        throw new ElementNotFound($value, "Checkbox or Radio by Label or CSS or XPath");
2977
                    }
2978
                    if ($field->isSelected()) {
2979
                        return;
2980
                    }
2981
                    $field->click();
2982
                    return;
2983
                } elseif ($type == 'radio') {
2984
                    $this->selectOption($field, $value);
2985
                    return;
2986
                }
2987
2988
                $el->sendKeys($value);
2989
                return;
2990
        }
2991
2992
        throw new ElementNotFound($field, "Field by name, label, CSS or XPath");
2993
    }
2994
2995
    /**
2996
     * @param $selector
2997
     * @return array
2998
     */
2999
    protected function matchVisible($selector)
3000
    {
3001
        $els = $this->match($this->getBaseElement(), $selector);
3002
        $nodes = array_filter(
3003
            $els,
3004
            function (WebDriverElement $el) {
3005
                return $el->isDisplayed();
3006
            }
3007
        );
3008
        return $nodes;
3009
    }
3010
3011
    /**
3012
     * @param $selector
3013
     * @return WebDriverBy
3014
     * @throws \InvalidArgumentException
3015
     */
3016
    protected function getLocator($selector)
3017
    {
3018
        if ($selector instanceof WebDriverBy) {
3019
            return $selector;
3020
        }
3021
        if (is_array($selector)) {
3022
            return $this->getStrictLocator($selector);
3023
        }
3024
        if (Locator::isID($selector)) {
3025
            return WebDriverBy::id(substr($selector, 1));
3026
        }
3027
        if (Locator::isCSS($selector)) {
3028
            return WebDriverBy::cssSelector($selector);
3029
        }
3030
        if (Locator::isXPath($selector)) {
3031
            return WebDriverBy::xpath($selector);
3032
        }
3033
        throw new \InvalidArgumentException("Only CSS or XPath allowed");
3034
    }
3035
3036
    public function saveSessionSnapshot($name)
3037
    {
3038
        $this->sessionSnapshots[$name] = [];
3039
3040
        foreach ($this->webDriver->manage()->getCookies() as $cookie) {
3041
            if (in_array(trim($cookie['name']), [LocalServer::COVERAGE_COOKIE, LocalServer::COVERAGE_COOKIE_ERROR])) {
3042
                continue;
3043
            }
3044
3045
            if ($this->cookieDomainMatchesConfigUrl($cookie)) {
3046
                $this->sessionSnapshots[$name][] = $cookie;
3047
            }
3048
        }
3049
3050
        $this->debugSection('Snapshot', "Saved \"$name\" session snapshot");
3051
    }
3052
3053
    public function loadSessionSnapshot($name)
3054
    {
3055
        if (!isset($this->sessionSnapshots[$name])) {
3056
            return false;
3057
        }
3058
        $this->webDriver->manage()->deleteAllCookies();
3059
        foreach ($this->sessionSnapshots[$name] as $cookie) {
3060
            $this->webDriver->manage()->addCookie($cookie);
3061
        }
3062
        $this->debugSection('Snapshot', "Restored \"$name\" session snapshot");
3063
        return true;
3064
    }
3065
3066
    public function deleteSessionSnapshot($name)
3067
    {
3068
        if (isset($this->sessionSnapshots[$name])) {
3069
            unset($this->sessionSnapshots[$name]);
3070
        }
3071
        $this->debugSection('Snapshot', "Deleted \"$name\" session snapshot");
3072
    }
3073
3074
    /**
3075
     * Check if the cookie domain matches the config URL.
3076
     *
3077
     * @param array|Cookie $cookie
3078
     * @return bool
3079
     */
3080
    private function cookieDomainMatchesConfigUrl($cookie)
3081
    {
3082
        if (!array_key_exists('domain', $cookie)) {
3083
            return true;
3084
        }
3085
3086
        $setCookie = new SetCookie();
3087
        $setCookie->setDomain($cookie['domain']);
3088
3089
        return $setCookie->matchesDomain(parse_url($this->config['url'], PHP_URL_HOST));
0 ignored issues
show
Security Bug introduced by
It seems like parse_url($this->config['url'], PHP_URL_HOST) targeting parse_url() can also be of type false; however, GuzzleHttp\Cookie\SetCookie::matchesDomain() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
3090
    }
3091
3092
    /**
3093
     * @return bool
3094
     */
3095
    protected function isPhantom()
3096
    {
3097
        return strpos($this->config['browser'], 'phantom') === 0;
3098
    }
3099
3100
    /**
3101
     * Move to the middle of the given element matched by the given locator.
3102
     * Extra shift, calculated from the top-left corner of the element,
3103
     * can be set by passing $offsetX and $offsetY parameters.
3104
     *
3105
     * ``` php
3106
     * <?php
3107
     * $I->scrollTo(['css' => '.checkout'], 20, 50);
3108
     * ?>
3109
     * ```
3110
     *
3111
     * @param $selector
3112
     * @param int $offsetX
3113
     * @param int $offsetY
3114
     */
3115
    public function scrollTo($selector, $offsetX = null, $offsetY = null)
3116
    {
3117
        $el = $this->matchFirstOrFail($this->getBaseElement(), $selector);
3118
        $x = $el->getLocation()->getX() + $offsetX;
3119
        $y = $el->getLocation()->getY() + $offsetY;
3120
        $this->webDriver->executeScript("window.scrollTo($x, $y)");
3121
    }
3122
3123
    /**
3124
     * Opens a new browser tab (wherever it is possible) and switches to it.
3125
     *
3126
     * ```php
3127
     * <?php
3128
     * $I->openNewTab();
3129
     * ```
3130
     * Tab is opened by using `window.open` javascript in a browser.
3131
     * Please note, that adblock can restrict creating such tabs.
3132
     *
3133
     * Can't be used with PhantomJS
3134
     *
3135
     */
3136
    public function openNewTab()
3137
    {
3138
        $this->executeJS("window.open('about:blank','_blank');");
3139
        $this->switchToNextTab();
3140
    }
3141
3142
    /**
3143
     * Closes current browser tab and switches to previous active tab.
3144
     *
3145
     * ```php
3146
     * <?php
3147
     * $I->closeTab();
3148
     * ```
3149
     *
3150
     * Can't be used with PhantomJS
3151
     */
3152
    public function closeTab()
3153
    {
3154
        $prevTab = $this->getRelativeTabHandle(-1);
3155
        $this->webDriver->close();
3156
        $this->webDriver->switchTo()->window($prevTab);
3157
    }
3158
3159
    /**
3160
     * Switches to next browser tab.
3161
     * An offset can be specified.
3162
     *
3163
     * ```php
3164
     * <?php
3165
     * // switch to next tab
3166
     * $I->switchToNextTab();
3167
     * // switch to 2nd next tab
3168
     * $I->switchToNextTab(2);
3169
     * ```
3170
     *
3171
     * Can't be used with PhantomJS
3172
     *
3173
     * @param int $offset 1
3174
     */
3175
    public function switchToNextTab($offset = 1)
3176
    {
3177
        $tab = $this->getRelativeTabHandle($offset);
3178
        $this->webDriver->switchTo()->window($tab);
3179
    }
3180
3181
    /**
3182
     * Switches to previous browser tab.
3183
     * An offset can be specified.
3184
     *
3185
     * ```php
3186
     * <?php
3187
     * // switch to previous tab
3188
     * $I->switchToPreviousTab();
3189
     * // switch to 2nd previous tab
3190
     * $I->switchToPreviousTab(2);
3191
     * ```
3192
     *
3193
     * Can't be used with PhantomJS
3194
     *
3195
     * @param int $offset 1
3196
     */
3197
    public function switchToPreviousTab($offset = 1)
3198
    {
3199
        $this->switchToNextTab(0 - $offset);
3200
    }
3201
3202
    protected function getRelativeTabHandle($offset)
3203
    {
3204
        if ($this->isPhantom()) {
3205
            throw new ModuleException($this, "PhantomJS doesn't support tab actions");
3206
        }
3207
        $handle = $this->webDriver->getWindowHandle();
3208
        $handles = $this->webDriver->getWindowHandles();
3209
        $idx = array_search($handle, $handles);
3210
        return $handles[($idx + $offset) % count($handles)];
3211
    }
3212
3213
    /**
3214
     * Waits for element and runs a sequence of actions inside its context.
3215
     * Actions can be defined with array, callback, or `Codeception\Util\ActionSequence` instance.
3216
     *
3217
     * Actions as array are recommended for simple to combine "waitForElement" with assertions;
3218
     * `waitForElement($el)` and `see('text', $el)` can be simplified to:
3219
     *
3220
     * ```php
3221
     * <?php
3222
     * $I->performOn($el, ['see' => 'text']);
3223
     * ```
3224
     *
3225
     * List of actions can be pragmatically build using `Codeception\Util\ActionSequence`:
3226
     *
3227
     * ```php
3228
     * <?php
3229
     * $I->performOn('.model', ActionSequence::build()
3230
     *     ->see('Warning')
3231
     *     ->see('Are you sure you want to delete this?')
3232
     *     ->click('Yes')
3233
     * );
3234
     * ```
3235
     *
3236
     * Actions executed from array or ActionSequence will print debug output for actions, and adds an action name to
3237
     * exception on failure.
3238
     *
3239
     * Whenever you need to define more actions a callback can be used. A WebDriver module is passed for argument:
3240
     *
3241
     * ```php
3242
     * <?php
3243
     * $I->performOn('.rememberMe', function (WebDriver $I) {
3244
     *      $I->see('Remember me next time');
3245
     *      $I->seeElement('#LoginForm_rememberMe');
3246
     *      $I->dontSee('Login');
3247
     * });
3248
     * ```
3249
     *
3250
     * In 3rd argument you can set number a seconds to wait for element to appear
3251
     *
3252
     * @param $element
3253
     * @param $actions
3254
     * @param int $timeout
3255
     */
3256
    public function performOn($element, $actions, $timeout = 10)
3257
    {
3258
        $this->waitForElement($element, $timeout);
3259
        $this->setBaseElement($element);
3260
        $this->debugSection('InnerText', $this->getBaseElement()->getText());
3261
3262
        if (is_callable($actions)) {
3263
            $actions($this);
3264
            $this->setBaseElement();
3265
            return;
3266
        }
3267
        if (is_array($actions)) {
3268
            $actions = ActionSequence::build()->fromArray($actions);
3269
        }
3270
3271
        if (!$actions instanceof ActionSequence) {
3272
            throw new \InvalidArgumentException("2nd parameter, actions should be callback, ActionSequence or array");
3273
        }
3274
3275
        $actions->run($this);
3276
        $this->setBaseElement();
3277
    }
3278
3279
    protected function setBaseElement($element = null)
3280
    {
3281
        if ($element === null) {
3282
            $this->baseElement = $this->webDriver;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->webDriver of type object<Facebook\WebDriver\Remote\RemoteWebDriver> is incompatible with the declared type object<Facebook\WebDrive...emote\RemoteWebElement> of property $baseElement.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3283
            return;
3284
        }
3285
        $this->baseElement = $this->matchFirstOrFail($this->webDriver, $element);
3286
    }
3287
3288
    protected function enableImplicitWait()
3289
    {
3290
        if (!$this->config['wait']) {
3291
            return;
3292
        }
3293
        $this->webDriver->manage()->timeouts()->implicitlyWait($this->config['wait']);
3294
    }
3295
3296
    protected function disableImplicitWait()
3297
    {
3298
        if (!$this->config['wait']) {
3299
            return;
3300
        }
3301
        $this->webDriver->manage()->timeouts()->implicitlyWait(0);
3302
    }
3303
}
3304