├── LICENSE ├── composer.json ├── phpcs.xml └── src ├── Constraint └── Page.php ├── Exception ├── ElementNotFound.php └── MalformedLocatorException.php ├── Lib └── Interfaces │ ├── ElementLocator.php │ ├── MultiSession.php │ ├── PageSourceSaver.php │ ├── Remote.php │ ├── ScreenshotSaver.php │ ├── SessionSnapshot.php │ └── Web.php └── Util ├── Locator.php └── Uri.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 Michael Bodnarchuk and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeception/lib-web", 3 | "description": "Library containing files used by module-webdriver and lib-innerbrowser or module-phpbrowser", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "codeception" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Gintautas Miselis" 12 | } 13 | ], 14 | "homepage": "https://codeception.com/", 15 | "require": { 16 | "php": "^8.1", 17 | "ext-mbstring": "*", 18 | "guzzlehttp/psr7": "^2.0", 19 | "phpunit/phpunit": "^9.5 | ^10.0 | ^11.0 | ^12", 20 | "symfony/css-selector": ">=4.4.24 <8.0" 21 | }, 22 | "require-dev": { 23 | "php-webdriver/webdriver": "^1.12" 24 | }, 25 | "conflict": { 26 | "codeception/codeception": "<5.0.0-alpha3" 27 | }, 28 | "autoload": { 29 | "classmap": [ 30 | "src/" 31 | ] 32 | }, 33 | "config": { 34 | "classmap-authoritative": true, 35 | "sort-packages": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Codeception code standard 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Constraint/Page.php: -------------------------------------------------------------------------------- 1 | string = $this->normalizeText($string); 26 | $this->uri = $uri; 27 | } 28 | 29 | /** 30 | * Evaluates the constraint for parameter $other. Returns true if the 31 | * constraint is met, false otherwise. 32 | * 33 | * @param string $other Value or object to evaluate. 34 | * @return bool 35 | */ 36 | protected function matches($other): bool 37 | { 38 | $other = $this->normalizeText($other); 39 | return mb_stripos($other, $this->string, 0, 'UTF-8') !== false; 40 | } 41 | 42 | private function normalizeText(string $text): string 43 | { 44 | $text = strtr($text, "\r\n", " "); 45 | return trim(preg_replace('/\\s{2,}/', ' ', $text)); 46 | } 47 | 48 | /** 49 | * Returns a string representation of the constraint. 50 | */ 51 | public function toString(): string 52 | { 53 | return sprintf( 54 | 'contains "%s"', 55 | $this->string 56 | ); 57 | } 58 | 59 | /** 60 | * @param string $pageContent 61 | */ 62 | protected function failureDescription($pageContent): string 63 | { 64 | $message = $this->uriMessage('on page'); 65 | $message .= "\n--> "; 66 | $message .= mb_substr($pageContent, 0, 300, 'utf-8'); 67 | if (mb_strlen($pageContent, 'utf-8') > 300 && function_exists('codecept_output_dir')) { 68 | $message .= "\n[Content too long to display. See complete response in '" 69 | . codecept_output_dir() . "' directory]"; 70 | } 71 | 72 | return $message . "\n--> " . $this->toString(); 73 | } 74 | 75 | protected function uriMessage(string $onPage = ''): string 76 | { 77 | if (!$this->uri) { 78 | return ''; 79 | } 80 | return "{$onPage} {$this->uri}"; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Exception/ElementNotFound.php: -------------------------------------------------------------------------------- 1 | getModule('{{MODULE_NAME}}')->_findElements('.items'); 19 | * $els = $this->getModule('{{MODULE_NAME}}')->_findElements(['name' => 'username']); 20 | * 21 | * $editLinks = $this->getModule('{{MODULE_NAME}}')->_findElements(['link' => 'Edit']); 22 | * // now you can iterate over $editLinks and check that all them have valid hrefs 23 | * ``` 24 | * 25 | * WebDriver module returns `Facebook\WebDriver\Remote\RemoteWebElement` instances 26 | * PhpBrowser and Framework modules return `Symfony\Component\DomCrawler\Crawler` instances 27 | * 28 | * @api 29 | */ 30 | public function _findElements(mixed $locator): iterable; 31 | } 32 | -------------------------------------------------------------------------------- /src/Lib/Interfaces/MultiSession.php: -------------------------------------------------------------------------------- 1 | getModule('{{MODULE_NAME}}')->_savePageSource(codecept_output_dir().'page.html'); 14 | * ``` 15 | * @api 16 | */ 17 | public function _savePageSource(string $filename): void; 18 | 19 | /** 20 | * Use this method within an [interactive pause](https://codeception.com/docs/02-GettingStarted#Interactive-Pause) to save the HTML source code of the current page. 21 | * 22 | * ```php 23 | * makeHtmlSnapshot('edit_page'); 25 | * // saved to: tests/_output/debug/edit_page.html 26 | * $I->makeHtmlSnapshot(); 27 | * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.html 28 | * ``` 29 | */ 30 | public function makeHtmlSnapshot(?string $name = null): void; 31 | } 32 | -------------------------------------------------------------------------------- /src/Lib/Interfaces/Remote.php: -------------------------------------------------------------------------------- 1 | amOnSubdomain('user'); 18 | * $I->amOnPage('/'); 19 | * // moves to https://user.mysite.com/ 20 | * ``` 21 | * 22 | */ 23 | public function amOnSubdomain(string $subdomain): void; 24 | 25 | /** 26 | * Open web page at the given absolute URL and sets its hostname as the base host. 27 | * 28 | * ``` php 29 | * amOnUrl('https://codeception.com'); 31 | * $I->amOnPage('/quickstart'); // moves to https://codeception.com/quickstart 32 | * ``` 33 | */ 34 | public function amOnUrl(string $url): void; 35 | 36 | public function _getUrl(); 37 | } 38 | -------------------------------------------------------------------------------- /src/Lib/Interfaces/ScreenshotSaver.php: -------------------------------------------------------------------------------- 1 | getModule('{{MODULE_NAME}}')->_saveScreenshot(codecept_output_dir().'screenshot_1.png'); 12 | * ``` 13 | * @api 14 | */ 15 | public function _saveScreenshot(string $filename); 16 | } 17 | -------------------------------------------------------------------------------- /src/Lib/Interfaces/SessionSnapshot.php: -------------------------------------------------------------------------------- 1 | loadSessionSnapshot('login')) return; 21 | * 22 | * // logging in 23 | * $I->amOnPage('/login'); 24 | * $I->fillField('name', 'jon'); 25 | * $I->fillField('password', '123345'); 26 | * $I->click('Login'); 27 | * 28 | * // saving snapshot 29 | * $I->saveSessionSnapshot('login'); 30 | * } 31 | * ``` 32 | * 33 | * @return mixed 34 | */ 35 | public function saveSessionSnapshot(string $name); 36 | 37 | /** 38 | * Loads cookies from a saved snapshot. 39 | * Allows to reuse same session across tests without additional login. 40 | * 41 | * See [saveSessionSnapshot](#saveSessionSnapshot) 42 | * 43 | * @return mixed 44 | */ 45 | public function loadSessionSnapshot(string $name); 46 | 47 | /** 48 | * Deletes session snapshot. 49 | * 50 | * See [saveSessionSnapshot](#saveSessionSnapshot) 51 | * 52 | * @return mixed 53 | */ 54 | public function deleteSessionSnapshot(string $name); 55 | } 56 | -------------------------------------------------------------------------------- /src/Lib/Interfaces/Web.php: -------------------------------------------------------------------------------- 1 | amOnPage('/'); 16 | * // opens /register page 17 | * $I->amOnPage('/register'); 18 | * ``` 19 | */ 20 | public function amOnPage(string $page): void; 21 | 22 | /** 23 | * Checks that the current page contains the given string (case insensitive). 24 | * 25 | * You can specify a specific HTML element (via CSS or XPath) as the second 26 | * parameter to only search within that element. 27 | * 28 | * ```php 29 | * see('Logout'); // I can suppose user is logged in 31 | * $I->see('Sign Up', 'h1'); // I can suppose it's a signup page 32 | * $I->see('Sign Up', '//body/h1'); // with XPath 33 | * $I->see('Sign Up', ['css' => 'body h1']); // with strict CSS locator 34 | * ``` 35 | * 36 | * Note that the search is done after stripping all HTML tags from the body, 37 | * so `$I->see('strong')` will return true for strings like: 38 | * 39 | * - `

I am Stronger than thou

` 40 | * - `` 41 | * 42 | * But will *not* be true for strings like: 43 | * 44 | * - `Home` 45 | * - `
Home` 46 | * - `` 47 | * 48 | * For checking the raw source code, use `seeInSource()`. 49 | * 50 | * @param array|string $selector optional 51 | */ 52 | public function see(string $text, $selector = null): void; 53 | 54 | /** 55 | * Checks that the current page doesn't contain the text specified (case insensitive). 56 | * Give a locator as the second parameter to match a specific region. 57 | * 58 | * ```php 59 | * dontSee('Login'); // I can suppose user is already logged in 61 | * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page 62 | * $I->dontSee('Sign Up','//body/h1'); // with XPath 63 | * $I->dontSee('Sign Up', ['css' => 'body h1']); // with strict CSS locator 64 | * ``` 65 | * 66 | * Note that the search is done after stripping all HTML tags from the body, 67 | * so `$I->dontSee('strong')` will fail on strings like: 68 | * 69 | * - `

I am Stronger than thou

` 70 | * - `` 71 | * 72 | * But will ignore strings like: 73 | * 74 | * - `Home` 75 | * - `
Home` 76 | * - `` 77 | * 78 | * For checking the raw source code, use `seeInSource()`. 79 | * 80 | * @param array|string $selector optional 81 | */ 82 | public function dontSee(string $text, $selector = null): void; 83 | 84 | /** 85 | * Checks that the current page contains the given string in its 86 | * raw source code. 87 | * 88 | * ```php 89 | * seeInSource('

Green eggs & ham

'); 91 | * ``` 92 | */ 93 | public function seeInSource(string $raw): void; 94 | 95 | /** 96 | * Checks that the current page contains the given string in its 97 | * raw source code. 98 | * 99 | * ```php 100 | * dontSeeInSource('

Green eggs & ham

'); 102 | * ``` 103 | */ 104 | public function dontSeeInSource(string $raw): void; 105 | 106 | /** 107 | * Submits the given form on the page, with the given form 108 | * values. Pass the form field's values as an array in the second 109 | * parameter. 110 | * 111 | * Although this function can be used as a short-hand version of 112 | * `fillField()`, `selectOption()`, `click()` etc. it has some important 113 | * differences: 114 | * 115 | * * Only field *names* may be used, not CSS/XPath selectors nor field labels 116 | * * If a field is sent to this function that does *not* exist on the page, 117 | * it will silently be added to the HTTP request. This is helpful for testing 118 | * some types of forms, but be aware that you will *not* get an exception 119 | * like you would if you called `fillField()` or `selectOption()` with 120 | * a missing field. 121 | * 122 | * Fields that are not provided will be filled by their values from the page, 123 | * or from any previous calls to `fillField()`, `selectOption()` etc. 124 | * You don't need to click the 'Submit' button afterwards. 125 | * This command itself triggers the request to form's action. 126 | * 127 | * You can optionally specify which button's value to include 128 | * in the request with the last parameter (as an alternative to 129 | * explicitly setting its value in the second parameter), as 130 | * button values are not otherwise included in the request. 131 | * 132 | * Examples: 133 | * 134 | * ```php 135 | * submitForm('#login', [ 137 | * 'login' => 'davert', 138 | * 'password' => '123456' 139 | * ]); 140 | * // or 141 | * $I->submitForm('#login', [ 142 | * 'login' => 'davert', 143 | * 'password' => '123456' 144 | * ], 'submitButtonName'); 145 | * 146 | * ``` 147 | * 148 | * For example, given this sample "Sign Up" form: 149 | * 150 | * ``` html 151 | *
152 | * Login: 153 | *
154 | * Password: 155 | *
156 | * Do you agree to our terms? 157 | *
158 | * Subscribe to our newsletter? 159 | *
160 | * Select pricing plan: 161 | * 165 | * 166 | *
167 | * ``` 168 | * 169 | * You could write the following to submit it: 170 | * 171 | * ```php 172 | * submitForm( 174 | * '#userForm', 175 | * [ 176 | * 'user' => [ 177 | * 'login' => 'Davert', 178 | * 'password' => '123456', 179 | * 'agree' => true 180 | * ] 181 | * ], 182 | * 'submitButton' 183 | * ); 184 | * ``` 185 | * Note that "2" will be the submitted value for the "plan" field, as it is 186 | * the selected option. 187 | * 188 | * To uncheck the pre-checked checkbox "newsletter", call `$I->uncheckOption(['name' => 'user[newsletter]']);` *before*, 189 | * then submit the form as shown here (i.e. without the "newsletter" field in the `$params` array). 190 | * 191 | * You can also emulate a JavaScript submission by not specifying any 192 | * buttons in the third parameter to submitForm. 193 | * 194 | * ```php 195 | * submitForm( 197 | * '#userForm', 198 | * [ 199 | * 'user' => [ 200 | * 'login' => 'Davert', 201 | * 'password' => '123456', 202 | * 'agree' => true 203 | * ] 204 | * ] 205 | * ); 206 | * ``` 207 | * 208 | * This function works well when paired with `seeInFormFields()` 209 | * for quickly testing CRUD interfaces and form validation logic. 210 | * 211 | * ```php 212 | * 'value', 215 | * 'field2' => 'another value', 216 | * 'checkbox1' => true, 217 | * // ... 218 | * ]; 219 | * $I->submitForm('#my-form', $form, 'submitButton'); 220 | * // $I->amOnPage('/path/to/form-page') may be needed 221 | * $I->seeInFormFields('#my-form', $form); 222 | * ``` 223 | * 224 | * Parameter values can be set to arrays for multiple input fields 225 | * of the same name, or multi-select combo boxes. For checkboxes, 226 | * you can use either the string value or boolean `true`/`false` which will 227 | * be replaced by the checkbox's value in the DOM. 228 | * 229 | * ```php 230 | * submitForm('#my-form', [ 232 | * 'field1' => 'value', 233 | * 'checkbox' => [ 234 | * 'value of first checkbox', 235 | * 'value of second checkbox', 236 | * ], 237 | * 'otherCheckboxes' => [ 238 | * true, 239 | * false, 240 | * false 241 | * ], 242 | * 'multiselect' => [ 243 | * 'first option value', 244 | * 'second option value' 245 | * ] 246 | * ]); 247 | * ``` 248 | * 249 | * Mixing string and boolean values for a checkbox's value is not supported 250 | * and may produce unexpected results. 251 | * 252 | * Field names ending in `[]` must be passed without the trailing square 253 | * bracket characters, and must contain an array for its value. This allows 254 | * submitting multiple values with the same name, consider: 255 | * 256 | * ```php 257 | * submitForm('#my-form', [ 260 | * 'field[]' => 'value', 261 | * 'field[]' => 'another value', // 'field[]' is already a defined key 262 | * ]); 263 | * ``` 264 | * 265 | * The solution is to pass an array value: 266 | * 267 | * ```php 268 | * submitForm('#my-form', [ 271 | * 'field' => [ 272 | * 'value', 273 | * 'another value', 274 | * ] 275 | * ]); 276 | * ``` 277 | */ 278 | public function submitForm($selector, array $params, ?string $button = null): void; 279 | 280 | /** 281 | * Perform a click on a link or a button, given by a locator. 282 | * If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string. 283 | * For buttons, the "value" attribute, "name" attribute, and inner text are searched. 284 | * For links, the link text is searched. 285 | * For images, the "alt" attribute and inner text of any parent links are searched. 286 | * 287 | * The second parameter is a context (CSS or XPath locator) to narrow the search. 288 | * 289 | * Note that if the locator matches a button of type `submit`, the form will be submitted. 290 | * 291 | * ```php 292 | * click('Logout'); 295 | * // button of form 296 | * $I->click('Submit'); 297 | * // CSS button 298 | * $I->click('#form input[type=submit]'); 299 | * // XPath 300 | * $I->click('//form/*[@type="submit"]'); 301 | * // link in context 302 | * $I->click('Logout', '#nav'); 303 | * // using strict locator 304 | * $I->click(['link' => 'Login']); 305 | * ``` 306 | * @param string|array $link 307 | */ 308 | public function click($link, $context = null): void; 309 | 310 | /** 311 | * Checks that there's a link with the specified text. 312 | * Give a full URL as the second parameter to match links with that exact URL. 313 | * 314 | * ```php 315 | * seeLink('Logout'); // matches Logout 317 | * $I->seeLink('Logout','/logout'); // matches Logout 318 | * ``` 319 | */ 320 | public function seeLink(string $text, ?string $url = null): void; 321 | 322 | /** 323 | * Checks that the page doesn't contain a link with the given string. 324 | * If the second parameter is given, only links with a matching "href" attribute will be checked. 325 | * 326 | * ```php 327 | * dontSeeLink('Logout'); // I suppose user is not logged in 329 | * $I->dontSeeLink('Checkout now', '/store/cart.php'); 330 | * ``` 331 | */ 332 | public function dontSeeLink(string $text, string $url = ''): void; 333 | 334 | /** 335 | * Checks that current URI contains the given string. 336 | * 337 | * ```php 338 | * seeInCurrentUrl('home'); 341 | * // to match: /users/1 342 | * $I->seeInCurrentUrl('/users/'); 343 | * ``` 344 | */ 345 | public function seeInCurrentUrl(string $uri): void; 346 | 347 | /** 348 | * Checks that the current URL is equal to the given string. 349 | * Unlike `seeInCurrentUrl`, this only matches the full URL. 350 | * 351 | * ```php 352 | * seeCurrentUrlEquals('/'); 355 | * ``` 356 | */ 357 | public function seeCurrentUrlEquals(string $uri): void; 358 | 359 | /** 360 | * Checks that the current URL matches the given regular expression. 361 | * 362 | * ```php 363 | * seeCurrentUrlMatches('~^/users/(\d+)~'); 366 | * ``` 367 | */ 368 | public function seeCurrentUrlMatches(string $uri): void; 369 | 370 | /** 371 | * Checks that the current URI doesn't contain the given string. 372 | * 373 | * ```php 374 | * dontSeeInCurrentUrl('/users/'); 376 | * ``` 377 | */ 378 | public function dontSeeInCurrentUrl(string $uri): void; 379 | 380 | /** 381 | * Checks that the current URL doesn't equal the given string. 382 | * Unlike `dontSeeInCurrentUrl`, this only matches the full URL. 383 | * 384 | * ```php 385 | * dontSeeCurrentUrlEquals('/'); 388 | * ``` 389 | */ 390 | public function dontSeeCurrentUrlEquals(string $uri): void; 391 | 392 | /** 393 | * Checks that current url doesn't match the given regular expression. 394 | * 395 | * ```php 396 | * dontSeeCurrentUrlMatches('~^/users/(\d+)~'); 399 | * ``` 400 | */ 401 | public function dontSeeCurrentUrlMatches(string $uri): void; 402 | 403 | /** 404 | * Executes the given regular expression against the current URI and returns the first capturing group. 405 | * If no parameters are provided, the full URI is returned. 406 | * 407 | * ```php 408 | * grabFromCurrentUrl('~^/user/(\d+)/~'); 410 | * $uri = $I->grabFromCurrentUrl(); 411 | * ``` 412 | */ 413 | public function grabFromCurrentUrl(?string $uri = null): mixed; 414 | 415 | /** 416 | * Checks that the specified checkbox is checked. 417 | * 418 | * ```php 419 | * seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms 421 | * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. 422 | * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); 423 | * ``` 424 | */ 425 | public function seeCheckboxIsChecked($checkbox): void; 426 | 427 | /** 428 | * Check that the specified checkbox is unchecked. 429 | * 430 | * ```php 431 | * dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms 433 | * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. 434 | * ``` 435 | */ 436 | public function dontSeeCheckboxIsChecked($checkbox): void; 437 | 438 | /** 439 | * Checks that the given input field or textarea *equals* (i.e. not just contains) the given value. 440 | * Fields are matched by label text, the "name" attribute, CSS, or XPath. 441 | * 442 | * ```php 443 | * seeInField('Body','Type your comment here'); 445 | * $I->seeInField('form textarea[name=body]','Type your comment here'); 446 | * $I->seeInField('form input[type=hidden]','hidden_value'); 447 | * $I->seeInField('#searchform input','Search'); 448 | * $I->seeInField('//form/*[@name=search]','Search'); 449 | * $I->seeInField(['name' => 'search'], 'Search'); 450 | * ``` 451 | * 452 | * @param string|array $field 453 | */ 454 | public function seeInField($field, $value): void; 455 | 456 | /** 457 | * Checks that an input field or textarea doesn't contain the given value. 458 | * For fuzzy locators, the field is matched by label text, CSS and XPath. 459 | * 460 | * ```php 461 | * dontSeeInField('Body','Type your comment here'); 463 | * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); 464 | * $I->dontSeeInField('form input[type=hidden]','hidden_value'); 465 | * $I->dontSeeInField('#searchform input','Search'); 466 | * $I->dontSeeInField('//form/*[@name=search]','Search'); 467 | * $I->dontSeeInField(['name' => 'search'], 'Search'); 468 | * ``` 469 | * @param string|array $field 470 | */ 471 | public function dontSeeInField($field, $value): void; 472 | 473 | /** 474 | * Checks if the array of form parameters (name => value) are set on the form matched with the 475 | * passed selector. 476 | * 477 | * ```php 478 | * seeInFormFields('form[name=myform]', [ 480 | * 'input1' => 'value', 481 | * 'input2' => 'other value', 482 | * ]); 483 | * ``` 484 | * 485 | * For multi-select elements, or to check values of multiple elements with the same name, an 486 | * array may be passed: 487 | * 488 | * ```php 489 | * seeInFormFields('.form-class', [ 491 | * 'multiselect' => [ 492 | * 'value1', 493 | * 'value2', 494 | * ], 495 | * 'checkbox[]' => [ 496 | * 'a checked value', 497 | * 'another checked value', 498 | * ], 499 | * ]); 500 | * ``` 501 | * 502 | * Additionally, checkbox values can be checked with a boolean. 503 | * 504 | * ```php 505 | * seeInFormFields('#form-id', [ 507 | * 'checkbox1' => true, // passes if checked 508 | * 'checkbox2' => false, // passes if unchecked 509 | * ]); 510 | * ``` 511 | * 512 | * Pair this with submitForm for quick testing magic. 513 | * 514 | * ```php 515 | * 'value', 518 | * 'field2' => 'another value', 519 | * 'checkbox1' => true, 520 | * // ... 521 | * ]; 522 | * $I->submitForm('//form[@id=my-form]', string $form, 'submitButton'); 523 | * // $I->amOnPage('/path/to/form-page') may be needed 524 | * $I->seeInFormFields('//form[@id=my-form]', string $form); 525 | * ``` 526 | */ 527 | public function seeInFormFields($formSelector, array $params): void; 528 | 529 | /** 530 | * Checks if the array of form parameters (name => value) are not set on the form matched with 531 | * the passed selector. 532 | * 533 | * ```php 534 | * dontSeeInFormFields('form[name=myform]', [ 536 | * 'input1' => 'non-existent value', 537 | * 'input2' => 'other non-existent value', 538 | * ]); 539 | * ``` 540 | * 541 | * To check that an element hasn't been assigned any one of many values, an array can be passed 542 | * as the value: 543 | * 544 | * ```php 545 | * dontSeeInFormFields('.form-class', [ 547 | * 'fieldName' => [ 548 | * 'This value shouldn\'t be set', 549 | * 'And this value shouldn\'t be set', 550 | * ], 551 | * ]); 552 | * ``` 553 | * 554 | * Additionally, checkbox values can be checked with a boolean. 555 | * 556 | * ```php 557 | * dontSeeInFormFields('#form-id', [ 559 | * 'checkbox1' => true, // fails if checked 560 | * 'checkbox2' => false, // fails if unchecked 561 | * ]); 562 | * ``` 563 | */ 564 | public function dontSeeInFormFields($formSelector, array $params): void; 565 | 566 | /** 567 | * Selects an option in a select tag or in radio button group. 568 | * 569 | * ```php 570 | * selectOption('form select[name=account]', 'Premium'); 572 | * $I->selectOption('form input[name=payment]', 'Monthly'); 573 | * $I->selectOption('//form/select[@name=account]', 'Monthly'); 574 | * ``` 575 | * 576 | * Provide an array for the second argument to select multiple options: 577 | * 578 | * ```php 579 | * selectOption('Which OS do you use?', ['Windows', 'Linux']); 581 | * ``` 582 | * 583 | * Or provide an associative array for the second argument to specifically define which selection method should be used: 584 | * 585 | * ```php 586 | * selectOption('Which OS do you use?', ['text' => 'Windows']); // Only search by text 'Windows' 588 | * $I->selectOption('Which OS do you use?', ['value' => 'windows']); // Only search by value 'windows' 589 | * ``` 590 | */ 591 | public function selectOption($select, $option): void; 592 | 593 | /** 594 | * Ticks a checkbox. For radio buttons, use the `selectOption` method instead. 595 | * 596 | * ```php 597 | * checkOption('#agree'); 599 | * ``` 600 | */ 601 | public function checkOption($option): void; 602 | 603 | /** 604 | * Unticks a checkbox. 605 | * 606 | * ```php 607 | * uncheckOption('#notify'); 609 | * ``` 610 | */ 611 | public function uncheckOption($option): void; 612 | 613 | /** 614 | * Fills a text field or textarea with the given string. 615 | * 616 | * ```php 617 | * fillField("//input[@type='text']", "Hello World!"); 619 | * $I->fillField(['name' => 'email'], 'jon@example.com'); 620 | * ``` 621 | */ 622 | public function fillField($field, $value): void; 623 | 624 | /** 625 | * Attaches a file relative to the Codeception `_data` directory to the given file upload field. 626 | * 627 | * ```php 628 | * attachFile('input[@type="file"]', 'prices.xls'); 631 | * ``` 632 | */ 633 | public function attachFile($field, string $filename): void; 634 | 635 | /** 636 | * Finds and returns the text contents of the given element. 637 | * If a fuzzy locator is used, the element is found using CSS, XPath, 638 | * and by matching the full page source by regular expression. 639 | * 640 | * ```php 641 | * grabTextFrom('h1'); 643 | * $heading = $I->grabTextFrom('descendant-or-self::h1'); 644 | * $value = $I->grabTextFrom('~grabValueFrom('Name'); 656 | * $name = $I->grabValueFrom('input[name=username]'); 657 | * $name = $I->grabValueFrom('descendant-or-self::form/descendant::input[@name = 'username']'); 658 | * $name = $I->grabValueFrom(['name' => 'username']); 659 | * ``` 660 | */ 661 | public function grabValueFrom($field): mixed; 662 | 663 | /** 664 | * Returns the value of the given attribute value from the given HTML element. For some attributes, the string `true` is returned instead of their literal value (e.g. `disabled="disabled"` or `required="required"`). 665 | * Fails if the element is not found. Returns `null` if the attribute is not present on the element. 666 | * 667 | * ```php 668 | * grabAttributeFrom('#tooltip', 'title'); 670 | * ``` 671 | */ 672 | public function grabAttributeFrom($cssOrXpath, string $attribute): mixed; 673 | 674 | /** 675 | * Grabs either the text content, or attribute values, of nodes 676 | * matched by $cssOrXpath and returns them as an array. 677 | * 678 | * ```html 679 | * First 680 | * Second 681 | * Third 682 | * ``` 683 | * 684 | * ```php 685 | * grabMultiple('a'); 688 | * 689 | * // would return ['#first', '#second', '#third'] 690 | * $aLinks = $I->grabMultiple('a', 'href'); 691 | * ``` 692 | * 693 | * @return string[] 694 | */ 695 | public function grabMultiple($cssOrXpath, ?string $attribute = null): array; 696 | 697 | /** 698 | * Checks that the given element exists on the page and is visible. 699 | * You can also specify expected attributes of this element. 700 | * Only works if `` tag is present. 701 | * 702 | * ```php 703 | * seeElement('.error'); 705 | * $I->seeElement('//form/input[1]'); 706 | * $I->seeElement('input', ['name' => 'login']); 707 | * $I->seeElement('input', ['value' => '123456']); 708 | * 709 | * // strict locator in first arg, attributes in second 710 | * $I->seeElement(['css' => 'form input'], ['name' => 'login']); 711 | * ``` 712 | */ 713 | public function seeElement($selector, array $attributes = []): void; 714 | 715 | /** 716 | * Checks that the given element is invisible or not present on the page. 717 | * You can also specify expected attributes of this element. 718 | * 719 | * ```php 720 | * dontSeeElement('.error'); 722 | * $I->dontSeeElement('//form/input[1]'); 723 | * $I->dontSeeElement('input', ['name' => 'login']); 724 | * $I->dontSeeElement('input', ['value' => '123456']); 725 | * ``` 726 | */ 727 | public function dontSeeElement($selector, array $attributes = []): void; 728 | 729 | /** 730 | * Checks that there are a certain number of elements matched by the given locator on the page. 731 | * 732 | * ```php 733 | * seeNumberOfElements('tr', 10); 735 | * $I->seeNumberOfElements('tr', [0,10]); // between 0 and 10 elements 736 | * ``` 737 | * 738 | * @param int|int[] $expected 739 | */ 740 | public function seeNumberOfElements($selector, array|int $expected): void; 741 | 742 | /** 743 | * Checks that the given option is selected. 744 | * 745 | * ```php 746 | * seeOptionIsSelected('#form input[name=payment]', 'Visa'); 748 | * ``` 749 | * 750 | * @return mixed|void 751 | */ 752 | public function seeOptionIsSelected($selector, string $optionText); 753 | 754 | /** 755 | * Checks that the given option is not selected. 756 | * 757 | * ```php 758 | * dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); 760 | * ``` 761 | * 762 | * @return mixed|void 763 | */ 764 | public function dontSeeOptionIsSelected($selector, string $optionText); 765 | 766 | /** 767 | * Checks that the page title contains the given string. 768 | * 769 | * ```php 770 | * seeInTitle('Blog - Post #1'); 772 | * ``` 773 | * 774 | * @return mixed|void 775 | */ 776 | public function seeInTitle(string $title); 777 | 778 | /** 779 | * Checks that the page title does not contain the given string. 780 | * 781 | * @return mixed|void 782 | */ 783 | public function dontSeeInTitle(string $title); 784 | 785 | /** 786 | * Checks that a cookie with the given name is set. 787 | * You can set additional cookie params like `domain`, `path` as array passed in last argument. 788 | * 789 | * ```php 790 | * seeCookie('PHPSESSID'); 792 | * ``` 793 | * 794 | * @return mixed|void 795 | */ 796 | public function seeCookie(string $cookie, array $params = []); 797 | 798 | /** 799 | * Checks that there isn't a cookie with the given name. 800 | * You can set additional cookie params like `domain`, `path` as array passed in last argument. 801 | * 802 | * @return mixed|void 803 | */ 804 | public function dontSeeCookie(string $cookie, array $params = []); 805 | 806 | /** 807 | * Sets a cookie with the given name and value. 808 | * You can set additional cookie params like `domain`, `path`, `expires`, `secure` in array passed as last argument. 809 | * 810 | * ```php 811 | * setCookie('PHPSESSID', 'el4ukv0kqbvoirg7nkp4dncpk3'); 813 | * ``` 814 | * 815 | * @return mixed|void 816 | */ 817 | public function setCookie(string $name, ?string $val, array $params = []); 818 | 819 | /** 820 | * Unsets cookie with the given name. 821 | * You can set additional cookie params like `domain`, `path` in array passed as last argument. 822 | * 823 | * @return mixed|void 824 | */ 825 | public function resetCookie(string $cookie, array $params = []); 826 | 827 | /** 828 | * Grabs a cookie value. 829 | * You can set additional cookie params like `domain`, `path` in array passed as last argument. 830 | * If the cookie is set by an ajax request (XMLHttpRequest), there might be some delay caused by the browser, so try `$I->wait(0.1)`. 831 | */ 832 | public function grabCookie(string $cookie, array $params = []): mixed; 833 | 834 | /** 835 | * Grabs current page source code. 836 | * 837 | * @return string Current page source code. 838 | */ 839 | public function grabPageSource(): string; 840 | } 841 | -------------------------------------------------------------------------------- /src/Util/Locator.php: -------------------------------------------------------------------------------- 1 | see('Title', Locator::combine('h1','h2','h3')); 44 | * ``` 45 | * 46 | * This will search for `Title` text in either `h1`, `h2`, or `h3` tag. 47 | * You can also combine CSS selector with XPath locator: 48 | * 49 | * ```php 50 | * fillField(Locator::combine('form input[type=text]','//form/textarea[2]'), 'qwerty'); 54 | * ``` 55 | * 56 | * As a result the Locator will produce a mixed XPath value that will be used in fillField action. 57 | * 58 | * @static 59 | * @throws Exception 60 | */ 61 | public static function combine(string $selector1, string $selector2): string 62 | { 63 | $selectors = func_get_args(); 64 | foreach ($selectors as $k => $v) { 65 | $selectors[$k] = self::toXPath($v); 66 | if (!$selectors[$k]) { 67 | throw new Exception("{$v} is invalid CSS or XPath"); 68 | } 69 | } 70 | return implode(' | ', $selectors); 71 | } 72 | 73 | /** 74 | * Matches the *a* element with given URL 75 | * 76 | * ```php 77 | * see('Log In', Locator::href('/login.php')); 81 | * ``` 82 | * @static 83 | */ 84 | public static function href(string $url): string 85 | { 86 | return sprintf('//a[@href=normalize-space(%s)]', Translator::getXpathLiteral($url)); 87 | } 88 | 89 | /** 90 | * Matches the element with given tab index 91 | * 92 | * Do you often use the `TAB` key to navigate through the web page? How do your site respond to this navigation? 93 | * You could try to match elements by their tab position using `tabIndex` method of `Locator` class. 94 | * ```php 95 | * fillField(Locator::tabIndex(1), 'davert'); 99 | * $I->fillField(Locator::tabIndex(2) , 'qwerty'); 100 | * $I->click('Login'); 101 | * ``` 102 | * @static 103 | */ 104 | public static function tabIndex(int $index): string 105 | { 106 | return sprintf('//*[@tabindex = normalize-space(%d)]', $index); 107 | } 108 | 109 | /** 110 | * Matches option by text: 111 | * 112 | * ```php 113 | * seeElement(Locator::option('Male'), '#select-gender'); 117 | * ``` 118 | */ 119 | public static function option(string $value): string 120 | { 121 | return sprintf('//option[.=normalize-space("%s")]', $value); 122 | } 123 | 124 | protected static function toXPath(string $selector): ?string 125 | { 126 | try { 127 | return (new CssSelectorConverter())->toXPath($selector); 128 | } catch (ParseException $parseException) { 129 | if (self::isXPath($selector)) { 130 | return $selector; 131 | } 132 | } 133 | return null; 134 | } 135 | 136 | /** 137 | * Finds element by it's attribute(s) 138 | * 139 | * ```php 140 | * seeElement(Locator::find('img', ['title' => 'diagram'])); 144 | * ``` 145 | * @static 146 | */ 147 | public static function find(string $element, array $attributes): string 148 | { 149 | $operands = []; 150 | foreach ($attributes as $attribute => $value) { 151 | if (is_int($attribute)) { 152 | $operands[] = '@' . $value; 153 | } else { 154 | $operands[] = '@' . $attribute . ' = ' . Translator::getXpathLiteral($value); 155 | } 156 | } 157 | return sprintf('//%s[%s]', $element, implode(' and ', $operands)); 158 | } 159 | 160 | /** 161 | * Checks that provided string is CSS selector 162 | * 163 | * ```php 164 | * true 166 | * Locator::isCSS('body') => true 167 | * Locator::isCSS('//body/p/user') => false 168 | * ``` 169 | */ 170 | public static function isCSS(string $selector): bool 171 | { 172 | try { 173 | (new CssSelectorConverter())->toXPath($selector); 174 | } catch (ParseException $e) { 175 | return false; 176 | } 177 | return true; 178 | } 179 | 180 | /** 181 | * Checks that locator is an XPath 182 | * 183 | * ```php 184 | * false 186 | * Locator::isXPath('body') => false 187 | * Locator::isXPath('//body/p/user') => true 188 | * ``` 189 | */ 190 | public static function isXPath(string $locator): bool 191 | { 192 | $domDocument = new DOMDocument('1.0', 'UTF-8'); 193 | $domxPath = new DOMXPath($domDocument); 194 | return @$domxPath->evaluate($locator, $domDocument) !== false; 195 | } 196 | 197 | public static function isPrecise(WebDriverBy|array|string $locator): bool 198 | { 199 | if (is_array($locator)) { 200 | return true; 201 | } 202 | if ($locator instanceof WebDriverBy) { 203 | return true; 204 | } 205 | if (Locator::isID($locator)) { 206 | return true; 207 | } 208 | if (str_starts_with($locator, '//')) { 209 | return true; // simple xpath check 210 | } 211 | return false; 212 | } 213 | 214 | /** 215 | * Checks that a string is valid CSS ID 216 | * 217 | * ```php 218 | * true 220 | * Locator::isID('body') => false 221 | * Locator::isID('//body/p/user') => false 222 | * ``` 223 | */ 224 | public static function isID(string $id): bool 225 | { 226 | return (bool)preg_match('~^#[\w.\-\[\]=^\~:]+$~', $id); 227 | } 228 | 229 | /** 230 | * Checks that a string is valid CSS class 231 | * 232 | * ```php 233 | * true 235 | * Locator::isClass('body') => false 236 | * Locator::isClass('//body/p/user') => false 237 | * ``` 238 | */ 239 | public static function isClass(string $class): bool 240 | { 241 | return (bool)preg_match('#^\.[\w.\-\[\]=^~:]+$#', $class); 242 | } 243 | 244 | /** 245 | * Locates an element containing a text inside. 246 | * Either CSS or XPath locator can be passed, however they will be converted to XPath. 247 | * 248 | * ```php 249 | * tr', -2); // previous than last row 275 | * ``` 276 | * 277 | * @param string $element CSS or XPath locator 278 | * @param int|string $position xPath index 279 | */ 280 | public static function elementAt(string $element, int|string $position): string 281 | { 282 | if (is_int($position) && $position < 0) { 283 | ++$position; // -1 points to the last element 284 | $position = 'last()-' . abs($position); 285 | } 286 | if ($position === 0) { 287 | throw new InvalidArgumentException( 288 | '0 is not valid element position. XPath expects first element to have index 1' 289 | ); 290 | } 291 | return sprintf('(%s)[position()=%s]', self::toXPath($element), $position); 292 | } 293 | 294 | /** 295 | * Locates first element of group elements. 296 | * Either CSS or XPath locator can be passed as locator, 297 | * Equal to `Locator::elementAt($locator, 1)` 298 | * 299 | * ```php 300 | * getMechanism(); 343 | $locator = $selector->getValue(); 344 | return "{$type} '{$locator}'"; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/Util/Uri.php: -------------------------------------------------------------------------------- 1 | withHost($parts['host']); 51 | $base = $base->withPath(''); 52 | $base = $base->withQuery(''); 53 | $base = $base->withFragment(''); 54 | } 55 | if (isset($parts['path'])) { 56 | $path = $parts['path']; 57 | $basePath = $base->getPath(); 58 | if ((!str_starts_with($path, '/')) && !empty($path)) { 59 | if ($basePath !== '') { 60 | // if it ends with a slash, relative paths are below it 61 | if (preg_match('#/$#', $basePath)) { 62 | $path = $basePath . $path; 63 | } else { 64 | // remove double slashes 65 | $dir = rtrim(dirname($basePath), '\\/'); 66 | $path = $dir . '/' . $path; 67 | } 68 | } else { 69 | $path = '/' . ltrim($path, '/'); 70 | } 71 | } 72 | $base = $base->withPath($path); 73 | $base = $base->withQuery(''); 74 | $base = $base->withFragment(''); 75 | } 76 | if (isset($parts['query'])) { 77 | $base = $base->withQuery($parts['query']); 78 | $base = $base->withFragment(''); 79 | } 80 | if (isset($parts['fragment'])) { 81 | $base = $base->withFragment($parts['fragment']); 82 | } 83 | 84 | return (string)$base; 85 | } 86 | 87 | /** 88 | * Retrieve /path?query#fragment part of URL 89 | */ 90 | public static function retrieveUri(string $url): string 91 | { 92 | $uri = new Psr7Uri($url); 93 | return (string)(new Psr7Uri()) 94 | ->withPath($uri->getPath()) 95 | ->withQuery($uri->getQuery()) 96 | ->withFragment($uri->getFragment()); 97 | } 98 | 99 | public static function retrieveHost(string $url): string 100 | { 101 | $urlParts = parse_url($url); 102 | if (!isset($urlParts['host']) || !isset($urlParts['scheme'])) { 103 | throw new InvalidArgumentException("Wrong URL passes, host and scheme not set"); 104 | } 105 | $host = $urlParts['scheme'] . '://' . $urlParts['host']; 106 | if (isset($urlParts['port'])) { 107 | $host .= ':' . $urlParts['port']; 108 | } 109 | return $host; 110 | } 111 | 112 | public static function appendPath(string $url, string $path): string 113 | { 114 | $uri = new Psr7Uri($url); 115 | $cutUrl = (string)$uri->withQuery('')->withFragment(''); 116 | 117 | if ($path === '' || $path[0] === '#') { 118 | return $cutUrl . $path; 119 | } 120 | 121 | return rtrim($cutUrl, '/') . '/' . ltrim($path, '/'); 122 | } 123 | } 124 | --------------------------------------------------------------------------------