Completed
Pull Request — 2.3 (#4741)
by
unknown
05:41 queued 40s
created

WebDriver::clearField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace Codeception\Module;
3
4
use Codeception\Coverage\Subscriber\LocalServer;
5
use Codeception\Exception\ConnectionException;
6
use Codeception\Exception\ElementNotFound;
7
use Codeception\Exception\MalformedLocatorException;
8
use Codeception\Exception\ModuleConfigException as ModuleConfigException;
9
use Codeception\Exception\ModuleException;
10
use Codeception\Exception\TestRuntimeException;
11
use Codeception\Lib\Interfaces\ConflictsWithModule;
12
use Codeception\Lib\Interfaces\ElementLocator;
13
use Codeception\Lib\Interfaces\MultiSession as MultiSessionInterface;
14
use Codeception\Lib\Interfaces\PageSourceSaver;
15
use Codeception\Lib\Interfaces\Remote as RemoteInterface;
16
use Codeception\Lib\Interfaces\RequiresPackage;
17
use Codeception\Lib\Interfaces\ScreenshotSaver;
18
use Codeception\Lib\Interfaces\SessionSnapshot;
19
use Codeception\Lib\Interfaces\Web as WebInterface;
20
use Codeception\Module as CodeceptionModule;
21
use Codeception\PHPUnit\Constraint\Page as PageConstraint;
22
use Codeception\PHPUnit\Constraint\WebDriver as WebDriverConstraint;
23
use Codeception\PHPUnit\Constraint\WebDriverNot as WebDriverConstraintNot;
24
use Codeception\Test\Descriptor;
25
use Codeception\Test\Interfaces\ScenarioDriven;
26
use Codeception\TestInterface;
27
use Codeception\Util\Debug;
28
use Codeception\Util\ActionSequence;
29
use Codeception\Util\Locator;
30
use Codeception\Util\Uri;
31
use Facebook\WebDriver\Cookie;
32
use Facebook\WebDriver\Exception\InvalidElementStateException;
33
use Facebook\WebDriver\Exception\InvalidSelectorException;
34
use Facebook\WebDriver\Exception\NoSuchElementException;
35
use Facebook\WebDriver\Exception\UnknownServerException;
36
use Facebook\WebDriver\Exception\WebDriverCurlException;
37
use Facebook\WebDriver\Interactions\WebDriverActions;
38
use Facebook\WebDriver\Remote\LocalFileDetector;
39
use Facebook\WebDriver\Remote\RemoteWebDriver;
40
use Facebook\WebDriver\Remote\UselessFileDetector;
41
use Facebook\WebDriver\Remote\RemoteWebElement;
42
use Facebook\WebDriver\Remote\WebDriverCapabilityType;
43
use Facebook\WebDriver\WebDriverBy;
44
use Facebook\WebDriver\WebDriverDimension;
45
use Facebook\WebDriver\WebDriverElement;
46
use Facebook\WebDriver\WebDriverExpectedCondition;
47
use Facebook\WebDriver\WebDriverKeys;
48
use Facebook\WebDriver\WebDriverSelect;
49
use GuzzleHttp\Cookie\SetCookie;
50
use Symfony\Component\DomCrawler\Crawler;
51
52
/**
53
 * New generation Selenium WebDriver module.
54
 *
55
 * ## Local Testing
56
 *
57
 * ### Selenium
58
 *
59
 * To run Selenium Server you need [Java](https://www.java.com/) as well as Chrome or Firefox browser installed.
60
 *
61
 * 1. Download [Selenium Standalone Server](http://docs.seleniumhq.org/download/)
62
 * 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).
63
 * 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`.
64
 * 4. Configure this module (in `acceptance.suite.yml`) by setting `url` and `browser`:
65
 *
66
 * ```yaml
67
 *     modules:
68
 *        enabled:
69
 *           - WebDriver:
70
 *              url: 'http://localhost/'
71
 *              browser: chrome # 'chrome' or 'firefox'
72
 * ```
73
 *
74
 * Launch Selenium Server before executing tests.
75
 *
76
 * ```
77
 * java -jar "/path/to/selenium-server-standalone-xxx.jar"
78
 * ```
79
 *
80
 * ### ChromeDriver
81
 *
82
 * To run tests in Chrome browser you may connect to ChromeDriver directly, without using Selenium Server.
83
 *
84
 * 1. Install [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/getting-started).
85
 * 2. Launch ChromeDriver: `chromedriver --url-base=/wd/hub`
86
 * 3. Configure this module to use ChromeDriver port:
87
 *
88
 * ```yaml
89
 *     modules:
90
 *        enabled:
91
 *           - WebDriver:
92
 *              url: 'http://localhost/'
93
 *              window_size: false # disabled in ChromeDriver
94
 *              port: 9515
95
 *              browser: chrome
96
 *              capabilities:
97
 *                  chromeOptions: # additional chrome options
98
 * ```
99
 *
100
 * Additional [Chrome options](https://sites.google.com/a/chromium.org/chromedriver/capabilities) can be set in `chromeOptions` capabilities.
101
 *
102
 *
103
 * ### PhantomJS
104
 *
105
 * PhantomJS is a [headless browser](https://en.wikipedia.org/wiki/Headless_browser) alternative to Selenium Server that implements
106
 * [the WebDriver protocol](https://code.google.com/p/selenium/wiki/JsonWireProtocol).
107
 * It allows you to run Selenium tests on a server without a GUI installed.
108
 *
109
 * 1. Download [PhantomJS](http://phantomjs.org/download.html)
110
 * 2. Run PhantomJS in WebDriver mode: `phantomjs --webdriver=4444`
111
 * 3. Configure this module (in `acceptance.suite.yml`) by setting url and `phantomjs` as browser:
112
 *
113
 * ```yaml
114
 *     modules:
115
 *        enabled:
116
 *           - WebDriver:
117
 *              url: 'http://localhost/'
118
 *              browser: phantomjs
119
 * ```
120
 *
121
 * 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.
122
 *
123
 * ### Headless Selenium in Docker
124
 *
125
 * Docker can ship Selenium Server with all its dependencies and browsers inside a single container.
126
 * Running tests inside Docker is as easy as pulling [official selenium image](https://github.com/SeleniumHQ/docker-selenium) and starting a container with Chrome:
127
 *
128
 * ```
129
 * docker run --net=host selenium/standalone-chrome
130
 * ```
131
 *
132
 * By using `--net=host` we allow selenium to access local websites.
133
 *
134
 * ## Cloud Testing
135
 *
136
 * Cloud Testing services can run your WebDriver tests in the cloud.
137
 * In case you want to test a local site or site behind a firewall
138
 * you should use a tunnel application provided by a service.
139
 *
140
 * ### SauceLabs
141
 *
142
 * 1. Create an account at [SauceLabs.com](http://SauceLabs.com) to get your username and access key
143
 * 2. In the module configuration use the format `username`:`access_key`@ondemand.saucelabs.com' for `host`
144
 * 3. Configure `platform` under `capabilities` to define the [Operating System](https://docs.saucelabs.com/reference/platforms-configurator/#/)
145
 * 4. run a tunnel app if your site can't be accessed from Internet
146
 *
147
 * ```yaml
148
 *     modules:
149
 *        enabled:
150
 *           - WebDriver:
151
 *              url: http://mysite.com
152
 *              host: '<username>:<access key>@ondemand.saucelabs.com'
153
 *              port: 80
154
 *              browser: chrome
155
 *              capabilities:
156
 *                  platform: 'Windows 10'
157
 * ```
158
 *
159
 * ### BrowserStack
160
 *
161
 * 1. Create an account at [BrowserStack](https://www.browserstack.com/) to get your username and access key
162
 * 2. In the module configuration use the format `username`:`access_key`@hub.browserstack.com' for `host`
163
 * 3. Configure `os` and `os_version` under `capabilities` to define the operating System
164
 * 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.
165
 *
166
 * ```yaml
167
 *     modules:
168
 *        enabled:
169
 *           - WebDriver:
170
 *              url: http://mysite.com
171
 *              host: '<username>:<access key>@hub.browserstack.com'
172
 *              port: 80
173
 *              browser: chrome
174
 *              capabilities:
175
 *                  os: Windows
176
 *                  os_version: 10
177
 *                  browserstack.local: true # for local testing
178
 * ```
179
 * ### TestingBot
180
 *
181
 * 1. Create an account at [TestingBot](https://testingbot.com/) to get your key and secret
182
 * 2. In the module configuration use the format `key`:`secret`@hub.testingbot.com' for `host`
183
 * 3. Configure `platform` under `capabilities` to define the [Operating System](https://testingbot.com/support/getting-started/browsers.html)
184
 * 4. Run [TestingBot Tunnel](https://testingbot.com/support/other/tunnel) if your site can't be accessed from Internet
185
 *
186
 * ```yaml
187
 *     modules:
188
 *        enabled:
189
 *           - WebDriver:
190
 *              url: http://mysite.com
191
 *              host: '<key>:<secret>@hub.testingbot.com'
192
 *              port: 80
193
 *              browser: chrome
194
 *              capabilities:
195
 *                  platform: Windows 10
196
 * ```
197
 *
198
 * ## Configuration
199
 *
200
 * * `url` *required* - Starting URL for your app.
201
 * * `browser` *required* - Browser to launch.
202
 * * `host` - Selenium server host (127.0.0.1 by default).
203
 * * `port` - Selenium server port (4444 by default).
204
 * * `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.
205
 * * `start` - Autostart a browser for tests. Can be disabled if browser session is started with `_initializeSession` inside a Helper.
206
 * * `window_size` - Initial window size. Set to `maximize` or a dimension in the format `640x480`.
207
 * * `clear_cookies` - Set to false to keep cookies, or set to true (default) to delete all cookies between tests.
208
 * * `wait` (default: 0 seconds) - Whenever element is required and is not on page, wait for n seconds to find it before fail.
209
 * * `capabilities` - Sets Selenium [desired capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities). Should be a key-value array.
210
 * * `connection_timeout` - timeout for opening a connection to remote selenium server (30 seconds by default).
211
 * * `request_timeout` - timeout for a request to return something from remote selenium server (30 seconds by default).
212
 * * `pageload_timeout` - amount of time to wait for a page load to complete before throwing an error (default 0 seconds).
213
 * * `http_proxy` - sets http proxy server url for testing a remote server.
214
 * * `http_proxy_port` - sets http proxy server port
215
 * * `debug_log_entries` - how many selenium entries to print with `debugWebDriverLogs` or on fail (15 by default).
216
 * * `log_js_errors` - Set to true to include possible JavaScript to HTML report, or set to false (default) to deactivate.
217
 *
218
 * Example (`acceptance.suite.yml`)
219
 *
220
 * ```yaml
221
 *     modules:
222
 *        enabled:
223
 *           - WebDriver:
224
 *              url: 'http://localhost/'
225
 *              browser: firefox
226
 *              window_size: 1024x768
227
 *              capabilities:
228
 *                  unexpectedAlertBehaviour: 'accept'
229
 *                  firefox_profile: '~/firefox-profiles/codeception-profile.zip.b64'
230
 * ```
231
 *
232
 * ## Usage
233
 *
234
 * ### Locating Elements
235
 *
236
 * Most methods in this module that operate on a DOM element (e.g. `click`) accept a locator as the first argument,
237
 * which can be either a string or an array.
238
 *
239
 * If the locator is an array, it should have a single element,
240
 * with the key signifying the locator type (`id`, `name`, `css`, `xpath`, `link`, or `class`)
241
 * and the value being the locator itself.
242
 * This is called a "strict" locator.
243
 * Examples:
244
 *
245
 * * `['id' => 'foo']` matches `<div id="foo">`
246
 * * `['name' => 'foo']` matches `<div name="foo">`
247
 * * `['css' => 'input[type=input][value=foo]']` matches `<input type="input" value="foo">`
248
 * * `['xpath' => "//input[@type='submit'][contains(@value, 'foo')]"]` matches `<input type="submit" value="foobar">`
249
 * * `['link' => 'Click here']` matches `<a href="google.com">Click here</a>`
250
 * * `['class' => 'foo']` matches `<div class="foo">`
251
 *
252
 * Writing good locators can be tricky.
253
 * 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/).
254
 *
255
 * If you prefer, you may also pass a string for the locator. This is called a "fuzzy" locator.
256
 * In this case, Codeception uses a a variety of heuristics (depending on the exact method called) to determine what element you're referring to.
257
 * For example, here's the heuristic used for the `submitForm` method:
258
 *
259
 * 1. Does the locator look like an ID selector (e.g. "#foo")? If so, try to find a form matching that ID.
260
 * 2. If nothing found, check if locator looks like a CSS selector. If so, run it.
261
 * 3. If nothing found, check if locator looks like an XPath expression. If so, run it.
262
 * 4. Throw an `ElementNotFound` exception.
263
 *
264
 * Be warned that fuzzy locators can be significantly slower than strict locators.
265
 * Especially if you use Selenium WebDriver with `wait` (aka implicit wait) option.
266
 * In the example above if you set `wait` to 5 seconds and use XPath string as fuzzy locator,
267
 * `submitForm` method will wait for 5 seconds at each step.
268
 * That means 5 seconds finding the form by ID, another 5 seconds finding by CSS
269
 * until it finally tries to find the form by XPath).
270
 * If speed is a concern, it's recommended you stick with explicitly specifying the locator type via the array syntax.
271
 *
272
 * ## Public Properties
273
 *
274
 * * `webDriver` - instance of `\Facebook\WebDriver\Remote\RemoteWebDriver`. Can be accessed from Helper classes for complex WebDriver interactions.
275
 *
276
 * ```php
277
 * // inside Helper class
278
 * $this->getModule('WebDriver')->webDriver->getKeyboard()->sendKeys('hello, webdriver');
279
 * ```
280
 *
281
 */
282
class WebDriver extends CodeceptionModule implements
283
    WebInterface,
284
    RemoteInterface,
285
    MultiSessionInterface,
286
    SessionSnapshot,
287
    ScreenshotSaver,
288
    PageSourceSaver,
289
    ElementLocator,
290
    ConflictsWithModule,
291
    RequiresPackage
292
{
293
    protected $requiredFields = ['browser', 'url'];
294
    protected $config = [
295
        'protocol'           => 'http',
296
        'host'               => '127.0.0.1',
297
        'port'               => '4444',
298
        'path'               => '/wd/hub',
299
        'start'              => true,
300
        'restart'            => false,
301
        'wait'               => 0,
302
        'clear_cookies'      => true,
303
        'window_size'        => false,
304
        'capabilities'       => [],
305
        'connection_timeout' => null,
306
        'request_timeout'    => null,
307
        'pageload_timeout'   => null,
308
        'http_proxy'         => null,
309
        'http_proxy_port'    => null,
310
        'ssl_proxy'          => null,
311
        'ssl_proxy_port'     => null,
312
        'debug_log_entries'  => 15,
313
        'log_js_errors'      => false
314
    ];
315
316
    protected $wdHost;
317
    protected $capabilities;
318
    protected $connectionTimeoutInMs;
319
    protected $requestTimeoutInMs;
320
    protected $test;
321
    protected $sessions = [];
322
    protected $sessionSnapshots = [];
323
    protected $httpProxy;
324
    protected $httpProxyPort;
325
    protected $sslProxy;
326
    protected $sslProxyPort;
327
328
    /**
329
     * @var RemoteWebDriver
330
     */
331
    public $webDriver;
332
333
    /**
334
     * @var RemoteWebElement
335
     */
336
    protected $baseElement;
337
338
    public function _requires()
339
    {
340
        return ['Facebook\WebDriver\Remote\RemoteWebDriver' => '"facebook/webdriver": "^1.0.1"'];
341
    }
342
343
    public function _initialize()
344
    {
345
        $this->wdHost = sprintf('%s://%s:%s%s', $this->config['protocol'], $this->config['host'], $this->config['port'], $this->config['path']);
346
        $this->capabilities = $this->config['capabilities'];
347
        $this->capabilities[WebDriverCapabilityType::BROWSER_NAME] = $this->config['browser'];
348
        if ($proxy = $this->getProxy()) {
349
            $this->capabilities[WebDriverCapabilityType::PROXY] = $proxy;
350
        }
351
        $this->connectionTimeoutInMs = $this->config['connection_timeout'] * 1000;
352
        $this->requestTimeoutInMs = $this->config['request_timeout'] * 1000;
353
        $this->loadFirefoxProfile();
354
    }
355
356
    /**
357
     * Change capabilities of WebDriver. Should be executed before starting a new browser session.
358
     * 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.
359
     * Additional [Chrome options](https://github.com/facebook/php-webdriver/wiki/ChromeOptions) (like adding extensions) can be passed as well.
360
     *
361
     * ```php
362
     * <?php // in helper
363
     * public function _before(TestInterface $test)
364
     * {
365
     *     $this->getModule('WebDriver')->_capabilities(function($currentCapabilities) {
366
     *         // or new \Facebook\WebDriver\Remote\DesiredCapabilities();
367
     *         return \Facebook\WebDriver\Remote\DesiredCapabilities::firefox();
368
     *     });
369
     * }
370
     * ```
371
     *
372
     * to make this work load `\Helper\Acceptance` before `WebDriver` in `acceptance.suite.yml`:
373
     *
374
     * ```yaml
375
     * modules:
376
     *     enabled:
377
     *         - \Helper\Acceptance
378
     *         - WebDriver
379
     * ```
380
     *
381
     * For instance, [**BrowserStack** cloud service](https://www.browserstack.com/automate/capabilities) may require a test name to be set in capabilities.
382
     * This is how it can be done via `_capabilities` method from `Helper\Acceptance`:
383
     *
384
     * ```php
385
     * <?php // inside Helper\Acceptance
386
     * public function _before(TestInterface $test)
387
     * {
388
     *      $name = $test->getMetadata()->getName();
389
     *      $this->getModule('WebDriver')->_capabilities(function($currentCapabilities) use ($name) {
390
     *          $currentCapabilities['name'] = $name;
391
     *          return $currentCapabilities;
392
     *      });
393
     * }
394
     * ```
395
     * In this case, please ensure that `\Helper\Acceptance` is loaded before WebDriver so new capabilities could be applied.
396
     *
397
     * @api
398
     * @param \Closure $capabilityFunction
399
     */
400
    public function _capabilities(\Closure $capabilityFunction)
401
    {
402
        $this->capabilities = $capabilityFunction($this->capabilities);
403
    }
404
405
    public function _conflicts()
406
    {
407
        return 'Codeception\Lib\Interfaces\Web';
408
    }
409
410
    public function _before(TestInterface $test)
411
    {
412
        if (!isset($this->webDriver) && $this->config['start']) {
413
            $this->_initializeSession();
414
        }
415
        $this->setBaseElement();
416
417
        if (method_exists($this->webDriver, 'getCapabilities')) {
418
            $browser = $this->webDriver->getCapabilities()->getBrowserName();
419
            $capabilities = $this->webDriver->getCapabilities()->toArray();
420
        } else {
421
            //Used with facebook/php-webdriver <1.3.0 (usually on PHP 5.4)
422
            $browser = $this->config['browser'];
423
            $capabilities = $this->config['capabilities'];
424
        }
425
        $test->getMetadata()->setCurrent([
426
            'browser' => $browser,
427
            'capabilities' => $capabilities,
428
        ]);
429
    }
430
431
    /**
432
     * Restarts a web browser.
433
     * Can be used with `_reconfigure` to open browser with different configuration
434
     *
435
     * ```php
436
     * <?php
437
     * // inside a Helper
438
     * $this->getModule('WebDriver')->_restart(); // just restart
439
     * $this->getModule('WebDriver')->_restart(['browser' => $browser]); // reconfigure + restart
440
     * ```
441
     *
442
     * @param array $config
443
     * @api
444
     */
445
    public function _restart($config = [])
446
    {
447
        $this->webDriver->quit();
448
        if (!empty($config)) {
449
            $this->_reconfigure($config);
450
        }
451
        $this->_initializeSession();
452
    }
453
454
    protected function onReconfigure()
455
    {
456
        $this->_initialize();
457
    }
458
459
    protected function loadFirefoxProfile()
460
    {
461
        if (!array_key_exists('firefox_profile', $this->config['capabilities'])) {
462
            return;
463
        }
464
465
        $firefox_profile = $this->config['capabilities']['firefox_profile'];
466
        if (file_exists($firefox_profile) === false) {
467
            throw new ModuleConfigException(
468
                __CLASS__,
469
                "Firefox profile does not exist under given path " . $firefox_profile
470
            );
471
        }
472
        // Set firefox profile as capability
473
        $this->capabilities['firefox_profile'] = file_get_contents($firefox_profile);
474
    }
475
476
    protected function initialWindowSize()
477
    {
478
        if ($this->config['window_size'] == 'maximize') {
479
            $this->maximizeWindow();
480
            return;
481
        }
482
        $size = explode('x', $this->config['window_size']);
483
        if (count($size) == 2) {
484
            $this->resizeWindow(intval($size[0]), intval($size[1]));
485
        }
486
    }
487
488
    public function _after(TestInterface $test)
489
    {
490
        if ($this->config['restart']) {
491
            $this->stopAllSessions();
492
            return;
493
        }
494
        if ($this->config['clear_cookies'] && isset($this->webDriver)) {
495
            $this->webDriver->manage()->deleteAllCookies();
496
        }
497
    }
498
499
    public function _failed(TestInterface $test, $fail)
500
    {
501
        $this->debugWebDriverLogs($test);
502
        $filename = preg_replace('~\W~', '.', Descriptor::getTestSignatureUnique($test));
503
        $outputDir = codecept_output_dir();
504
        $this->_saveScreenshot($report = $outputDir . mb_strcut($filename, 0, 245, 'utf-8') . '.fail.png');
505
        $test->getMetadata()->addReport('png', $report);
506
        $this->_savePageSource($report = $outputDir . mb_strcut($filename, 0, 244, 'utf-8') . '.fail.html');
507
        $test->getMetadata()->addReport('html', $report);
508
        $this->debug("Screenshot and page source were saved into '$outputDir' dir");
509
    }
510
511
    /**
512
     * Print out latest Selenium Logs in debug mode
513
     *
514
     * @param TestInterface $test
515
     */
516
    public function debugWebDriverLogs(TestInterface $test = null)
517
    {
518
        if (!isset($this->webDriver)) {
519
            $this->debug('WebDriver::debugWebDriverLogs method has been called when webDriver is not set');
520
            return;
521
        }
522
        try {
523
            // Dump out latest Selenium logs
524
            $logs = $this->webDriver->manage()->getAvailableLogTypes();
525
            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...
526
                $logEntries = array_slice(
527
                    $this->webDriver->manage()->getLog($logType),
528
                    -$this->config['debug_log_entries']
529
                );
530
531
                if (empty($logEntries)) {
532
                    $this->debugSection("Selenium {$logType} Logs", " EMPTY ");
533
                    continue;
534
                }
535
                $this->debugSection("Selenium {$logType} Logs", "\n" . $this->formatLogEntries($logEntries));
536
537
                if ($logType === 'browser' && $this->config['log_js_errors']
538
                    && ($test instanceof ScenarioDriven)
539
                ) {
540
                    $this->logJSErrors($test, $logEntries);
541
                }
542
            }
543
        } catch (\Exception $e) {
544
            $this->debug('Unable to retrieve Selenium logs : ' . $e->getMessage());
545
        }
546
    }
547
548
    /**
549
     * Turns an array of log entries into a human-readable string.
550
     * Each log entry is an array with the keys "timestamp", "level", and "message".
551
     * See https://code.google.com/p/selenium/wiki/JsonWireProtocol#Log_Entry_JSON_Object
552
     *
553
     * @param array $logEntries
554
     * @return string
555
     */
556
    protected function formatLogEntries(array $logEntries)
557
    {
558
        $formattedLogs = '';
559
560
        foreach ($logEntries as $logEntry) {
561
            // Timestamp is in milliseconds, but date() requires seconds.
562
            $time = date('H:i:s', $logEntry['timestamp'] / 1000) .
563
                // Append the milliseconds to the end of the time string
564
                '.' . ($logEntry['timestamp'] % 1000);
565
            $formattedLogs .= "{$time} {$logEntry['level']} - {$logEntry['message']}\n";
566
        }
567
        return $formattedLogs;
568
    }
569
570
    /**
571
     * Logs JavaScript errors as comments.
572
     *
573
     * @param ScenarioDriven $test
574
     * @param array $browserLogEntries
575
     */
576
    protected function logJSErrors(ScenarioDriven $test, array $browserLogEntries)
577
    {
578
        foreach ($browserLogEntries as $logEntry) {
579
            if (true === isset($logEntry['level'])
580
                && true === isset($logEntry['message'])
581
                && $this->isJSError($logEntry['level'], $logEntry['message'])
582
            ) {
583
                // Timestamp is in milliseconds, but date() requires seconds.
584
                $time = date('H:i:s', $logEntry['timestamp'] / 1000) .
585
                    // Append the milliseconds to the end of the time string
586
                    '.' . ($logEntry['timestamp'] % 1000);
587
                $test->getScenario()->comment("{$time} {$logEntry['level']} - {$logEntry['message']}");
588
            }
589
        }
590
    }
591
592
    /**
593
     * Determines if the log entry is an error.
594
     * The decision is made depending on browser and log-level.
595
     *
596
     * @param string $logEntryLevel
597
     * @param string $message
598
     * @return bool
599
     */
600
    protected function isJSError($logEntryLevel, $message)
601
    {
602
        return
603
        (
604
            ($this->isPhantom() && $logEntryLevel != 'INFO')          // phantomjs logs errors as "WARNING"
605
            || $logEntryLevel === 'SEVERE'                            // other browsers log errors as "SEVERE"
606
        )
607
        && strpos($message, 'ERR_PROXY_CONNECTION_FAILED') === false;  // ignore blackhole proxy
608
    }
609
610
    public function _afterSuite()
611
    {
612
        // this is just to make sure webDriver is cleared after suite
613
        $this->stopAllSessions();
614
    }
615
616
    protected function stopAllSessions()
617
    {
618
        foreach ($this->sessions as $session) {
619
            $this->_closeSession($session);
620
        }
621
        $this->webDriver = null;
622
        $this->baseElement = null;
623
    }
624
625 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...
626
    {
627
        $url = $this->config['url'];
628
        $url = preg_replace('~(https?:\/\/)(.*\.)(.*\.)~', "$1$3", $url); // removing current subdomain
629
        $url = preg_replace('~(https?:\/\/)(.*)~', "$1$subdomain.$2", $url); // inserting new
630
        $this->_reconfigure(['url' => $url]);
631
    }
632
633
    /**
634
     * Returns URL of a host.
635
     *
636
     * @api
637
     * @return mixed
638
     * @throws ModuleConfigException
639
     */
640
    public function _getUrl()
641
    {
642
        if (!isset($this->config['url'])) {
643
            throw new ModuleConfigException(
644
                __CLASS__,
645
                "Module connection failure. The URL for client can't bre retrieved"
646
            );
647
        }
648
        return $this->config['url'];
649
    }
650
651
    protected function getProxy()
652
    {
653
        $proxyConfig = [];
654
        if ($this->config['http_proxy']) {
655
            $proxyConfig['httpProxy'] = $this->config['http_proxy'];
656
            if ($this->config['http_proxy_port']) {
657
                $proxyConfig['httpProxy'] .= ':' . $this->config['http_proxy_port'];
658
            }
659
        }
660
        if ($this->config['ssl_proxy']) {
661
            $proxyConfig['sslProxy'] = $this->config['ssl_proxy'];
662
            if ($this->config['ssl_proxy_port']) {
663
                $proxyConfig['sslProxy'] .= ':' . $this->config['ssl_proxy_port'];
664
            }
665
        }
666
        if (!empty($proxyConfig)) {
667
            $proxyConfig['proxyType'] = 'manual';
668
            return $proxyConfig;
669
        }
670
        return null;
671
    }
672
673
    /**
674
     * Uri of currently opened page.
675
     * @return string
676
     * @api
677
     * @throws ModuleException
678
     */
679
    public function _getCurrentUri()
680
    {
681
        $url = $this->webDriver->getCurrentURL();
682
        if ($url == 'about:blank') {
683
            throw new ModuleException($this, 'Current url is blank, no page was opened');
684
        }
685
        return Uri::retrieveUri($url);
686
    }
687
688 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...
689
    {
690
        if (!isset($this->webDriver)) {
691
            $this->debug('WebDriver::_saveScreenshot method has been called when webDriver is not set');
692
            return;
693
        }
694
        try {
695
            $this->webDriver->takeScreenshot($filename);
696
        } catch (\Exception $e) {
697
            $this->debug('Unable to retrieve screenshot from Selenium : ' . $e->getMessage());
698
        }
699
    }
700
701
    public function _findElements($locator)
702
    {
703
        return $this->match($this->webDriver, $locator);
704
    }
705
706
    /**
707
     * Saves HTML source of a page to a file
708
     * @param $filename
709
     */
710 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...
711
    {
712
        if (!isset($this->webDriver)) {
713
            $this->debug('WebDriver::_savePageSource method has been called when webDriver is not set');
714
            return;
715
        }
716
        try {
717
            file_put_contents($filename, $this->webDriver->getPageSource());
718
        } catch (\Exception $e) {
719
            $this->debug('Unable to retrieve source page from Selenium : ' . $e->getMessage());
720
        }
721
    }
722
723
    /**
724
     * Takes a screenshot of the current window and saves it to `tests/_output/debug`.
725
     *
726
     * ``` php
727
     * <?php
728
     * $I->amOnPage('/user/edit');
729
     * $I->makeScreenshot('edit_page');
730
     * // saved to: tests/_output/debug/edit_page.png
731
     * $I->makeScreenshot();
732
     * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.png
733
     * ```
734
     *
735
     * @param $name
736
     */
737
    public function makeScreenshot($name = null)
738
    {
739
        if (empty($name)) {
740
            $name = uniqid(date("Y-m-d_H-i-s_"));
741
        }
742
        $debugDir = codecept_log_dir() . 'debug';
743
        if (!is_dir($debugDir)) {
744
            mkdir($debugDir, 0777);
745
        }
746
        $screenName = $debugDir . DIRECTORY_SEPARATOR . $name . '.png';
747
        $this->_saveScreenshot($screenName);
748
        $this->debug("Screenshot saved to $screenName");
749
    }
750
751
    /**
752
     * Resize the current window.
753
     *
754
     * ``` php
755
     * <?php
756
     * $I->resizeWindow(800, 600);
757
     *
758
     * ```
759
     *
760
     * @param int $width
761
     * @param int $height
762
     */
763
    public function resizeWindow($width, $height)
764
    {
765
        $this->webDriver->manage()->window()->setSize(new WebDriverDimension($width, $height));
766
    }
767
768 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...
769
    {
770
        $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params);
771
        $cookies = array_map(
772
            function ($c) {
773
                return $c['name'];
774
            },
775
            $cookies
776
        );
777
        $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies()));
