Completed
Pull Request — 2.3 (#4589)
by
unknown
03:56
created

WebDriver::attachFile()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 2
dl 0
loc 20
rs 9.2
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::getTestSignature($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]"
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
    public function attachFile($field, $filename)
1628
    {
1629
        $el = $this->findField($field);
1630
        // in order to be compatible on different OS
1631
        $filePath = codecept_data_dir() . $filename;
1632
        if (!file_exists($filePath)) {
1633
            throw new \InvalidArgumentException("File does not exist: $filePath");
1634
        }
1635
        if (!is_readable($filePath)) {
1636
            throw new \InvalidArgumentException("File is not readable: $filePath");
1637
        }
1638
        // in order for remote upload to be enabled
1639
        $el->setFileDetector(new LocalFileDetector());
1640
1641
        // skip file detector for phantomjs
1642
        if ($this->isPhantom()) {
1643
            $el->setFileDetector(new UselessFileDetector());
1644
        }
1645
        $el->sendKeys(realpath($filePath));
1646
    }
1647
1648
    /**
1649
     * Grabs all visible text from the current page.
1650
     *
1651
     * @return string
1652
     */
1653
    protected function getVisibleText()
1654
    {
1655
        if ($this->baseElement instanceof RemoteWebElement) {
1656
            return $this->baseElement->getText();
1657
        }
1658
        $els = $this->baseElement->findElements(WebDriverBy::cssSelector('body'));
1659
        if (isset($els[0])) {
1660
            return $els[0]->getText();
1661
        }
1662
        return '';
1663
    }
1664
1665
    public function grabTextFrom($cssOrXPathOrRegex)
1666
    {
1667
        $els = $this->match($this->baseElement, $cssOrXPathOrRegex, false);
1668
        if (count($els)) {
1669
            return $els[0]->getText();
1670
        }
1671
        if (@preg_match($cssOrXPathOrRegex, $this->webDriver->getPageSource(), $matches)) {
1672
            return $matches[1];
1673
        }
1674
        throw new ElementNotFound($cssOrXPathOrRegex, 'CSS or XPath or Regex');
1675
    }
1676
1677
    public function grabAttributeFrom($cssOrXpath, $attribute)
1678
    {
1679
        $el = $this->matchFirstOrFail($this->baseElement, $cssOrXpath);
1680
        return $el->getAttribute($attribute);
1681
    }
1682
1683
    public function grabValueFrom($field)
1684
    {
1685
        $el = $this->findField($field);
1686
        // value of multiple select is the value of the first selected option
1687
        if ($el->getTagName() == 'select') {
1688
            $select = new WebDriverSelect($el);
1689
            return $select->getFirstSelectedOption()->getAttribute('value');
1690
        }
1691
        return $el->getAttribute('value');
1692
    }
1693
1694
    public function grabMultiple($cssOrXpath, $attribute = null)
1695
    {
1696
        $els = $this->match($this->baseElement, $cssOrXpath);
1697
        return array_map(
1698
            function (WebDriverElement $e) use ($attribute) {
1699
                if ($attribute) {
1700
                    return $e->getAttribute($attribute);
1701
                }
1702
                return $e->getText();
1703
            },
1704
            $els
1705
        );
1706
    }
1707
1708
1709
    protected function filterByAttributes($els, array $attributes)
1710
    {
1711
        foreach ($attributes as $attr => $value) {
1712
            $els = array_filter(
1713
                $els,
1714
                function (WebDriverElement $el) use ($attr, $value) {
1715
                    return $el->getAttribute($attr) == $value;
1716
                }
1717
            );
1718
        }
1719
        return $els;
1720
    }
1721
1722
    public function seeElement($selector, $attributes = [])
1723
    {
1724
        $this->enableImplicitWait();
1725
        $els = $this->matchVisible($selector);
1726
        $this->disableImplicitWait();
1727
        $els = $this->filterByAttributes($els, $attributes);
1728
        $this->assertNotEmpty($els);
1729
    }
1730
1731
    public function dontSeeElement($selector, $attributes = [])
1732
    {
1733
        $els = $this->matchVisible($selector);
1734
        $els = $this->filterByAttributes($els, $attributes);
1735
        $this->assertEmpty($els);
1736
    }
1737
1738
    /**
1739
     * Checks that the given element exists on the page, even it is invisible.
1740
     *
1741
     * ``` php
1742
     * <?php
1743
     * $I->seeElementInDOM('//form/input[type=hidden]');
1744
     * ?>
1745
     * ```
1746
     *
1747
     * @param $selector
1748
     * @param array $attributes
1749
     */
1750
    public function seeElementInDOM($selector, $attributes = [])
1751
    {
1752
        $this->enableImplicitWait();
1753
        $els = $this->match($this->baseElement, $selector);
1754
        $els = $this->filterByAttributes($els, $attributes);
1755
        $this->disableImplicitWait();
1756
        $this->assertNotEmpty($els);
1757
    }
1758
1759
1760
    /**
1761
     * Opposite of `seeElementInDOM`.
1762
     *
1763
     * @param $selector
1764
     * @param array $attributes
1765
     */
1766
    public function dontSeeElementInDOM($selector, $attributes = [])
1767
    {
1768
        $els = $this->match($this->baseElement, $selector);
1769
        $els = $this->filterByAttributes($els, $attributes);
1770
        $this->assertEmpty($els);
1771
    }
1772
1773 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...
1774
    {
1775
        $counted = count($this->matchVisible($selector));
1776
        if (is_array($expected)) {
1777
            list($floor, $ceil) = $expected;
1778
            $this->assertTrue(
1779
                $floor <= $counted && $ceil >= $counted,
1780
                'Number of elements counted differs from expected range'
1781
            );
1782
        } else {
1783
            $this->assertEquals(
1784
                $expected,
1785
                $counted,
1786
                'Number of elements counted differs from expected number'
1787
            );
1788
        }
1789
    }
1790
1791 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...
1792
    {
1793
        $counted = count($this->match($this->baseElement, $selector));
1794
        if (is_array($expected)) {
1795
            list($floor, $ceil) = $expected;
1796
            $this->assertTrue(
1797
                $floor <= $counted && $ceil >= $counted,
1798
                'Number of elements counted differs from expected range'
1799
            );
1800
        } else {
1801
            $this->assertEquals(
1802
                $expected,
1803
                $counted,
1804
                'Number of elements counted differs from expected number'
1805
            );
1806
        }
1807
    }
1808
1809 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...
1810
    {
1811
        $el = $this->findField($selector);
1812
        if ($el->getTagName() !== 'select') {
1813
            $els = $this->matchCheckables($selector);
1814
            foreach ($els as $k => $el) {
1815
                $els[$k] = $this->findCheckable($el, $optionText, true);
1816
            }
1817
            $this->assertNotEmpty(
1818
                array_filter(
1819
                    $els,
1820
                    function ($e) {
1821
                        return $e && $e->isSelected();
1822
                    }
1823
                )
1824
            );
1825
            return;
1826
        }
1827
        $select = new WebDriverSelect($el);
1828
        $this->assertNodesContain($optionText, $select->getAllSelectedOptions(), 'option');
1829
    }
1830
1831 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...
1832
    {
1833
        $el = $this->findField($selector);
1834
        if ($el->getTagName() !== 'select') {
1835
            $els = $this->matchCheckables($selector);
1836
            foreach ($els as $k => $el) {
1837
                $els[$k] = $this->findCheckable($el, $optionText, true);
1838
            }
1839
            $this->assertEmpty(
1840
                array_filter(
1841
                    $els,
1842
                    function ($e) {
1843
                        return $e && $e->isSelected();
1844
                    }
1845
                )
1846
            );
1847
            return;
1848
        }
1849
        $select = new WebDriverSelect($el);
1850
        $this->assertNodesNotContain($optionText, $select->getAllSelectedOptions(), 'option');
1851
    }
1852
1853
    public function seeInTitle($title)
1854
    {
1855
        $this->assertContains($title, $this->webDriver->getTitle());
1856
    }
1857
1858
    public function dontSeeInTitle($title)
1859
    {
1860
        $this->assertNotContains($title, $this->webDriver->getTitle());
1861
    }
1862
1863
    /**
1864
     * Accepts the active JavaScript native popup window, as created by `window.alert`|`window.confirm`|`window.prompt`.
1865
     * Don't confuse popups with modal windows,
1866
     * as created by [various libraries](http://jster.net/category/windows-modals-popups).
1867
     */
1868
    public function acceptPopup()
1869
    {
1870
        if ($this->isPhantom()) {
1871
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1872
        }
1873
        $this->webDriver->switchTo()->alert()->accept();
1874
    }
1875
1876
    /**
1877
     * Dismisses the active JavaScript popup, as created by `window.alert`, `window.confirm`, or `window.prompt`.
1878
     */
1879
    public function cancelPopup()
1880
    {
1881
        if ($this->isPhantom()) {
1882
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1883
        }
1884
        $this->webDriver->switchTo()->alert()->dismiss();
1885
    }
1886
1887
    /**
1888
     * Checks that the active JavaScript popup,
1889
     * as created by `window.alert`|`window.confirm`|`window.prompt`, contains the given string.
1890
     *
1891
     * @param $text
1892
     *
1893
     * @throws \Codeception\Exception\ModuleException
1894
     */
1895 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...
1896
    {
1897
        if ($this->isPhantom()) {
1898
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1899
        }
1900
        $alert = $this->webDriver->switchTo()->alert();
1901
        try {
1902
            $this->assertContains($text, $alert->getText());
1903
        } catch (\PHPUnit_Framework_AssertionFailedError $e) {
1904
            $alert->dismiss();
1905
            throw $e;
1906
        }
1907
    }
1908
1909
    /**
1910
     * Checks that the active JavaScript popup,
1911
     * as created by `window.alert`|`window.confirm`|`window.prompt`, does NOT contain the given string.
1912
     *
1913
     * @param $text
1914
     *
1915
     * @throws \Codeception\Exception\ModuleException
1916
     */
1917 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...
1918
    {
1919
        if ($this->isPhantom()) {
1920
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1921
        }
1922
        $alert = $this->webDriver->switchTo()->alert();
1923
        try {
1924
            $this->assertNotContains($text, $alert->getText());
1925
        } catch (\PHPUnit_Framework_AssertionFailedError $e) {
1926
            $alert->dismiss();
1927
            throw $e;
1928
        }
1929
    }
1930
1931
    /**
1932
     * Enters text into a native JavaScript prompt popup, as created by `window.prompt`.
1933
     *
1934
     * @param $keys
1935
     *
1936
     * @throws \Codeception\Exception\ModuleException
1937
     */
1938
    public function typeInPopup($keys)
1939
    {
1940
        if ($this->isPhantom()) {
1941
            throw new ModuleException($this, 'PhantomJS does not support working with popups');
1942
        }
1943
        $this->webDriver->switchTo()->alert()->sendKeys($keys);
1944
    }
1945
1946
    /**
1947
     * Reloads the current page.
1948
     */
1949
    public function reloadPage()
1950
    {
1951
        $this->webDriver->navigate()->refresh();
1952
    }
1953
1954
    /**
1955
     * Moves back in history.
1956
     */
1957
    public function moveBack()
1958
    {
1959
        $this->webDriver->navigate()->back();
1960
        $this->debug($this->_getCurrentUri());
1961
    }
1962
1963
    /**
1964
     * Moves forward in history.
1965
     */
1966
    public function moveForward()
1967
    {
1968
        $this->webDriver->navigate()->forward();
1969
        $this->debug($this->_getCurrentUri());
1970
    }
1971
1972 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...
1973
    {
1974
        if (substr($name, -2) === '[]') {
1975
            return substr($name, 0, -2);
1976
        }
1977
        return $name;
1978
    }
1979
1980
    /**
1981
     * Submits the given form on the page, optionally with the given form
1982
     * values.  Give the form fields values as an array. Note that hidden fields
1983
     * can't be accessed.
1984
     *
1985
     * Skipped fields will be filled by their values from the page.
1986
     * You don't need to click the 'Submit' button afterwards.
1987
     * This command itself triggers the request to form's action.
1988
     *
1989
     * You can optionally specify what button's value to include
1990
     * in the request with the last parameter as an alternative to
1991
     * explicitly setting its value in the second parameter, as
1992
     * button values are not otherwise included in the request.
1993
     *
1994
     * Examples:
1995
     *
1996
     * ``` php
1997
     * <?php
1998
     * $I->submitForm('#login', [
1999
     *     'login' => 'davert',
2000
     *     'password' => '123456'
2001
     * ]);
2002
     * // or
2003
     * $I->submitForm('#login', [
2004
     *     'login' => 'davert',
2005
     *     'password' => '123456'
2006
     * ], 'submitButtonName');
2007
     *
2008
     * ```
2009
     *
2010
     * For example, given this sample "Sign Up" form:
2011
     *
2012
     * ``` html
2013
     * <form action="/sign_up">
2014
     *     Login:
2015
     *     <input type="text" name="user[login]" /><br/>
2016
     *     Password:
2017
     *     <input type="password" name="user[password]" /><br/>
2018
     *     Do you agree to our terms?
2019
     *     <input type="checkbox" name="user[agree]" /><br/>
2020
     *     Select pricing plan:
2021
     *     <select name="plan">
2022
     *         <option value="1">Free</option>
2023
     *         <option value="2" selected="selected">Paid</option>
2024
     *     </select>
2025
     *     <input type="submit" name="submitButton" value="Submit" />
2026
     * </form>
2027
     * ```
2028
     *
2029
     * You could write the following to submit it:
2030
     *
2031
     * ``` php
2032
     * <?php
2033
     * $I->submitForm(
2034
     *     '#userForm',
2035
     *     [
2036
     *         'user[login]' => 'Davert',
2037
     *         'user[password]' => '123456',
2038
     *         'user[agree]' => true
2039
     *     ],
2040
     *     'submitButton'
2041
     * );
2042
     * ```
2043
     * Note that "2" will be the submitted value for the "plan" field, as it is
2044
     * the selected option.
2045
     *
2046
     * Also note that this differs from PhpBrowser, in that
2047
     * ```'user' => [ 'login' => 'Davert' ]``` is not supported at the moment.
2048
     * Named array keys *must* be included in the name as above.
2049
     *
2050
     * Pair this with seeInFormFields for quick testing magic.
2051
     *
2052
     * ``` php
2053
     * <?php
2054
     * $form = [
2055
     *      'field1' => 'value',
2056
     *      'field2' => 'another value',
2057
     *      'checkbox1' => true,
2058
     *      // ...
2059
     * ];
2060
     * $I->submitForm('//form[@id=my-form]', $form, 'submitButton');
2061
     * // $I->amOnPage('/path/to/form-page') may be needed
2062
     * $I->seeInFormFields('//form[@id=my-form]', $form);
2063
     * ?>
2064
     * ```
2065
     *
2066
     * Parameter values must be set to arrays for multiple input fields
2067
     * of the same name, or multi-select combo boxes.  For checkboxes,
2068
     * either the string value can be used, or boolean values which will
2069
     * be replaced by the checkbox's value in the DOM.
2070
     *
2071
     * ``` php
2072
     * <?php
2073
     * $I->submitForm('#my-form', [
2074
     *      'field1' => 'value',
2075
     *      'checkbox' => [
2076
     *          'value of first checkbox',
2077
     *          'value of second checkbox,
2078
     *      ],
2079
     *      'otherCheckboxes' => [
2080
     *          true,
2081
     *          false,
2082
     *          false
2083
     *      ],
2084
     *      'multiselect' => [
2085
     *          'first option value',
2086
     *          'second option value'
2087
     *      ]
2088
     * ]);
2089
     * ?>
2090
     * ```
2091
     *
2092
     * Mixing string and boolean values for a checkbox's value is not supported
2093
     * and may produce unexpected results.
2094
     *
2095
     * Field names ending in "[]" must be passed without the trailing square
2096
     * bracket characters, and must contain an array for its value.  This allows
2097
     * submitting multiple values with the same name, consider:
2098
     *
2099
     * ```php
2100
     * $I->submitForm('#my-form', [
2101
     *     'field[]' => 'value',
2102
     *     'field[]' => 'another value', // 'field[]' is already a defined key
2103
     * ]);
2104
     * ```
2105
     *
2106
     * The solution is to pass an array value:
2107
     *
2108
     * ```php
2109
     * // this way both values are submitted
2110
     * $I->submitForm('#my-form', [
2111
     *     'field' => [
2112
     *         'value',
2113
     *         'another value',
2114
     *     ]
2115
     * ]);
2116
     * ```
2117
     *
2118
     * The `$button` parameter can be either a string, an array or an instance
2119
     * of Facebook\WebDriver\WebDriverBy. When it is a string, the
2120
     * button will be found by its "name" attribute. If $button is an
2121
     * array then it will be treated as a strict selector and a WebDriverBy
2122
     * will be used verbatim.
2123
     *
2124
     * For example, given the following HTML:
2125
     *
2126
     * ``` html
2127
     * <input type="submit" name="submitButton" value="Submit" />
2128
     * ```
2129
     *
2130
     * `$button` could be any one of the following:
2131
     *   - 'submitButton'
2132
     *   - ['name' => 'submitButton']
2133
     *   - WebDriverBy::name('submitButton')
2134
     *
2135
     * @param $selector
2136
     * @param $params
2137
     * @param $button
2138
     */
2139
    public function submitForm($selector, array $params, $button = null)
2140
    {
2141
        $form = $this->matchFirstOrFail($this->baseElement, $selector);
2142
2143
        $fields = $form->findElements(
2144
            WebDriverBy::cssSelector('input:enabled,textarea:enabled,select:enabled,input[type=hidden]')
2145
        );
2146
        foreach ($fields as $field) {
2147
            $fieldName = $this->getSubmissionFormFieldName($field->getAttribute('name'));
2148
            if (!isset($params[$fieldName])) {
2149
                continue;
2150
            }
2151
            $value = $params[$fieldName];
2152
            if (is_array($value) && $field->getTagName() !== 'select') {
2153
                if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') {
2154
                    $found = false;
2155
                    foreach ($value as $index => $val) {
2156
                        if (!is_bool($val) && $val === $field->getAttribute('value')) {
2157
                            array_splice($params[$fieldName], $index, 1);
2158
                            $value = $val;
2159
                            $found = true;
2160
                            break;
2161
                        }
2162
                    }
2163
                    if (!$found && !empty($value) && is_bool(reset($value))) {
2164
                        $value = array_pop($params[$fieldName]);
2165
                    }
2166
                } else {
2167
                    $value = array_pop($params[$fieldName]);
2168
                }
2169
            }
2170
2171
            if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') {
2172
                if ($value === true || $value === $field->getAttribute('value')) {
2173
                    $this->checkOption($field);
2174
                } else {
2175
                    $this->uncheckOption($field);
2176
                }
2177
            } elseif ($field->getAttribute('type') === 'button' || $field->getAttribute('type') === 'submit') {
2178
                continue;
2179
            } elseif ($field->getTagName() === 'select') {
2180
                $this->selectOption($field, $value);
2181
            } else {
2182
                $this->fillField($field, $value);
2183
            }
2184
        }