778
        $this->assertContains($cookie, $cookies);
779
    }
780
781 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...
782
    {
783
        $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params);
784
        $cookies = array_map(
785
            function ($c) {
786
                return $c['name'];
787
            },
788
            $cookies
789
        );
790
        $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies()));
791
        $this->assertNotContains($cookie, $cookies);
792
    }
793
794
    public function setCookie($cookie, $value, array $params = [])
795
    {
796
        $params['name'] = $cookie;
797
        $params['value'] = $value;
798
        if (isset($params['expires'])) { // PhpBrowser compatibility
799
            $params['expiry'] = $params['expires'];
800
        }
801
        if (!isset($params['domain'])) {
802
            $urlParts = parse_url($this->config['url']);
803
            if (isset($urlParts['host'])) {
804
                $params['domain'] = $urlParts['host'];
805
            }
806
        }
807
        $this->webDriver->manage()->addCookie($params);
808
        $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies()));
809
    }
810
811
    public function resetCookie($cookie, array $params = [])
812
    {
813
        $this->webDriver->manage()->deleteCookieNamed($cookie);
814
        $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies()));
815
    }
816
817
    public function grabCookie($cookie, array $params = [])
818
    {
819
        $params['name'] = $cookie;
820
        $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params);
821
        if (empty($cookies)) {
822
            return null;
823
        }
824
        $cookie = reset($cookies);
825
        return $cookie['value'];
826
    }
827
828
    /**
829
     * Grabs current page source code.
830
     *
831
     * @throws ModuleException if no page was opened.
832
     *
833
     * @return string Current page source code.
834
     */
835
    public function grabPageSource()
836
    {
837
        // Make sure that some page was opened.
838
        $this->_getCurrentUri();
839
840
        return $this->webDriver->getPageSource();
841
    }
842
843
    protected function filterCookies($cookies, $params = [])
844
    {
845
        foreach (['domain', 'path', 'name'] as $filter) {
846
            if (!isset($params[$filter])) {
847
                continue;
848
            }
849
            $cookies = array_filter(
850
                $cookies,
851
                function ($item) use ($filter, $params) {
852
                    return $item[$filter] == $params[$filter];
853
                }
854
            );
855
        }
856
        return $cookies;
857
    }
858
859
    public function amOnUrl($url)
860
    {
861
        $host = Uri::retrieveHost($url);
862
        $this->_reconfigure(['url' => $host]);
863
        $this->debugSection('Host', $host);
864
        $this->webDriver->get($url);
865
    }
866
867
    public function amOnPage($page)
868
    {
869
        $url = Uri::appendPath($this->config['url'], $page);
870
        $this->debugSection('GET', $url);
871
        $this->webDriver->get($url);
872
    }
873
874
    public function see($text, $selector = null)
875
    {
876
        if (!$selector) {
877
            return $this->assertPageContains($text);
878
        }
879
        $this->enableImplicitWait();
880
        $nodes = $this->matchVisible($selector);
881
        $this->disableImplicitWait();
882
        $this->assertNodesContain($text, $nodes, $selector);
883
    }
884
885
    public function dontSee($text, $selector = null)
886
    {
887
        if (!$selector) {
888
            return $this->assertPageNotContains($text);
889
        }
890
        $nodes = $this->matchVisible($selector);
891
        $this->assertNodesNotContain($text, $nodes, $selector);
892
    }
893
894
    public function seeInSource($raw)
895
    {
896
        $this->assertPageSourceContains($raw);
897
    }
898
899
    public function dontSeeInSource($raw)
900
    {
901
        $this->assertPageSourceNotContains($raw);
902
    }
903
904
    /**
905
     * Checks that the page source contains the given string.
906
     *
907
     * ```php
908
     * <?php
909
     * $I->seeInPageSource('<link rel="apple-touch-icon"');
910
     * ```
911
     *
912
     * @param $text
913
     */
914
    public function seeInPageSource($text)
915
    {
916
        $this->assertThat(
917
            $this->webDriver->getPageSource(),
918
            new PageConstraint($text, $this->_getCurrentUri()),
919
            ''
920
        );
921
    }
922
923
    /**
924
     * Checks that the page source doesn't contain the given string.
925
     *
926
     * @param $text
927
     */
928
    public function dontSeeInPageSource($text)
929
    {
930
        $this->assertThatItsNot(
931
            $this->webDriver->getPageSource(),
932
            new PageConstraint($text, $this->_getCurrentUri()),
933
            ''
934
        );
935
    }
936
937
    public function click($link, $context = null)
938
    {
939
        $page = $this->webDriver;
940
        if ($context) {
941
            $page = $this->matchFirstOrFail($this->webDriver, $context);
942
        }
943
        $el = $this->_findClickable($page, $link);
944
        if (!$el) { // check one more time if this was a CSS selector we didn't match
945
            try {
946
                $els = $this->match($page, $link);
947
            } catch (MalformedLocatorException $e) {
948
                throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button");
949
            }
950
            $el = reset($els);
951
        }
952
        if (!$el) {
953
            throw new ElementNotFound($link, 'Link or Button or CSS or XPath');
954
        }
955
        $el->click();
956
    }
957
958
    /**
959
     * Locates a clickable element.
960
     *
961
     * Use it in Helpers or GroupObject or Extension classes:
962
     *
963
     * ```php
964
     * <?php
965
     * $module = $this->getModule('WebDriver');
966
     * $page = $module->webDriver;
967
     *
968
     * // search a link or button on a page
969
     * $el = $module->_findClickable($page, 'Click Me');
970
     *
971
     * // search a link or button within an element
972
     * $topBar = $module->_findElements('.top-bar')[0];
973
     * $el = $module->_findClickable($topBar, 'Click Me');
974
     *
975
     * ```
976
     * @api
977
     * @param $page WebDriver instance or an element to search within
978
     * @param $link a link text or locator to click
979
     * @return WebDriverElement
980
     */
981
    public function _findClickable($page, $link)
982
    {
983
        if (is_array($link) or ($link instanceof WebDriverBy)) {
984
            return $this->matchFirstOrFail($page, $link);
985
        }
986
987
        // try to match by strict locators, CSS Ids or XPath
988
        if (Locator::isPrecise($link)) {
989
            return $this->matchFirstOrFail($page, $link);
990
        }
991
992
        $locator = Crawler::xpathLiteral(trim($link));
993
994
        // narrow
995
        $xpath = Locator::combine(
996
            ".//a[normalize-space(.)=$locator]",
997
            ".//button[normalize-space(.)=$locator]",
998
            ".//a/img[normalize-space(@alt)=$locator]/ancestor::a",
999
            ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][normalize-space(@value)=$locator]"
1000
        );
1001
1002
        $els = $page->findElements(WebDriverBy::xpath($xpath));
1003
        if (count($els)) {
1004
            return reset($els);
1005
        }
1006
1007
        // wide
1008
        $xpath = Locator::combine(
1009
            ".//a[./@href][((contains(normalize-space(string(.)), $locator)) or contains(./@title, $locator) or .//img[contains(./@alt, $locator)])]",
1010
            ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][contains(./@value, $locator)]",
1011
            ".//input[./@type = 'image'][contains(./@alt, $locator)]",
1012
            ".//button[contains(normalize-space(string(.)), $locator)]",
1013
            ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][./@name = $locator]",
1014
            ".//button[./@name = $locator or ./@title = $locator]"
1015
        );
1016
1017
        $els = $page->findElements(WebDriverBy::xpath($xpath));
1018
        if (count($els)) {
1019
            return reset($els);
1020
        }
1021
1022
        return null;
1023
    }
1024
1025
    /**
1026
     * @param $selector
1027
     * @return WebDriverElement[]
1028
     * @throws \Codeception\Exception\ElementNotFound
1029
     */
1030
    protected function findFields($selector)
1031
    {
1032
        if ($selector instanceof WebDriverElement) {
1033
            return [$selector];
1034
        }
1035
        if (is_array($selector) || ($selector instanceof WebDriverBy)) {
1036
            $fields = $this->match($this->webDriver, $selector);
1037
1038
            if (empty($fields)) {
1039
                throw new ElementNotFound($selector);
1040
            }
1041
            return $fields;
1042
        }
1043
1044
        $locator = Crawler::xpathLiteral(trim($selector));
1045
        // by text or label
1046
        $xpath = Locator::combine(
1047
            // @codingStandardsIgnoreStart
1048
            ".//*[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)]",
1049
            ".//label[contains(normalize-space(string(.)), $locator)]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]"
1050
            // @codingStandardsIgnoreEnd
1051
        );
1052
        $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath));
1053
        if (!empty($fields)) {
1054
            return $fields;
1055
        }
1056
1057
        // by name
1058
        $xpath = ".//*[self::input | self::textarea | self::select][@name = $locator]";
1059
        $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath));
1060
        if (!empty($fields)) {
1061
            return $fields;
1062
        }
1063
1064
        // try to match by CSS or XPath
1065
        $fields = $this->match($this->webDriver, $selector, false);
1066
        if (!empty($fields)) {
1067
            return $fields;
1068
        }
1069
1070
        throw new ElementNotFound($selector, "Field by name, label, CSS or XPath");
1071
    }
1072
1073
    /**
1074
     * @param $selector
1075
     * @return WebDriverElement
1076
     * @throws \Codeception\Exception\ElementNotFound
1077
     */
1078
    protected function findField($selector)
1079
    {
1080
        $arr = $this->findFields($selector);
1081
        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 1081 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...
1082
    }
1083
1084
    public function seeLink($text, $url = null)
1085
    {
1086
        $this->enableImplicitWait();
1087
        $nodes = $this->baseElement->findElements(WebDriverBy::partialLinkText($text));
1088
        $this->disableImplicitWait();
1089
        $currentUri = $this->_getCurrentUri();
1090
1091
        if (empty($nodes)) {
1092
            $this->fail("No links containing text '$text' were found in page $currentUri");
1093
        }
1094
        if ($url) {
1095
            $nodes = $this->filterNodesByHref($url, $nodes);
1096
        }
1097
        $this->assertNotEmpty($nodes, "No links containing text '$text' and URL '$url' were found in page $currentUri");
1098
    }
1099
1100
    public function dontSeeLink($text, $url = null)
1101
    {
1102
        $nodes = $this->baseElement->findElements(WebDriverBy::partialLinkText($text));
1103
        $currentUri = $this->_getCurrentUri();
1104
        if (!$url) {
1105
            $this->assertEmpty($nodes, "Link containing text '$text' was found in page $currentUri");
1106
        } else {
1107
            $nodes = $this->filterNodesByHref($url, $nodes);
1108
            $this->assertEmpty($nodes, "Link containing text '$text' and URL '$url' was found in page $currentUri");
1109
        }
1110
    }
1111
1112
    /**
1113
     * @param string $url
1114
     * @param $nodes
1115
     * @return array
1116
     */
1117
    private function filterNodesByHref($url, $nodes)
1118
    {
1119
        //current uri can be relative, merging it with configured base url gives absolute url
1120
        $absoluteCurrentUrl = Uri::mergeUrls($this->_getUrl(), $this->_getCurrentUri());
1121
        $expectedUrl = Uri::mergeUrls($absoluteCurrentUrl, $url);
1122
1123
        $nodes = array_filter(
1124
            $nodes,
1125
            function (WebDriverElement $e) use ($expectedUrl, $absoluteCurrentUrl) {
1126
                $elementHref = Uri::mergeUrls($absoluteCurrentUrl, $e->getAttribute('href'));
1127
                return $elementHref === $expectedUrl;
1128
            }
1129
        );
1130
        return $nodes;
1131
    }
1132
1133
    public function seeInCurrentUrl($uri)
1134
    {
1135
        $this->assertContains($uri, $this->_getCurrentUri());
1136
    }
1137
1138
    public function seeCurrentUrlEquals($uri)
1139
    {
1140
        $this->assertEquals($uri, $this->_getCurrentUri());
1141
    }