2185
2186
        $this->debugSection(
2187
            'Uri',
2188
            $form->getAttribute('action') ? $form->getAttribute('action') : $this->_getCurrentUri()
2189
        );
2190
        $this->debugSection('Method', $form->getAttribute('method') ? $form->getAttribute('method') : 'GET');
2191
        $this->debugSection('Parameters', json_encode($params));
2192
2193
        $submitted = false;
2194
        if (!empty($button)) {
2195
            if (is_array($button)) {
2196
                $buttonSelector = $this->getStrictLocator($button);
2197
            } elseif ($button instanceof WebDriverBy) {
2198
                $buttonSelector = $button;
2199
            } else {
2200
                $buttonSelector = WebDriverBy::name($button);
2201
            }
2202
2203
            $els = $form->findElements($buttonSelector);
2204
2205
            if (!empty($els)) {
2206
                $el = reset($els);
2207
                $el->click();
2208
                $submitted = true;
2209
            }
2210
        }
2211
2212
        if (!$submitted) {
2213
            $form->submit();
2214
        }
2215
        $this->debugSection('Page', $this->_getCurrentUri());
2216
    }
2217
2218
    /**
2219
     * Waits up to $timeout seconds for the given element to change.
2220
     * Element "change" is determined by a callback function which is called repeatedly
2221
     * until the return value evaluates to true.
2222
     *
2223
     * ``` php
2224
     * <?php
2225
     * use \Facebook\WebDriver\WebDriverElement
2226
     * $I->waitForElementChange('#menu', function(WebDriverElement $el) {
2227
     *     return $el->isDisplayed();
2228
     * }, 100);
2229
     * ?>
2230
     * ```
2231
     *
2232
     * @param $element
2233
     * @param \Closure $callback
2234
     * @param int $timeout seconds
2235
     * @throws \Codeception\Exception\ElementNotFound
2236
     */