1142
1143
    public function seeCurrentUrlMatches($uri)
1144
    {
1145
        $this->assertRegExp($uri, $this->_getCurrentUri());
1146
    }
1147
1148
    public function dontSeeInCurrentUrl($uri)
1149
    {
1150
        $this->assertNotContains($uri, $this->_getCurrentUri());
1151
    }
1152
1153
    public function dontSeeCurrentUrlEquals($uri)
1154
    {
1155
        $this->assertNotEquals($uri, $this->_getCurrentUri());
1156
    }
1157
1158
    public function dontSeeCurrentUrlMatches($uri)
1159
    {
1160
        $this->assertNotRegExp($uri, $this->_getCurrentUri());
1161
    }
1162
1163 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...
1164
    {
1165
        if (!$uri) {
1166
            return $this->_getCurrentUri();
1167
        }
1168
        $matches = [];
1169
        $res = preg_match($uri, $this->_getCurrentUri(), $matches);
1170
        if (!$res) {
1171
            $this->fail("Couldn't match $uri in " . $this->_getCurrentUri());
1172
        }
1173
        if (!isset($matches[1])) {
1174
            $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'");
1175
        }
1176
        return $matches[1];
1177
    }
1178
1179
    public function seeCheckboxIsChecked($checkbox)
1180
    {
1181
        $this->assertTrue($this->findField($checkbox)->isSelected());
1182
    }
1183
1184
    public function dontSeeCheckboxIsChecked($checkbox)
1185
    {
1186
        $this->assertFalse($this->findField($checkbox)->isSelected());
1187
    }
1188
1189
    public function seeInField($field, $value)
1190
    {
1191
        $els = $this->findFields($field);
1192
        $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...
1193
    }
1194
1195
    public function dontSeeInField($field, $value)
1196
    {
1197
        $els = $this->findFields($field);
1198
        $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...
1199
    }
1200
1201
    public function seeInFormFields($formSelector, array $params)
1202
    {
1203
        $this->proceedSeeInFormFields($formSelector, $params, false);
1204
    }
1205
1206
    public function dontSeeInFormFields($formSelector, array $params)
1207
    {
1208
        $this->proceedSeeInFormFields($formSelector, $params, true);
1209
    }
1210
1211
    protected function proceedSeeInFormFields($formSelector, array $params, $assertNot)
1212
    {
1213
        $form = $this->match($this->baseElement, $formSelector);
1214
        if (empty($form)) {
1215
            throw new ElementNotFound($formSelector, "Form via CSS or XPath");
1216
        }
1217
        $form = reset($form);
1218
1219
        $els = [];
1220
        foreach ($params as $name => $values) {
1221
            $this->pushFormField($els, $form, $name, $values);
1222
        }
1223
1224 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...
1225
            list($el, $values) = $arrayElement;
1226
1227
            if (!is_array($values)) {
1228
                $values = [$values];
1229
            }
1230
1231
            foreach ($values as $value) {
1232
                $ret = $this->proceedSeeInField($el, $value);
1233
                if ($assertNot) {
1234
                    $this->assertNot($ret);
1235
                } else {
1236
                    $this->assert($ret);
1237
                }
1238
            }
1239
        }
1240
    }
1241
1242
    /**
1243
     * Map an array element passed to seeInFormFields to its corresponding WebDriver element,
1244
     * recursing through array values if the field is not found.
1245
     *
1246
     * @param array $els The previously found elements.
1247
     * @param RemoteWebElement $form The form in which to search for fields.
1248
     * @param string $name The field's name.
1249
     * @param mixed $values
1250
     * @return void
1251
     */
1252
    protected function pushFormField(&$els, $form, $name, $values)
1253
    {
1254
        $el = $form->findElements(WebDriverBy::name($name));
1255
1256
        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...
1257
            $els[] = [$el, $values];
1258 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...
1259
            foreach ($values as $key => $value) {
1260
                $this->pushFormField($els, $form, "{$name}[$key]", $value);
1261
            }
1262
        } else {
1263
            throw new ElementNotFound($name);
1264
        }
1265
    }
1266
1267
    /**
1268
     * @param RemoteWebElement[] $elements
1269
     * @param $value
1270
     * @return array
1271
     */
1272
    protected function proceedSeeInField(array $elements, $value)
1273
    {
1274
        $strField = reset($elements)->getAttribute('name');
1275
        if (reset($elements)->getTagName() === 'select') {
1276
            $el = reset($elements);
1277
            $elements = $el->findElements(WebDriverBy::xpath('.//option'));
1278
            if (empty($value) && empty($elements)) {
1279
                return ['True', true];
1280
            }
1281
        }
1282
1283
        $currentValues = [];
1284
        if (is_bool($value)) {
1285
            $currentValues = [false];
1286
        }
1287
        foreach ($elements as $el) {
1288
            switch ($el->getTagName()) {
1289
                case 'input':
1290
                    if ($el->getAttribute('type') === 'radio' || $el->getAttribute('type') === 'checkbox') {
1291
                        if ($el->getAttribute('checked')) {
1292
                            if (is_bool($value)) {
1293
                                $currentValues = [true];
1294
                                break;
1295
                            } else {
1296
                                $currentValues[] = $el->getAttribute('value');
1297
                            }
1298
                        }
1299
                    } else {
1300
                        $currentValues[] = $el->getAttribute('value');
1301
                    }
1302
                    break;
1303
                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...
1304
                    // no break we need the trim text and the value also
1305
                    if (!$el->isSelected()) {
1306
                        break;
1307
                    }
1308
                    $currentValues[] = $el->getText();
1309
                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...
1310
                    // we include trimmed and real value of textarea for check
1311
                    $currentValues[] = trim($el->getText());
1312
                default:
1313
                    $currentValues[] = $el->getAttribute('value'); // raw value
1314
                    break;
1315
            }
1316
        }
1317
1318
        return [
1319
            'Contains',
1320
            $value,
1321
            $currentValues,
1322
            "Failed testing for '$value' in $strField's value: '" . implode("', '", $currentValues) . "'"
1323
        ];
1324
    }
1325
1326
    public function selectOption($select, $option)
1327
    {
1328
        $el = $this->findField($select);
1329
        if ($el->getTagName() != 'select') {
1330
            $els = $this->matchCheckables($select);
1331
            $radio = null;
1332
            foreach ($els as $el) {
1333
                $radio = $this->findCheckable($el, $option, true);
1334
                if ($radio) {
1335
                    break;
1336
                }
1337
            }
1338
            if (!$radio) {
1339
                throw new ElementNotFound($select, "Radiobutton with value or name '$option in");
1340
            }
1341
            $radio->click();
1342
            return;
1343
        }
1344
1345
        $wdSelect = new WebDriverSelect($el);
1346
        if ($wdSelect->isMultiple()) {
1347
            $wdSelect->deselectAll();
1348
        }
1349
        if (!is_array($option)) {
1350
            $option = [$option];
1351
        }
1352
1353
        $matched = false;
1354
1355
        if (key($option) !== 'value') {
1356
            foreach ($option as $opt) {
1357
                try {
1358
                    $wdSelect->selectByVisibleText($opt);
1359
                    $matched = true;
1360
                } catch (NoSuchElementException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1361
                }
1362
            }
1363
        }
1364
1365
        if ($matched) {
1366
            return;
1367
        }
1368
1369
        if (key($option) !== 'text') {
1370
            foreach ($option as $opt) {
1371
                try {
1372
                    $wdSelect->selectByValue($opt);
1373
                    $matched = true;
1374
                } catch (NoSuchElementException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1375
                }
1376
            }
1377
        }
1378
1379
        if ($matched) {
1380
            return;
1381
        }
1382
1383
        // partially matching
1384
        foreach ($option as $opt) {
1385
            try {
1386
                $optElement = $el->findElement(WebDriverBy::xpath('.//option [contains (., "' . $opt . '")]'));
1387
                $matched = true;
1388
                if (!$optElement->isSelected()) {
1389
                    $optElement->click();
1390
                }
1391
            } catch (NoSuchElementException $e) {
1392
                // exception treated at the end
1393
            }
1394
        }
1395
        if ($matched) {
1396
            return;
1397
        }
1398
        throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value");
1399
    }
1400
1401
    /**
1402
     * Manually starts a new browser session.
1403
     *
1404
     * ```php
1405
     * <?php
1406
     * $this->getModule('WebDriver')->_initializeSession();
1407
     * ```
1408
     *
1409
     * @api
1410
     */
1411
    public function _initializeSession()
1412
    {
1413
        try {
1414
            $this->sessions[] = $this->webDriver;
1415
            $this->webDriver = RemoteWebDriver::create(
1416
                $this->wdHost,
1417
                $this->capabilities,
1418
                $this->connectionTimeoutInMs,
1419
                $this->requestTimeoutInMs,
1420
                $this->httpProxy,
1421
                $this->httpProxyPort
1422
            );
1423
            if (!is_null($this->config['pageload_timeout'])) {
1424
                $this->webDriver->manage()->timeouts()->pageLoadTimeout($this->config['pageload_timeout']);
1425
            }
1426
            $this->setBaseElement();
1427
            $this->initialWindowSize();
1428
        } catch (WebDriverCurlException $e) {
1429
            throw new ConnectionException("Can't connect to Webdriver at {$this->wdHost}. Please make sure that Selenium Server or PhantomJS is running.");
1430
        }
1431
    }
1432
1433
    /**
1434
     * Loads current RemoteWebDriver instance as a session
1435
     *
1436
     * @api
1437
     * @param RemoteWebDriver $session
1438
     */
1439
    public function _loadSession($session)
1440
    {
1441
        $this->webDriver = $session;
1442
        $this->setBaseElement();
1443
    }
1444
1445
    /**
1446
     * Returns current WebDriver session for saving
1447
     *
1448
     * @api
1449
     * @return RemoteWebDriver
1450
     */
1451
    public function _backupSession()
1452
    {
1453
        return $this->webDriver;
1454
    }
1455
1456
    /**
1457
     * Manually closes current WebDriver session.
1458
     *
1459
     * ```php
1460
     * <?php
1461
     * $this->getModule('WebDriver')->_closeSession();
1462
     *
1463
     * // close a specific session
1464
     * $webDriver = $this->getModule('WebDriver')->webDriver;
1465
     * $this->getModule('WebDriver')->_closeSession($webDriver);
1466
     * ```
1467
     *
1468
     * @api
1469
     * @param $webDriver (optional) a specific webdriver session instance
1470
     */
1471
    public function _closeSession($webDriver = null)
1472
    {
1473
        if (!$webDriver and $this->webDriver) {
1474
            $webDriver = $this->webDriver;
1475
        }
1476
        if (!$webDriver) {
1477
            return;
1478
        }
1479
        try {
1480
            $webDriver->quit();
1481
            unset($webDriver);
1482
        } catch (UnknownServerException $e) {
1483
            // Session already closed so nothing to do
1484
        }
1485
    }
1486
1487
    /**
1488
     * Unselect an option in the given select box.
1489
     *
1490
     * @param $select
1491
     * @param $option
1492
     */
1493
    public function unselectOption($select, $option)
1494
    {
1495
        $el = $this->findField($select);
1496
1497
        $wdSelect = new WebDriverSelect($el);
1498
1499
        if (!is_array($option)) {
1500
            $option = [$option];
1501
        }
1502
1503
        $matched = false;
1504
1505
        foreach ($option as $opt) {
1506
            try {
1507
                $wdSelect->deselectByVisibleText($opt);
1508
                $matched = true;
1509
            } catch (NoSuchElementException $e) {
1510
                // exception treated at the end
1511
            }
1512
1513
            try {
1514
                $wdSelect->deselectByValue($opt);
1515
                $matched = true;
1516
            } catch (NoSuchElementException $e) {
1517
                // exception treated at the end
1518
            }
1519
        }
1520
1521
        if ($matched) {
1522
            return;
1523
        }
1524
        throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value");
1525
    }
1526
1527
    /**
1528
     * @param $context
1529
     * @param $radioOrCheckbox
1530
     * @param bool $byValue
1531
     * @return mixed|null
1532
     */
1533
    protected function findCheckable($context, $radioOrCheckbox, $byValue = false)
1534
    {
1535
        if ($radioOrCheckbox instanceof WebDriverElement) {
1536
            return $radioOrCheckbox;
1537
        }
1538
1539
        if (is_array($radioOrCheckbox) or ($radioOrCheckbox instanceof WebDriverBy)) {
1540
            return $this->matchFirstOrFail($this->baseElement, $radioOrCheckbox);
1541
        }
1542
1543
        $locator = Crawler::xpathLiteral($radioOrCheckbox);
1544
        if ($context instanceof WebDriverElement && $context->getTagName() === 'input') {
1545
            $contextType = $context->getAttribute('type');
1546
            if (!in_array($contextType, ['checkbox', 'radio'], true)) {
1547
                return null;
1548
            }
1549
            $nameLiteral = Crawler::xPathLiteral($context->getAttribute('name'));
1550
            $typeLiteral = Crawler::xPathLiteral($contextType);
1551
            $inputLocatorFragment = "input[@type = $typeLiteral][@name = $nameLiteral]";
1552
            $xpath = Locator::combine(
1553
                // @codingStandardsIgnoreStart
1554
                "ancestor::form//{$inputLocatorFragment}[(@id = ancestor::form//label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator]",
1555
                // @codingStandardsIgnoreEnd
1556
                "ancestor::form//label[contains(normalize-space(string(.)), $locator)]//{$inputLocatorFragment}"
1557
            );
1558
            if ($byValue) {
1559
                $xpath = Locator::combine($xpath, "ancestor::form//{$inputLocatorFragment}[@value = $locator]");
1560
            }
1561
        } else {
1562
            $xpath = Locator::combine(
1563
                // @codingStandardsIgnoreStart
1564
                "//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator or @name = $locator]",
1565
                // @codingStandardsIgnoreEnd
1566
                "//label[contains(normalize-space(string(.)), $locator)]//input[@type = 'radio' or @type = 'checkbox']"
1567
            );
1568
            if ($byValue) {
1569
                $xpath = Locator::combine($xpath, "//input[@type = 'checkbox' or @type = 'radio'][@value = $locator]");
1570
            }
1571
        }
1572
        $els = $context->findElements(WebDriverBy::xpath($xpath));
1573
        if (count($els)) {
1574
            return reset($els);
1575
        }
1576
        $els = $context->findElements(WebDriverBy::xpath(str_replace('ancestor::form', '', $xpath)));
1577
        if (count($els)) {
1578
            return reset($els);
1579
        }
1580
        $els = $this->match($context, $radioOrCheckbox);
1581
        if (count($els)) {
1582
            return reset($els);
1583
        }
1584
        return null;
1585
    }