2237
    public function waitForElementChange($element, \Closure $callback, $timeout = 30)
2238
    {
2239
        $el = $this->matchFirstOrFail($this->baseElement, $element);
2240
        $checker = function () use ($el, $callback) {
2241
            return $callback($el);
2242
        };
2243
        $this->webDriver->wait($timeout)->until($checker);
2244
    }
2245
2246
    /**
2247
     * Waits up to $timeout seconds for an element to appear on the page.
2248
     * If the element doesn't appear, a timeout exception is thrown.
2249
     *
2250
     * ``` php
2251
     * <?php
2252
     * $I->waitForElement('#agree_button', 30); // secs
2253
     * $I->click('#agree_button');
2254
     * ?>
2255
     * ```
2256
     *
2257
     * @param $element
2258
     * @param int $timeout seconds
2259
     * @throws \Exception
2260
     */
2261
    public function waitForElement($element, $timeout = 10)
2262
    {
2263
        $condition = WebDriverExpectedCondition::presenceOfElementLocated($this->getLocator($element));
2264
        $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...
2265
    }
2266
2267
    /**
2268
     * Waits up to $timeout seconds for the given element to be visible on the page.
2269
     * If element doesn't appear, a timeout exception is thrown.
2270
     *
2271
     * ``` php
2272
     * <?php
2273
     * $I->waitForElementVisible('#agree_button', 30); // secs
2274
     * $I->click('#agree_button');
2275
     * ?>
2276
     * ```
2277
     *
2278
     * @param $element
2279
     * @param int $timeout seconds
2280
     * @throws \Exception
2281
     */
2282
    public function waitForElementVisible($element, $timeout = 10)
2283
    {
2284
        $condition = WebDriverExpectedCondition::visibilityOfElementLocated($this->getLocator($element));
2285
        $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...
2286
    }
2287
2288
    /**
2289
     * Waits up to $timeout seconds for the given element to become invisible.
2290
     * If element stays visible, a timeout exception is thrown.
2291
     *
2292
     * ``` php
2293
     * <?php
2294
     * $I->waitForElementNotVisible('#agree_button', 30); // secs
2295
     * ?>
2296
     * ```
2297
     *
2298
     * @param $element
2299
     * @param int $timeout seconds
2300
     * @throws \Exception
2301
     */
2302
    public function waitForElementNotVisible($element, $timeout = 10)
2303
    {
2304
        $condition = WebDriverExpectedCondition::invisibilityOfElementLocated($this->getLocator($element));
2305
        $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...
2306
    }
2307
2308
    /**
2309
     * Waits up to $timeout seconds for the given string to appear on the page.
2310
     *
2311
     * Can also be passed a selector to search in, be as specific as possible when using selectors.
2312
     * waitForText() will only watch the first instance of the matching selector / text provided.
2313
     * If the given text doesn't appear, a timeout exception is thrown.
2314
     *
2315
     * ``` php
2316
     * <?php
2317
     * $I->waitForText('foo', 30); // secs
2318
     * $I->waitForText('foo', 30, '.title'); // secs
2319
     * ?>
2320
     * ```
2321
     *
2322
     * @param string $text
2323
     * @param int $timeout seconds
2324
     * @param string $selector optional
2325
     * @throws \Exception
2326
     */
2327
    public function waitForText($text, $timeout = 10, $selector = null)
2328
    {
2329
        $message = sprintf(
2330
            'Waited for %d secs but text %s still not found',
2331
            $timeout,
2332
            Locator::humanReadableString($text)
2333
        );
2334
        if (!$selector) {
2335
            $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...
2336
            $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...
2337
            return;
2338
        }
2339
2340
        $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...
2341
        $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...
2342
    }
2343
2344
    /**
2345
     * Wait for $timeout seconds.
2346
     *
2347
     * @param int|float $timeout secs
2348
     * @throws \Codeception\Exception\TestRuntimeException
2349
     */
2350
    public function wait($timeout)
2351
    {
2352
        if ($timeout >= 1000) {
2353
            throw new TestRuntimeException(
2354
                "
2355
                Waiting for more then 1000 seconds: 16.6667 mins\n
2356
                Please note that wait method accepts number of seconds as parameter."
2357
            );
2358
        }
2359
        usleep($timeout * 1000000);
2360
    }
2361
2362
    /**
2363
     * Low-level API method.
2364
     * If Codeception commands are not enough, this allows you to use Selenium WebDriver methods directly:
2365
     *
2366
     * ``` php
2367
     * $I->executeInSelenium(function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) {
2368
     *   $webdriver->get('http://google.com');
2369
     * });
2370
     * ```
2371
     *
2372
     * This runs in the context of the
2373
     * [RemoteWebDriver class](https://github.com/facebook/php-webdriver/blob/master/lib/remote/RemoteWebDriver.php).
2374
     * Try not to use this command on a regular basis.
2375
     * If Codeception lacks a feature you need, please implement it and submit a patch.
2376
     *
2377
     * @param callable $function
2378
     */
2379
    public function executeInSelenium(\Closure $function)
2380
    {
2381
        return $function($this->webDriver);
2382
    }
2383
2384
    /**
2385
     * Switch to another window identified by name.
2386
     *
2387
     * The window can only be identified by name. If the $name parameter is blank, the parent window will be used.
2388
     *
2389
     * Example:
2390
     * ``` html
2391
     * <input type="button" value="Open window" onclick="window.open('http://example.com', 'another_window')">
2392
     * ```
2393
     *
2394
     * ``` php
2395
     * <?php
2396
     * $I->click("Open window");
2397
     * # switch to another window
2398
     * $I->switchToWindow("another_window");
2399
     * # switch to parent window
2400
     * $I->switchToWindow();
2401
     * ?>
2402
     * ```
2403
     *
2404
     * If the window has no name, match it by switching to next active tab using `switchToNextTab` method.
2405
     *
2406
     * Or use native Selenium functions to get access to all opened windows:
2407
     *
2408
     * ``` php
2409
     * <?php
2410
     * $I->executeInSelenium(function (\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) {
2411
     *      $handles=$webdriver->getWindowHandles();
2412
     *      $last_window = end($handles);
2413
     *      $webdriver->switchTo()->window($last_window);
2414
     * });
2415
     * ?>
2416
     * ```
2417
     *
2418
     * @param string|null $name
2419
     */
2420
    public function switchToWindow($name = null)
2421
    {
2422
        $this->webDriver->switchTo()->window($name);
2423
    }
2424
2425
    /**
2426
     * Switch to another frame on the page.
2427
     *
2428
     * Example:
2429
     * ``` html
2430
     * <iframe name="another_frame" src="http://example.com">
2431
     *
2432
     * ```
2433
     *
2434
     * ``` php
2435
     * <?php
2436
     * # switch to iframe
2437
     * $I->switchToIFrame("another_frame");
2438
     * # switch to parent page
2439
     * $I->switchToIFrame();
2440
     *
2441
     * ```
2442
     *
2443
     * @param string|null $name
2444
     */