1586
1587
    protected function matchCheckables($selector)
1588
    {
1589
        $els = $this->match($this->webDriver, $selector);
1590
        if (!count($els)) {
1591
            throw new ElementNotFound($selector, "Element containing radio by CSS or XPath");
1592
        }
1593
        return $els;
1594
    }
1595
1596 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...
1597
    {
1598
        $field = $this->findCheckable($this->webDriver, $option);
1599
        if (!$field) {
1600
            throw new ElementNotFound($option, "Checkbox or Radio by Label or CSS or XPath");
1601
        }
1602
        if ($field->isSelected()) {
1603
            return;
1604
        }
1605
        $field->click();
1606
    }
1607
1608 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...
1609
    {
1610
        $field = $this->findCheckable($this->baseElement, $option);
1611
        if (!$field) {
1612
            throw new ElementNotFound($option, "Checkbox by Label or CSS or XPath");
1613
        }
1614
        if (!$field->isSelected()) {
1615
            return;
1616
        }
1617
        $field->click();
1618
    }
1619
1620
    public function fillField($field, $value)
1621
    {
1622
        $el = $this->findField($field);
1623
        $el->clear();
1624
        $el->sendKeys($value);
1625
    }
1626
1627
    /**
1628
    * Clears given field which isn't empty.
1629
    *
1630
    * ``` php
1631
    * <?php
1632
    * $I->clearField('#username');
1633
    * ?>
1634
    * ```
1635
    *
1636
    * @param $field
1637
    */    
1638
    public function clearField($field)
1639
    {
1640
        $el = $this->findField($field);
1641
        $el->clear();
1642
    }
1643
        
1644
    public function attachFile($field, $filename)
1645
    {
1646
        $el = $this->findField($field);
1647
        // in order to be compatible on different OS
1648
        $filePath = codecept_data_dir() . $filename;
1649
        if (!file_exists($filePath)) {
1650
            throw new \InvalidArgumentException("File does not exist: $filePath");
1651
        }
1652
        if (!is_readable($filePath)) {
1653
            throw new \InvalidArgumentException("File is not readable: $filePath");
1654
        }
1655
        // in order for remote upload to be enabled
1656
        $el->setFileDetector(new LocalFileDetector());
1657
1658
        // skip file detector for phantomjs
1659
        if ($this->isPhantom()) {
1660
            $el->setFileDetector(new UselessFileDetector());
1661
        }
1662
        $el->sendKeys(realpath($filePath));
1663
    }
1664
1665
    /**
1666
     * Grabs all visible text from the current page.
1667
     *
1668
     * @return string
1669
     */
1670
    protected function getVisibleText()
1671
    {
1672
        if ($this->baseElement instanceof RemoteWebElement) {
1673
            return $this->baseElement->getText();
1674
        }
1675
        $els = $this->baseElement->findElements(WebDriverBy::cssSelector('body'));
1676
        if (isset($els[0])) {
1677
            return $els[0]->getText();
1678
        }
1679
        return '';
1680
    }
1681
1682
    public function grabTextFrom($cssOrXPathOrRegex)
1683
    {
1684
        $els = $this->match($this->baseElement, $cssOrXPathOrRegex, false);
1685
        if (count($els)) {
1686
            return $els[0]->getText();
1687
        }
1688
        if (@preg_match($cssOrXPathOrRegex, $this->webDriver->getPageSource(), $matches)) {
1689
            return $matches[1];
1690
        }
1691
        throw new ElementNotFound($cssOrXPathOrRegex, 'CSS or XPath or Regex');
1692
    }
1693
1694
    public function grabAttributeFrom($cssOrXpath, $attribute)
1695
    {
1696
        $el = $this->matchFirstOrFail($this->baseElement, $cssOrXpath);
1697
        return $el->getAttribute($attribute);
1698
    }
1699
1700
    public function grabValueFrom($field)
1701
    {
1702
        $el = $this->findField($field);
1703
        // value of multiple select is the value of the first selected option
1704
        if ($el->getTagName() == 'select') {
1705
            $select = new WebDriverSelect($el);
1706
            return $select->getFirstSelectedOption()->getAttribute('value');
1707
        }
1708
        return $el->getAttribute('value');
1709
    }
1710
1711
    public function grabMultiple($cssOrXpath, $attribute = null)
1712
    {
1713
        $els = $this->match($this->baseElement, $cssOrXpath);
1714
        return array_map(
1715
            function (WebDriverElement $e) use ($attribute) {
1716
                if ($attribute) {
1717
                    return $e->getAttribute($attribute);
1718
                }
1719
                return $e->getText();
1720
            },
1721
            $els
1722
        );
1723
    }
1724
1725
1726
    protected function filterByAttributes($els, array $attributes)
1727
    {
1728
        foreach ($attributes as $attr => $value) {
1729
            $els = array_filter(
1730
                $els,
1731
                function (WebDriverElement $el) use ($attr, $value) {
1732
                    return $el->getAttribute($attr) == $value;
1733
                }
1734
            );
1735
        }
1736
        return $els;
1737
    }
1738
1739
    public function seeElement($selector, $attributes = [])
1740
    {
1741
        $this->enableImplicitWait();
1742
        $els = $this->matchVisible($selector);
1743
        $this->disableImplicitWait();
1744
        $els = $this->filterByAttributes($els, $attributes);
1745
        $this->assertNotEmpty($els);
1746
    }
1747
1748
    public function dontSeeElement($selector, $attributes = [])
1749
    {
1750
        $els = $this->matchVisible($selector);
1751
        $els = $this->filterByAttributes($els, $attributes);
1752
        $this->assertEmpty($els);
1753
    }
1754
1755
    /**
1756
     * Checks that the given element exists on the page, even it is invisible.
1757
     *
1758
     * ``` php
1759
     * <?php
1760
     * $I->seeElementInDOM('//form/input[type=hidden]');
1761
     * ?>
1762
     * ```
1763
     *
1764
     * @param $selector
1765
     * @param array $attributes
1766
     */
1767
    public function seeElementInDOM($selector, $attributes = [])
1768
    {
1769
        $this->enableImplicitWait();
1770
        $els = $this->match($this->baseElement, $selector);
1771
        $els = $this->filterByAttributes($els, $attributes);
1772
        $this->disableImplicitWait();
1773
        $this->assertNotEmpty($els);
1774
    }
1775
1776
1777
    /**
1778
     * Opposite of `seeElementInDOM`.
1779
     *
1780
     * @param $selector
1781
     * @param array $attributes
1782
     */
1783
    public function dontSeeElementInDOM($selector, $attributes = [])
1784
    {
1785
        $els = $this->match($this->baseElement, $selector);
1786
        $els = $this->filterByAttributes($els, $attributes);
1787
        $this->assertEmpty($els);
1788
    }
1789
1790 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...
1791
    {
1792
        $counted = count($this->matchVisible($selector));
1793
        if (is_array($expected)) {
1794
            list($floor, $ceil) = $expected;
1795
            $this->assertTrue(
1796
                $floor <= $counted && $ceil >= $counted,
1797
                'Number of elements counted differs from expected range'
1798
            );
1799
        } else {
1800
            $this->assertEquals(
1801
                $expected,
1802
                $counted,
1803
                'Number of elements counted differs from expected number'
1804
            );
1805
        }
1806
    }
1807
1808 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...
1809
    {
1810
        $counted = count($this->match($this->baseElement, $selector));
1811
        if (is_array($expected)) {
1812
            list($floor, $ceil) = $expected;
1813
            $this->assertTrue(
1814
                $floor <= $counted && $ceil >= $counted,
1815
                'Number of elements counted differs from expected range'
1816
            );
1817
        } else {
1818
            $this->assertEquals(
1819
                $expected,
1820
                $counted,
1821
                'Number of elements counted differs from expected number'
1822
            );
1823
        }
1824
    }
1825
1826 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...
1827
    {
1828
        $el = $this->findField($selector);
1829
        if ($el->getTagName() !== 'select') {
1830
            $els = $this->matchCheckables($selector);
1831
            foreach ($els as $k => $el) {
1832
                $els[$k] = $this->findCheckable($el, $optionText, true);
1833
            }
1834
            $this->assertNotEmpty(
1835
                array_filter(
1836
                    $els,
1837
                    function ($e) {
1838
                        return $e && $e->isSelected();
1839
                    }
1840
                )
1841
            );
1842
            return;
1843
        }
1844
        $select = new WebDriverSelect($el);
1845
        $this->assertNodesContain($optionText, $select->getAllSelectedOptions(), 'option');
1846
    }
1847
1848 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...
1849
    {
1850
        $el = $this->findField($selector);
1851
        if ($el->getTagName() !== 'select') {
1852
            $els = $this->matchCheckables($selector);
1853
            foreach ($els as $k => $el) {
1854
                $els[$k] = $this->findCheckable($el, $optionText, true);
1855
            }
1856
            $this->assertEmpty(
1857
                array_filter(
1858
                    $els,
1859
                    function ($e) {
1860
                        return $e && $e->isSelected();
1861
                    }
1862
                )
1863
            );
1864
            return;
1865
        }
1866
        $select = new WebDriverSelect($el);
1867
        $this->assertNodesNotContain($optionText, $select->getAllSelectedOptions(), 'option');
1868
    }
1869
1870
    public function seeInTitle($title)
1871
    {
1872
        $this->assertContains($title, $this->webDriver->getTitle());
1873
    }
1874
1875
    public function dontSeeInTitle($title)
1876
    {
1877
        $this->assertNotContains($title, $this->webDriver->getTitle());
1878
    }
1879
1880
    /**
1881
     * Accepts the active JavaScript native popup window, as created by `window.alert`|`window.confirm`|`window.prompt`.
1882
     * Don't confuse popups with modal windows,
1883
     * as created by [various libraries](http://jster.net/category/windows-modals-popups).
1884
     */
1885
    public function acceptPopup()
1886
    {
1887
        if ($this->isPhantom()) {
1888
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1889
        }
1890
        $this->webDriver->switchTo()->alert()->accept();
1891
    }
1892
1893
    /**
1894
     * Dismisses the active JavaScript popup, as created by `window.alert`, `window.confirm`, or `window.prompt`.
1895
     */
1896
    public function cancelPopup()
1897
    {
1898
        if ($this->isPhantom()) {
1899
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1900
        }
1901
        $this->webDriver->switchTo()->alert()->dismiss();
1902
    }
1903
1904
    /**
1905
     * Checks that the active JavaScript popup,
1906
     * as created by `window.alert`|`window.confirm`|`window.prompt`, contains the given string.
1907
     *
1908
     * @param $text
1909
     *
1910
     * @throws \Codeception\Exception\ModuleException
1911
     */
1912 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...
1913
    {
1914
        if ($this->isPhantom()) {
1915
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1916
        }
1917
        $alert = $this->webDriver->switchTo()->alert();
1918
        try {
1919
            $this->assertContains($text, $alert->getText());
1920
        } catch (\PHPUnit_Framework_AssertionFailedError $e) {
1921
            $alert->dismiss();
1922
            throw $e;
1923
        }
1924
    }
1925
1926
    /**
1927
     * Checks that the active JavaScript popup,
1928
     * as created by `window.alert`|`window.confirm`|`window.prompt`, does NOT contain the given string.
1929
     *
1930
     * @param $text
1931
     *
1932
     * @throws \Codeception\Exception\ModuleException
1933
     */
1934 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...
1935
    {
1936
        if ($this->isPhantom()) {
1937
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1938
        }
1939
        $alert = $this->webDriver->switchTo()->alert();
1940
        try {
1941
            $this->assertNotContains($text, $alert->getText());
1942
        } catch (\PHPUnit_Framework_AssertionFailedError $e) {
1943
            $alert->dismiss();
1944
            throw $e;
1945
        }
1946
    }
1947
1948
    /**
1949
     * Enters text into a native JavaScript prompt popup, as created by `window.prompt`.
1950
     *
1951
     * @param $keys
1952
     *
1953
     * @throws \Codeception\Exception\ModuleException
1954
     */
1955
    public function typeInPopup($keys)
1956
    {
1957
        if ($this->isPhantom()) {
1958
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1959
        }
1960
        $this->webDriver->switchTo()->alert()->sendKeys($keys);
1961
    }
1962
1963
    /**
1964
     * Reloads the current page.
1965
     */
1966
    public function reloadPage()
1967
    {
1968
        $this->webDriver->navigate()->refresh();
1969
    }
1970
1971
    /**
1972
     * Moves back in history.
1973
     */
1974
    public function moveBack()
1975
    {
1976
        $this->webDriver->navigate()->back();
1977
        $this->debug($this->_getCurrentUri());
1978
    }
1979
1980
    /**
1981
     * Moves forward in history.
1982
     */
1983
    public function moveForward()
1984
    {
1985
        $this->webDriver->navigate()->forward();
1986
        $this->debug($this->_getCurrentUri());
1987
    }
1988
1989 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...
1990
    {
1991
        if (substr($name, -2) === '[]') {
1992
            return substr($name, 0, -2);
1993
        }
1994
        return $name;
1995
    }