2445
    public function switchToIFrame($name = null)
2446
    {
2447
        if (is_null($name)) {
2448
            $this->webDriver->switchTo()->defaultContent();
2449
            return;
2450
        }
2451
        $this->webDriver->switchTo()->frame($name);
2452
    }
2453
2454
    /**
2455
     * Executes JavaScript and waits up to $timeout seconds for it to return true.
2456
     *
2457
     * In this example we will wait up to 60 seconds for all jQuery AJAX requests to finish.
2458
     *
2459
     * ``` php
2460
     * <?php
2461
     * $I->waitForJS("return $.active == 0;", 60);
2462
     * ?>
2463
     * ```
2464
     *
2465
     * @param string $script
2466
     * @param int $timeout seconds
2467
     */
2468
    public function waitForJS($script, $timeout = 5)
2469
    {
2470
        $condition = function ($wd) use ($script) {
2471
            return $wd->executeScript($script);
2472
        };
2473
        $message = sprintf(
2474
            'Waited for %d secs but script %s still not executed',
2475
            $timeout,
2476
            Locator::humanReadableString($script)
2477
        );
2478
        $this->webDriver->wait($timeout)->until($condition, $message);
2479
    }
2480
2481
    /**
2482
     * Executes custom JavaScript.
2483
     *
2484
     * This example uses jQuery to get a value and assigns that value to a PHP variable:
2485
     *
2486
     * ```php
2487
     * <?php
2488
     * $myVar = $I->executeJS('return $("#myField").val()');
2489
     * ?>
2490
     * ```
2491
     *
2492
     * @param $script
2493
     * @return mixed
2494
     */
2495
    public function executeJS($script)
2496
    {
2497
        return $this->webDriver->executeScript($script);
2498
    }
2499
2500
    /**
2501
     * Maximizes the current window.
2502
     */
2503
    public function maximizeWindow()
2504
    {
2505
        $this->webDriver->manage()->window()->maximize();
2506
    }
2507
2508
    /**
2509
     * Performs a simple mouse drag-and-drop operation.
2510
     *
2511
     * ``` php
2512
     * <?php
2513
     * $I->dragAndDrop('#drag', '#drop');
2514
     * ?>
2515
     * ```
2516
     *
2517
     * @param string $source (CSS ID or XPath)
2518
     * @param string $target (CSS ID or XPath)
2519
     */
2520
    public function dragAndDrop($source, $target)
2521
    {
2522
        $snodes = $this->matchFirstOrFail($this->baseElement, $source);
2523
        $tnodes = $this->matchFirstOrFail($this->baseElement, $target);
2524
2525
        $action = new WebDriverActions($this->webDriver);
2526
        $action->dragAndDrop($snodes, $tnodes)->perform();
2527
    }
2528
2529
    /**
2530
     * Move mouse over the first element matched by the given locator.
2531
     * If the first parameter null then the page is used.
2532
     * If the second and third parameters are given,
2533
     * then the mouse is moved to an offset of the element's top-left corner.
2534
     * Otherwise, the mouse is moved to the center of the element.
2535
     *
2536
     * ``` php
2537
     * <?php
2538
     * $I->moveMouseOver(['css' => '.checkout']);
2539
     * $I->moveMouseOver(null, 20, 50);
2540
     * $I->moveMouseOver(['css' => '.checkout'], 20, 50);
2541
     * ?>
2542
     * ```
2543
     *
2544
     * @param string $cssOrXPath css or xpath of the web element
2545
     * @param int $offsetX
2546
     * @param int $offsetY
2547
     *
2548
     * @throws \Codeception\Exception\ElementNotFound
2549
     */
2550
    public function moveMouseOver($cssOrXPath = null, $offsetX = null, $offsetY = null)
2551
    {
2552
        $where = null;
2553
        if (null !== $cssOrXPath) {
2554
            $el = $this->matchFirstOrFail($this->baseElement, $cssOrXPath);
2555
            $where = $el->getCoordinates();
2556
        }
2557
2558
        $this->webDriver->getMouse()->mouseMove($where, $offsetX, $offsetY);
2559
    }
2560
2561
    /**
2562
     * Performs click with the left mouse button on an element.
2563
     * If the first parameter `null` then the offset is relative to the actual mouse position.
2564
     * If the second and third parameters are given,
2565
     * then the mouse is moved to an offset of the element's top-left corner.
2566
     * Otherwise, the mouse is moved to the center of the element.
2567
     *
2568
     * ``` php
2569
     * <?php
2570
     * $I->clickWithLeftButton(['css' => '.checkout']);
2571
     * $I->clickWithLeftButton(null, 20, 50);
2572
     * $I->clickWithLeftButton(['css' => '.checkout'], 20, 50);
2573
     * ?>
2574
     * ```
2575
     *
2576
     * @param string $cssOrXPath css or xpath of the web element (body by default).
2577
     * @param int $offsetX
2578
     * @param int $offsetY
2579
     *
2580
     * @throws \Codeception\Exception\ElementNotFound
2581
     */
2582
    public function clickWithLeftButton($cssOrXPath = null, $offsetX = null, $offsetY = null)
2583
    {
2584
        $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY);
2585
        $this->webDriver->getMouse()->click();
2586
    }
2587
2588
    /**
2589
     * Performs contextual click with the right mouse button on an element.
2590
     * If the first parameter `null` then the offset is relative to the actual mouse position.
2591
     * If the second and third parameters are given,
2592
     * then the mouse is moved to an offset of the element's top-left corner.
2593
     * Otherwise, the mouse is moved to the center of the element.
2594
     *
2595
     * ``` php
2596
     * <?php
2597
     * $I->clickWithRightButton(['css' => '.checkout']);
2598
     * $I->clickWithRightButton(null, 20, 50);
2599
     * $I->clickWithRightButton(['css' => '.checkout'], 20, 50);
2600
     * ?>
2601
     * ```
2602
     *
2603
     * @param string $cssOrXPath css or xpath of the web element (body by default).
2604
     * @param int    $offsetX
2605
     * @param int    $offsetY
2606
     *
2607
     * @throws \Codeception\Exception\ElementNotFound
2608
     */
2609
    public function clickWithRightButton($cssOrXPath = null, $offsetX = null, $offsetY = null)
2610
    {
2611
        $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY);
2612
        $this->webDriver->getMouse()->contextClick();
2613
    }
2614
2615
    /**
2616
     * Pauses test execution in debug mode.
2617
     * To proceed test press "ENTER" in console.
2618
     *
2619
     * This method is useful while writing tests,
2620
     * since it allows you to inspect the current page in the middle of a test case.
2621
     */
2622
    public function pauseExecution()
2623
    {
2624
        Debug::pause();
2625
    }
2626
2627
    /**
2628
     * Performs a double-click on an element matched by CSS or XPath.
2629
     *
2630
     * @param $cssOrXPath
2631
     * @throws \Codeception\Exception\ElementNotFound
2632
     */
2633
    public function doubleClick($cssOrXPath)