1996
1997
    /**
1998
     * Submits the given form on the page, optionally with the given form
1999
     * values.  Give the form fields values as an array. Note that hidden fields
2000
     * can't be accessed.
2001
     *
2002
     * Skipped fields will be filled by their values from the page.
2003
     * You don't need to click the 'Submit' button afterwards.
2004
     * This command itself triggers the request to form's action.
2005
     *
2006
     * You can optionally specify what button's value to include
2007
     * in the request with the last parameter as an alternative to
2008
     * explicitly setting its value in the second parameter, as
2009
     * button values are not otherwise included in the request.
2010
     *
2011
     * Examples:
2012
     *
2013
     * ``` php
2014
     * <?php
2015
     * $I->submitForm('#login', [
2016
     *     'login' => 'davert',
2017
     *     'password' => '123456'
2018
     * ]);
2019
     * // or
2020
     * $I->submitForm('#login', [
2021
     *     'login' => 'davert',
2022
     *     'password' => '123456'
2023
     * ], 'submitButtonName');
2024
     *
2025
     * ```
2026
     *
2027
     * For example, given this sample "Sign Up" form:
2028
     *
2029
     * ``` html
2030
     * <form action="/sign_up">
2031
     *     Login:
2032
     *     <input type="text" name="user[login]" /><br/>
2033
     *     Password:
2034
     *     <input type="password" name="user[password]" /><br/>
2035
     *     Do you agree to our terms?
2036
     *     <input type="checkbox" name="user[agree]" /><br/>
2037
     *     Select pricing plan:
2038
     *     <select name="plan">
2039
     *         <option value="1">Free</option>
2040
     *         <option value="2" selected="selected">Paid</option>
2041
     *     </select>
2042
     *     <input type="submit" name="submitButton" value="Submit" />
2043
     * </form>
2044
     * ```
2045
     *
2046
     * You could write the following to submit it:
2047
     *
2048
     * ``` php
2049
     * <?php
2050
     * $I->submitForm(
2051
     *     '#userForm',
2052
     *     [
2053
     *         'user[login]' => 'Davert',
2054
     *         'user[password]' => '123456',
2055
     *         'user[agree]' => true
2056
     *     ],
2057
     *     'submitButton'
2058
     * );
2059
     * ```
2060
     * Note that "2" will be the submitted value for the "plan" field, as it is
2061
     * the selected option.
2062
     *
2063
     * Also note that this differs from PhpBrowser, in that
2064
     * ```'user' => [ 'login' => 'Davert' ]``` is not supported at the moment.
2065
     * Named array keys *must* be included in the name as above.
2066
     *
2067
     * Pair this with seeInFormFields for quick testing magic.
2068
     *
2069
     * ``` php
2070
     * <?php
2071
     * $form = [
2072
     *      'field1' => 'value',
2073
     *      'field2' => 'another value',
2074
     *      'checkbox1' => true,
2075
     *      // ...
2076
     * ];
2077
     * $I->submitForm('//form[@id=my-form]', $form, 'submitButton');
2078
     * // $I->amOnPage('/path/to/form-page') may be needed
2079
     * $I->seeInFormFields('//form[@id=my-form]', $form);
2080
     * ?>
2081
     * ```
2082
     *
2083
     * Parameter values must be set to arrays for multiple input fields
2084
     * of the same name, or multi-select combo boxes.  For checkboxes,
2085
     * either the string value can be used, or boolean values which will
2086
     * be replaced by the checkbox's value in the DOM.
2087
     *
2088
     * ``` php
2089
     * <?php
2090
     * $I->submitForm('#my-form', [
2091
     *      'field1' => 'value',
2092
     *      'checkbox' => [
2093
     *          'value of first checkbox',
2094
     *          'value of second checkbox,
2095
     *      ],
2096
     *      'otherCheckboxes' => [
2097
     *          true,
2098
     *          false,
2099
     *          false
2100
     *      ],
2101
     *      'multiselect' => [
2102
     *          'first option value',
2103
     *          'second option value'
2104
     *      ]
2105
     * ]);
2106
     * ?>
2107
     * ```
2108
     *
2109
     * Mixing string and boolean values for a checkbox's value is not supported
2110
     * and may produce unexpected results.
2111
     *
2112
     * Field names ending in "[]" must be passed without the trailing square
2113
     * bracket characters, and must contain an array for its value.  This allows
2114
     * submitting multiple values with the same name, consider:
2115
     *
2116
     * ```php
2117
     * $I->submitForm('#my-form', [
2118
     *     'field[]' => 'value',
2119
     *     'field[]' => 'another value', // 'field[]' is already a defined key
2120
     * ]);
2121
     * ```
2122
     *
2123
     * The solution is to pass an array value:
2124
     *
2125
     * ```php
2126
     * // this way both values are submitted
2127
     * $I->submitForm('#my-form', [
2128
     *     'field' => [
2129
     *         'value',
2130
     *         'another value',
2131
     *     ]
2132
     * ]);
2133
     * ```
2134
     *
2135
     * The `$button` parameter can be either a string, an array or an instance
2136
     * of Facebook\WebDriver\WebDriverBy. When it is a string, the
2137
     * button will be found by its "name" attribute. If $button is an
2138
     * array then it will be treated as a strict selector and a WebDriverBy
2139
     * will be used verbatim.
2140
     *
2141
     * For example, given the following HTML:
2142
     *
2143
     * ``` html
2144
     * <input type="submit" name="submitButton" value="Submit" />
2145
     * ```
2146
     *
2147
     * `$button` could be any one of the following:
2148
     *   - 'submitButton'
2149
     *   - ['name' => 'submitButton']
2150
     *   - WebDriverBy::name('submitButton')
2151
     *
2152
     * @param $selector
2153
     * @param $params
2154
     * @param $button
2155
     */
2156
    public function submitForm($selector, array $params, $button = null)
2157
    {
2158
        $form = $this->matchFirstOrFail($this->baseElement, $selector);
2159
2160
        $fields = $form->findElements(
2161
            WebDriverBy::cssSelector('input:enabled,textarea:enabled,select:enabled,input[type=hidden]')
2162
        );
2163
        foreach ($fields as $field) {
2164
            $fieldName = $this->getSubmissionFormFieldName($field->getAttribute('name'));
2165
            if (!isset($params[$fieldName])) {
2166
                continue;
2167
            }
2168
            $value = $params[$fieldName];
2169
            if (is_array($value) && $field->getTagName() !== 'select') {
2170
                if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') {
2171
                    $found = false;
2172
                    foreach ($value as $index => $val) {
2173
                        if (!is_bool($val) && $val === $field->getAttribute('value')) {
2174
                            array_splice($params[$fieldName], $index, 1);
2175
                            $value = $val;
2176
                            $found = true;
2177
                            break;
2178
                        }
2179
                    }
2180
                    if (!$found && !empty($value) && is_bool(reset($value))) {
2181
                        $value = array_pop($params[$fieldName]);
2182
                    }
2183
                } else {
2184
                    $value = array_pop($params[$fieldName]);
2185
                }
2186
            }
2187
2188
            if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') {
2189
                if ($value === true || $value === $field->getAttribute('value')) {
2190
                    $this->checkOption($field);
2191
                } else {
2192
                    $this->uncheckOption($field);
2193
                }
2194
            } elseif ($field->getAttribute('type') === 'button' || $field->getAttribute('type') === 'submit') {
2195
                continue;
2196
            } elseif ($field->getTagName() === 'select') {
2197
                $this->selectOption($field, $value);
2198
            } else {
2199
                $this->fillField($field, $value);
2200
            }
2201
        }
2202
2203
        $this->debugSection(
2204
            'Uri',
2205
            $form->getAttribute('action') ? $form->getAttribute('action') : $this->_getCurrentUri()
2206
        );
2207
        $this->debugSection('Method', $form->getAttribute('method') ? $form->getAttribute('method') : 'GET');
2208
        $this->debugSection('Parameters', json_encode($params));
2209
2210
        $submitted = false;
2211
        if (!empty($button)) {
2212
            if (is_array($button)) {
2213
                $buttonSelector = $this->getStrictLocator($button);
2214
            } elseif ($button instanceof WebDriverBy) {
2215
                $buttonSelector = $button;
2216
            } else {
2217
                $buttonSelector = WebDriverBy::name($button);
2218
            }
2219
2220
            $els = $form->findElements($buttonSelector);
2221
2222
            if (!empty($els)) {
2223
                $el = reset($els);
2224
                $el->click();
2225
                $submitted = true;
2226
            }
2227
        }
2228
2229
        if (!$submitted) {
2230
            $form->submit();
2231
        }
2232
        $this->debugSection('Page', $this->_getCurrentUri());
2233
    }
2234
2235
    /**
2236
     * Waits up to $timeout seconds for the given element to change.
2237
     * Element "change" is determined by a callback function which is called repeatedly
2238
     * until the return value evaluates to true.
2239
     *
2240
     * ``` php
2241
     * <?php
2242
     * use \Facebook\WebDriver\WebDriverElement
2243
     * $I->waitForElementChange('#menu', function(WebDriverElement $el) {
2244
     *     return $el->isDisplayed();
2245
     * }, 100);
2246
     * ?>
2247
     * ```
2248
     *
2249
     * @param $element
2250
     * @param \Closure $callback
2251
     * @param int $timeout seconds
2252
     * @throws \Codeception\Exception\ElementNotFound
2253
     */
2254
    public function waitForElementChange($element, \Closure $callback, $timeout = 30)
2255
    {
2256
        $el = $this->matchFirstOrFail($this->baseElement, $element);
2257
        $checker = function () use ($el, $callback) {
2258
            return $callback($el);
2259
        };
2260
        $this->webDriver->wait($timeout)->until($checker);
2261
    }
2262
2263
    /**
2264
     * Waits up to $timeout seconds for an element to appear on the page.
2265
     * If the element doesn't appear, a timeout exception is thrown.
2266
     *
2267
     * ``` php
2268
     * <?php
2269
     * $I->waitForElement('#agree_button', 30); // secs
2270
     * $I->click('#agree_button');
2271
     * ?>
2272
     * ```
2273
     *
2274
     * @param $element
2275
     * @param int $timeout seconds
2276
     * @throws \Exception
2277
     */
2278
    public function waitForElement($element, $timeout = 10)
2279
    {
2280
        $condition = WebDriverExpectedCondition::presenceOfElementLocated($this->getLocator($element));
2281
        $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...
2282
    }
2283
2284
    /**
2285
     * Waits up to $timeout seconds for the given element to be visible on the page.
2286
     * If element doesn't appear, a timeout exception is thrown.
2287
     *
2288
     * ``` php
2289
     * <?php
2290
     * $I->waitForElementVisible('#agree_button', 30); // secs
2291
     * $I->click('#agree_button');
2292
     * ?>
2293
     * ```
2294
     *
2295
     * @param $element
2296
     * @param int $timeout seconds
2297
     * @throws \Exception
2298
     */
2299
    public function waitForElementVisible($element, $timeout = 10)
2300
    {
2301
        $condition = WebDriverExpectedCondition::visibilityOfElementLocated($this->getLocator($element));
2302
        $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...
2303
    }
2304
2305
    /**
2306
     * Waits up to $timeout seconds for the given element to become invisible.
2307
     * If element stays visible, a timeout exception is thrown.
2308
     *
2309
     * ``` php
2310
     * <?php
2311
     * $I->waitForElementNotVisible('#agree_button', 30); // secs
2312
     * ?>
2313
     * ```
2314
     *
2315
     * @param $element
2316
     * @param int $timeout seconds
2317
     * @throws \Exception
2318
     */
2319
    public function waitForElementNotVisible($element, $timeout = 10)
2320
    {
2321
        $condition = WebDriverExpectedCondition::invisibilityOfElementLocated($this->getLocator($element));
2322
        $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...
2323
    }
2324
2325
    /**
2326
     * Waits up to $timeout seconds for the given string to appear on the page.
2327
     *
2328
     * Can also be passed a selector to search in, be as specific as possible when using selectors.
2329
     * waitForText() will only watch the first instance of the matching selector / text provided.
2330
     * If the given text doesn't appear, a timeout exception is thrown.
2331
     *
2332
     * ``` php
2333
     * <?php
2334
     * $I->waitForText('foo', 30); // secs
2335
     * $I->waitForText('foo', 30, '.title'); // secs
2336
     * ?>
2337
     * ```
2338
     *
2339
     * @param string $text
2340
     * @param int $timeout seconds
2341
     * @param string $selector optional
2342
     * @throws \Exception
2343
     */
2344
    public function waitForText($text, $timeout = 10, $selector = null)
2345
    {
2346
        $message = sprintf(
2347
            'Waited for %d secs but text %s still not found',
2348
            $timeout,
2349
            Locator::humanReadableString($text)
2350
        );
2351
        if (!$selector) {
2352
            $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...
2353
            $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...
2354
            return;
2355
        }
2356
2357
        $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...
2358
        $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...
2359
    }
2360
2361
    /**
2362
     * Wait for $timeout seconds.
2363
     *
2364
     * @param int|float $timeout secs
2365
     * @throws \Codeception\Exception\TestRuntimeException
2366
     */
2367
    public function wait($timeout)
2368
    {
2369
        if ($timeout >= 1000) {
2370
            throw new TestRuntimeException(
2371
                "
2372
                Waiting for more then 1000 seconds: 16.6667 mins\n
2373
                Please note that wait method accepts number of seconds as parameter."
2374
            );
2375
        }
2376
        usleep($timeout * 1000000);
2377
    }
2378
2379
    /**
2380
     * Low-level API method.
2381
     * If Codeception commands are not enough, this allows you to use Selenium WebDriver methods directly:
2382
     *
2383
     * ``` php
2384
     * $I->executeInSelenium(function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) {
2385
     *   $webdriver->get('http://google.com');
2386
     * });
2387
     * ```
2388
     *
2389
     * This runs in the context of the
2390
     * [RemoteWebDriver class](https://github.com/facebook/php-webdriver/blob/master/lib/remote/RemoteWebDriver.php).
2391
     * Try not to use this command on a regular basis.
2392
     * If Codeception lacks a feature you need, please implement it and submit a patch.
2393
     *
2394
     * @param callable $function
2395
     */
2396
    public function executeInSelenium(\Closure $function)
2397
    {
2398
        return $function($this->webDriver);
2399
    }
2400
2401
    /**
2402
     * Switch to another window identified by name.
2403
     *
2404
     * The window can only be identified by name. If the $name parameter is blank, the parent window will be used.
2405
     *
2406
     * Example:
2407
     * ``` html
2408
     * <input type="button" value="Open window" onclick="window.open('http://example.com', 'another_window')">
2409
     * ```
2410
     *
2411
     * ``` php
2412
     * <?php
2413
     * $I->click("Open window");
2414
     * # switch to another window
2415
     * $I->switchToWindow("another_window");
2416
     * # switch to parent window
2417
     * $I->switchToWindow();
2418
     * ?>
2419
     * ```
2420
     *
2421
     * If the window has no name, match it by switching to next active tab using `switchToNextTab` method.
2422
     *
2423
     * Or use native Selenium functions to get access to all opened windows:
2424
     *
2425
     * ``` php
2426
     * <?php
2427
     * $I->executeInSelenium(function (\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) {
2428
     *      $handles=$webdriver->getWindowHandles();
2429
     *      $last_window = end($handles);
2430
     *      $webdriver->switchTo()->window($last_window);
2431
     * });
2432
     * ?>
2433
     * ```
2434
     *
2435
     * @param string|null $name
2436
     */