2634
    {
2635
        $el = $this->matchFirstOrFail($this->baseElement, $cssOrXPath);
2636
        $this->webDriver->getMouse()->doubleClick($el->getCoordinates());
2637
    }
2638
2639
    /**
2640
     * @param $page
2641
     * @param $selector
2642
     * @param bool $throwMalformed
2643
     * @return array
2644
     */
2645
    protected function match($page, $selector, $throwMalformed = true)
2646
    {
2647
        if (is_array($selector)) {
2648
            try {
2649
                return $page->findElements($this->getStrictLocator($selector));
2650
            } catch (InvalidSelectorException $e) {
2651
                throw new MalformedLocatorException(key($selector) . ' => ' . reset($selector), "Strict locator");
2652
            } catch (InvalidElementStateException $e) {
2653
                if ($this->isPhantom() and $e->getResults()['status'] == 12) {
2654
                    throw new MalformedLocatorException(
2655
                        key($selector) . ' => ' . reset($selector),
2656
                        "Strict locator ".$e->getCode()
2657
                    );
2658
                }
2659
            }
2660
        }
2661
        if ($selector instanceof WebDriverBy) {
2662
            try {
2663
                return $page->findElements($selector);
2664
            } catch (InvalidSelectorException $e) {
2665
                throw new MalformedLocatorException(
2666
                    sprintf(
2667
                        "WebDriverBy::%s('%s')",
2668
                        $selector->getMechanism(),
2669
                        $selector->getValue()
2670
                    ),
2671
                    'WebDriver'
2672
                );
2673
            }
2674
        }
2675
        $isValidLocator = false;
2676
        $nodes = [];
2677
        try {
2678
            if (Locator::isID($selector)) {
2679
                $isValidLocator = true;
2680
                $nodes = $page->findElements(WebDriverBy::id(substr($selector, 1)));
2681
            }
2682
            if (Locator::isClass($selector)) {
2683
                $isValidLocator = true;
2684
                $nodes = $page->findElements(WebDriverBy::className(substr($selector, 1)));
2685
            }
2686
            if (empty($nodes) and Locator::isCSS($selector)) {
2687
                $isValidLocator = true;
2688
                try {
2689
                    $nodes = $page->findElements(WebDriverBy::cssSelector($selector));
2690
                } catch (InvalidElementStateException $e) {
2691
                    $nodes = $page->findElements(WebDriverBy::linkText($selector));
2692
                }
2693
            }
2694
            if (empty($nodes) and Locator::isXPath($selector)) {
2695
                $isValidLocator = true;
2696
                $nodes = $page->findElements(WebDriverBy::xpath($selector));
2697
            }
2698
        } catch (InvalidSelectorException $e) {
2699
            throw new MalformedLocatorException($selector);
2700
        }
2701
        if (!$isValidLocator and $throwMalformed) {
2702
            throw new MalformedLocatorException($selector);
2703
        }
2704
        return $nodes;
2705
    }
2706
2707
    /**
2708
     * @param array $by
2709
     * @return WebDriverBy
2710
     */
2711
    protected function getStrictLocator(array $by)
2712
    {
2713
        $type = key($by);
2714
        $locator = $by[$type];
2715
        switch ($type) {
2716
            case 'id':
2717
                return WebDriverBy::id($locator);
2718
            case 'name':
2719
                return WebDriverBy::name($locator);
2720
            case 'css':
2721
                return WebDriverBy::cssSelector($locator);
2722
            case 'xpath':
2723
                return WebDriverBy::xpath($locator);
2724
            case 'link':
2725
                return WebDriverBy::linkText($locator);
2726
            case 'class':
2727
                return WebDriverBy::className($locator);
2728
            default:
2729
                throw new MalformedLocatorException(
2730
                    "$by => $locator",
2731
                    "Strict locator can be either xpath, css, id, link, class, name: "
2732
                );
2733
        }
2734
    }
2735
2736
    /**
2737
     * @param $page
2738
     * @param $selector
2739
     * @return WebDriverElement
2740
     * @throws \Codeception\Exception\ElementNotFound
2741
     */
2742
    protected function matchFirstOrFail($page, $selector)
2743
    {
2744
        $this->enableImplicitWait();
2745
        $els = $this->match($page, $selector);
2746
        $this->disableImplicitWait();
2747
        if (!count($els)) {
2748
            throw new ElementNotFound($selector, "CSS or XPath");
2749
        }
2750
        return reset($els);
2751
    }
2752
2753
    /**
2754
     * Presses the given key on the given element.
2755
     * To specify a character and modifier (e.g. ctrl, alt, shift, meta), pass an array for $char with
2756
     * the modifier as the first element and the character as the second.
2757
     * For special keys use key constants from WebDriverKeys class.
2758
     *
2759
     * ``` php
2760
     * <?php
2761
     * // <input id="page" value="old" />
2762
     * $I->pressKey('#page','a'); // => olda
2763
     * $I->pressKey('#page',array('ctrl','a'),'new'); //=> new
2764
     * $I->pressKey('#page',array('shift','111'),'1','x'); //=> old!!!1x
2765
     * $I->pressKey('descendant-or-self::*[@id='page']','u'); //=> oldu
2766
     * $I->pressKey('#name', array('ctrl', 'a'), \Facebook\WebDriver\WebDriverKeys::DELETE); //=>''
2767
     * ?>
2768
     * ```
2769
     *
2770
     * @param $element
2771
     * @param $char string|array Can be char or array with modifier. You can provide several chars.
2772
     * @throws \Codeception\Exception\ElementNotFound
2773
     */
2774
    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...
2775
    {
2776
        $el = $this->matchFirstOrFail($this->baseElement, $element);
2777
        $args = func_get_args();
2778
        array_shift($args);
2779
        $keys = [];
2780
        foreach ($args as $key) {
2781
            $keys[] = $this->convertKeyModifier($key);
2782
        }
2783
        $el->sendKeys($keys);
2784
    }
2785
2786
    protected function convertKeyModifier($keys)
2787
    {
2788
        if (!is_array($keys)) {
2789
            return $keys;
2790
        }
2791
        if (!isset($keys[1])) {
2792
            return $keys;
2793
        }
2794
        list($modifier, $key) = $keys;
2795
2796
        switch ($modifier) {
2797
            case 'ctrl':
2798
            case 'control':
2799
                return [WebDriverKeys::CONTROL, $key];
2800
            case 'alt':
2801
                return [WebDriverKeys::ALT, $key];
2802
            case 'shift':
2803
                return [WebDriverKeys::SHIFT, $key];
2804
            case 'meta':
2805
                return [WebDriverKeys::META, $key];
2806
        }
2807
        return $keys;
2808
    }
2809
2810
    protected function assertNodesContain($text, $nodes, $selector = null)
2811
    {
2812
        $this->assertThat($nodes, new WebDriverConstraint($text, $this->_getCurrentUri()), $selector);
2813
    }
2814
2815
    protected function assertNodesNotContain($text, $nodes, $selector = null)
2816
    {
2817
        $this->assertThat($nodes, new WebDriverConstraintNot($text, $this->_getCurrentUri()), $selector);
2818
    }