2437
    public function switchToWindow($name = null)
2438
    {
2439
        $this->webDriver->switchTo()->window($name);
2440
    }
2441
2442
    /**
2443
     * Switch to another frame on the page.
2444
     *
2445
     * Example:
2446
     * ``` html
2447
     * <iframe name="another_frame" src="http://example.com">
2448
     *
2449
     * ```
2450
     *
2451
     * ``` php
2452
     * <?php
2453
     * # switch to iframe
2454
     * $I->switchToIFrame("another_frame");
2455
     * # switch to parent page
2456
     * $I->switchToIFrame();
2457
     *
2458
     * ```
2459
     *
2460
     * @param string|null $name
2461
     */
2462
    public function switchToIFrame($name = null)
2463
    {
2464
        if (is_null($name)) {
2465
            $this->webDriver->switchTo()->defaultContent();
2466
            return;
2467
        }
2468
        $this->webDriver->switchTo()->frame($name);
2469
    }
2470
2471
    /**
2472
     * Executes JavaScript and waits up to $timeout seconds for it to return true.
2473
     *
2474
     * In this example we will wait up to 60 seconds for all jQuery AJAX requests to finish.
2475
     *
2476
     * ``` php
2477
     * <?php
2478
     * $I->waitForJS("return $.active == 0;", 60);
2479
     * ?>
2480
     * ```
2481
     *
2482
     * @param string $script
2483
     * @param int $timeout seconds
2484
     */
2485
    public function waitForJS($script, $timeout = 5)
2486
    {
2487
        $condition = function ($wd) use ($script) {
2488
            return $wd->executeScript($script);
2489
        };
2490
        $message = sprintf(
2491
            'Waited for %d secs but script %s still not executed',
2492
            $timeout,
2493
            Locator::humanReadableString($script)
2494
        );
2495
        $this->webDriver->wait($timeout)->until($condition, $message);
2496
    }
2497
2498
    /**
2499
     * Executes custom JavaScript.
2500
     *
2501
     * This example uses jQuery to get a value and assigns that value to a PHP variable:
2502
     *
2503
     * ```php
2504
     * <?php
2505
     * $myVar = $I->executeJS('return $("#myField").val()');
2506
     * ?>
2507
     * ```
2508
     *
2509
     * @param $script
2510
     * @return mixed
2511
     */
2512
    public function executeJS($script)
2513
    {
2514
        return $this->webDriver->executeScript($script);
2515
    }
2516
2517
    /**
2518
     * Maximizes the current window.
2519
     */
2520
    public function maximizeWindow()
2521
    {
2522
        $this->webDriver->manage()->window()->maximize();
2523
    }
2524
2525
    /**
2526
     * Performs a simple mouse drag-and-drop operation.
2527
     *
2528
     * ``` php
2529
     * <?php
2530
     * $I->dragAndDrop('#drag', '#drop');
2531
     * ?>
2532
     * ```
2533
     *
2534
     * @param string $source (CSS ID or XPath)
2535
     * @param string $target (CSS ID or XPath)
2536
     */
2537
    public function dragAndDrop($source, $target)
2538
    {
2539
        $snodes = $this->matchFirstOrFail($this->baseElement, $source);
2540
        $tnodes = $this->matchFirstOrFail($this->baseElement, $target);
2541
2542
        $action = new WebDriverActions($this->webDriver);
2543
        $action->dragAndDrop($snodes, $tnodes)->perform();
2544
    }
2545
2546
    /**
2547
     * Move mouse over the first element matched by the given locator.
2548
     * If the first parameter null then the page is used.
2549
     * If the second and third parameters are given,
2550
     * then the mouse is moved to an offset of the element's top-left corner.
2551
     * Otherwise, the mouse is moved to the center of the element.
2552
     *
2553
     * ``` php
2554
     * <?php
2555
     * $I->moveMouseOver(['css' => '.checkout']);
2556
     * $I->moveMouseOver(null, 20, 50);
2557
     * $I->moveMouseOver(['css' => '.checkout'], 20, 50);
2558
     * ?>
2559
     * ```
2560
     *
2561
     * @param string $cssOrXPath css or xpath of the web element
2562
     * @param int $offsetX
2563
     * @param int $offsetY
2564
     *
2565
     * @throws \Codeception\Exception\ElementNotFound
2566
     */
2567
    public function moveMouseOver($cssOrXPath = null, $offsetX = null, $offsetY = null)
2568
    {
2569
        $where = null;
2570
        if (null !== $cssOrXPath) {
2571
            $el = $this->matchFirstOrFail($this->baseElement, $cssOrXPath);
2572
            $where = $el->getCoordinates();
2573
        }
2574
2575
        $this->webDriver->getMouse()->mouseMove($where, $offsetX, $offsetY);
2576
    }
2577
2578
    /**
2579
     * Performs click with the left mouse button on an element.
2580
     * If the first parameter `null` then the offset is relative to the actual mouse position.
2581
     * If the second and third parameters are given,
2582
     * then the mouse is moved to an offset of the element's top-left corner.
2583
     * Otherwise, the mouse is moved to the center of the element.
2584
     *
2585
     * ``` php
2586
     * <?php
2587
     * $I->clickWithLeftButton(['css' => '.checkout']);
2588
     * $I->clickWithLeftButton(null, 20, 50);
2589
     * $I->clickWithLeftButton(['css' => '.checkout'], 20, 50);
2590
     * ?>
2591
     * ```
2592
     *
2593
     * @param string $cssOrXPath css or xpath of the web element (body by default).
2594
     * @param int $offsetX
2595
     * @param int $offsetY
2596
     *
2597
     * @throws \Codeception\Exception\ElementNotFound
2598
     */
2599
    public function clickWithLeftButton($cssOrXPath = null, $offsetX = null, $offsetY = null)
2600
    {
2601
        $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY);
2602
        $this->webDriver->getMouse()->click();
2603
    }
2604
2605
    /**
2606
     * Performs contextual click with the right mouse button on an element.
2607
     * If the first parameter `null` then the offset is relative to the actual mouse position.
2608
     * If the second and third parameters are given,
2609
     * then the mouse is moved to an offset of the element's top-left corner.
2610
     * Otherwise, the mouse is moved to the center of the element.
2611
     *
2612
     * ``` php
2613
     * <?php
2614
     * $I->clickWithRightButton(['css' => '.checkout']);
2615
     * $I->clickWithRightButton(null, 20, 50);
2616
     * $I->clickWithRightButton(['css' => '.checkout'], 20, 50);
2617
     * ?>
2618
     * ```
2619
     *
2620
     * @param string $cssOrXPath css or xpath of the web element (body by default).
2621
     * @param int    $offsetX
2622
     * @param int    $offsetY
2623
     *
2624
     * @throws \Codeception\Exception\ElementNotFound
2625
     */
2626
    public function clickWithRightButton($cssOrXPath = null, $offsetX = null, $offsetY = null)
2627
    {
2628
        $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY);
2629
        $this->webDriver->getMouse()->contextClick();
2630
    }
2631
2632
    /**
2633
     * Pauses test execution in debug mode.
2634
     * To proceed test press "ENTER" in console.
2635
     *
2636
     * This method is useful while writing tests,
2637
     * since it allows you to inspect the current page in the middle of a test case.
2638
     */
2639
    public function pauseExecution()
2640
    {
2641
        Debug::pause();
2642
    }
2643
2644
    /**
2645
     * Performs a double-click on an element matched by CSS or XPath.
2646
     *
2647
     * @param $cssOrXPath
2648
     * @throws \Codeception\Exception\ElementNotFound
2649
     */
2650
    public function doubleClick($cssOrXPath)
2651
    {
2652
        $el = $this->matchFirstOrFail($this->baseElement, $cssOrXPath);
2653
        $this->webDriver->getMouse()->doubleClick($el->getCoordinates());
2654
    }
2655
2656
    /**
2657
     * @param $page
2658
     * @param $selector
2659
     * @param bool $throwMalformed
2660
     * @return array
2661
     */
2662
    protected function match($page, $selector, $throwMalformed = true)
2663
    {
2664
        if (is_array($selector)) {
2665
            try {
2666
                return $page->findElements($this->getStrictLocator($selector));
2667
            } catch (InvalidSelectorException $e) {
2668
                throw new MalformedLocatorException(key($selector) . ' => ' . reset($selector), "Strict locator");
2669
            } catch (InvalidElementStateException $e) {
2670
                if ($this->isPhantom() and $e->getResults()['status'] == 12) {
2671
                    throw new MalformedLocatorException(
2672
                        key($selector) . ' => ' . reset($selector),
2673
                        "Strict locator ".$e->getCode()
2674
                    );
2675
                }
2676
            }
2677
        }
2678
        if ($selector instanceof WebDriverBy) {
2679
            try {
2680
                return $page->findElements($selector);
2681
            } catch (InvalidSelectorException $e) {
2682
                throw new MalformedLocatorException(
2683
                    sprintf(
2684
                        "WebDriverBy::%s('%s')",
2685
                        $selector->getMechanism(),
2686
                        $selector->getValue()
2687
                    ),
2688
                    'WebDriver'
2689
                );
2690
            }
2691
        }
2692
        $isValidLocator = false;
2693
        $nodes = [];
2694
        try {
2695
            if (Locator::isID($selector)) {
2696
                $isValidLocator = true;
2697
                $nodes = $page->findElements(WebDriverBy::id(substr($selector, 1)));
2698
            }
2699
            if (Locator::isClass($selector)) {
2700
                $isValidLocator = true;
2701
                $nodes = $page->findElements(WebDriverBy::className(substr($selector, 1)));
2702
            }
2703
            if (empty($nodes) and Locator::isCSS($selector)) {
2704
                $isValidLocator = true;
2705
                try {
2706
                    $nodes = $page->findElements(WebDriverBy::cssSelector($selector));
2707
                } catch (InvalidElementStateException $e) {
2708
                    $nodes = $page->findElements(WebDriverBy::linkText($selector));
2709
                }
2710
            }
2711
            if (empty($nodes) and Locator::isXPath($selector)) {
2712
                $isValidLocator = true;
2713
                $nodes = $page->findElements(WebDriverBy::xpath($selector));
2714
            }
2715
        } catch (InvalidSelectorException $e) {
2716
            throw new MalformedLocatorException($selector);
2717
        }
2718
        if (!$isValidLocator and $throwMalformed) {
2719
            throw new MalformedLocatorException($selector);
2720
        }
2721
        return $nodes;
2722
    }
2723
2724
    /**
2725
     * @param array $by
2726
     * @return WebDriverBy
2727
     */
2728
    protected function getStrictLocator(array $by)
2729
    {
2730
        $type = key($by);
2731
        $locator = $by[$type];
2732
        switch ($type) {
2733
            case 'id':
2734
                return WebDriverBy::id($locator);
2735
            case 'name':
2736
                return WebDriverBy::name($locator);
2737
            case 'css':
2738
                return WebDriverBy::cssSelector($locator);
2739
            case 'xpath':
2740
                return WebDriverBy::xpath($locator);
2741
            case 'link':
2742
                return WebDriverBy::linkText($locator);
2743
            case 'class':
2744
                return WebDriverBy::className($locator);
2745
            default:
2746
                throw new MalformedLocatorException(
2747
                    "$by => $locator",
2748
                    "Strict locator can be either xpath, css, id, link, class, name: "
2749
                );
2750
        }
2751
    }
2752
2753
    /**
2754
     * @param $page
2755
     * @param $selector
2756
     * @return WebDriverElement
2757
     * @throws \Codeception\Exception\ElementNotFound
2758
     */
2759
    protected function matchFirstOrFail($page, $selector)
2760
    {
2761
        $this->enableImplicitWait();
2762
        $els = $this->match($page, $selector);
2763
        $this->disableImplicitWait();
2764
        if (!count($els)) {
2765
            throw new ElementNotFound($selector, "CSS or XPath");
2766
        }
2767
        return reset($els);
2768
    }
2769
2770
    /**
2771
     * Presses the given key on the given element.
2772
     * To specify a character and modifier (e.g. ctrl, alt, shift, meta), pass an array for $char with
2773
     * the modifier as the first element and the character as the second.
2774
     * For special keys use key constants from WebDriverKeys class.
2775
     *
2776
     * ``` php
2777
     * <?php
2778
     * // <input id="page" value="old" />
2779
     * $I->pressKey('#page','a'); // => olda
2780
     * $I->pressKey('#page',array('ctrl','a'),'new'); //=> new
2781
     * $I->pressKey('#page',array('shift','111'),'1','x'); //=> old!!!1x
2782
     * $I->pressKey('descendant-or-self::*[@id='page']','u'); //=> oldu
2783
     * $I->pressKey('#name', array('ctrl', 'a'), \Facebook\WebDriver\WebDriverKeys::DELETE); //=>''
2784
     * ?>
2785
     * ```
2786
     *
2787
     * @param $element
2788
     * @param $char string|array Can be char or array with modifier. You can provide several chars.
2789
     * @throws \Codeception\Exception\ElementNotFound
2790
     */
2791
    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...
2792
    {
2793
        $el = $this->matchFirstOrFail($this->baseElement, $element);
2794
        $args = func_get_args();
2795
        array_shift($args);
2796
        $keys = [];
2797
        foreach ($args as $key) {
2798
            $keys[] = $this->convertKeyModifier($key);
2799
        }
2800
        $el->sendKeys($keys);
2801
    }
2802
2803
    protected function convertKeyModifier($keys)
2804
    {
2805
        if (!is_array($keys)) {
2806
            return $keys;
2807
        }
2808
        if (!isset($keys[1])) {
2809
            return $keys;
2810
        }
2811
        list($modifier, $key) = $keys;
2812
2813
        switch ($modifier) {
2814
            case 'ctrl':
2815
            case 'control':
2816
                return [WebDriverKeys::CONTROL, $key];
2817
            case 'alt':
2818
                return [WebDriverKeys::ALT, $key];
2819
            case 'shift':
2820
                return [WebDriverKeys::SHIFT, $key];
2821
            case 'meta':
2822
                return [WebDriverKeys::META, $key];
2823
        }
2824
        return $keys;
2825
    }
2826
2827
    protected function assertNodesContain($text, $nodes, $selector = null)