2819
2820
    protected function assertPageContains($needle, $message = '')
2821
    {
2822
        $this->assertThat(
2823
            htmlspecialchars_decode($this->getVisibleText()),
2824
            new PageConstraint($needle, $this->_getCurrentUri()),
2825
            $message
2826
        );
2827
    }
2828
2829
    protected function assertPageNotContains($needle, $message = '')
2830
    {
2831
        $this->assertThatItsNot(
2832
            htmlspecialchars_decode($this->getVisibleText()),
2833
            new PageConstraint($needle, $this->_getCurrentUri()),
2834
            $message
2835
        );
2836
    }
2837
2838
    protected function assertPageSourceContains($needle, $message = '')
2839
    {
2840
        $this->assertThat(
2841
            $this->webDriver->getPageSource(),
2842
            new PageConstraint($needle, $this->_getCurrentUri()),
2843
            $message
2844
        );
2845
    }
2846
2847
    protected function assertPageSourceNotContains($needle, $message = '')
2848
    {
2849
        $this->assertThatItsNot(
2850
            $this->webDriver->getPageSource(),
2851
            new PageConstraint($needle, $this->_getCurrentUri()),
2852
            $message
2853
        );
2854
    }
2855
2856
    /**
2857
     * Append the given text to the given element.
2858
     * Can also add a selection to a select box.
2859
     *
2860
     * ``` php
2861
     * <?php
2862
     * $I->appendField('#mySelectbox', 'SelectValue');
2863
     * $I->appendField('#myTextField', 'appended');
2864
     * ?>
2865
     * ```
2866
     *
2867
     * @param string $field
2868
     * @param string $value
2869
     * @throws \Codeception\Exception\ElementNotFound
2870
     */
2871
    public function appendField($field, $value)
2872
    {
2873
        $el = $this->findField($field);
2874
2875
        switch ($el->getTagName()) {
2876
            //Multiple select
2877
            case "select":
2878
                $matched = false;
2879
                $wdSelect = new WebDriverSelect($el);
2880
                try {
2881
                    $wdSelect->selectByVisibleText($value);
2882
                    $matched = true;
2883
                } catch (NoSuchElementException $e) {
2884
                    // exception treated at the end
2885
                }
2886
2887
                try {
2888
                    $wdSelect->selectByValue($value);
2889
                    $matched = true;
2890
                } catch (NoSuchElementException $e) {
2891
                    // exception treated at the end
2892
                }
2893
                if ($matched) {
2894
                    return;
2895
                }
2896
2897
                throw new ElementNotFound(json_encode($value), "Option inside $field matched by name or value");
2898
            case "textarea":
2899
                $el->sendKeys($value);
2900
                return;
2901
            case "div": //allows for content editable divs
2902
                $el->sendKeys(WebDriverKeys::END);
2903
                $el->sendKeys($value);
2904
                return;
2905
            //Text, Checkbox, Radio
2906
            case "input":
2907
                $type = $el->getAttribute('type');
2908
2909
                if ($type == 'checkbox') {
2910
                    //Find by value or css,id,xpath
2911
                    $field = $this->findCheckable($this->baseElement, $value, true);
2912
                    if (!$field) {
2913
                        throw new ElementNotFound($value, "Checkbox or Radio by Label or CSS or XPath");
2914
                    }
2915
                    if ($field->isSelected()) {
2916
                        return;
2917
                    }
2918
                    $field->click();
2919
                    return;
2920
                } elseif ($type == 'radio') {
2921
                    $this->selectOption($field, $value);
2922
                    return;
2923
                } else {
2924
                    $el->sendKeys($value);
2925
                    return;
2926
                }
2927
        }
2928
2929
        throw new ElementNotFound($field, "Field by name, label, CSS or XPath");
2930
    }
2931
2932
    /**
2933
     * @param $selector
2934
     * @return array
2935
     */
2936
    protected function matchVisible($selector)
2937
    {
2938
        $els = $this->match($this->baseElement, $selector);
2939
        $nodes = array_filter(
2940
            $els,
2941
            function (WebDriverElement $el) {
2942
                return $el->isDisplayed();
2943
            }
2944
        );
2945
        return $nodes;
2946
    }
2947
2948
    /**
2949
     * @param $selector
2950
     * @return WebDriverBy
2951
     * @throws \InvalidArgumentException
2952
     */
2953
    protected function getLocator($selector)
2954
    {
2955
        if ($selector instanceof WebDriverBy) {
2956
            return $selector;
2957
        }
2958
        if (is_array($selector)) {
2959
            return $this->getStrictLocator($selector);
2960
        }
2961
        if (Locator::isID($selector)) {
2962
            return WebDriverBy::id(substr($selector, 1));
2963
        }
2964
        if (Locator::isCSS($selector)) {
2965
            return WebDriverBy::cssSelector($selector);
2966
        }
2967
        if (Locator::isXPath($selector)) {
2968
            return WebDriverBy::xpath($selector);
2969
        }
2970
        throw new \InvalidArgumentException("Only CSS or XPath allowed");
2971
    }
2972
2973
    /**
2974
     * @param string $name
2975
     */
2976
    public function saveSessionSnapshot($name)
2977
    {
2978
        $this->sessionSnapshots[$name] = [];
2979
2980
        foreach ($this->webDriver->manage()->getCookies() as $cookie) {
2981
            if (in_array(trim($cookie['name']), [LocalServer::COVERAGE_COOKIE, LocalServer::COVERAGE_COOKIE_ERROR])) {
2982
                continue;
2983
            }
2984
2985
            if ($this->cookieDomainMatchesConfigUrl($cookie)) {
2986
                $this->sessionSnapshots[$name][] = $cookie;
2987
            }
2988
        }
2989
2990
        $this->debugSection('Snapshot', "Saved \"$name\" session snapshot");
2991
    }
2992
2993
    /**
2994
     * @param string $name
2995
     * @return bool
2996
     */
2997
    public function loadSessionSnapshot($name)
2998
    {
2999
        if (!isset($this->sessionSnapshots[$name])) {
3000
            return false;
3001
        }
3002
        $this->webDriver->manage()->deleteAllCookies();
3003
        foreach ($this->sessionSnapshots[$name] as $cookie) {
3004
            $this->webDriver->manage()->addCookie($cookie);
3005
        }
3006
        $this->debugSection('Snapshot', "Restored \"$name\" session snapshot");
3007
        return true;
3008
    }
3009
3010
    /**
3011
     * Check if the cookie domain matches the config URL.
3012
     *
3013
     * @param array|Cookie $cookie
3014
     * @return bool
3015
     */
3016
    private function cookieDomainMatchesConfigUrl($cookie)
3017
    {
3018
        if (!array_key_exists('domain', $cookie)) {
3019
            return true;
3020
        }
3021
3022
        $setCookie = new SetCookie();
3023
        $setCookie->setDomain($cookie['domain']);
3024
3025
        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...
3026
    }
3027
3028
    /**
3029
     * @return bool
3030
     */
3031
    protected function isPhantom()
3032
    {
3033
        return strpos($this->config['browser'], 'phantom') === 0;
3034
    }