2828
    {
2829
        $this->assertThat($nodes, new WebDriverConstraint($text, $this->_getCurrentUri()), $selector);
2830
    }
2831
2832
    protected function assertNodesNotContain($text, $nodes, $selector = null)
2833
    {
2834
        $this->assertThat($nodes, new WebDriverConstraintNot($text, $this->_getCurrentUri()), $selector);
2835
    }
2836
2837
    protected function assertPageContains($needle, $message = '')
2838
    {
2839
        $this->assertThat(
2840
            htmlspecialchars_decode($this->getVisibleText()),
2841
            new PageConstraint($needle, $this->_getCurrentUri()),
2842
            $message
2843
        );
2844
    }
2845
2846
    protected function assertPageNotContains($needle, $message = '')
2847
    {
2848
        $this->assertThatItsNot(
2849
            htmlspecialchars_decode($this->getVisibleText()),
2850
            new PageConstraint($needle, $this->_getCurrentUri()),
2851
            $message
2852
        );
2853
    }
2854
2855
    protected function assertPageSourceContains($needle, $message = '')
2856
    {
2857
        $this->assertThat(
2858
            $this->webDriver->getPageSource(),
2859
            new PageConstraint($needle, $this->_getCurrentUri()),
2860
            $message
2861
        );
2862
    }
2863
2864
    protected function assertPageSourceNotContains($needle, $message = '')
2865
    {
2866
        $this->assertThatItsNot(
2867
            $this->webDriver->getPageSource(),
2868
            new PageConstraint($needle, $this->_getCurrentUri()),
2869
            $message
2870
        );
2871
    }
2872
2873
    /**
2874
     * Append the given text to the given element.
2875
     * Can also add a selection to a select box.
2876
     *
2877
     * ``` php
2878
     * <?php
2879
     * $I->appendField('#mySelectbox', 'SelectValue');
2880
     * $I->appendField('#myTextField', 'appended');
2881
     * ?>
2882
     * ```
2883
     *
2884
     * @param string $field
2885
     * @param string $value
2886
     * @throws \Codeception\Exception\ElementNotFound
2887
     */
2888
    public function appendField($field, $value)
2889
    {
2890
        $el = $this->findField($field);
2891
2892
        switch ($el->getTagName()) {
2893
            //Multiple select
2894
            case "select":
2895
                $matched = false;
2896
                $wdSelect = new WebDriverSelect($el);
2897
                try {
2898
                    $wdSelect->selectByVisibleText($value);
2899
                    $matched = true;
2900
                } catch (NoSuchElementException $e) {
2901
                    // exception treated at the end
2902
                }
2903
2904
                try {
2905
                    $wdSelect->selectByValue($value);
2906
                    $matched = true;
2907
                } catch (NoSuchElementException $e) {
2908
                    // exception treated at the end
2909
                }
2910
                if ($matched) {
2911
                    return;
2912
                }
2913
2914
                throw new ElementNotFound(json_encode($value), "Option inside $field matched by name or value");
2915
            case "textarea":
2916
                $el->sendKeys($value);
2917
                return;
2918
            case "div": //allows for content editable divs
2919
                $el->sendKeys(WebDriverKeys::END);
2920
                $el->sendKeys($value);
2921
                return;
2922
            //Text, Checkbox, Radio
2923
            case "input":
2924
                $type = $el->getAttribute('type');
2925
2926
                if ($type == 'checkbox') {
2927
                    //Find by value or css,id,xpath
2928
                    $field = $this->findCheckable($this->baseElement, $value, true);
2929
                    if (!$field) {
2930
                        throw new ElementNotFound($value, "Checkbox or Radio by Label or CSS or XPath");
2931
                    }
2932
                    if ($field->isSelected()) {
2933
                        return;
2934
                    }
2935
                    $field->click();
2936
                    return;
2937
                } elseif ($type == 'radio') {
2938
                    $this->selectOption($field, $value);
2939
                    return;
2940
                }
2941
2942
                $el->sendKeys($value);
2943
                return;
2944
        }
2945
2946
        throw new ElementNotFound($field, "Field by name, label, CSS or XPath");
2947
    }
2948
2949
    /**
2950
     * @param $selector
2951
     * @return array
2952
     */
2953
    protected function matchVisible($selector)
2954
    {
2955
        $els = $this->match($this->baseElement, $selector);
2956
        $nodes = array_filter(
2957
            $els,
2958
            function (WebDriverElement $el) {
2959
                return $el->isDisplayed();
2960
            }
2961
        );
2962
        return $nodes;
2963
    }
2964
2965
    /**
2966
     * @param $selector
2967
     * @return WebDriverBy
2968
     * @throws \InvalidArgumentException
2969
     */
2970
    protected function getLocator($selector)
2971
    {
2972
        if ($selector instanceof WebDriverBy) {
2973
            return $selector;
2974
        }
2975
        if (is_array($selector)) {
2976
            return $this->getStrictLocator($selector);
2977
        }
2978
        if (Locator::isID($selector)) {
2979
            return WebDriverBy::id(substr($selector, 1));
2980
        }
2981
        if (Locator::isCSS($selector)) {
2982
            return WebDriverBy::cssSelector($selector);
2983
        }
2984
        if (Locator::isXPath($selector)) {
2985
            return WebDriverBy::xpath($selector);
2986
        }
2987
        throw new \InvalidArgumentException("Only CSS or XPath allowed");
2988
    }
2989
2990
    /**
2991
     * @param string $name
2992
     */
2993
    public function saveSessionSnapshot($name)
2994
    {
2995
        $this->sessionSnapshots[$name] = [];
2996
2997
        foreach ($this->webDriver->manage()->getCookies() as $cookie) {
2998
            if (in_array(trim($cookie['name']), [LocalServer::COVERAGE_COOKIE, LocalServer::COVERAGE_COOKIE_ERROR])) {
2999
                continue;
3000
            }
3001
3002
            if ($this->cookieDomainMatchesConfigUrl($cookie)) {
3003
                $this->sessionSnapshots[$name][] = $cookie;
3004
            }
3005
        }
3006
3007
        $this->debugSection('Snapshot', "Saved \"$name\" session snapshot");
3008
    }
3009
3010
    /**
3011
     * @param string $name
3012
     * @return bool
3013
     */
3014
    public function loadSessionSnapshot($name)
3015
    {
3016
        if (!isset($this->sessionSnapshots[$name])) {
3017
            return false;
3018
        }
3019
        $this->webDriver->manage()->deleteAllCookies();
3020
        foreach ($this->sessionSnapshots[$name] as $cookie) {
3021
            $this->webDriver->manage()->addCookie($cookie);
3022
        }
3023
        $this->debugSection('Snapshot', "Restored \"$name\" session snapshot");
3024
        return true;
3025
    }
3026
3027
    /**
3028
     * Check if the cookie domain matches the config URL.
3029
     *
3030
     * @param array|Cookie $cookie
3031
     * @return bool
3032
     */
3033
    private function cookieDomainMatchesConfigUrl($cookie)
3034
    {
3035
        if (!array_key_exists('domain', $cookie)) {
3036
            return true;
3037
        }
3038
3039
        $setCookie = new SetCookie();
3040
        $setCookie->setDomain($cookie['domain']);
3041
3042
        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...
3043
    }
3044
3045
    /**
3046
     * @return bool
3047
     */
3048
    protected function isPhantom()
3049
    {
3050
        return strpos($this->config['browser'], 'phantom') === 0;
3051
    }
3052
3053
    /**
3054
     * Move to the middle of the given element matched by the given locator.
3055
     * Extra shift, calculated from the top-left corner of the element,
3056
     * can be set by passing $offsetX and $offsetY parameters.
3057
     *
3058
     * ``` php
3059
     * <?php
3060
     * $I->scrollTo(['css' => '.checkout'], 20, 50);
3061
     * ?>
3062
     * ```
3063
     *
3064
     * @param $selector
3065
     * @param int $offsetX
3066
     * @param int $offsetY
3067
     */
3068
    public function scrollTo($selector, $offsetX = null, $offsetY = null)
3069
    {
3070
        $el = $this->matchFirstOrFail($this->baseElement, $selector);
3071
        $x = $el->getLocation()->getX() + $offsetX;
3072
        $y = $el->getLocation()->getY() + $offsetY;
3073
        $this->webDriver->executeScript("window.scrollTo($x, $y)");
3074
    }
3075
3076
    /**
3077
     * Opens a new browser tab (wherever it is possible) and switches to it.
3078
     *
3079
     * ```php
3080
     * <?php
3081
     * $I->openNewTab();
3082
     * ```
3083
     * Tab is opened by using `window.open` javascript in a browser.
3084
     * Please note, that adblock can restrict creating such tabs.
3085
     *
3086
     * Can't be used with PhantomJS
3087
     *
3088
     */
3089
    public function openNewTab()
3090
    {
3091
        $this->executeJS("window.open('about:blank','_blank');");
3092
        $this->switchToNextTab();
3093
    }
3094
3095
    /**
3096
     * Closes current browser tab and switches to previous active tab.
3097
     *
3098
     * ```php
3099
     * <?php
3100
     * $I->closeTab();
3101
     * ```
3102
     *
3103
     * Can't be used with PhantomJS
3104
     */
3105
    public function closeTab()
3106
    {
3107
        $prevTab = $this->getRelativeTabHandle(-1);
3108
        $this->webDriver->close();
3109
        $this->webDriver->switchTo()->window($prevTab);
3110
    }
3111
3112
    /**
3113
     * Switches to next browser tab.
3114
     * An offset can be specified.
3115
     *
3116
     * ```php
3117
     * <?php
3118
     * // switch to next tab
3119
     * $I->switchToNextTab();
3120
     * // switch to 2nd next tab
3121
     * $I->switchToNextTab(2);
3122
     * ```
3123
     *
3124
     * Can't be used with PhantomJS
3125
     *
3126
     * @param int $offset 1
3127
     */
3128
    public function switchToNextTab($offset = 1)
3129
    {
3130
        $tab = $this->getRelativeTabHandle($offset);
3131
        $this->webDriver->switchTo()->window($tab);
3132
    }
3133
3134
    /**
3135
     * Switches to previous browser tab.
3136
     * An offset can be specified.
3137
     *
3138
     * ```php
3139
     * <?php
3140
     * // switch to previous tab
3141
     * $I->switchToPreviousTab();
3142
     * // switch to 2nd previous tab
3143
     * $I->switchToPreviousTab(2);
3144
     * ```
3145
     *
3146
     * Can't be used with PhantomJS
3147
     *
3148
     * @param int $offset 1
3149
     */
3150
    public function switchToPreviousTab($offset = 1)
3151
    {
3152
        $this->switchToNextTab(0 - $offset);
3153
    }
3154
3155
    protected function getRelativeTabHandle($offset)
3156
    {
3157
        if ($this->isPhantom()) {
3158
            throw new ModuleException($this, "PhantomJS doesn't support tab actions");
3159
        }
3160
        $handle = $this->webDriver->getWindowHandle();
3161
        $handles = $this->webDriver->getWindowHandles();
3162
        $idx = array_search($handle, $handles);
3163
        return $handles[($idx + $offset) % count($handles)];
3164
    }
3165
3166
    /**
3167
     * Waits for element and runs a sequence of actions inside its context.
3168
     * Actions can be defined with array, callback, or `Codeception\Util\ActionSequence` instance.
3169
     *
3170
     * Actions as array are recommended for simple to combine "waitForElement" with assertions;
3171
     * `waitForElement($el)` and `see('text', $el)` can be simplified to:
3172
     *
3173
     * ```php
3174
     * <?php
3175
     * $I->performOn($el, ['see' => 'text']);
3176
     * ```
3177
     *
3178
     * List of actions can be pragmatically build using `Codeception\Util\ActionSequence`:
3179
     *
3180
     * ```php
3181
     * <?php
3182
     * $I->performOn('.model', ActionSequence::build()
3183
     *     ->see('Warning')
3184
     *     ->see('Are you sure you want to delete this?')
3185
     *     ->click('Yes')
3186
     * );
3187
     * ```
3188
     *
3189
     * Actions executed from array or ActionSequence will print debug output for actions, and adds an action name to
3190
     * exception on failure.
3191
     *
3192
     * Whenever you need to define more actions a callback can be used. A WebDriver module is passed for argument:
3193
     *
3194
     * ```php
3195
     * <?php
3196
     * $I->performOn('.rememberMe', function (WebDriver $I) {
3197
     *      $I->see('Remember me next time');
3198
     *      $I->seeElement('#LoginForm_rememberMe');
3199
     *      $I->dontSee('Login');
3200
     * });
3201
     * ```
3202
     *
3203
     * In 3rd argument you can set number a seconds to wait for element to appear
3204
     *
3205
     * @param $element
3206
     * @param $actions
3207
     * @param int $timeout
3208
     */
3209
    public function performOn($element, $actions, $timeout = 10)
3210
    {
3211
        $this->waitForElement($element, $timeout);
3212
        $this->setBaseElement($element);
3213
        $this->debugSection('InnerText', $this->baseElement->getText());
3214
3215
        if (is_callable($actions)) {
3216
            $actions($this);
3217
            $this->setBaseElement();
3218
            return;
3219
        }
3220
        if (is_array($actions)) {
3221
            $actions = ActionSequence::build()->fromArray($actions);
3222
        }
3223
3224
        if (!$actions instanceof ActionSequence) {
3225
            throw new \InvalidArgumentException("2nd parameter, actions should be callback, ActionSequence or array");
3226
        }
3227
3228
        $actions->run($this);
3229
        $this->setBaseElement();
3230
    }
3231
3232
    protected function setBaseElement($element = null)
3233
    {
3234
        if ($element === null) {
3235
            $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...
3236
            return;
3237
        }
3238
        $this->baseElement = $this->matchFirstOrFail($this->webDriver, $element);
3239
    }
3240
3241
    protected function enableImplicitWait()
3242
    {
3243
        if (!$this->config['wait']) {
3244
            return;
3245
        }
3246
        $this->webDriver->manage()->timeouts()->implicitlyWait($this->config['wait']);
3247
    }
3248
3249
    protected function disableImplicitWait()
3250
    {
3251
        if (!$this->config['wait']) {
3252
            return;
3253
        }
3254
        $this->webDriver->manage()->timeouts()->implicitlyWait(0);
3255
    }
3256
}
3257