3035
3036
    /**
3037
     * Move to the middle of the given element matched by the given locator.
3038
     * Extra shift, calculated from the top-left corner of the element,
3039
     * can be set by passing $offsetX and $offsetY parameters.
3040
     *
3041
     * ``` php
3042
     * <?php
3043
     * $I->scrollTo(['css' => '.checkout'], 20, 50);
3044
     * ?>
3045
     * ```
3046
     *
3047
     * @param $selector
3048
     * @param int $offsetX
3049
     * @param int $offsetY
3050
     */
3051
    public function scrollTo($selector, $offsetX = null, $offsetY = null)
3052
    {
3053
        $el = $this->matchFirstOrFail($this->baseElement, $selector);
3054
        $x = $el->getLocation()->getX() + $offsetX;
3055
        $y = $el->getLocation()->getY() + $offsetY;
3056
        $this->webDriver->executeScript("window.scrollTo($x, $y)");
3057
    }
3058
3059
    /**
3060
     * Opens a new browser tab (wherever it is possible) and switches to it.
3061
     *
3062
     * ```php
3063
     * <?php
3064
     * $I->openNewTab();
3065
     * ```
3066
     * Tab is opened by using `window.open` javascript in a browser.
3067
     * Please note, that adblock can restrict creating such tabs.
3068
     *
3069
     * Can't be used with PhantomJS
3070
     *
3071
     */
3072
    public function openNewTab()
3073
    {
3074
        $this->executeJS("window.open('about:blank','_blank');");
3075
        $this->switchToNextTab();
3076
    }
3077
3078
    /**
3079
     * Closes current browser tab and switches to previous active tab.
3080
     *
3081
     * ```php
3082
     * <?php
3083
     * $I->closeTab();
3084
     * ```
3085
     *
3086
     * Can't be used with PhantomJS
3087
     */
3088
    public function closeTab()
3089
    {
3090
        $prevTab = $this->getRelativeTabHandle(-1);
3091
        $this->webDriver->close();
3092
        $this->webDriver->switchTo()->window($prevTab);
3093
    }
3094
3095
    /**
3096
     * Switches to next browser tab.
3097
     * An offset can be specified.
3098
     *
3099
     * ```php
3100
     * <?php
3101
     * // switch to next tab
3102
     * $I->switchToNextTab();
3103
     * // switch to 2nd next tab
3104
     * $I->switchToNextTab(2);
3105
     * ```
3106
     *
3107
     * Can't be used with PhantomJS
3108
     *
3109
     * @param int $offset 1
3110
     */
3111
    public function switchToNextTab($offset = 1)
3112
    {
3113
        $tab = $this->getRelativeTabHandle($offset);
3114
        $this->webDriver->switchTo()->window($tab);
3115
    }
3116
3117
    /**
3118
     * Switches to previous browser tab.
3119
     * An offset can be specified.
3120
     *
3121
     * ```php
3122
     * <?php
3123
     * // switch to previous tab
3124
     * $I->switchToPreviousTab();
3125
     * // switch to 2nd previous tab
3126
     * $I->switchToPreviousTab(2);
3127
     * ```
3128
     *
3129
     * Can't be used with PhantomJS
3130
     *
3131
     * @param int $offset 1
3132
     */
3133
    public function switchToPreviousTab($offset = 1)
3134
    {
3135
        $this->switchToNextTab(0 - $offset);
3136
    }
3137
3138
    protected function getRelativeTabHandle($offset)
3139
    {
3140
        if ($this->isPhantom()) {
3141
            throw new ModuleException($this, "PhantomJS doesn't support tab actions");
3142
        }
3143
        $handle = $this->webDriver->getWindowHandle();
3144
        $handles = $this->webDriver->getWindowHandles();
3145
        $idx = array_search($handle, $handles);
3146
        return $handles[($idx + $offset) % count($handles)];
3147
    }
3148
3149
    /**
3150
     * Waits for element and runs a sequence of actions inside its context.
3151
     * Actions can be defined with array, callback, or `Codeception\Util\ActionSequence` instance.
3152
     *
3153
     * Actions as array are recommended for simple to combine "waitForElement" with assertions;
3154
     * `waitForElement($el)` and `see('text', $el)` can be simplified to:
3155
     *
3156
     * ```php
3157
     * <?php
3158
     * $I->performOn($el, ['see' => 'text']);
3159
     * ```
3160
     *
3161
     * List of actions can be pragmatically build using `Codeception\Util\ActionSequence`:
3162
     *
3163
     * ```php
3164
     * <?php
3165
     * $I->performOn('.model', ActionSequence::build()
3166
     *     ->see('Warning')
3167
     *     ->see('Are you sure you want to delete this?')
3168
     *     ->click('Yes')
3169
     * );
3170
     * ```
3171
     *
3172
     * Actions executed from array or ActionSequence will print debug output for actions, and adds an action name to
3173
     * exception on failure.
3174
     *
3175
     * Whenever you need to define more actions a callback can be used. A WebDriver module is passed for argument:
3176
     *
3177
     * ```php
3178
     * <?php
3179
     * $I->performOn('.rememberMe', function (WebDriver $I) {
3180
     *      $I->see('Remember me next time');
3181
     *      $I->seeElement('#LoginForm_rememberMe');
3182
     *      $I->dontSee('Login');
3183
     * });
3184
     * ```
3185
     *
3186
     * In 3rd argument you can set number a seconds to wait for element to appear
3187
     *
3188
     * @param $element
3189
     * @param $actions
3190
     * @param int $timeout
3191
     */
3192
    public function performOn($element, $actions, $timeout = 10)
3193
    {
3194
        $this->waitForElement($element, $timeout);
3195
        $this->setBaseElement($element);
3196
        $this->debugSection('InnerText', $this->baseElement->getText());
3197
3198
        if (is_callable($actions)) {
3199
            $actions($this);
3200
            $this->setBaseElement();
3201
            return;
3202
        }
3203
        if (is_array($actions)) {
3204
            $actions = ActionSequence::build()->fromArray($actions);
3205
        }
3206
3207
        if (!$actions instanceof ActionSequence) {
3208
            throw new \InvalidArgumentException("2nd parameter, actions should be callback, ActionSequence or array");
3209
        }
3210
3211
        $actions->run($this);
3212
        $this->setBaseElement();
3213
    }
3214
3215
    protected function setBaseElement($element = null)
3216
    {
3217
        if ($element === null) {
3218
            $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...
3219
            return;
3220
        }
3221
        $this->baseElement = $this->matchFirstOrFail($this->webDriver, $element);
3222
    }
3223
3224
    protected function enableImplicitWait()
3225
    {
3226
        if (!$this->config['wait']) {
3227
            return;
3228
        }
3229
        $this->webDriver->manage()->timeouts()->implicitlyWait($this->config['wait']);
3230
    }
3231
3232
    protected function disableImplicitWait()
3233
    {
3234
        if (!$this->config['wait']) {
3235
            return;
3236
        }
3237
        $this->webDriver->manage()->timeouts()->implicitlyWait(0);
3238
    }
3239
}
3240