From 4fa7d47388f60e62debdfc62461c213f61a356da Mon Sep 17 00:00:00 2001 From: Rafael Pinto Date: Tue, 11 Oct 2016 17:32:13 +0200 Subject: [PATCH] Initial public release --- .gitignore | 5 + .travis.yml | 39 + AUTHORS | 3 + CHANGELOG.md | 5 + CONTRIBUTING.md | 43 + LICENSE | 192 ++ README.md | 147 ++ athena.json | 5 + composer.json | 25 + composer.lock | 1620 +++++++++++++++++ docs/LICENSE.md | 192 ++ docs/README.md | 147 ++ docs/SUMMARY.md | 22 + docs/_layouts/website/page.html | 53 + docs/assets/dsl.png | Bin 0 -> 44587 bytes docs/book.json | 3 + docs/guides/contributing.md | 43 + docs/guides/versioning.md | 5 + docs/publish_docs | 38 + docs/sami.php | 14 + phpunit.xml | 24 + src/Browser/Browser.php | 405 +++++ src/Browser/BrowserDriverBuilder.php | 244 +++ src/Browser/BrowserInterface.php | 92 + .../Assertion/AbstractElementAssertion.php | 72 + .../ElementDoesNotExistAssertion.php | 16 + .../Assertion/ElementExistsAssertion.php | 16 + .../ElementIsDeselectedAssertion.php | 13 + .../Assertion/ElementIsDisabledAssertion.php | 13 + .../Assertion/ElementIsDisplayedAssertion.php | 15 + .../Assertion/ElementIsEnabledAssertion.php | 15 + .../Assertion/ElementIsHiddenAssertion.php | 15 + .../Assertion/ElementIsSelectedAssertion.php | 15 + .../ElementTextEqualsToAssertion.php | 38 + .../ElementTextIsNotEqualToAssertion.php | 38 + .../ElementValueEqualsToAssertion.php | 38 + .../ElementValueIsNotEqualToAssertion.php | 38 + src/Browser/Page/Element/ElementAction.php | 90 + src/Browser/Page/Element/ElementSelector.php | 101 + src/Browser/Page/Element/Find/ElementFind.php | 73 + .../Find/ElementFindWithAssertions.php | 197 ++ .../Page/Element/Find/ElementFindWithWait.php | 126 ++ .../Element/Find/ElementFinderInterface.php | 28 + .../Assertion/AllElementsApplyToAssertion.php | 66 + .../AllElementsAreHiddenAssertion.php | 18 + .../AllElementsAreVisibleAssertion.php | 18 + .../AtLeastOneElementAppliesToAssertion.php | 56 + .../ElementExistsAtLeastOnceAssertion.php | 18 + .../ElementExistsOnlyOnceAssertion.php | 15 + .../ElementShouldNotExistAssertion.php | 23 + .../Assertion/ElementTextEqualsAssertion.php | 31 + .../Assertion/ElementValueEqualsAssertion.php | 37 + .../Decorator/AbstractPageFinderDecorator.php | 162 ++ .../Decorator/CachedPageFinderDecorator.php | 144 ++ .../Page/Find/Decorator/ClosureHolder.php | 56 + .../Decorator/PageFinderWithAssertions.php | 115 ++ .../Find/Decorator/PageFinderWithWaits.php | 91 + .../Decorator/TargetDecoratorInterface.php | 18 + src/Browser/Page/Find/PageFinder.php | 108 ++ src/Browser/Page/Find/PageFinderBuilder.php | 78 + src/Browser/Page/Find/PageFinderInterface.php | 67 + src/Browser/Page/Find/Wait/AbstractWait.php | 60 + .../Page/Find/Wait/WaitUntilAbsence.php | 18 + .../Page/Find/Wait/WaitUntilClickable.php | 23 + .../Page/Find/Wait/WaitUntilPresence.php | 18 + .../Page/Find/Wait/WaitUntilVisibility.php | 24 + src/Browser/Page/Page.php | 100 + src/Browser/Page/PageInterface.php | 38 + src/Exception/CriteriaNotMetException.php | 8 + src/Exception/DirectoryNotFoundException.php | 8 + src/Exception/ElementIsHiddenException.php | 8 + src/Exception/ElementIsVisibleException.php | 8 + src/Exception/ElementNotEnabledException.php | 8 + src/Exception/ElementNotExpectedException.php | 8 + src/Exception/ElementNotSelectedException.php | 8 + src/Exception/ElementNotVisibleException.php | 8 + src/Exception/EmptyResultException.php | 8 + src/Exception/InvalidUrlException.php | 8 + .../NoElementAppliesToCriteriaException.php | 8 + src/Exception/NoSuchElementException.php | 8 + ...NotAllElementsApplyToCriteriaException.php | 8 + src/Exception/NotAnArrayException.php | 8 + src/Exception/SettingNotFoundException.php | 8 + src/Exception/StopChainException.php | 8 + src/Exception/UnexpectedValueException.php | 8 + src/Exception/UnsupportedBrowserException.php | 8 + src/Translator/UrlTranslator.php | 147 ++ tests/Helpers/ElementAssertionHelperTrait.php | 15 + tests/unit/Browser/BrowserTest.php | 73 + .../AbstractElementAssertionTest.php | 41 + .../ElementDoesNotExistAssertionTest.php | 29 + .../Assertion/ElementExistsAssertionTest.php | 29 + .../ElementIsDeselectedAssertionTest.php | 42 + .../ElementIsDisabledAssertionTest.php | 42 + .../ElementIsDisplayedAssertionTest.php | 42 + .../ElementIsEnabledAssertionTest.php | 42 + .../ElementIsHiddenAssertionTest.php | 42 + .../ElementIsSelectedAssertionTest.php | 42 + .../ElementTextEqualsToAssertionTest.php | 42 + .../Assertion/ElementTextIsNotEqualToTest.php | 44 + .../ElementValueEqualsToAssertionTest.php | 42 + .../ElementValueIsNotEqualToAssertionTest.php | 44 + .../Page/Element/ElementActionTest.php | 56 + .../Page/Element/ElementSelectorTest.php | 50 + .../Page/Element/Find/ElementFindTest.php | 75 + .../Find/ElementFindWithAssertionsTest.php | 164 ++ .../Element/Find/ElementFindWithWaitTest.php | 98 + .../AllElementsApplyToAssertionTest.php | 98 + .../AllElementsAreHiddenAssertionTest.php | 47 + .../AllElementsAreVisibleAssertionTest.php | 47 + ...tLeastOneElementAppliesToAssertionTest.php | 89 + .../ElementExistsAtLeastOnceAssertionTest.php | 43 + .../ElementExistsOnlyOnceAssertionTest.php | 46 + .../ElementShouldNotExistAssertionTest.php | 41 + .../ElementTextEqualsAssertionTest.php | 42 + .../ElementValueEqualsAssertionTest.php | 57 + .../AbstractPageFinderDecoratorTest.php | 56 + .../CachedPageFinderDecoratorTest.php | 60 + .../Page/Find/Decorator/ClosureHolderTest.php | 73 + .../Page/Find/PageFinderBuilderTest.php | 36 + .../Page/Find/Wait/WaitUntilAbsenceTest.php | 73 + .../Page/Find/Wait/WaitUntilClickableTest.php | 53 + .../Page/Find/Wait/WaitUntilPresenceTest.php | 76 + .../Find/Wait/WaitUntilVisibilityTest.php | 53 + tests/unit/Translator/UrlTranslatorTest.php | 74 + 125 files changed, 8134 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 AUTHORS create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 athena.json create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 docs/LICENSE.md create mode 100644 docs/README.md create mode 100644 docs/SUMMARY.md create mode 100644 docs/_layouts/website/page.html create mode 100644 docs/assets/dsl.png create mode 100644 docs/book.json create mode 100644 docs/guides/contributing.md create mode 100644 docs/guides/versioning.md create mode 100755 docs/publish_docs create mode 100644 docs/sami.php create mode 100644 phpunit.xml create mode 100644 src/Browser/Browser.php create mode 100644 src/Browser/BrowserDriverBuilder.php create mode 100644 src/Browser/BrowserInterface.php create mode 100644 src/Browser/Page/Element/Assertion/AbstractElementAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementDoesNotExistAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementExistsAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementIsDeselectedAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementIsDisabledAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementIsDisplayedAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementIsEnabledAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementIsHiddenAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementIsSelectedAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementTextEqualsToAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementTextIsNotEqualToAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementValueEqualsToAssertion.php create mode 100644 src/Browser/Page/Element/Assertion/ElementValueIsNotEqualToAssertion.php create mode 100644 src/Browser/Page/Element/ElementAction.php create mode 100644 src/Browser/Page/Element/ElementSelector.php create mode 100644 src/Browser/Page/Element/Find/ElementFind.php create mode 100644 src/Browser/Page/Element/Find/ElementFindWithAssertions.php create mode 100644 src/Browser/Page/Element/Find/ElementFindWithWait.php create mode 100644 src/Browser/Page/Element/Find/ElementFinderInterface.php create mode 100644 src/Browser/Page/Find/Assertion/AllElementsApplyToAssertion.php create mode 100644 src/Browser/Page/Find/Assertion/AllElementsAreHiddenAssertion.php create mode 100644 src/Browser/Page/Find/Assertion/AllElementsAreVisibleAssertion.php create mode 100644 src/Browser/Page/Find/Assertion/AtLeastOneElementAppliesToAssertion.php create mode 100644 src/Browser/Page/Find/Assertion/ElementExistsAtLeastOnceAssertion.php create mode 100644 src/Browser/Page/Find/Assertion/ElementExistsOnlyOnceAssertion.php create mode 100644 src/Browser/Page/Find/Assertion/ElementShouldNotExistAssertion.php create mode 100644 src/Browser/Page/Find/Assertion/ElementTextEqualsAssertion.php create mode 100644 src/Browser/Page/Find/Assertion/ElementValueEqualsAssertion.php create mode 100644 src/Browser/Page/Find/Decorator/AbstractPageFinderDecorator.php create mode 100644 src/Browser/Page/Find/Decorator/CachedPageFinderDecorator.php create mode 100644 src/Browser/Page/Find/Decorator/ClosureHolder.php create mode 100644 src/Browser/Page/Find/Decorator/PageFinderWithAssertions.php create mode 100644 src/Browser/Page/Find/Decorator/PageFinderWithWaits.php create mode 100644 src/Browser/Page/Find/Decorator/TargetDecoratorInterface.php create mode 100644 src/Browser/Page/Find/PageFinder.php create mode 100644 src/Browser/Page/Find/PageFinderBuilder.php create mode 100644 src/Browser/Page/Find/PageFinderInterface.php create mode 100644 src/Browser/Page/Find/Wait/AbstractWait.php create mode 100644 src/Browser/Page/Find/Wait/WaitUntilAbsence.php create mode 100644 src/Browser/Page/Find/Wait/WaitUntilClickable.php create mode 100644 src/Browser/Page/Find/Wait/WaitUntilPresence.php create mode 100644 src/Browser/Page/Find/Wait/WaitUntilVisibility.php create mode 100644 src/Browser/Page/Page.php create mode 100644 src/Browser/Page/PageInterface.php create mode 100644 src/Exception/CriteriaNotMetException.php create mode 100644 src/Exception/DirectoryNotFoundException.php create mode 100644 src/Exception/ElementIsHiddenException.php create mode 100644 src/Exception/ElementIsVisibleException.php create mode 100644 src/Exception/ElementNotEnabledException.php create mode 100644 src/Exception/ElementNotExpectedException.php create mode 100644 src/Exception/ElementNotSelectedException.php create mode 100644 src/Exception/ElementNotVisibleException.php create mode 100644 src/Exception/EmptyResultException.php create mode 100644 src/Exception/InvalidUrlException.php create mode 100644 src/Exception/NoElementAppliesToCriteriaException.php create mode 100644 src/Exception/NoSuchElementException.php create mode 100644 src/Exception/NotAllElementsApplyToCriteriaException.php create mode 100644 src/Exception/NotAnArrayException.php create mode 100644 src/Exception/SettingNotFoundException.php create mode 100644 src/Exception/StopChainException.php create mode 100644 src/Exception/UnexpectedValueException.php create mode 100644 src/Exception/UnsupportedBrowserException.php create mode 100644 src/Translator/UrlTranslator.php create mode 100644 tests/Helpers/ElementAssertionHelperTrait.php create mode 100644 tests/unit/Browser/BrowserTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/AbstractElementAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementDoesNotExistAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementExistsAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementIsDeselectedAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementIsDisabledAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementIsDisplayedAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementIsEnabledAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementIsHiddenAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementIsSelectedAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementTextEqualsToAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementTextIsNotEqualToTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementValueEqualsToAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/Assertion/ElementValueIsNotEqualToAssertionTest.php create mode 100644 tests/unit/Browser/Page/Element/ElementActionTest.php create mode 100644 tests/unit/Browser/Page/Element/ElementSelectorTest.php create mode 100644 tests/unit/Browser/Page/Element/Find/ElementFindTest.php create mode 100644 tests/unit/Browser/Page/Element/Find/ElementFindWithAssertionsTest.php create mode 100644 tests/unit/Browser/Page/Element/Find/ElementFindWithWaitTest.php create mode 100644 tests/unit/Browser/Page/Find/Assertion/AllElementsApplyToAssertionTest.php create mode 100644 tests/unit/Browser/Page/Find/Assertion/AllElementsAreHiddenAssertionTest.php create mode 100644 tests/unit/Browser/Page/Find/Assertion/AllElementsAreVisibleAssertionTest.php create mode 100644 tests/unit/Browser/Page/Find/Assertion/AtLeastOneElementAppliesToAssertionTest.php create mode 100644 tests/unit/Browser/Page/Find/Assertion/ElementExistsAtLeastOnceAssertionTest.php create mode 100644 tests/unit/Browser/Page/Find/Assertion/ElementExistsOnlyOnceAssertionTest.php create mode 100644 tests/unit/Browser/Page/Find/Assertion/ElementShouldNotExistAssertionTest.php create mode 100644 tests/unit/Browser/Page/Find/Assertion/ElementTextEqualsAssertionTest.php create mode 100644 tests/unit/Browser/Page/Find/Assertion/ElementValueEqualsAssertionTest.php create mode 100644 tests/unit/Browser/Page/Find/Decorator/AbstractPageFinderDecoratorTest.php create mode 100644 tests/unit/Browser/Page/Find/Decorator/CachedPageFinderDecoratorTest.php create mode 100644 tests/unit/Browser/Page/Find/Decorator/ClosureHolderTest.php create mode 100644 tests/unit/Browser/Page/Find/PageFinderBuilderTest.php create mode 100644 tests/unit/Browser/Page/Find/Wait/WaitUntilAbsenceTest.php create mode 100644 tests/unit/Browser/Page/Find/Wait/WaitUntilClickableTest.php create mode 100644 tests/unit/Browser/Page/Find/Wait/WaitUntilPresenceTest.php create mode 100644 tests/unit/Browser/Page/Find/Wait/WaitUntilVisibilityTest.php create mode 100644 tests/unit/Translator/UrlTranslatorTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c03c25a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +vendor/ +build/ +cache/ +coverage/ +docs/_book/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..769aa5f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,39 @@ +sudo: required +dist: trusty + +language: php +services: + - docker +php: + - 5.6 +before_install: + - sudo rm -rf ~/.nvm + - curl -sL "https://deb.nodesource.com/setup_6.x" | sudo -E bash - + - sudo apt-get install -y nodejs + - wget -O "$HOME/sami.phar" http://get.sensiolabs.org/sami.phar && chmod +x "$HOME/sami.phar" + - wget -O "$HOME/coveralls.phar" https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar && chmod +x "$HOME/coveralls.phar" +install: + - composer install + - sudo npm install -g gitbook-cli +before_script: + - wget -qO- "https://github.com/athena-oss/athena/archive/v${ATHENA_RELEASE}.tar.gz" | tar -xz -C $HOME && mv "$HOME/athena-${ATHENA_RELEASE}" "$HOME/athena" + - wget -qO- "https://github.com/athena-oss/plugin-php/archive/v${ATHENA_PHP_RELEASE}.tar.gz" | tar -xz -C $HOME && mv "$HOME/plugin-php-${ATHENA_PHP_RELEASE}" "$HOME/athena/plugins/php" + - ln -s "$TRAVIS_BUILD_DIR" "$HOME/athena/plugins/php" && touch $HOME/athena/plugins/base/athena.lock + - mkdir -p build/logs/ +script: + - "$HOME/athena/athena php unit . -f=athena.json --coverage-clover=build/logs/clover.xml" +after_success: + - $HOME/sami.phar update docs/sami.php + - sed -i "s#/opt/tests/##" build/logs/clover.xml + - $HOME/coveralls.phar -v --exclude-no-stmt + - docs/publish_docs +cache: + directories: + - "$(npm config get prefix)" + - $HOME/.composer/cache +env: + global: + - ATHENA_RELEASE=0.5.0 + - ATHENA_PHP_RELEASE=0.7.0 + - ATHENA_SUDO_DISABLED=true + - secure: CkXAOJoVCQEBS9JOklhOhy1vpB9pAOVbZpVbvFZ0GtlIRVz4qqV9sQG/c/GlWquvpXikKyGCtIfzYBqpskwH42mRUxeMvGXKTgVpYopcz7u3r0n2TeL3DqS2eibPO+IMDUU4ajHcgY5I1Vtzs7AgMNkSfTvJN7y7fV3CIxxf4VSZFEckAKuE2w4fbHt9H4eXZfXcuNIaYMG/6iCbnnLVjgbJD1H7GK0siN1dKSOXoPriuYCVZfDK3OH/gCXFodb9g4PFIVuPRzjlOwbu2S+N9JIT0dEE4DyxdsGz/ns50mHYFOALrpy/l2c2ak91d8HyY+pmF6ibNANQibi0F76YzY7v5XGiCTVMVnut7hhxiGQp+QOpRARzQLGw9B2V0gC44V8wNG1AL8+SQDF/04EhpQWR5pocypIIgOOGz6b9RKExa37u/r1y0TswlykHVKx1MGCjigY4iQPhm6q7tSYfKlzt9oltosx3+mCsvd0UnfPuGhg0YmMA8axVu9seAdPWAdXHYBq2d8Gsgll66sbos1etgKQNj4gTBbR3yz065rpSS/OMvSxuIF5+XS2dNJFs6ORNC60lD9FMmMhVbqCRmeRYfFjEeWaGht22ZKTpsBfq9HHe8Mm/BDDoMLVO1YHgE7DLL6/bFE7W1U7iS/764rhWR7OxqDwBi75hz1pbl0k= diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..36f9315 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,3 @@ +Caio Ronchi +Pedro Proença +Rafael Pinto diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..00ed3b6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +## 0.7.0 (October 11, 2016) + +### PHP Fluent WebDriver Client + +- Initial public release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e96018e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to the PHP Fluent WebDriver Client + +## Our Development Process + +Some of our core contributors will be working directly on GitHub. These changes will be public from the beginning. + +### `master` changes fast + +We move fast and most likely things will break. Every time there is a commit our CI server will run the tests and hopefully they will pass all times. We will do our best to properly communicate the changes that can affect the application API and always version appropriately in order to make easier for you to use a specific version. + +### Pull Requests + +The core contributors will be monitoring for pull requests. When we get one, we will pull it in and apply it to our codebase and run our test suite to ensure nothing breaks. Then one of the core contributors needs to verify that all is working appropriately. When the API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. + +*Before* submitting a pull request, please make sure the following is done: + +1. Fork the repo and create your branch from `master`. +2. If you've added code that should be tested, add tests! +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. + + +## Bugs + +### Where to Find Known Issues + +We will be using GitHub Issues for our public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist. + +### Reporting New Issues + +The best way to get your bug fixed is to provide a reduced test case. + +## How to Get in Touch + +Glitter Room: https://gitter.im/athena-oss/Lobby + +## Development best practices + +We are using [PSR-2](http://www.php-fig.org/psr/psr-2/) coding style for our development, so please apply it as well if you want to contribute. + +## License + +By contributing to PHP Fluent WebDriver Client, you agree that your contributions will be licensed under the [Apache License Version 2.0 (APLv2)](LICENSE). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4036fe5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,192 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + + END OF TERMS AND CONDITIONS + + + Copyright 2016 OLX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9cb6633 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# PHP Fluent WebDriver Client + +[![Build Status](https://travis-ci.org/athena-oss/php-fluent-webdriver-client.svg?branch=master)](https://travis-ci.org/athena-oss/php-fluent-webdriver-client) +[![Coverage Status](https://coveralls.io/repos/github/athena-oss/php-fluent-webdriver-client/badge.svg?branch=master)](https://coveralls.io/github/athena-oss/php-fluent-webdriver-client?branch=master) + +A fluent DSL for writing browser tests. + +## Table of Contents +* [Installation](README.md#installation) +* [External requirements](README.md#external-requirements) +* [Usage](README.md#usage) + * [Synchronous assertions](README.md#synchronous-assertions) + * [Asynchronous assertions](README.md#asynchronous-assertions) +* [Visual representation of the DSL](README.md#visual-representation-of-the-dsl) +* [Facebook WebDriver dependency](README.md#facebook-webdriver-dependency) +* [API docs](README.md#api-docs) +* [Contributing](README.md#contributing) +* [Versioning](README.md#versioning) +* [License](README.md#license) + +## Installation + +The recommended way of installing it is by using Composer: +```sh +$ composer require athena-oss/php-fluent-webdriver-client:dev-master +``` + +## External requirements + +The library is meant to be used alongside [Selenium](http://www.seleniumhq.org/), which is in charge of wrapping different browser vendors behind a unified [WebDriver spec](https://www.w3.org/TR/webdriver/). In order to be able to run the code samples contained in this document you'll need to download and run Selenium locally. For running the code examples you'll additionally need to install [PhantomJS](http://phantomjs.org/). + +## Usage + +The library attempts to reduce the boilerplate code needed to write browser tests by providing an opinionated DSL. The DSL allows for two distinct patterns of tests: +- Synchronous assertions +- Asynchronous assertions + +### Synchronous assertions + +Synchronous assertions are those expressed with the following pattern: +- Fetch URL +- Convert fetched HTML document into a Page Object +- Find an element by custom selector +- Assert that the element is enabled/selected/visible etc. +- (Optionally) Perform action on element (click, clear, submit) + +Sample: + +```php +namespace OLX\SampleWebDriver\Tests; + +use OLX\FluentWebDriverClient\Browser\Browser; +use OLX\FluentWebDriverClient\Browser\BrowserDriverBuilder; + +class WikipediaBrowserTest extends \PHPUnit_Framework_TestCase +{ + public function testArticlePage_RegularArticleInEnglish_ShouldDisplayArticleTitleAsHeader() + { + $driver = (new BrowserDriverBuilder('http://localhost:4444/wd/hub')) + ->withType('phantomjs') + ->build(); + + $browser = new Browser($driver); + + $browser->get('https://en.wikipedia.org/wiki/Athena') + ->getElement() + ->withCss('h1#firstHeading') + ->assertThat() + ->isHidden() + ->thenFind() + ->asHtmlElement(); + } +} +``` + +Running the above test would fail (an Exception is thrown), as an element matching the given CSS exists in the DOM and is visible. + +### Asynchronous assertions + +Synchronous assertions are those expressed with the following pattern: +- Fetch URL +- Convert fetched HTML document into a Page Object +- Find an element by custom selector +- Wait for a condition on the element (a timeout means the assertion failed) +- (Optionally) Perform action on element (click, clear, submit) + +Sample: +```php +namespace OLX\SampleWebDriver\Tests; + +use OLX\FluentWebDriverClient\Browser\Browser; +use OLX\FluentWebDriverClient\Browser\BrowserDriverBuilder; + +class WikipediaBrowserTest extends \PHPUnit_Framework_TestCase +{ + public function testArticlePage_RegularArticleInEnglish_ShouldDisplaySpecialHeaderAfter3Seconds() + { + $driver = (new BrowserDriverBuilder('http://localhost:4444/wd/hub')) + ->withType('phantomjs') + ->build(); + + $browser = new Browser($driver); + + $browser->get('https://en.wikipedia.org/wiki/Athena') + ->getElement() + ->withCss('h1#specialHeading') + ->wait(3) + ->toBeVisible() + ->thenFind() + ->asHtmlElement(); + } +} +``` + +Running the above test would fail (an Exception is thrown), as an element matching the given CSS doesn't exist in the DOM 3 seconds after the DOM was ready. + +## Visual representation of the DSL + +The diagram bellow illustrates the methods that can be called in each state of the call chain. A few key points: +- The names inside each rectangle, when not prefixed, correspond to interfaces and classes in the library +- The FB prefix corresponds to the Facebook PHP WebDriver package + +![Visual representation of the DSL](docs/assets/dsl.png) + +## Facebook WebDriver dependency + +The [Facebook PHP WebDriver](https://github.com/facebook/php-webdriver) is the underlying implementation of all communication between the library and the Selenium HTTP API. At its current state, the DSL can't hide away the Facebook implementation completely. Therefore it is recommended that you read their documentation in case you're using any of the DSL methods which return a Facebook type. + +Replacing the Facebook implementation by our own Selenium API abstraction is currently not among one of the project top priorities, but it's an improvement we're considering implementing (as a major, backward-incompatible version). + +## API docs + +An [API documentation](http://athena-oss.github.io/php-fluent-webdriver-client/sami) is provided. + +## Contributing + +Checkout our guidelines on how to contribute in [CONTRIBUTING.md](CONTRIBUTING.md). + +## Versioning + +Releases are managed using github's release feature. We use [Semantic Versioning](http://semver.org) for all +the releases. Every change made to the code base will be referred to in the release notes (except for +cleanups and refactorings). + +## License + +Licensed under the [Apache License Version 2.0 (APLv2)](LICENSE). diff --git a/athena.json b/athena.json new file mode 100644 index 0000000..c170f88 --- /dev/null +++ b/athena.json @@ -0,0 +1,5 @@ +{ + "bootstrap": [ + "vendor/autoload.php" + ] +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..711e126 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "athena-oss/php-fluent-webdriver-client", + "description": "PHP Fluent WebDriver Client", + "license": "Apache-2.0", + "type": "library", + "require": { + "facebook/webdriver": "1.1.1", + "guzzlehttp/guzzle": "5.3.0", + "phake/phake": "2.2.1", + "symfony/event-dispatcher": "3.1.3" + }, + "require-dev" : { + "phpunit/phpunit": "5.1.7" + }, + "autoload": { + "psr-4": { + "OLX\\FluentWebDriverClient\\": ["src/", "tests/"] + } + }, + "autoload-dev": { + "psr-4": { + "OLX\\FluentWebDriverClient\\Tests\\": "tests/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..217e2ce --- /dev/null +++ b/composer.lock @@ -0,0 +1,1620 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "c900de5944e4c3cf52c483c18580eba5", + "content-hash": "36519c80db51305d2b3466dfe4ba9bcf", + "packages": [ + { + "name": "facebook/webdriver", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/facebook/php-webdriver.git", + "reference": "1c98108ba3eb435b681655764de11502a0653705" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/1c98108ba3eb435b681655764de11502a0653705", + "reference": "1c98108ba3eb435b681655764de11502a0653705", + "shasum": "" + }, + "require": { + "php": ">=5.3.19" + }, + "require-dev": { + "phpunit/phpunit": "4.6.*" + }, + "suggest": { + "phpdocumentor/phpdocumentor": "2.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "A PHP client for WebDriver", + "homepage": "https://github.com/facebook/php-webdriver", + "keywords": [ + "facebook", + "php", + "selenium", + "webdriver" + ], + "time": "2015-12-31 15:58:49" + }, + { + "name": "guzzlehttp/guzzle", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "f3c8c22471cb55475105c14769644a49c3262b93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f3c8c22471cb55475105c14769644a49c3262b93", + "reference": "f3c8c22471cb55475105c14769644a49c3262b93", + "shasum": "" + }, + "require": { + "guzzlehttp/ringphp": "^1.1", + "php": ">=5.4.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0", + "psr/log": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2015-05-20 03:47:55" + }, + { + "name": "guzzlehttp/ringphp", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/RingPHP.git", + "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/RingPHP/zipball/dbbb91d7f6c191e5e405e900e3102ac7f261bc0b", + "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b", + "shasum": "" + }, + "require": { + "guzzlehttp/streams": "~3.0", + "php": ">=5.4.0", + "react/promise": "~2.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "ext-curl": "Guzzle will use specific adapters if cURL is present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Ring\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.", + "time": "2015-05-20 03:37:09" + }, + { + "name": "guzzlehttp/streams", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/streams.git", + "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", + "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple abstraction over streams of data", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "stream" + ], + "time": "2014-10-12 19:18:40" + }, + { + "name": "phake/phake", + "version": "v2.2.1", + "source": { + "type": "git", + "url": "https://github.com/mlively/Phake.git", + "reference": "50d50a01e397e55acc2114c906a46d2aab966a94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mlively/Phake/zipball/50d50a01e397e55acc2114c906a46d2aab966a94", + "reference": "50d50a01e397e55acc2114c906a46d2aab966a94", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/comparator": "~1.1" + }, + "require-dev": { + "codeclimate/php-test-reporter": "dev-master", + "doctrine/common": "2.3.*", + "ext-soap": "*", + "hamcrest/hamcrest-php": "1.1.*", + "phpunit/phpunit": "3.7.*" + }, + "suggest": { + "doctrine/common": "Allows mock annotations to use import statements for classes.", + "hamcrest/hamcrest-php": "Use Hamcrest matchers." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.0-dev" + } + }, + "autoload": { + "psr-0": { + "Phake": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Mike Lively", + "email": "m@digitalsandwich.com" + } + ], + "description": "The Phake mock testing library", + "homepage": "https://github.com/mlively/Phake", + "keywords": [ + "mock", + "testing" + ], + "time": "2016-02-14 06:53:26" + }, + { + "name": "react/promise", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8025426794f1944de806618671d4fa476dc7626f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8025426794f1944de806618671d4fa476dc7626f", + "reference": "8025426794f1944de806618671d4fa476dc7626f", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "time": "2016-05-03 17:50:52" + }, + { + "name": "sebastian/comparator", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2015-07-26 15:48:44" + }, + { + "name": "sebastian/diff", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08 07:14:41" + }, + { + "name": "sebastian/exporter", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-06-17 09:04:28" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-11-11 19:50:13" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c0c00c80b3a69132c4e55c3e7db32b4a387615e5", + "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/stopwatch": "~2.8|~3.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2016-07-19 10:45:57" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "myclabs/deep-copy", + "version": "1.5.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/ea74994a3dc7f8d2f65a06009348f2d63c81e61f", + "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "doctrine/collections": "1.*", + "phpunit/phpunit": "~4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "homepage": "https://github.com/myclabs/DeepCopy", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2016-09-16 13:37:59" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2015-12-27 11:43:31" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-09-30 07:12:33" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2016-06-10 07:14:17" + }, + { + "name": "phpspec/prophecy", + "version": "v1.6.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "58a8137754bc24b25740d4281399a4a3596058e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0", + "reference": "58a8137754bc24b25740d4281399a4a3596058e0", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2016-06-07 08:13:47" + }, + { + "name": "phpunit/php-code-coverage", + "version": "3.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "44cd8e3930e431658d1a5de7d282d5cb37837fd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/44cd8e3930e431658d1a5de7d282d5cb37837fd5", + "reference": "44cd8e3930e431658d1a5de7d282d5cb37837fd5", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "^1.4.2", + "sebastian/code-unit-reverse-lookup": "~1.0", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0|~2.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~5" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.4.0", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2016-05-27 16:24:29" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2015-06-21 13:08:43" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2016-05-12 18:03:57" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2015-09-15 10:49:45" + }, + { + "name": "phpunit/phpunit", + "version": "5.1.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "d0f7ae467dcbe7a6ad050540c9d1d39a7aefff26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0f7ae467dcbe7a6ad050540c9d1d39a7aefff26", + "reference": "d0f7ae467dcbe7a6ad050540c9d1d39a7aefff26", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "myclabs/deep-copy": "~1.3", + "php": ">=5.6", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "~3.0", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": ">=1.0.6", + "phpunit/phpunit-mock-objects": ">=3.0.5", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.3", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2016-02-02 09:03:29" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "151c96874bff6fe61a25039df60e776613a61489" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/151c96874bff6fe61a25039df60e776613a61489", + "reference": "151c96874bff6fe61a25039df60e776613a61489", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": ">=5.6", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2016-04-20 14:39:26" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2016-02-13 06:45:14" + }, + { + "name": "sebastian/environment", + "version": "1.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-08-18 05:49:44" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28 20:34:47" + }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21 13:59:46" + }, + { + "name": "symfony/yaml", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "368b9738d4033c8b93454cb0dbd45d305135a6d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/368b9738d4033c8b93454cb0dbd45d305135a6d3", + "reference": "368b9738d4033c8b93454cb0dbd45d305135a6d3", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2016-09-25 08:27:07" + }, + { + "name": "webmozart/assert", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bb2d123231c095735130cc8f6d31385a44c7b308" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bb2d123231c095735130cc8f6d31385a44c7b308", + "reference": "bb2d123231c095735130cc8f6d31385a44c7b308", + "shasum": "" + }, + "require": { + "php": "^5.3.3|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-08-09 15:02:57" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/docs/LICENSE.md b/docs/LICENSE.md new file mode 100644 index 0000000..4036fe5 --- /dev/null +++ b/docs/LICENSE.md @@ -0,0 +1,192 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + + END OF TERMS AND CONDITIONS + + + Copyright 2016 OLX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..220a33a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,147 @@ +# PHP Fluent WebDriver Client + +[![Build Status](https://travis-ci.org/athena-oss/php-fluent-webdriver-client.svg?branch=master)](https://travis-ci.org/athena-oss/php-fluent-webdriver-client) +[![Coverage Status](https://coveralls.io/repos/github/athena-oss/php-fluent-webdriver-client/badge.svg?branch=master)](https://coveralls.io/github/athena-oss/php-fluent-webdriver-client?branch=master) + +A fluent DSL for writing browser tests. + +## Table of Contents +* [Installation](README.md#installation) +* [External requirements](README.md#external-requirements) +* [Usage](README.md#usage) + * [Synchronous assertions](README.md#synchronous-assertions) + * [Asynchronous assertions](README.md#asynchronous-assertions) +* [Visual representation of the DSL](README.md#visual-representation-of-the-dsl) +* [Facebook WebDriver dependency](README.md#facebook-webdriver-dependency) +* [API docs](README.md#api-docs) +* [Contributing](README.md#contributing) +* [Versioning](README.md#versioning) +* [License](README.md#license) + +## Installation + +The recommended way of installing it is by using Composer: +```sh +$ composer require athena-oss/php-fluent-webdriver-client:dev-master +``` + +## External requirements + +The library is meant to be used alongside [Selenium](http://www.seleniumhq.org/), which is in charge of wrapping different browser vendors behind a unified [WebDriver spec](https://www.w3.org/TR/webdriver/). In order to be able to run the code samples contained in this document you'll need to download and run Selenium locally. For running the code examples you'll additionally need to install [PhantomJS](http://phantomjs.org/). + +## Usage + +The library attempts to reduce the boilerplate code needed to write browser tests by providing an opinionated DSL. The DSL allows for two distinct patterns of tests: +- Synchronous assertions +- Asynchronous assertions + +### Synchronous assertions + +Synchronous assertions are those expressed with the following pattern: +- Fetch URL +- Convert fetched HTML document into a Page Object +- Find an element by custom selector +- Assert that the element is enabled/selected/visible etc. +- (Optionally) Perform action on element (click, clear, submit) + +Sample: + +```php +namespace OLX\SampleWebDriver\Tests; + +use OLX\FluentWebDriverClient\Browser\Browser; +use OLX\FluentWebDriverClient\Browser\BrowserDriverBuilder; + +class WikipediaBrowserTest extends \PHPUnit_Framework_TestCase +{ + public function testArticlePage_RegularArticleInEnglish_ShouldDisplayArticleTitleAsHeader() + { + $driver = (new BrowserDriverBuilder('http://localhost:4444/wd/hub')) + ->withType('phantomjs') + ->build(); + + $browser = new Browser($driver); + + $browser->get('https://en.wikipedia.org/wiki/Athena') + ->getElement() + ->withCss('h1#firstHeading') + ->assertThat() + ->isHidden() + ->thenFind() + ->asHtmlElement(); + } +} +``` + +Running the above test would fail (an Exception is thrown), as an element matching the given CSS exists in the DOM and is visible. + +### Asynchronous assertions + +Synchronous assertions are those expressed with the following pattern: +- Fetch URL +- Convert fetched HTML document into a Page Object +- Find an element by custom selector +- Wait for a condition on the element (a timeout means the assertion failed) +- (Optionally) Perform action on element (click, clear, submit) + +Sample: +```php +namespace OLX\SampleWebDriver\Tests; + +use OLX\FluentWebDriverClient\Browser\Browser; +use OLX\FluentWebDriverClient\Browser\BrowserDriverBuilder; + +class WikipediaBrowserTest extends \PHPUnit_Framework_TestCase +{ + public function testArticlePage_RegularArticleInEnglish_ShouldDisplaySpecialHeaderAfter3Seconds() + { + $driver = (new BrowserDriverBuilder('http://localhost:4444/wd/hub')) + ->withType('phantomjs') + ->build(); + + $browser = new Browser($driver); + + $browser->get('https://en.wikipedia.org/wiki/Athena') + ->getElement() + ->withCss('h1#specialHeading') + ->wait(3) + ->toBeVisible() + ->thenFind() + ->asHtmlElement(); + } +} +``` + +Running the above test would fail (an Exception is thrown), as an element matching the given CSS doesn't exist in the DOM 3 seconds after the DOM was ready. + +## Visual representation of the DSL + +The diagram bellow illustrates the methods that can be called in each state of the call chain. A few key points: +- The names inside each rectangle, when not prefixed, correspond to interfaces and classes in the library +- The FB prefix corresponds to the Facebook PHP WebDriver package + +![Visual representation of the DSL](assets/dsl.png) + +## Facebook WebDriver dependency + +The [Facebook PHP WebDriver](https://github.com/facebook/php-webdriver) is the underlying implementation of all communication between the library and the Selenium HTTP API. At its current state, the DSL can't hide away the Facebook implementation completely. Therefore it is recommended that you read their documentation in case you're using any of the DSL methods which return a Facebook type. + +Replacing the Facebook implementation by our own Selenium API abstraction is currently not among one of the project top priorities, but it's an improvement we're considering implementing (as a major, backward-incompatible version). + +## API docs + +An [API documentation](http://athena-oss.github.io/php-fluent-webdriver-client/sami) is provided. + +## Contributing + +Checkout our guidelines on how to contribute in [CONTRIBUTING](guides/contributing.md). + +## Versioning + +Releases are managed using github's release feature. We use [Semantic Versioning](http://semver.org) for all +the releases. Every change made to the code base will be referred to in the release notes (except for +cleanups and refactorings). + +## License + +Licensed under the [Apache License Version 2.0 (APLv2)](/LICENSE.html). diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000..54a4145 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,22 @@ +# Summary + +### Getting Started + +* [Installation](README.md#installation) +* [External requirements](README.md#external-requirements) +* [Usage](README.md#usage) + * [Synchronous assertions](README.md#synchronous-assertions) + * [Asynchronous assertions](README.md#asynchronous-assertions) +* [Visual representation of the DSL](README.md#visual-representation-of-the-dsl) +* [Facebook WebDriver dependency](README.md#facebook-webdriver-dependency) +* [API docs](README.md#api-docs) + +### Guides + +* [Contributing](guides/contributing.md) +* [License](LICENSE.md) +* [Versioning](guides/versioning.md) + +### API + +* [API Reference](https://athena-oss.github.io/php-fluent-webdriver-client/sami) diff --git a/docs/_layouts/website/page.html b/docs/_layouts/website/page.html new file mode 100644 index 0000000..545cfcf --- /dev/null +++ b/docs/_layouts/website/page.html @@ -0,0 +1,53 @@ +{% extends template.self %} + +{% block head %} + +{% endblock %} diff --git a/docs/assets/dsl.png b/docs/assets/dsl.png new file mode 100644 index 0000000000000000000000000000000000000000..0576115b57e7df83ceab300854e7936383fc69eb GIT binary patch literal 44587 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
a0DV<1HWe`s7ky1=MgHc1E{$HwfMA%cNa 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 SZh`co;CR^e5ZUg;#s34g8i(@$ literal 0 HcmV?d00001 diff --git a/docs/book.json b/docs/book.json new file mode 100644 index 0000000..efe58c8 --- /dev/null +++ b/docs/book.json @@ -0,0 +1,3 @@ +{ + "plugins": ["theme-default"] +} diff --git a/docs/guides/contributing.md b/docs/guides/contributing.md new file mode 100644 index 0000000..e96018e --- /dev/null +++ b/docs/guides/contributing.md @@ -0,0 +1,43 @@ +# Contributing to the PHP Fluent WebDriver Client + +## Our Development Process + +Some of our core contributors will be working directly on GitHub. These changes will be public from the beginning. + +### `master` changes fast + +We move fast and most likely things will break. Every time there is a commit our CI server will run the tests and hopefully they will pass all times. We will do our best to properly communicate the changes that can affect the application API and always version appropriately in order to make easier for you to use a specific version. + +### Pull Requests + +The core contributors will be monitoring for pull requests. When we get one, we will pull it in and apply it to our codebase and run our test suite to ensure nothing breaks. Then one of the core contributors needs to verify that all is working appropriately. When the API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. + +*Before* submitting a pull request, please make sure the following is done: + +1. Fork the repo and create your branch from `master`. +2. If you've added code that should be tested, add tests! +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. + + +## Bugs + +### Where to Find Known Issues + +We will be using GitHub Issues for our public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist. + +### Reporting New Issues + +The best way to get your bug fixed is to provide a reduced test case. + +## How to Get in Touch + +Glitter Room: https://gitter.im/athena-oss/Lobby + +## Development best practices + +We are using [PSR-2](http://www.php-fig.org/psr/psr-2/) coding style for our development, so please apply it as well if you want to contribute. + +## License + +By contributing to PHP Fluent WebDriver Client, you agree that your contributions will be licensed under the [Apache License Version 2.0 (APLv2)](LICENSE). diff --git a/docs/guides/versioning.md b/docs/guides/versioning.md new file mode 100644 index 0000000..05e208d --- /dev/null +++ b/docs/guides/versioning.md @@ -0,0 +1,5 @@ +# Versioning + +Releases are managed using github's release feature. We use [Semantic Versioning](http://semver.org) for all +the releases. Every change made to the code base will be referred to in the release notes (except for +cleanups and refactorings). diff --git a/docs/publish_docs b/docs/publish_docs new file mode 100755 index 0000000..f9ebf2f --- /dev/null +++ b/docs/publish_docs @@ -0,0 +1,38 @@ +#!/bin/bash -e + +function error() { + echo "ERROR: $1" + exit 1 +} + +function info() { + echo "INFO: $1" +} + +if [[ "$TRAVIS_BRANCH" != "master" ]] || [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then + info "Only commits pushed to origin master will trigger documentation publishing." + exit 0 +fi + +GITBOOK_DOCS_DIR="$TRAVIS_BUILD_DIR/docs" +GITBOOK_HTML_OUTPUT_DIR="$TRAVIS_BUILD_DIR/docs/_book" + +if [[ ! -d "$GITBOOK_DOCS_DIR" ]]; then + error "Documentation dir $GITBOOK_DOCS_DIR does not exist or it's not a valid directory." +fi + +gitbook build "$GITBOOK_DOCS_DIR" "$GITBOOK_HTML_OUTPUT_DIR" + +pushd "$GITBOOK_HTML_OUTPUT_DIR" +git init +git config --global user.name "Travis CI" +git config --global user.email "athena@olx.com" + +git remote add upstream "https://$GH_TOKEN@github.com/athena-oss/php-fluent-webdriver-client.git" +git fetch upstream +git reset upstream/gh-pages + +git add -A . +git commit -m"Automatic page rebuild for $TRAVIS_COMMIT." +git push -q upstream HEAD:gh-pages +popd diff --git a/docs/sami.php b/docs/sami.php new file mode 100644 index 0000000..b3619fc --- /dev/null +++ b/docs/sami.php @@ -0,0 +1,14 @@ + 'default', + 'title' => 'Fluent WebDriver Client API', + 'build_dir' => __DIR__.'/../docs/sami', + 'cache_dir' => __DIR__.'/../cache', + 'default_opened_level' => 2, +]); diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..ec32269 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,24 @@ + + + + + + ./tests + + + + + + ./src + + + diff --git a/src/Browser/Browser.php b/src/Browser/Browser.php new file mode 100644 index 0000000..2b2fbd6 --- /dev/null +++ b/src/Browser/Browser.php @@ -0,0 +1,405 @@ +remoteDriverBuilder = $builder; + $this->remoteWebDriver = null; + } + + /** + * @codeCoverageIgnore + */ + public function __destruct() + { + try { + $this->cleanup(); + } catch (\Exception $e) { + //void as intended + } + } + + public function get($url) + { + $this->remoteWebDriver = $this->getDriver()->get($this->urlTranslator->get($url)); + return new Page($this); + } + + /** + * @codeCoverageIgnore + */ + public function getCurrentPage() + { + $this->getDriver(); + return new Page($this); + } + + public function setSession($sessionId, $path = "/", $isSecure = false) + { + $this->sessionCookie = [ + "name" => "PHPSESSID", + "value" => $sessionId, + "path" => $path, + "secure" => $isSecure + ]; + $this->getDriver()->manage()->deleteCookieNamed('PHPSESSID'); + $this->getDriver()->manage()->addCookie($this->sessionCookie); + } + + /** + * @codeCoverageIgnore + */ + public function deleteSession() + { + $this->sessionCookie = null; + $this->getDriver()->manage()->deleteCookieNamed('PHPSESSID'); + } + + /** + * @codeCoverageIgnore + */ + public function getSession() + { + if (!empty($this->sessionCookie)) { + return $this->sessionCookie; + } + return $this->getDriver()->manage()->getCookieNamed('PHPSESSID'); + } + + /** + * @codeCoverageIgnore + */ + public function deleteAllCookies() + { + $this->sessionCookie = null; + $this->getDriver()->manage()->deleteAllCookies(); + } + + /** + * @codeCoverageIgnore + */ + public function cleanup() + { + if (!$this->wasCleanedUp && !is_null($this->remoteWebDriver)) { + try { + $this->remoteWebDriver->quit(); + } catch (UnknownServerException $e) { + // if it has already TIMEOUT we don't care + if (strpos($e->getMessage(), 'TIMEOUT') === false) { + throw $e; + } + } finally { + $this->reset(); + $this->wasCleanedUp = true; + return true; + } + } + return false; + } + + /** + * @codeCoverageIgnore + */ + public function withSession($sessionId, $path = "/", $isSecure = false) + { + $this->setSession($sessionId, $path, $isSecure); + return $this; + } + + /** + * @codeCoverageIgnore + */ + public function getUrlTranslator() + { + return $this->urlTranslator; + } + + /** + * @codeCoverageIgnore + */ + public function getMouse() + { + return $this->getDriver()->getMouse(); + } + + /** + * Close the current window. + * + * @codeCoverageIgnore + * @return WebDriver The current instance. + */ + public function close() + { + return $this->getDriver()->close(); + } + + /** + * Get a string representing the current URL that the browser is looking at. + * + * @codeCoverageIgnore + * @return string The current URL. + */ + public function getCurrentURL() + { + return $this->getDriver()->getCurrentURL(); + } + + /** + * Get the source of the last loaded page. + * + * @codeCoverageIgnore + * @return string The current page source. + */ + public function getPageSource() + { + return $this->getDriver()->getPageSource(); + } + + /** + * Get the title of the current page. + * + * @codeCoverageIgnore + * @return string The title of the current page. + */ + public function getTitle() + { + return $this->getDriver()->getTitle(); + } + + /** + * Return an opaque handle to this window that uniquely identifies it within + * this driver instance. + * + * @codeCoverageIgnore + * @return string The current window handle. + */ + public function getWindowHandle() + { + return $this->getDriver()->getWindowHandle(); + } + + /** + * Get all window handles available to the current session. + * + * @codeCoverageIgnore + * @return array An array of string containing all available window handles. + */ + public function getWindowHandles() + { + return $this->getDriver()->getWindowHandles(); + } + + /** + * Quits this driver, closing every associated window. + * + * @codeCoverageIgnore + * @return void + */ + public function quit() + { + $this->getDriver()->quit(); + } + + /** + * Take a screenshot of the current page. + * + * @codeCoverageIgnore + * @param string $saveAs The path of the screenshot to be saved. + * @return string The screenshot in PNG format. + */ + public function takeScreenshot($saveAs = null) + { + return $this->getDriver()->takeScreenshot($saveAs); + } + + /** + * Construct a new WebDriverWait by the current WebDriver instance. + * Sample usage: + * + * $driver->wait(20, 1000)->until( + * WebDriverExpectedCondition::titleIs('WebDriver Page') + * ); + * + * @codeCoverageIgnore + * @param int $timeoutInSeconds + * @param int $intervalInMillisecond + * @return WebDriverWait + */ + public function wait($timeoutInSeconds = 30, $intervalInMillisecond = 250) + { + return $this->getDriver()->wait($timeoutInSeconds, $intervalInMillisecond); + } + + /** + * An abstraction for managing stuff you would do in a browser menu. For + * example, adding and deleting cookies. + * + * @codeCoverageIgnore + * @return WebDriverOptions + */ + public function manage() + { + return $this->getDriver()->manage(); + } + + /** + * An abstraction allowing the driver to access the browser's history and to + * navigate to a given URL. + * + * @codeCoverageIgnore + * @return WebDriverNavigation + * @see WebDriverNavigation + */ + public function navigate() + { + return $this->getDriver()->navigate(); + } + + /** + * Switch to a different window or frame. + * + * @codeCoverageIgnore + * @return WebDriverTargetLocator + * @see WebDriverTargetLocator + */ + public function switchTo() + { + return $this->getDriver()->switchTo(); + } + + /** + * @codeCoverageIgnore + * @param string $name + * @param array $params + * @return mixed + */ + public function execute($name, $params) + { + return $this->getDriver()->execute($name, $params); + } + + /** + * Find the first WebDriverElement within this element using the given + * mechanism. + * + * @codeCoverageIgnore + * @param WebDriverBy $locator + * @return WebDriverElement NoSuchElementException is thrown in + * HttpCommandExecutor if no element is found. + * @see WebDriverBy + */ + public function findElement(WebDriverBy $locator) + { + return $this->getDriver()->findElement($locator); + } + + /** + * Find all WebDriverElements within this element using the given mechanism. + * + * @codeCoverageIgnore + * @param WebDriverBy $locator + * @return array A list of all WebDriverElements, or an empty array if + * nothing matches + * @see WebDriverBy + */ + public function findElements(WebDriverBy $locator) + { + return $this->getDriver()->findElements($locator); + } + + /** + * Inject a snippet of JavaScript into the page for execution in the context + * of the currently selected frame. The executed script is assumed to be + * synchronous and the result of evaluating the script will be returned. + * + * @codeCoverageIgnore + * + * @param string $script The script to inject. + * @param array $arguments The arguments of the script. + * + * @return mixed The return value of the script. + */ + public function executeScript($script, array $arguments = []) + { + $this->getDriver()->executeScript($script, $arguments); + } + + /** + * Inject a snippet of JavaScript into the page for asynchronous execution in + * the context of the currently selected frame. + * + * The driver will pass a callback as the last argument to the snippet, and + * block until the callback is invoked. + * + * @see WebDriverExecuteAsyncScriptTestCase + * + * @codeCoverageIgnore + * + * @param string $script The script to inject. + * @param array $arguments The arguments of the script. + * + * @return mixed The value passed by the script to the callback. + */ + public function executeAsyncScript($script, array $arguments = []) + { + $this->getDriver()->executeAsyncScript($script, $arguments); + } + + /** + * @codeCoverageIgnore + * @return void + */ + private function reset() + { + $this->sessionCookie = null; + $this->remoteWebDriver = null; + $this->urlTranslator = null; + $this->remoteDriverBuilder = null; + } + + /** + * @codeCoverageIgnore + * @return RemoteWebDriver + */ + private function getDriver() + { + if (is_null($this->remoteWebDriver)) { + $this->remoteDriverBuilder->build(); + $this->remoteWebDriver = $this->remoteDriverBuilder->getRemoteWebDriver(); + $this->urlTranslator = $this->remoteDriverBuilder->getUrlTranslator(); + } + return $this->remoteWebDriver; + } +} + diff --git a/src/Browser/BrowserDriverBuilder.php b/src/Browser/BrowserDriverBuilder.php new file mode 100644 index 0000000..b7c2350 --- /dev/null +++ b/src/Browser/BrowserDriverBuilder.php @@ -0,0 +1,244 @@ +withType('phantomjs') + * ->build() + * ->getRemoteWebDriver(); + * + * @codeCoverageIgnore + */ +class BrowserDriverBuilder +{ + /** @var RemoteWebDriver */ + private $remoteWebDriver; + + /** @var UrlTranslator */ + private $urlTranslator; + + /** @var string */ + private $type; + + /** @var string */ + private $url; + + /** @var array */ + private $urls; + + /** @var array */ + private $extraCapabilities; + + /** @var int */ + private $implicitTimeout; + + /** @var int */ + private $connectionTimeout; + + /** @var int */ + private $requestTimeout; + + /** + * @param string $url + */ + public function __construct($url) + { + $this->url = $url; + $this->extraCapabilities = []; + $this->urls = []; + } + + public function __destruct() + { + $this->remoteWebDriver = null; + } + + /** + * Sets the given type and returns self. + * + * @param string $type + * + * @return $this + */ + public function withType($type) + { + $this->type = $type; + return $this; + } + + /** + * Sets the given proxy settings and returns self. + * + * @param array $proxySettings + * + * @return $this + */ + public function withProxySettings($proxySettings) + { + if (!empty($proxySettings)) { + $this->extraCapabilities[WebDriverCapabilityType::PROXY] = [ + 'proxyType' => $proxySettings['proxyType'], + 'httpProxy' => $proxySettings['httpProxy'], + 'sslProxy' => $proxySettings['sslProxy'], + ]; + } + return $this; + } + + /** + * Sets the given urls and returns self. + * + * @param $urls + * + * @return $this + */ + public function withUrls(array $urls) + { + $this->urls = $urls; + return $this; + } + + /** + * @todo Can we rename this to implicit wait, to keep the same lingo that Selenium uses? + * + * Sets the given implicit timeout and returns self. + * + * @param int $timeout + * + * @return $this + */ + public function withImplicitTimeout($timeout) + { + $this->implicitTimeout = $timeout; + return $this; + } + + /** + * Sets the given connection timeout and returns self. + * + * @param int $seconds + * + * @return $this + */ + public function withConnectionTimeout($seconds) + { + $this->connectionTimeout = $seconds ? $seconds * 1000 : null; + return $this; + } + + /** + * Sets the given request timeout and returns self. + * + * @param int $seconds + * + * @return $this + */ + public function withRequestTimeout($seconds) + { + $this->requestTimeout = $seconds ? $seconds * 1000 : null; + return $this; + } + + /** + * Sets the given extra capabilities and returns self. + * + * @param array $capabilities + * + * @return $this + */ + public function withExtraCapabilities(array $capabilities) + { + $this->extraCapabilities = array_merge($this->extraCapabilities, $capabilities); + return $this; + } + + /** + * Builds a Facebook RemoteWebDriver. + * + * @throws UnsupportedBrowserException + * + * @return $this + */ + public function build() + { + $capabilities = $this->makeCapabilities($this->type, $this->extraCapabilities); + + $this->remoteWebDriver = RemoteWebDriver::create( + $this->url, + $capabilities, + $this->connectionTimeout, + $this->requestTimeout + ); + + // define web driver configurations before being decorated + if ($this->implicitTimeout > 0) { + $this->remoteWebDriver->manage()->timeouts()->implicitlyWait($this->implicitTimeout); + } + + // translator + $baseUrlId = UrlTranslator::BASE_URL_IDENTIFIER; + $baseUrl = array_key_exists($baseUrlId, $this->urls) ? $this->urls[$baseUrlId] : null; + $this->urlTranslator = new UrlTranslator($this->urls, $baseUrl); + + return $this; + } + + /** + * Returns the driver built during the call to build(). + * + * @return RemoteWebDriver + */ + public function getRemoteWebDriver() + { + return $this->remoteWebDriver; + } + + /** + * @return UrlTranslator + */ + public function getUrlTranslator() + { + return $this->urlTranslator; + } + + /** + * @param string $browserType + * @param array $desiredCapabilities + * + * @return array + * @throws \OLX\FluentWebDriverClient\Exception\UnsupportedBrowserException + */ + private function makeCapabilities($browserType, $desiredCapabilities = []) + { + switch ($browserType) { + case 'chrome': + return array_merge( + DesiredCapabilities::chrome()->toArray(), + $desiredCapabilities + ); + case 'firefox': + return array_merge( + DesiredCapabilities::firefox()->toArray(), + $desiredCapabilities + ); + case 'phantomjs': + return array_merge( + DesiredCapabilities::phantomjs()->toArray(), + $desiredCapabilities + ); + default: + throw new UnsupportedBrowserException("Browser not supported '$browserType'"); + } + } +} + diff --git a/src/Browser/BrowserInterface.php b/src/Browser/BrowserInterface.php new file mode 100644 index 0000000..7d8781e --- /dev/null +++ b/src/Browser/BrowserInterface.php @@ -0,0 +1,92 @@ +elementFinder = $elementFinder; + } + + /** + * @return \Facebook\WebDriver\WebDriverSelect + * @throws \OLX\FluentWebDriverClient\Exception\ElementNotExpectedException + * @throws \OLX\FluentWebDriverClient\Exception\StopChainException + */ + public function asDropDown() + { + return $this->assert(function () { + return $this->elementFinder->asDropDown(); + }); + } + + /** + * @return \Facebook\WebDriver\Support\Events\EventFiringWebElement + */ + public function asHtmlElement() + { + return $this->assert(function () { + return $this->elementFinder->asHtmlElement(); + }); + } + + /** + * @codeCoverageIgnore + * @return \Facebook\WebDriver\Remote\RemoteWebDriver + */ + public function getBrowser() + { + return $this->elementFinder->getBrowser(); + } + + /** + * @codeCoverageIgnore + * @return \Facebook\WebDriver\WebDriverBy + */ + public function getSearchCriteria() + { + return $this->elementFinder->getSearchCriteria(); + } + + + /** + * @param \Closure $getElementClosure + * + * @return mixed + */ + abstract public function assert(Closure $getElementClosure); +} + diff --git a/src/Browser/Page/Element/Assertion/ElementDoesNotExistAssertion.php b/src/Browser/Page/Element/Assertion/ElementDoesNotExistAssertion.php new file mode 100644 index 0000000..a064451 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementDoesNotExistAssertion.php @@ -0,0 +1,16 @@ + 0) { + throw new \Exception('Expected element not to be found in the page [it was found %d time(s)]', sizeof($element)); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementExistsAssertion.php b/src/Browser/Page/Element/Assertion/ElementExistsAssertion.php new file mode 100644 index 0000000..7b13ba3 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementExistsAssertion.php @@ -0,0 +1,16 @@ +isSelected()) { + throw new \Exception('Failed asserting that element is deselected.'); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementIsDisabledAssertion.php b/src/Browser/Page/Element/Assertion/ElementIsDisabledAssertion.php new file mode 100644 index 0000000..d2b8ce4 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementIsDisabledAssertion.php @@ -0,0 +1,13 @@ +isEnabled()) { + throw new \Exception('Failed asserting that element is disabled.'); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementIsDisplayedAssertion.php b/src/Browser/Page/Element/Assertion/ElementIsDisplayedAssertion.php new file mode 100644 index 0000000..0ec4fb2 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementIsDisplayedAssertion.php @@ -0,0 +1,15 @@ +isDisplayed()) { + throw new ElementIsHiddenException('Failed asserting that element is displayed.'); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementIsEnabledAssertion.php b/src/Browser/Page/Element/Assertion/ElementIsEnabledAssertion.php new file mode 100644 index 0000000..5b716af --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementIsEnabledAssertion.php @@ -0,0 +1,15 @@ +isEnabled()) { + throw new ElementNotEnabledException('Failed asserting that element is enabled.'); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementIsHiddenAssertion.php b/src/Browser/Page/Element/Assertion/ElementIsHiddenAssertion.php new file mode 100644 index 0000000..12ccaf3 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementIsHiddenAssertion.php @@ -0,0 +1,15 @@ +isDisplayed()) { + throw new ElementIsVisibleException('Failed asserting that element is hidden.'); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementIsSelectedAssertion.php b/src/Browser/Page/Element/Assertion/ElementIsSelectedAssertion.php new file mode 100644 index 0000000..6973c32 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementIsSelectedAssertion.php @@ -0,0 +1,15 @@ +isSelected()) { + throw new ElementNotSelectedException('Failed asserting that element is selected.'); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementTextEqualsToAssertion.php b/src/Browser/Page/Element/Assertion/ElementTextEqualsToAssertion.php new file mode 100644 index 0000000..3aeebf6 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementTextEqualsToAssertion.php @@ -0,0 +1,38 @@ +expectedString = $expectedString; + + parent::__construct($elementFinder); + } + + /** + * @throws \UnexpectedValueException + */ + public function assert(\Closure $getElementClosure) + { + if (($element = $getElementClosure()) && $this->expectedString != $element->getText()) { + throw new \UnexpectedValueException( + sprintf( + "Expected element innerHTML to equal '%s' [actual innerHTML is '%s']", + $element->getText(), + $this->expectedString + ) + ); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementTextIsNotEqualToAssertion.php b/src/Browser/Page/Element/Assertion/ElementTextIsNotEqualToAssertion.php new file mode 100644 index 0000000..532d0c0 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementTextIsNotEqualToAssertion.php @@ -0,0 +1,38 @@ +expectedString = $expectedString; + + parent::__construct($elementFinder); + } + + /** + * @throws \UnexpectedValueException + */ + public function assert(\Closure $getElementClosure) + { + if (($element = $getElementClosure()) && $this->expectedString === $element->getText()) { + throw new \UnexpectedValueException( + sprintf( + "Expected element innerHTML not to equal '%s' [actual innerHTML is '%s']", + $element->getText(), + $this->expectedString + ) + ); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementValueEqualsToAssertion.php b/src/Browser/Page/Element/Assertion/ElementValueEqualsToAssertion.php new file mode 100644 index 0000000..b0f3301 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementValueEqualsToAssertion.php @@ -0,0 +1,38 @@ +expectedString = $expectedString; + + parent::__construct($elementFinder); + } + + /** + * @throws \UnexpectedValueException + */ + public function assert(\Closure $getElementClosure) + { + if (($element = $getElementClosure()) && $this->expectedString != $element->getAttribute('value')) { + throw new \UnexpectedValueException( + sprintf( + "Expected element value to equal '%s' [actual value is '%s']", + $element->getAttribute('value'), + $this->expectedString + ) + ); + } + } +} diff --git a/src/Browser/Page/Element/Assertion/ElementValueIsNotEqualToAssertion.php b/src/Browser/Page/Element/Assertion/ElementValueIsNotEqualToAssertion.php new file mode 100644 index 0000000..6bbc7f2 --- /dev/null +++ b/src/Browser/Page/Element/Assertion/ElementValueIsNotEqualToAssertion.php @@ -0,0 +1,38 @@ +expectedString = $expectedString; + + parent::__construct($elementFinder); + } + + /** + * @throws \UnexpectedValueException + */ + public function assert(\Closure $getElementClosure) + { + if (($element = $getElementClosure()) && $this->expectedString === $element->getAttribute('value')) { + throw new \UnexpectedValueException( + sprintf( + "Expected element value not to equal '%s' [actual value is '%s']", + $element->getAttribute('value'), + $this->expectedString + ) + ); + } + } +} diff --git a/src/Browser/Page/Element/ElementAction.php b/src/Browser/Page/Element/ElementAction.php new file mode 100644 index 0000000..921b768 --- /dev/null +++ b/src/Browser/Page/Element/ElementAction.php @@ -0,0 +1,90 @@ +get($url)->getElement()->withId('the-id') + * ->assertThat() + * ->isEnabled() + * ->thenFind() + * ->asHtmlElement(); + * + * // asynchronous assertion + * $browser->get($url)->getElement()->withId('the-id') + * ->wait(3) + * ->toBePresent() + * ->thenFind() + * ->asHtmlElement(); + * + * // simply filter and assign the selected RemoteWebElement to a variable + * $el = $browser->get($url)->getElement()->withId('the-id') + * ->thenFind() + * ->asHtmlElement(); + */ +class ElementAction +{ + /** + * @var \Facebook\WebDriver\WebDriverBy + */ + private $findBy; + + /** + * @var \Facebook\WebDriver\Remote\RemoteWebDriver + */ + private $browser; + + /** + * @param \Facebook\WebDriver\WebDriverBy $findBy + * @param BrowserInterface $browser + */ + public function __construct(WebDriverBy $findBy, BrowserInterface $browser) + { + $this->findBy = $findBy; + $this->browser = $browser; + } + + /** + * Allows the retrieval of the selected element. + * + * @return ElementFinderInterface + */ + public function thenFind() + { + return new ElementFind($this->findBy, $this->browser); + } + + /** + * Creates and returns a synchronous assertion context. + * + * @return ElementFindWithAssertions + */ + public function assertThat() + { + return new ElementFindWithAssertions($this->thenFind()); + } + + /** + * Creates and returns an asynchronous assertion context. + * + * @param int $timeInSeconds + * + * @return ElementFindWithWait + */ + public function wait($timeInSeconds) + { + return new ElementFindWithWait($timeInSeconds, $this->thenFind()); + } +} + diff --git a/src/Browser/Page/Element/ElementSelector.php b/src/Browser/Page/Element/ElementSelector.php new file mode 100644 index 0000000..53b3d51 --- /dev/null +++ b/src/Browser/Page/Element/ElementSelector.php @@ -0,0 +1,101 @@ +get($url)->getElement()->withName('some-name'); + * + * // by the element id property + * $browser->get($url)->getElement()->withId('some-id'); + * + * // by XPath selector + * $browser->get($url)->getElement()->withXpath('//p'); + * + * // by CSS selector + * $browser->get($url)->getElement()->withCss('#some-id p'); + * + * // by visible anchor text + * $browser->get($url)->getElement()->withLinkText('the link text'); + * + * @codeCoverageIgnore + */ +class ElementSelector +{ + /** @var \Facebook\WebDriver\Remote\RemoteWebDriver */ + private $browser; + + /** + * @param BrowserInterface $driver + */ + public function __construct(BrowserInterface $driver) + { + $this->browser = $driver; + } + + /** + * Filters elements by the element name property. + * + * @param string $name + * + * @return ElementAction + */ + public function withName($name) + { + return new ElementAction(WebDriverBy::name($name), $this->browser); + } + + /** + * Filters elements by the element id property. + * + * @param string $elementId + * + * @return ElementAction + */ + public function withId($elementId) + { + return new ElementAction(WebDriverBy::id($elementId), $this->browser); + } + + /** + * Filters elements by XPath selector. + * + * @param string $xPath + * + * @return ElementAction + */ + public function withXpath($xPath) + { + return new ElementAction(WebDriverBy::xpath($xPath), $this->browser); + } + + /** + * Filters elements by CSS selector. + * + * @param string $cssSelector + * + * @return ElementAction + */ + public function withCss($cssSelector) + { + return new ElementAction(WebDriverBy::cssSelector($cssSelector), $this->browser); + } + + /** + * Filters elements by visible anchor text. + * + * @param $linkText + * @return ElementAction + */ + public function withLinkText($linkText){ + return new ElementAction(WebDriverBy::linkText($linkText),$this->browser); + } +} + diff --git a/src/Browser/Page/Element/Find/ElementFind.php b/src/Browser/Page/Element/Find/ElementFind.php new file mode 100644 index 0000000..32e3c1d --- /dev/null +++ b/src/Browser/Page/Element/Find/ElementFind.php @@ -0,0 +1,73 @@ +findBy = $findBy; + $this->browser = $browser; + } + + /** + * @return \Facebook\WebDriver\WebDriverSelect + */ + public function asDropDown() + { + return new WebDriverSelect($this->findElement()); + } + + /** + * @return \Facebook\WebDriver\Support\Events\EventFiringWebElement + */ + public function asHtmlElement() + { + return $this->findElement(); + } + + /** + * @return \Facebook\WebDriver\Support\Events\EventFiringWebElement + */ + private function findElement() + { + return $this->browser->findElement($this->findBy); + } + + /** + * @internal + * @return \Facebook\WebDriver\Remote\RemoteWebDriver + */ + public function getBrowser() + { + return $this->browser; + } + + /** + * @internal + * @return \Facebook\WebDriver\WebDriverBy + */ + public function getSearchCriteria() + { + return $this->findBy; + } +} + diff --git a/src/Browser/Page/Element/Find/ElementFindWithAssertions.php b/src/Browser/Page/Element/Find/ElementFindWithAssertions.php new file mode 100644 index 0000000..af2e42f --- /dev/null +++ b/src/Browser/Page/Element/Find/ElementFindWithAssertions.php @@ -0,0 +1,197 @@ +get($url)->getElement()->withId('the-id') + * ->assertThat() + * ->isEnabled() + * ->thenFind() + * ->asHtmlElement(); + */ +class ElementFindWithAssertions +{ + /** @var ElementFinderInterface */ + private $elementFinder; + + public function __construct(ElementFinderInterface $elementFinder) + { + $this->elementFinder = $elementFinder; + } + + /** + * Asserts that the element exists in the DOM. + * + * @return $this + * @throws \Exception + */ + public function exists() + { + $this->elementFinder = new ElementExistsAssertion($this->elementFinder); + return $this; + } + + /** + * Asserts that the element does not exist in the DOM. + * + * @return $this + * @throws \Exception + */ + public function doesNotExist() + { + $this->elementFinder = new ElementDoesNotExistAssertion($this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and is visible to the user. + * + * @return $this + * @throws \AssertionError + */ + public function isDisplayed() + { + $this->elementFinder = new ElementIsDisplayedAssertion($this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and is not visible to the user. + * + * @return $this + * @throws \AssertionError + */ + public function isHidden() + { + $this->elementFinder = new ElementIsHiddenAssertion($this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and is enabled. + * + * @return $this + */ + public function isEnabled() + { + $this->elementFinder = new ElementIsEnabledAssertion($this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and is not enabled. + * + * @return $this + */ + public function isDisabled() + { + $this->elementFinder = new ElementIsDisabledAssertion($this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and is selected. + * + * @return $this + */ + public function isSelected() + { + $this->elementFinder = new ElementIsSelectedAssertion($this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and is not selected. + * + * @return $this + * @throws \Exception + */ + public function isDeselected() + { + $this->elementFinder = new ElementIsDeselectedAssertion($this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and its value equals the given value. + * + * @param string $expectedValue + * + * @return $this + * @throws \UnexpectedValueException + */ + public function valueEqualTo($expectedValue) + { + $this->elementFinder = new ElementValueEqualsToAssertion($expectedValue, $this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and its value does not equal the given value. + * + * @param string $expectedValue + * + * @return $this + * @throws \UnexpectedValueException + */ + public function valueIsNotEqualTo($value) + { + $this->elementFinder = new ElementValueIsNotEqualToAssertion($value, $this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and its text equals the given value. + * + * @param string $expectedValue + * + * @return $this + * @throws \UnexpectedValueException + */ + public function textEqualTo($expectedValue) + { + $this->elementFinder = new ElementTextEqualsToAssertion($expectedValue, $this->elementFinder); + return $this; + } + + /** + * Asserts that the element exists in the DOM and its text does not equal the given value. + * + * @param string $value + * + * @return $this + * @throws \UnexpectedValueException + */ + public function textIsNotEqualTo($value) + { + $this->elementFinder = new ElementTextIsNotEqualToAssertion($value, $this->elementFinder); + return $this; + } + + /** + * Allows the retrieval of the selected element. + * + * @return ElementFinderInterface + */ + public function thenFind() + { + return $this->elementFinder; + } +} diff --git a/src/Browser/Page/Element/Find/ElementFindWithWait.php b/src/Browser/Page/Element/Find/ElementFindWithWait.php new file mode 100644 index 0000000..f47b478 --- /dev/null +++ b/src/Browser/Page/Element/Find/ElementFindWithWait.php @@ -0,0 +1,126 @@ +get($url)->getElement()->withId('the-id'); + * ->wait(3) + * ->toBePresent() + * ->thenFind() + * ->asHtmlElement(); + */ +class ElementFindWithWait +{ + /** @var int */ + private $timeInSeconds; + + /** + * @var ElementFinderInterface + */ + private $elementFinder; + + /** + * @param int $timeInSeconds Timeout in seconds + * @param ElementFinderInterface $elementFinder Element finder + */ + public function __construct($timeInSeconds, ElementFinderInterface $elementFinder) + { + $this->timeInSeconds = $timeInSeconds; + $this->elementFinder = $elementFinder; + } + + /** + * Asserts that the element exists in the DOM. + * + * @return $this + * @throws \Facebook\WebDriver\Exception\NoSuchElementException + * @throws \Facebook\WebDriver\Exception\TimeOutException + */ + public function toBePresent() + { + $this->wait()->until(WebDriverExpectedCondition::presenceOfElementLocated($this->elementFinder->getSearchCriteria())); + + return $this; + } + + /** + * Asserts that the elements exists in the DOM and is visible to the user. + * + * @return $this + * @throws \Facebook\WebDriver\Exception\NoSuchElementException + * @throws \Facebook\WebDriver\Exception\TimeOutException + */ + public function toBeVisible() + { + $this->wait()->until(WebDriverExpectedCondition::visibilityOfElementLocated($this->elementFinder->getSearchCriteria())); + + return $this; + } + + /** + * Asserts that the element exists in the DOM and is not visible to the user. + * + * @return $this + * @throws \Facebook\WebDriver\Exception\NoSuchElementException + * @throws \Facebook\WebDriver\Exception\TimeOutException + */ + public function toBeInvisible() + { + $this->wait()->until(WebDriverExpectedCondition::invisibilityOfElementLocated($this->elementFinder->getSearchCriteria())); + + return $this; + } + + /** + * Asserts that the element exists in the DOM and is clickable by the user. + * + * @return $this + * @throws \Facebook\WebDriver\Exception\NoSuchElementException + * @throws \Facebook\WebDriver\Exception\TimeOutException + */ + public function toBeClickable() + { + $this->wait()->until(WebDriverExpectedCondition::elementToBeClickable($this->elementFinder->getSearchCriteria())); + + return $this; + } + + /** + * Asserts that the element exists in the DOM and is selected. + * + * @return $this + * @throws \Facebook\WebDriver\Exception\NoSuchElementException + * @throws \Facebook\WebDriver\Exception\TimeOutException + */ + public function toBeSelected() + { + $this->wait()->until(WebDriverExpectedCondition::elementToBeSelected($this->elementFinder->getSearchCriteria())); + + return $this; + } + + /** + * @return \Facebook\WebDriver\WebDriverWait + */ + private function wait() + { + return $this->elementFinder->getBrowser()->wait($this->timeInSeconds, 250); + } + + /** + * Allows the retrieval of the selected element. + * + * @return ElementFinderInterface + */ + public function thenFind() + { + return $this->elementFinder; + } +} + diff --git a/src/Browser/Page/Element/Find/ElementFinderInterface.php b/src/Browser/Page/Element/Find/ElementFinderInterface.php new file mode 100644 index 0000000..d96138c --- /dev/null +++ b/src/Browser/Page/Element/Find/ElementFinderInterface.php @@ -0,0 +1,28 @@ +") + { + $this->criteria = $criteria; + $this->criteriaDescription = $criteriaDescription; + } + + + public function decorate($targetClosure, $locator) + { + $elements = $targetClosure(); + $nrOfElements = sizeof($elements); + + if ($nrOfElements === 0) { + throw new EmptyResultException('No elements found.'); + } + + if (!is_array($elements)) { + throw new NotAnArrayException('Elements is not an array.'); + } + + $criteria = $this->criteria; + $nrOfElementsThatApply = array_reduce($elements, function ($carry, RemoteWebElement $currentElement) use ($criteria) { + return $criteria($currentElement) === true ? $carry + 1 : $carry; + }); + + if ($nrOfElements !== $nrOfElementsThatApply) { + throw new NotAllElementsApplyToCriteriaException( + sprintf( + "Number of elements that are '%s' [%d] is different that total of elements [%d]", + $this->criteriaDescription, + $nrOfElementsThatApply, + $nrOfElements + ) + ); + } + + return true; + } +} + diff --git a/src/Browser/Page/Find/Assertion/AllElementsAreHiddenAssertion.php b/src/Browser/Page/Find/Assertion/AllElementsAreHiddenAssertion.php new file mode 100644 index 0000000..0f832c3 --- /dev/null +++ b/src/Browser/Page/Find/Assertion/AllElementsAreHiddenAssertion.php @@ -0,0 +1,18 @@ +isDisplayed(); + }, 'hidden'); + } +} + diff --git a/src/Browser/Page/Find/Assertion/AllElementsAreVisibleAssertion.php b/src/Browser/Page/Find/Assertion/AllElementsAreVisibleAssertion.php new file mode 100644 index 0000000..afa4913 --- /dev/null +++ b/src/Browser/Page/Find/Assertion/AllElementsAreVisibleAssertion.php @@ -0,0 +1,18 @@ +isDisplayed(); + }, 'visible'); + } +} + diff --git a/src/Browser/Page/Find/Assertion/AtLeastOneElementAppliesToAssertion.php b/src/Browser/Page/Find/Assertion/AtLeastOneElementAppliesToAssertion.php new file mode 100644 index 0000000..354f43a --- /dev/null +++ b/src/Browser/Page/Find/Assertion/AtLeastOneElementAppliesToAssertion.php @@ -0,0 +1,56 @@ +') + { + $this->criteria = $criteria; + $this->criteriaDescription = $criteriaDescription; + } + + public function decorate($targetClosure, $locator) + { + $elements = $targetClosure(); + $nrOfElements = sizeof($elements); + + if ($nrOfElements === 0) { + throw new \Exception('No elements found.'); + } + + if (!is_array($elements)) { + throw new \Exception('Elements is not an array.'); + } + + $criteria = $this->criteria; + $nrOfElementsThatApply = array_reduce($elements, function ($carry, RemoteWebElement $currentElement) use ($criteria) { + return $criteria($currentElement) === true ? $carry + 1 : $carry; + }, 0); + + if ($nrOfElementsThatApply === 0) { + throw new NoElementAppliesToCriteriaException(sprintf("No element applies to the criteria '%s'", $this->criteriaDescription)); + } + + return true; + } +} + diff --git a/src/Browser/Page/Find/Assertion/ElementExistsAtLeastOnceAssertion.php b/src/Browser/Page/Find/Assertion/ElementExistsAtLeastOnceAssertion.php new file mode 100644 index 0000000..5a5d293 --- /dev/null +++ b/src/Browser/Page/Find/Assertion/ElementExistsAtLeastOnceAssertion.php @@ -0,0 +1,18 @@ + 0) { + throw new ElementNotExpectedException( + sprintf("Expected element should not exist on the page and was found '%d' time(s)", $count) + ); + } + } catch (NoSuchElementException $e) { + throw new StopChainException(); + } + } // @codeCoverageIgnore +} diff --git a/src/Browser/Page/Find/Assertion/ElementTextEqualsAssertion.php b/src/Browser/Page/Find/Assertion/ElementTextEqualsAssertion.php new file mode 100644 index 0000000..06e6367 --- /dev/null +++ b/src/Browser/Page/Find/Assertion/ElementTextEqualsAssertion.php @@ -0,0 +1,31 @@ +expectedText = $compareToText; + } + + public function decorate($targetClosure, $locator) + { + $text = $targetClosure()->getText(); + if ($text != $this->expectedText) { + throw new UnexpectedValueException( + sprintf("Element's text is different than expected. Found '%s' instead of '%s'", $text, $this->expectedText) + ); + } + return true; + } +} + diff --git a/src/Browser/Page/Find/Assertion/ElementValueEqualsAssertion.php b/src/Browser/Page/Find/Assertion/ElementValueEqualsAssertion.php new file mode 100644 index 0000000..0328f9c --- /dev/null +++ b/src/Browser/Page/Find/Assertion/ElementValueEqualsAssertion.php @@ -0,0 +1,37 @@ +expectedText = $compareToValue; + } + + public function decorate($targetClosure, $locator) + { + $element = $targetClosure(); + + if (is_array($element)) { + throw new \Exception('Element should not be an array'); + } + + $text = strtolower($element->getTagName()) == "textarea" ? $element->getValue() : $element->getAttribute("value"); + if ($text != $this->expectedText) { + throw new UnexpectedValueException( + sprintf("Element's value is different than expected. Found '%s' instead of '%s'", $text, $this->expectedText) + ); + } + return true; + } +} + diff --git a/src/Browser/Page/Find/Decorator/AbstractPageFinderDecorator.php b/src/Browser/Page/Find/Decorator/AbstractPageFinderDecorator.php new file mode 100644 index 0000000..c1ea86d --- /dev/null +++ b/src/Browser/Page/Find/Decorator/AbstractPageFinderDecorator.php @@ -0,0 +1,162 @@ +pageFinder = $pageFinder; + $this->decorators = []; + } + + /** + * @codeCoverageIgnore + * @param $name + * @return RemoteWebElement + */ + public function elementWithName($name) + { + return $this->makeDecorateMethod('elementWithName', $name, WebDriverBy::name($name)); + } + + /** + * @codeCoverageIgnore + * @param $name + * @return RemoteWebElement[] + */ + public function elementsWithName($name) + { + return $this->makeDecorateMethod('elementsWithName', $name, WebDriverBy::name($name)); + } + + /** + * @codeCoverageIgnore + * @param $id + * @return RemoteWebElement + */ + public function elementWithId($id) + { + return $this->makeDecorateMethod('elementWithId', $id, WebDriverBy::id($id)); + } + + /** + * @codeCoverageIgnore + * @param $id + * @return RemoteWebElement[] + */ + public function elementsWithId($id) + { + return $this->makeDecorateMethod('elementsWithId', $id, WebDriverBy::id($id)); + } + + /** + * @codeCoverageIgnore + * @param $css + * @return RemoteWebElement + */ + public function elementWithCss($css) + { + return $this->makeDecorateMethod('elementWithCss', $css, WebDriverBy::cssSelector($css)); + } + + /** + * @codeCoverageIgnore + * @param $css + * @return RemoteWebElement[] + */ + public function elementsWithCss($css) + { + return $this->makeDecorateMethod('elementsWithCss', $css, WebDriverBy::cssSelector($css)); + } + + /** + * @codeCoverageIgnore + * @param $xpath + * @return RemoteWebElement + */ + public function elementWithXpath($xpath) + { + return $this->makeDecorateMethod('elementWithXpath', $xpath, WebDriverBy::xpath($xpath)); + } + + /** + * @codeCoverageIgnore + * @param $xpath + * @return RemoteWebElement[] + */ + public function elementsWithXpath($xpath) + { + return $this->makeDecorateMethod('elementsWithXpath', $xpath, WebDriverBy::xpath($xpath)); + } + + + /** + * @codeCoverageIgnore + * @param $methodName + * @param $value + * @param $locator + * @return mixed + */ + protected function makeDecorateMethod($methodName, $value, $locator) + { + return $this->decorate( + function () use ($methodName, $value) { + return $this->pageFinder->$methodName($value); + }, + $locator + ); + } + + + /** + * @codeCoverageIgnore + * @return PageFinderInterface + */ + public function getPageFinder() + { + return $this->pageFinder; + } + + /** + * @param TargetDecoratorInterface $decorator + */ + protected function registerDecorator(TargetDecoratorInterface $decorator) + { + $this->decorators[] = $decorator; + } + + /** + * @param $targetClosure + * @param $locator + * @return mixed + */ + protected function decorate($targetClosure, $locator) + { + try { + $closureHolder = new ClosureHolder($targetClosure); + foreach ($this->decorators as $decorator) { + $decorator->decorate($closureHolder, $locator); + } + } catch (StopChainException $e) { + return null; + } + + return $closureHolder->get(); + } +} + diff --git a/src/Browser/Page/Find/Decorator/CachedPageFinderDecorator.php b/src/Browser/Page/Find/Decorator/CachedPageFinderDecorator.php new file mode 100644 index 0000000..ee854b7 --- /dev/null +++ b/src/Browser/Page/Find/Decorator/CachedPageFinderDecorator.php @@ -0,0 +1,144 @@ +pageFinder = $pageFinder; + $this->cache = [ + static::BY_NAME => [], + static::BY_ID => [], + static::BY_CSS => [], + static::BY_XPATH => [] + ]; + } + + /** + * @codeCoverageIgnore + * @param $name + * @return RemoteWebElement + */ + public function elementWithName($name) + { + return $this->get('elementWithName', $name, static::BY_NAME); + } + + /** + * @codeCoverageIgnore + * @param $name + * @return RemoteWebElement[] + */ + public function elementsWithName($name) + { + return $this->get('elementsWithName', $name, static::BY_NAME); + } + + /** + * @codeCoverageIgnore + * @param $id + * @return RemoteWebElement + */ + public function elementWithId($id) + { + return $this->get('elementWithId', $id, static::BY_ID); + } + + /** + * @codeCoverageIgnore + * @param $id + * @return RemoteWebElement[] + */ + public function elementsWithId($id) + { + return $this->get('elementsWithId', $id, static::BY_ID); + } + + /** + * @codeCoverageIgnore + * @param $css + * @return RemoteWebElement + */ + public function elementWithCss($css) + { + return $this->get('elementWithCss', $css, static::BY_CSS); + } + + /** + * @codeCoverageIgnore + * @param $css + * @return RemoteWebElement[] + */ + public function elementsWithCss($css) + { + return $this->get('elementsWithCss', $css, static::BY_CSS); + } + + /** + * @codeCoverageIgnore + * @param $xpath + * @return RemoteWebElement + */ + public function elementWithXpath($xpath) + { + return $this->get('elementWithXpath', $xpath, static::BY_XPATH); + } + + /** + * @codeCoverageIgnore + * @param $xpath + * @return RemoteWebElement[] + */ + public function elementsWithXpath($xpath) + { + return $this->get('elementsWithXpath', $xpath, static::BY_XPATH); + } + + /** + * @param $method + * @param $identifier + * @param $bucket + * @return RemoteWebElement + */ + protected function get($method, $identifier, $bucket) + { + $key = $method . '_' . $identifier; + if (array_key_exists($key, $this->cache[$bucket])) { + return $this->cache[$bucket][$key]; + } + $this->cache[$bucket][$key] = $this->pageFinder->$method($identifier); + return $this->cache[$bucket][$key]; + } + + /** + * @codeCoverageIgnore + * @return BrowserInterface + */ + public function getBrowser() + { + return $this->pageFinder->getBrowser(); + } +} + diff --git a/src/Browser/Page/Find/Decorator/ClosureHolder.php b/src/Browser/Page/Find/Decorator/ClosureHolder.php new file mode 100644 index 0000000..495fdd3 --- /dev/null +++ b/src/Browser/Page/Find/Decorator/ClosureHolder.php @@ -0,0 +1,56 @@ +closure = $closure; + $this->instance = null; + $this->hasBeenInvoked = false; + } + + /** + * The __invoke method is called when a script tries to call an object as a function. + * + * @return mixed + * @link http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.invoke + */ + public function __invoke() + { + return $this->get(); + } + + /** + * @return mixed + */ + public function get() + { + if (!$this->hasBeenInvoked) { + $closure = $this->closure; + $this->instance = $closure(); + $this->hasBeenInvoked = true; + } + return $this->instance; + } +} + diff --git a/src/Browser/Page/Find/Decorator/PageFinderWithAssertions.php b/src/Browser/Page/Find/Decorator/PageFinderWithAssertions.php new file mode 100644 index 0000000..1871f33 --- /dev/null +++ b/src/Browser/Page/Find/Decorator/PageFinderWithAssertions.php @@ -0,0 +1,115 @@ +registerDecorator(new ElementExistsOnlyOnceAssertion()); + return $this; + } + + /** + * @return $this + */ + public function existsAtLeastOnce() + { + $this->registerDecorator(new ElementExistsAtLeastOnceAssertion()); + return $this; + } + + /** + * @return $this + */ + public function doesNotExist() + { + $this->registerDecorator(new ElementShouldNotExistAssertion()); + return $this; + } + + /** + * @param $expectedText + * @return $this + */ + public function textEquals($expectedText) + { + $this->registerDecorator(new ElementTextEqualsAssertion($expectedText)); + return $this; + } + + /** + * @param $expectedValue + * @return $this + */ + public function valueEquals($expectedValue) + { + $this->registerDecorator(new ElementValueEqualsAssertion($expectedValue)); + return $this; + } + + /** + * @param callable $criteria + * @param string $criteriaDescription + * @return $this + */ + public function allApplyTo(callable $criteria, $criteriaDescription = '') + { + $this->registerDecorator(new AllElementsApplyToAssertion($criteria, $criteriaDescription)); + return $this; + } + + /** + * @param callable $criteria + * @param string $criteriaDescription + * @return $this + */ + public function anyAppliesTo(callable $criteria, $criteriaDescription = '') + { + $this->registerDecorator(new AtLeastOneElementAppliesToAssertion($criteria, $criteriaDescription)); + return $this; + } + + /** + * @return $this + */ + public function allAreVisible() + { + $this->registerDecorator(new AllElementsAreVisibleAssertion()); + return $this; + } + + /** + * @return $this + */ + public function allAreHidden() + { + $this->registerDecorator(new AllElementsAreHiddenAssertion()); + return $this; + } + + /** + * @return BrowserInterface + */ + public function getBrowser() + { + return $this->getPageFinder()->getBrowser(); + } +} + diff --git a/src/Browser/Page/Find/Decorator/PageFinderWithWaits.php b/src/Browser/Page/Find/Decorator/PageFinderWithWaits.php new file mode 100644 index 0000000..2c25dd7 --- /dev/null +++ b/src/Browser/Page/Find/Decorator/PageFinderWithWaits.php @@ -0,0 +1,91 @@ +timeOutInSeconds = $timeOutInSeconds; + } + + /** + * @return $this + */ + public function untilPresenceOf() + { + $this->registerDecorator(new WaitUntilPresence($this->getPageFinder()->getBrowser(), $this->timeOutInSeconds)); + return $this; + } + + /** + * @return $this + */ + public function untilAbsenceOf() + { + $this->registerDecorator(new WaitUntilAbsence($this->getPageFinder()->getBrowser(), $this->timeOutInSeconds)); + return $this; + } + + /** + * @return $this + */ + public function untilVisibilityOf() + { + $this->registerDecorator(new WaitUntilVisibility($this->getPageFinder()->getBrowser(), $this->timeOutInSeconds)); + return $this; + } + + /** + * @return $this + */ + public function untilClickabilityOf() + { + $this->registerDecorator(new WaitUntilClickable($this->getPageFinder()->getBrowser(), $this->timeOutInSeconds)); + return $this; + } + + /** + * @return BrowserInterface + */ + public function getBrowser() + { + return $this->getPageFinder()->getBrowser(); + } + + /** + * @param WebDriverExpectedCondition $expectedCondition + * + * @return void + * + * @throws \Exception + * @throws \Facebook\WebDriver\Exception\NoSuchElementException + * @throws \Facebook\WebDriver\Exception\TimeOutException + */ + public function forExpectedCondition(WebDriverExpectedCondition $expectedCondition, $message = "") + { + $this->getBrowser()->wait($this->timeOutInSeconds)->until($expectedCondition, $message); + } +} + diff --git a/src/Browser/Page/Find/Decorator/TargetDecoratorInterface.php b/src/Browser/Page/Find/Decorator/TargetDecoratorInterface.php new file mode 100644 index 0000000..a4ed1a0 --- /dev/null +++ b/src/Browser/Page/Find/Decorator/TargetDecoratorInterface.php @@ -0,0 +1,18 @@ +browser = $browser; + } + + /** + * @param $name + * @return RemoteWebElement + */ + public function elementWithName($name) + { + return $this->browser->findElement(WebDriverBy::name($name)); + } + + /** + * @param $name + * @return RemoteWebElement[] + */ + public function elementsWithName($name) + { + return $this->browser->findElements(WebDriverBy::name($name)); + } + + /** + * @param $id + * @return RemoteWebElement + */ + public function elementWithId($id) + { + return $this->browser->findElement(WebDriverBy::id($id)); + } + + /** + * @param $id + * @return RemoteWebElement[] + */ + public function elementsWithId($id) + { + return $this->browser->findElements(WebDriverBy::id($id)); + } + + /** + * @param $css + * @return RemoteWebElement + */ + public function elementWithCss($css) + { + return $this->browser->findElement(WebDriverBy::cssSelector($css)); + } + + /** + * @param $css + * @return RemoteWebElement[] + */ + public function elementsWithCss($css) + { + return $this->browser->findElements(WebDriverBy::cssSelector($css)); + } + + /** + * @param $xpath + * @return RemoteWebElement + */ + public function elementWithXpath($xpath) + { + return $this->browser->findElement(WebDriverBy::xpath($xpath)); + } + + /** + * @param $xpath + * @return RemoteWebElement[] + */ + public function elementsWithXpath($xpath) + { + return $this->browser->findElements(WebDriverBy::xpath($xpath)); + } + + /** + * @return BrowserInterface + */ + public function getBrowser() + { + return $this->browser; + } +} + diff --git a/src/Browser/Page/Find/PageFinderBuilder.php b/src/Browser/Page/Find/PageFinderBuilder.php new file mode 100644 index 0000000..9486188 --- /dev/null +++ b/src/Browser/Page/Find/PageFinderBuilder.php @@ -0,0 +1,78 @@ +browser = $remoteWebDriver; + $this->isWithWaits = false; + $this->isWithAssertions = false; + } + + /** + * Sets assertions to true and returns self. + * + * @return $this + */ + public function withAssertions() + { + $this->isWithAssertions = true; + + return $this; + } + + /** + * Sets waits to true, sets the timeout in seconds and returns self. + * + * @param int $timeOutInSeconds + * @return $this + */ + public function withWaits($timeOutInSeconds) + { + $this->isWithWaits = true; + $this->timeOutInSeconds = $timeOutInSeconds; + + return $this; + } + + /** + * Creates and returns a new PageFinder object according to the current builder state. + * + * @return PageFinderInterface + */ + public function build() + { + $pageFinder = new PageFinder($this->browser); + + if ($this->isWithAssertions) { + $pageFinder = new PageFinderWithAssertions($pageFinder); + } + + if ($this->isWithWaits) { + $pageFinder = new PageFinderWithWaits($pageFinder, $this->timeOutInSeconds); + } + + return $pageFinder; + } +} diff --git a/src/Browser/Page/Find/PageFinderInterface.php b/src/Browser/Page/Find/PageFinderInterface.php new file mode 100644 index 0000000..63ed252 --- /dev/null +++ b/src/Browser/Page/Find/PageFinderInterface.php @@ -0,0 +1,67 @@ +browser = $browser; + $this->timeOutInSeconds = $timeOutInSeconds; + } + + /** + * @param $targetClosure + * @param $locator + * + * @return bool + * @throws CriteriaNotMetException + */ + public function decorate($targetClosure, $locator) + { + try { + $validator = function () use ($targetClosure, $locator) { + return $this->validate($targetClosure, $locator); // @codeCoverageIgnore + }; + + $this->browser->wait($this->timeOutInSeconds, 50)->until($validator); + return true; + } catch (\Exception $e) { + throw new CriteriaNotMetException($e->getMessage()); + } + } + + /** + * @param $targetClosure + * @param null $locator + * @return mixed + */ + abstract protected function validate($targetClosure, $locator = null); +} + diff --git a/src/Browser/Page/Find/Wait/WaitUntilAbsence.php b/src/Browser/Page/Find/Wait/WaitUntilAbsence.php new file mode 100644 index 0000000..5574bf4 --- /dev/null +++ b/src/Browser/Page/Find/Wait/WaitUntilAbsence.php @@ -0,0 +1,18 @@ +timeOutInSeconds) + ); + } + return true; + } +} + diff --git a/src/Browser/Page/Find/Wait/WaitUntilClickable.php b/src/Browser/Page/Find/Wait/WaitUntilClickable.php new file mode 100644 index 0000000..8e731a3 --- /dev/null +++ b/src/Browser/Page/Find/Wait/WaitUntilClickable.php @@ -0,0 +1,23 @@ +timeOutInSeconds) + ); + } + } +} + diff --git a/src/Browser/Page/Find/Wait/WaitUntilPresence.php b/src/Browser/Page/Find/Wait/WaitUntilPresence.php new file mode 100644 index 0000000..41c64ff --- /dev/null +++ b/src/Browser/Page/Find/Wait/WaitUntilPresence.php @@ -0,0 +1,18 @@ +timeOutInSeconds) + ); + } + return true; + } +} + diff --git a/src/Browser/Page/Find/Wait/WaitUntilVisibility.php b/src/Browser/Page/Find/Wait/WaitUntilVisibility.php new file mode 100644 index 0000000..2afce35 --- /dev/null +++ b/src/Browser/Page/Find/Wait/WaitUntilVisibility.php @@ -0,0 +1,24 @@ +timeOutInSeconds) + ); + } + } +} + diff --git a/src/Browser/Page/Page.php b/src/Browser/Page/Page.php new file mode 100644 index 0000000..6079df9 --- /dev/null +++ b/src/Browser/Page/Page.php @@ -0,0 +1,100 @@ +browser = $browser; + } + + /** + * @return PageFinderInterface + */ + public function find() + { + return (new PageFinderBuilder($this->browser))->build(); + } + + /** + * @return PageFinderWithAssertions + */ + public function findAndAssertThat() + { + return (new PageFinderBuilder($this->browser)) + ->withAssertions() + ->build(); + } + + /** + * @param $timeOutInSeconds + * @return PageFinderWithWaits + */ + public function wait($timeOutInSeconds) + { + return (new PageFinderBuilder($this->browser)) + ->withWaits($timeOutInSeconds) + ->build(); + } + + /** + * @return \OLX\FluentWebDriverClient\Browser\Page\Element\ElementSelector + */ + public function getElement() + { + return new ElementSelector($this->browser); + } + + /** + * Inject a snippet of JavaScript into the page for execution in the context + * of the currently selected frame. The executed script is assumed to be + * synchronous and the result of evaluating the script will be returned. + * + * @param string $script The script to inject. + * @param array $arguments The arguments of the script. + * @return mixed The return value of the script. + */ + public function executeScript($script, array $arguments = array()) + { + return $this->browser->executeScript($script, $arguments); + } + + /** + * Inject a snippet of JavaScript into the page for asynchronous execution in + * the context of the currently selected frame. + * + * The driver will pass a callback as the last argument to the snippet, and + * block until the callback is invoked. + * + * @see WebDriverExecuteAsyncScriptTestCase + * + * @param string $script The script to inject. + * @param array $arguments The arguments of the script. + * @return mixed The value passed by the script to the callback. + */ + public function executeAsyncScript($script, array $arguments = array()) + { + return $this->browser->executeAsyncScript($script, $arguments); + } +} + diff --git a/src/Browser/Page/PageInterface.php b/src/Browser/Page/PageInterface.php new file mode 100644 index 0000000..bf089ce --- /dev/null +++ b/src/Browser/Page/PageInterface.php @@ -0,0 +1,38 @@ +urlMappings = $urlMappings; + $this->urlMappings[static::BASE_URL_IDENTIFIER] = $baseUrl; + $this->baseUrl = $baseUrl; + $this->fullUrls = []; + } + + /** + * @param $url + * @return string + */ + public function get($url) + { + // direct url + if (filter_var($url, FILTER_VALIDATE_URL) !== false) { + return $url; + } + + // already stored + if (array_key_exists($url, $this->fullUrls)) { + return $this->fullUrls[$url]; + } + + // convert, store and return + return $this->fullUrls[$url] = $this->getUrl($url); + } + + /** + * @codeCoverageIgnore + * @param $url + * @return string + * @throws InvalidUrlException + */ + private function getUrl($url) + { + if ($this->isBaseUrl($url)) { + return $this->validateUrlAndReturn($this->baseUrl); + } + + if ($this->isRelativeUrl($url)) { + return $this->getRelativeUrl($url); + } + + return $this->getUrlFromKey($url); + } + + /** + * @codeCoverageIgnore + * @param $url + * + * @return bool + * @throws InvalidUrlException + */ + private function isBaseUrl($url) + { + if ($url === static::BASE_URL_IDENTIFIER + || array_key_exists($url, $this->urlMappings) && $this->baseUrl === $this->urlMappings[$url] + ) { + return true; + } + + return false; + } + + /** + * @codeCoverageIgnore + * @param $url + * @return bool + */ + private function isRelativeUrl($url) + { + return preg_match('/^\//', $url) === 1; + } + + /** + * @codeCoverageIgnore + * @param $url + * @return mixed + * @throws InvalidUrlException + */ + private function getRelativeUrl($url) + { + $url = $this->baseUrl . $url; + return $this->validateUrlAndReturn($url); + } + + /** + * @codeCoverageIgnore + * @param $key + * @return mixed + * @throws InvalidUrlException + */ + private function getUrlFromKey($key) + { + if (!array_key_exists($key, $this->urlMappings)) { + throw new InvalidUrlException("URL is invalid '$key'"); + } + + $url = $this->baseUrl . $this->urlMappings[$key]; + return $this->validateUrlAndReturn($url); + } + + /** + * @codeCoverageIgnore + * @param $url + * @return mixed + * @throws InvalidUrlException + */ + private function validateUrlAndReturn($url) + { + if (filter_var($url, FILTER_VALIDATE_URL) === false) { + throw new InvalidUrlException("URL is invalid '$url'"); + } + return $url; + } +} + diff --git a/tests/Helpers/ElementAssertionHelperTrait.php b/tests/Helpers/ElementAssertionHelperTrait.php new file mode 100644 index 0000000..8201e65 --- /dev/null +++ b/tests/Helpers/ElementAssertionHelperTrait.php @@ -0,0 +1,15 @@ +mockElement = \Phake::mock(RemoteWebElement::class); + } +} diff --git a/tests/unit/Browser/BrowserTest.php b/tests/unit/Browser/BrowserTest.php new file mode 100644 index 0000000..598e9c6 --- /dev/null +++ b/tests/unit/Browser/BrowserTest.php @@ -0,0 +1,73 @@ +mockBrowserDriverBuilder = \Phake::mock(BrowserDriverBuilder::class); + $this->mockRemoteWebDriver = \Phake::mock(RemoteWebDriver::class); + $this->mockUrlTranslator = \Phake::mock(UrlTranslator::class); + $this->mockWebDriverOptions = \Phake::mock(WebDriverOptions::class); + + \Phake::when($this->mockRemoteWebDriver)->manage()->thenReturn($this->mockWebDriverOptions); + + \Phake::when($this->mockBrowserDriverBuilder)->getRemoteWebDriver()->thenReturn($this->mockRemoteWebDriver); + \Phake::when($this->mockBrowserDriverBuilder)->getUrlTranslator()->thenReturn($this->mockUrlTranslator); + + $this->browser = new Browser($this->mockBrowserDriverBuilder); + } + + public function testGet_ShouldProxyRequestToDriver() + { + $path = '/page.html'; + $fullUrl = 'http://example.com/page.html'; + + \Phake::when($this->mockUrlTranslator)->get($path)->thenReturn($fullUrl); + + $this->assertInstanceOf(Page::class, $this->browser->get('/page.html')); + + \Phake::verify($this->mockUrlTranslator)->get($path); + \Phake::verify($this->mockRemoteWebDriver)->get($fullUrl); + } + + public function testSetSession_ShouldDeleteExistingCookieAndAddNewCookieToDriver() + { + $sessionId = 'the id'; + $path = '/test'; + $isSecure = true; + + $this->browser->setSession($sessionId, $path, $isSecure); + + \Phake::verify($this->mockWebDriverOptions)->deleteCookieNamed('PHPSESSID'); + + \Phake::verify($this->mockWebDriverOptions)->addCookie([ + 'name' => 'PHPSESSID', + 'value' => $sessionId, + 'path' => $path, + 'secure' => $isSecure, + ]); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/AbstractElementAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/AbstractElementAssertionTest.php new file mode 100644 index 0000000..4124c4a --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/AbstractElementAssertionTest.php @@ -0,0 +1,41 @@ +mockElementFinder = \Phake::mock(ElementFinderInterface::class); + $this->assertion = new SampleAssertion($this->mockElementFinder); + } + + public function testAsDropDown_ShouldProxyCallToElementFinder() + { + $this->assertion->asDropDown(); + \Phake::verify($this->mockElementFinder, \Phake::times(1))->asDropDown(); + } + + public function testAsHtmlElement_ShouldProxyCallToElementFinder() + { + $this->assertion->asHtmlElement(); + \Phake::verify($this->mockElementFinder, \Phake::times(1))->asHtmlElement(); + } +} + +class SampleAssertion extends AbstractElementAssertion +{ + public function assert(\Closure $getElementClosure) + { + $getElementClosure(); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementDoesNotExistAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementDoesNotExistAssertionTest.php new file mode 100644 index 0000000..150a31d --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementDoesNotExistAssertionTest.php @@ -0,0 +1,29 @@ +assert(function() use ($element) { return $element; }); + } + + public function testAssertion_NoMatches_ShouldSucceed() + { + $this->runAssertion([]); + } + + /** + * @expectedException \Exception + */ + public function testAssertion_ThereAreMatches_ShouldFail() + { + $this->runAssertion(['element']); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementExistsAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementExistsAssertionTest.php new file mode 100644 index 0000000..ddd9597 --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementExistsAssertionTest.php @@ -0,0 +1,29 @@ +assert(function() use ($element) { return $element; }); + } + + public function testAssertion_ThereAreMatches_ShouldSucceed() + { + $this->runAssertion(['element']); + } + + /** + * @expectedException \Exception + */ + public function testAssertion_NoMatches_ShouldFail() + { + $this->runAssertion([]); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementIsDeselectedAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementIsDeselectedAssertionTest.php new file mode 100644 index 0000000..54a967c --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementIsDeselectedAssertionTest.php @@ -0,0 +1,42 @@ +assert(function() use ($element) { return $element; }); + } + + /** + * @after + */ + public function verify() + { + \Phake::verify($this->mockElement, \Phake::times(1))->isSelected(); + } + + public function testAssertion_DeselectedElement_ShouldSucceed() + { + \Phake::when($this->mockElement)->isSelected()->thenReturn(false); + $this->runAssertion($this->mockElement); + } + + /** + * @expectedException \Exception + */ + public function testAssertion_SelectedElement_ShouldFail() + { + \Phake::when($this->mockElement)->isSelected()->thenReturn(true); + $this->runAssertion($this->mockElement); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementIsDisabledAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementIsDisabledAssertionTest.php new file mode 100644 index 0000000..247a94f --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementIsDisabledAssertionTest.php @@ -0,0 +1,42 @@ +assert(function() use ($element) { return $element; }); + } + + /** + * @after + */ + public function verify() + { + \Phake::verify($this->mockElement, \Phake::times(1))->isEnabled(); + } + + public function testAssertion_DisabledElement_ShouldSucceed() + { + \Phake::when($this->mockElement)->isEnabled()->thenReturn(false); + $this->runAssertion($this->mockElement); + } + + /** + * @expectedException \Exception + */ + public function testAssertion_EnabledElement_ShouldFail() + { + \Phake::when($this->mockElement)->isEnabled()->thenReturn(true); + $this->runAssertion($this->mockElement); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementIsDisplayedAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementIsDisplayedAssertionTest.php new file mode 100644 index 0000000..800c5aa --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementIsDisplayedAssertionTest.php @@ -0,0 +1,42 @@ +assert(function() use ($element) { + return $element; + }); + } + + public function testAssertion_VisibleElement_ShouldSucceed() + { + \Phake::when($this->mockElement)->isDisplayed()->thenReturn(true); + + $this->runAssertion($this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->isDisplayed(); + } + + /** + * @expectedException \OLX\FluentWebDriverClient\Exception\ElementIsHiddenException + */ + public function testAssertion_HiddenElement_ShouldFail() + { + \Phake::when($this->mockElement)->isDisplayed()->thenReturn(false); + + $this->runAssertion($this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->isDisplayed(); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementIsEnabledAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementIsEnabledAssertionTest.php new file mode 100644 index 0000000..26c2c07 --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementIsEnabledAssertionTest.php @@ -0,0 +1,42 @@ +assert(function() use ($element) { + return $element; + }); + } + + public function testAssertion_EnabledElement_ShouldSucceed() + { + \Phake::when($this->mockElement)->isEnabled()->thenReturn(true); + + $this->runAssertion($this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->isEnabled(); + } + + /** + * @expectedException \OLX\FluentWebDriverClient\Exception\ElementNotEnabledException + */ + public function testAssertion_NotEnabledElement_ShouldFail() + { + \Phake::when($this->mockElement)->isEnabled()->thenReturn(false); + + $this->runAssertion($this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->isEnabled(); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementIsHiddenAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementIsHiddenAssertionTest.php new file mode 100644 index 0000000..9cef298 --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementIsHiddenAssertionTest.php @@ -0,0 +1,42 @@ +assert(function() use ($element) { + return $element; + }); + } + + public function testAssertion_HiddenElement_ShouldSucceed() + { + \Phake::when($this->mockElement)->isDisplayed()->thenReturn(false); + + $this->runAssertion($this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->isDisplayed(); + } + + /** + * @expectedException \OLX\FluentWebDriverClient\Exception\ElementIsVisibleException + */ + public function testAssertion_VisibleElement_ShouldFail() + { + \Phake::when($this->mockElement)->isDisplayed()->thenReturn(true); + + $this->runAssertion($this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->isDisplayed(); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementIsSelectedAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementIsSelectedAssertionTest.php new file mode 100644 index 0000000..6286fc3 --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementIsSelectedAssertionTest.php @@ -0,0 +1,42 @@ +assert(function() use ($element) { + return $element; + }); + } + + public function testAssertion_SelectedElement_ShouldSucceed() + { + \Phake::when($this->mockElement)->isSelected()->thenReturn(true); + + $this->runAssertion($this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->isSelected(); + } + + /** + * @expectedException \OLX\FluentWebDriverClient\Exception\ElementNotSelectedException + */ + public function testAssertion_NotSelectedElement_ShouldFail() + { + \Phake::when($this->mockElement)->isSelected()->thenReturn(false); + + $this->runAssertion($this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->isSelected(); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementTextEqualsToAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementTextEqualsToAssertionTest.php new file mode 100644 index 0000000..8aa5b9a --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementTextEqualsToAssertionTest.php @@ -0,0 +1,42 @@ +assert(function() use ($element) { + return $element; + }); + } + + public function testAssertion_GivenTextMatchesElementText_ShouldSucceed() + { + \Phake::when($this->mockElement)->getText()->thenReturn('foo'); + + $this->runAssertion('foo', $this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->getText(); + } + + /** + * @expectedException \UnexpectedValueException + */ + public function testAssertion_GivenTextDoesNotMatchElementText_ShouldFail() + { + \Phake::when($this->mockElement)->getText()->thenReturn('bar'); + + $this->runAssertion('foo', $this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->getText(); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementTextIsNotEqualToTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementTextIsNotEqualToTest.php new file mode 100644 index 0000000..6c909fd --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementTextIsNotEqualToTest.php @@ -0,0 +1,44 @@ +assert(function() use ($element) { + return $element; + }); + } + + /** + * @after + */ + public function verify() + { + \Phake::verify($this->mockElement, \Phake::atLeast(1))->getText(); + } + + public function testAssertion_GivenTextDoesNotMatchElementText_ShouldSucceed() + { + \Phake::when($this->mockElement)->getText()->thenReturn('foo'); + $this->runAssertion('bar', $this->mockElement); + } + + /** + * @expectedException \UnexpectedValueException + */ + public function testAssertion_GivenTextMatchesElementText_ShouldFail() + { + \Phake::when($this->mockElement)->getText()->thenReturn('foo'); + $this->runAssertion('foo', $this->mockElement); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementValueEqualsToAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementValueEqualsToAssertionTest.php new file mode 100644 index 0000000..cbf540e --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementValueEqualsToAssertionTest.php @@ -0,0 +1,42 @@ +assert(function() use ($element) { + return $element; + }); + } + + public function testAssertion_GivenValueMatchesElementValue_ShouldSucceed() + { + \Phake::when($this->mockElement)->getAttribute('value')->thenReturn('foo'); + + $this->runAssertion('foo', $this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->getAttribute('value'); + } + + /** + * @expectedException \UnexpectedValueException + */ + public function testAssertion_GivenValueDoesNotMatchElementValue_ShouldFail() + { + \Phake::when($this->mockElement)->getAttribute('value')->thenReturn('bar'); + + $this->runAssertion('foo', $this->mockElement); + + \Phake::verify($this->mockElement, \Phake::times(1))->getAttribute('value'); + } +} diff --git a/tests/unit/Browser/Page/Element/Assertion/ElementValueIsNotEqualToAssertionTest.php b/tests/unit/Browser/Page/Element/Assertion/ElementValueIsNotEqualToAssertionTest.php new file mode 100644 index 0000000..c20ef8e --- /dev/null +++ b/tests/unit/Browser/Page/Element/Assertion/ElementValueIsNotEqualToAssertionTest.php @@ -0,0 +1,44 @@ +assert(function() use ($element) { + return $element; + }); + } + + /** + * @after + */ + public function verify() + { + \Phake::verify($this->mockElement, \Phake::atLeast(1))->getAttribute('value'); + } + + public function testAssertion_GivenValueDoesNotMatchElementValue_ShouldSucceed() + { + \Phake::when($this->mockElement)->getAttribute('value')->thenReturn('foo'); + $this->runAssertion('bar', $this->mockElement); + } + + /** + * @expectedException \UnexpectedValueException + */ + public function testAssertion_GivenValueMatchesElementValue_ShouldFail() + { + \Phake::when($this->mockElement)->getAttribute('value')->thenReturn('foo'); + $this->runAssertion('foo', $this->mockElement); + } +} diff --git a/tests/unit/Browser/Page/Element/ElementActionTest.php b/tests/unit/Browser/Page/Element/ElementActionTest.php new file mode 100644 index 0000000..8022f36 --- /dev/null +++ b/tests/unit/Browser/Page/Element/ElementActionTest.php @@ -0,0 +1,56 @@ +assertEquals($elementFind, $elementAction->thenFind()); + } + + public function testAssertThat_FakeDependencies_ShouldReturnElementFindWithAssertionsInstance() + { + $fakeWebDriverBy = Phake::mock(WebDriverBy::class); + $fakeWebDriver = Phake::mock(BrowserInterface::class); + + $elementAction = new ElementAction($fakeWebDriverBy, $fakeWebDriver); + $elementFindWithAssertions = new ElementFindWithAssertions($elementAction->thenFind()); + + $this->assertEquals($elementFindWithAssertions, $elementAction->assertThat()); + } + + public function testWait_FakeDependencies_ShouldReturnElementFindWithAssertionsInstance() + { + $fakeWebDriverBy = Phake::mock(WebDriverBy::class); + $fakeWebDriver = Phake::mock(BrowserInterface::class); + + $elementAction = new ElementAction($fakeWebDriverBy, $fakeWebDriver); + $elementFindWithWait = new ElementFindWithWait(10, $elementAction->thenFind()); + + $this->assertEquals($elementFindWithWait, $elementAction->wait(10)); + } +} diff --git a/tests/unit/Browser/Page/Element/ElementSelectorTest.php b/tests/unit/Browser/Page/Element/ElementSelectorTest.php new file mode 100644 index 0000000..b6c9c2a --- /dev/null +++ b/tests/unit/Browser/Page/Element/ElementSelectorTest.php @@ -0,0 +1,50 @@ +assertEquals($elementAction, $elementSelector->withName('spinpans')); + } + + public function testWithId_RandomId_ShouldReturnElementActionInstance() + { + $fakeBrowser = Phake::mock(BrowserInterface::class); + + $elementSelector = new ElementSelector($fakeBrowser); + $elementAction = new ElementAction(WebDriverBy::id('spinpans'), $fakeBrowser); + + $this->assertEquals($elementAction, $elementSelector->withId('spinpans')); + } + + public function testWithId_RandomXpath_ShouldReturnElementActionInstance() + { + $fakeBrowser = Phake::mock(BrowserInterface::class); + + $elementSelector = new ElementSelector($fakeBrowser); + $elementAction = new ElementAction(WebDriverBy::xpath('spinpans'), $fakeBrowser); + + $this->assertEquals($elementAction, $elementSelector->withXpath('spinpans')); + } +} diff --git a/tests/unit/Browser/Page/Element/Find/ElementFindTest.php b/tests/unit/Browser/Page/Element/Find/ElementFindTest.php new file mode 100644 index 0000000..e163fb6 --- /dev/null +++ b/tests/unit/Browser/Page/Element/Find/ElementFindTest.php @@ -0,0 +1,75 @@ +getTagName()->thenReturn("select"); + Phake::when($fakeBrowser)->findElement($fakeFindBy)->thenReturn($fakeRemoteWebElement); + + return new ElementFind($fakeFindBy, $fakeBrowser); + } + + public function testAsDropdown_ElementIsFound_ShouldReturnWebDriverSelectInstance() + { + $fakeRemoteWebElement = Phake::mock(RemoteWebElement::class); + + $elementFind = $this->makeElementFind($fakeRemoteWebElement); + + $this->assertInstanceOf(WebDriverSelect::class, $elementFind->asDropDown()); + } + + public function testAsHtmlElement_ElementIsFound_ShouldReturnFoundRemoteWebElementInstance() + { + $fakeRemoteWebElement = Phake::mock(RemoteWebElement::class); + + $elementFind = $this->makeElementFind($fakeRemoteWebElement); + + $this->assertSame($fakeRemoteWebElement, $elementFind->asHtmlElement()); + } + + public function testGetWebDriver_WebDriverInjected_ShouldReturnWebDriverInjectedInstance() + { + $fakeDriverBy = Phake::mock(WebDriverBy::class); + $fakeBrowser = Phake::mock(BrowserInterface::class); + + $elementFind = new ElementFind($fakeDriverBy, $fakeBrowser); + + $this->assertSame($fakeBrowser, $elementFind->getBrowser()); + } + + public function testGetSearchCriteria_WebDriverByInjected_ShouldReturnWebDriverByInjectedInstance() + { + $fakeDriverBy = Phake::mock(WebDriverBy::class); + $fakeBrowser = Phake::mock(BrowserInterface::class); + + $elementFind = new ElementFind($fakeDriverBy, $fakeBrowser); + + $this->assertSame($fakeDriverBy, $elementFind->getSearchCriteria()); + } +} diff --git a/tests/unit/Browser/Page/Element/Find/ElementFindWithAssertionsTest.php b/tests/unit/Browser/Page/Element/Find/ElementFindWithAssertionsTest.php new file mode 100644 index 0000000..0333882 --- /dev/null +++ b/tests/unit/Browser/Page/Element/Find/ElementFindWithAssertionsTest.php @@ -0,0 +1,164 @@ +exists(); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testDoesNotExist_MethodIsCalled_ShouldChangeElementFinderPropertyToElementDoesNotExistAssertionInstance() + { + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementDoesNotExistAssertion($fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->doesNotExist(); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testIsDisplayed_MethodIsCalled_ShouldChangeElementFinderPropertyToElementIsDisplayedAssertionInstance() + { + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementIsDisplayedAssertion($fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->isDisplayed(); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testIsHidden_MethodIsCalled_ShouldChangeElementFinderPropertyToElementIsHiddenAssertionInstance() + { + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementIsHiddenAssertion($fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->isHidden(); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testIsEnabled_MethodIsCalled_ShouldChangeElementFinderPropertyToElementIsEnabledAssertionInstance() + { + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementIsEnabledAssertion($fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->isEnabled(); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testIsDisabled_MethodIsCalled_ShouldChangeElementFinderPropertyToElementIsDisabledAssertionInstance() + { + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementIsDisabledAssertion($fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->isDisabled(); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testIsSelected_MethodIsCalled_ShouldChangeElementFinderPropertyToElementIsSelectedAssertionInstance() + { + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementIsSelectedAssertion($fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->isSelected(); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testIsDeselected_MethodIsCalled_ShouldChangeElementFinderPropertyToElementIsDeselectedAssertionInstance() + { + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementIsDeselectedAssertion($fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->isDeselected(); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testValueEqualTo_MethodIsCalledWithExpectedValue_ShouldChangeElementFinderPropertyToElementValueEqualsToAssertionInstance() + { + $expectedString = 'my string'; + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementValueEqualsToAssertion($expectedString, $fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->valueEqualTo($expectedString); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testValueIsNotEqualTo_MethodIsCalledWithExpectedValue_ShouldChangeElementFinderPropertyToElementValueIsNotEqualToAssertionInstance() + { + $expectedString = 'my string'; + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementValueIsNotEqualToAssertion($expectedString, $fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->valueIsNotEqualTo($expectedString); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testTextEqualTo_MethodIsCalledWithExpectedValue_ShouldChangeElementFinderPropertyToElementTextEqualsToAssertionInstance() + { + $expectedString = 'my string'; + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementTextEqualsToAssertion($expectedString, $fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->textEqualTo($expectedString); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } + + public function testTextIsNotEqualTo_MethodIsCalledWithExpectedValue_ShouldChangeElementFinderPropertyToElementTextIsNotEqualToAssertionInstance() + { + $expectedString = 'my string'; + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + $expectedDecorator = new ElementTextIsNotEqualToAssertion($expectedString, $fakeElementFinder); + + $elementFinder = new ElementFindWithAssertions($fakeElementFinder); + $elementFinder->textIsNotEqualTo($expectedString); + + $this->assertEquals($expectedDecorator, $elementFinder->thenFind()); + } +} diff --git a/tests/unit/Browser/Page/Element/Find/ElementFindWithWaitTest.php b/tests/unit/Browser/Page/Element/Find/ElementFindWithWaitTest.php new file mode 100644 index 0000000..6f29c28 --- /dev/null +++ b/tests/unit/Browser/Page/Element/Find/ElementFindWithWaitTest.php @@ -0,0 +1,98 @@ +wait(Phake::anyParameters())->thenReturn($fakeWebDriverWait); + Phake::when($fakeElementFinder)->getSearchCriteria()->thenReturn($fakeDriverBy); + Phake::when($fakeElementFinder)->getBrowser()->thenReturn($fakeBrowser); + + return new ElementFindWithWait(10, $fakeElementFinder); + } + + public function testToBePresent_MethodIsCalled_ShouldCallWebDriverWaitUntil() + { + $fakeWebDriverWait = Phake::mock(WebDriverWait::class); + + $elementFindWaitInstance = $this->makeElementFindWithWait($fakeWebDriverWait); + $elementFindWaitInstance->toBePresent(); + + Phake::verify($fakeWebDriverWait, Phake::times(1))->until(Phake::anyParameters()); + } + + public function testToBeVisible_MethodIsCalled_ShouldCallWebDriverWaitUntil() + { + $fakeWebDriverWait = Phake::mock(WebDriverWait::class); + + $elementFindWaitInstance = $this->makeElementFindWithWait($fakeWebDriverWait); + $elementFindWaitInstance->toBeVisible(); + + Phake::verify($fakeWebDriverWait, Phake::times(1))->until(Phake::anyParameters()); + } + + public function testToBeInvisible_MethodIsCalled_ShouldCallWebDriverWaitUntil() + { + $fakeWebDriverWait = Phake::mock(WebDriverWait::class); + + $elementFindWaitInstance = $this->makeElementFindWithWait($fakeWebDriverWait); + $elementFindWaitInstance->toBeInvisible(); + + Phake::verify($fakeWebDriverWait, Phake::times(1))->until(Phake::anyParameters()); + } + + public function testToBeClickable_MethodIsCalled_ShouldCallWebDriverWaitUntil() + { + $fakeWebDriverWait = Phake::mock(WebDriverWait::class); + + $elementFindWaitInstance = $this->makeElementFindWithWait($fakeWebDriverWait); + $elementFindWaitInstance->toBeClickable(); + + Phake::verify($fakeWebDriverWait, Phake::times(1))->until(Phake::anyParameters()); + } + + public function testToBeSelected_MethodIsCalled_ShouldCallWebDriverWaitUntil() + { + $fakeWebDriverWait = Phake::mock(WebDriverWait::class); + + $elementFindWaitInstance = $this->makeElementFindWithWait($fakeWebDriverWait); + $elementFindWaitInstance->toBeSelected(); + + Phake::verify($fakeWebDriverWait, Phake::times(1))->until(Phake::anyParameters()); + } + + public function testThenFind_ElementFinderIsInjected_ShouldReturnElementFinderInjectedInstance() + { + $fakeElementFinder = Phake::mock(ElementFinderInterface::class); + + $elementFindWaitInstance = new ElementFindWithWait(10, $fakeElementFinder); + + $this->assertSame($elementFindWaitInstance->thenFind(), $fakeElementFinder); + } +} diff --git a/tests/unit/Browser/Page/Find/Assertion/AllElementsApplyToAssertionTest.php b/tests/unit/Browser/Page/Find/Assertion/AllElementsApplyToAssertionTest.php new file mode 100644 index 0000000..82d57ec --- /dev/null +++ b/tests/unit/Browser/Page/Find/Assertion/AllElementsApplyToAssertionTest.php @@ -0,0 +1,98 @@ +getAttribute('age') === 20; + }; + + $targetClosure = function () { + $elem1 = \Phake::mock(RemoteWebElement::class); + $elem2 = \Phake::mock(RemoteWebElement::class); + $elem3 = \Phake::mock(RemoteWebElement::class); + \Phake::when($elem1)->getAttribute('age')->thenReturn(20); + \Phake::when($elem2)->getAttribute('age')->thenReturn(20); + \Phake::when($elem3)->getAttribute('age')->thenReturn(20); + + return [$elem1, $elem2, $elem3]; + }; + + $assertion = new AllElementsApplyToAssertion($criteria); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * + * @expectedException \OLX\FluentWebDriverClient\Exception\NotAllElementsApplyToCriteriaException + */ + public function testDecorate_NotAllElementsMeetTheCriteria_ShouldThrowNotAllElementsApplyToCriteriaException() + { + $criteria = function (RemoteWebElement $element) { + return $element->getAttribute('age') === 20; + }; + + $targetClosure = function () { + $elem1 = \Phake::mock(RemoteWebElement::class); + $elem2 = \Phake::mock(RemoteWebElement::class); + $elem3 = \Phake::mock(RemoteWebElement::class); + \Phake::when($elem1)->getAttribute('age')->thenReturn(20); + \Phake::when($elem2)->getAttribute('age')->thenReturn(30); + \Phake::when($elem3)->getAttribute('age')->thenReturn(20); + + return [$elem1, $elem2, $elem3]; + }; + + $assertion = new AllElementsApplyToAssertion($criteria); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * + * @expectedException \OLX\FluentWebDriverClient\Exception\EmptyResultException + */ + public function testDecorate_ClosureReturnsEmptyArray_ShouldThrowEmptyResultException() + { + $criteria = function (RemoteWebElement $element) { + return $element->getAttribute('age') === 20; + }; + + $targetClosure = function () { + return []; + }; + + $assertion = new AllElementsApplyToAssertion($criteria); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * + * @expectedException \OLX\FluentWebDriverClient\Exception\NotAnArrayException + */ + public function testDecorate_ClosureDoesNotReturnAnArray_ShouldThrowNotAnArrayException() + { + $criteria = function ($element) { + return $element->getAttribute('age') === 20; + }; + + $targetClosure = function () { + return new \stdClass(); + }; + + $assertion = new AllElementsApplyToAssertion($criteria); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Browser/Page/Find/Assertion/AllElementsAreHiddenAssertionTest.php b/tests/unit/Browser/Page/Find/Assertion/AllElementsAreHiddenAssertionTest.php new file mode 100644 index 0000000..914b50e --- /dev/null +++ b/tests/unit/Browser/Page/Find/Assertion/AllElementsAreHiddenAssertionTest.php @@ -0,0 +1,47 @@ +isDisplayed()->thenReturn(false); + \Phake::when($elem2)->isDisplayed()->thenReturn(false); + \Phake::when($elem3)->isDisplayed()->thenReturn(false); + return [$elem1, $elem2, $elem3]; + }; + + $assertion = new AllElementsAreHiddenAssertion(); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\NotAllElementsApplyToCriteriaException + */ + public function testDecorate_NotAllElementsAreHidden_ShouldThrowNotAllElementsApplyToCriteriaException() + { + $targetClosure = function () { + $elem1 = \Phake::mock(RemoteWebElement::class); + $elem2 = \Phake::mock(RemoteWebElement::class); + $elem3 = \Phake::mock(RemoteWebElement::class); + \Phake::when($elem1)->isDisplayed()->thenReturn(false); + \Phake::when($elem2)->isDisplayed()->thenReturn(true); + \Phake::when($elem3)->isDisplayed()->thenReturn(false); + return [$elem1, $elem2, $elem3]; + }; + + $assertion = new AllElementsAreHiddenAssertion(); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Browser/Page/Find/Assertion/AllElementsAreVisibleAssertionTest.php b/tests/unit/Browser/Page/Find/Assertion/AllElementsAreVisibleAssertionTest.php new file mode 100644 index 0000000..ab48fb6 --- /dev/null +++ b/tests/unit/Browser/Page/Find/Assertion/AllElementsAreVisibleAssertionTest.php @@ -0,0 +1,47 @@ +isDisplayed()->thenReturn(true); + \Phake::when($elem2)->isDisplayed()->thenReturn(true); + \Phake::when($elem3)->isDisplayed()->thenReturn(true); + return [$elem1, $elem2, $elem3]; + }; + + $assertion = new AllElementsAreVisibleAssertion(); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\NotAllElementsApplyToCriteriaException + */ + public function testDecorate_NotAllElementsAreVisible_ShouldThrowNotAllElementsApplyToCriteriaException() + { + $targetClosure = function () { + $elem1 = \Phake::mock(RemoteWebElement::class); + $elem2 = \Phake::mock(RemoteWebElement::class); + $elem3 = \Phake::mock(RemoteWebElement::class); + \Phake::when($elem1)->isDisplayed()->thenReturn(true); + \Phake::when($elem2)->isDisplayed()->thenReturn(true); + \Phake::when($elem3)->isDisplayed()->thenReturn(false); + return [$elem1, $elem2, $elem3]; + }; + + $assertion = new AllElementsAreVisibleAssertion(); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Browser/Page/Find/Assertion/AtLeastOneElementAppliesToAssertionTest.php b/tests/unit/Browser/Page/Find/Assertion/AtLeastOneElementAppliesToAssertionTest.php new file mode 100644 index 0000000..2de82c2 --- /dev/null +++ b/tests/unit/Browser/Page/Find/Assertion/AtLeastOneElementAppliesToAssertionTest.php @@ -0,0 +1,89 @@ +isDisplayed() === true; + }; + + $targetClosure = function () { + $elem1 = \Phake::mock(RemoteWebElement::class); + $elem2 = \Phake::mock(RemoteWebElement::class); + $elem3 = \Phake::mock(RemoteWebElement::class); + \Phake::when($elem1)->isDisplayed()->thenReturn(false); + \Phake::when($elem2)->isDisplayed()->thenReturn(true); + \Phake::when($elem3)->isDisplayed()->thenReturn(false); + return [$elem1, $elem2, $elem3]; + }; + + $assertion = new AtLeastOneElementAppliesToAssertion($criteria); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\NoElementAppliesToCriteriaException + */ + public function testDecorate_NotAllElementsAreVisible_ShouldThrowNotAllElementsApplyToCriteriaException() + { + $criteria = function (RemoteWebElement $element) { + return $element->isDisplayed() === true; + }; + + $targetClosure = function () { + $elem1 = \Phake::mock(RemoteWebElement::class); + $elem2 = \Phake::mock(RemoteWebElement::class); + $elem3 = \Phake::mock(RemoteWebElement::class); + \Phake::when($elem1)->isDisplayed()->thenReturn(false); + \Phake::when($elem2)->isDisplayed()->thenReturn(false); + \Phake::when($elem3)->isDisplayed()->thenReturn(false); + return [$elem1, $elem2, $elem3]; + }; + + $assertion = new AtLeastOneElementAppliesToAssertion($criteria); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @expectedException \Exception + */ + public function testDecorate_ClosureReturnsNoElements_ShouldThrowException() + { + $criteria = function (RemoteWebElement $element) { + return $element->isDisplayed() === true; + }; + + $targetClosure = function () { + return []; + }; + + $assertion = new AtLeastOneElementAppliesToAssertion($criteria); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @expectedException \Exception + */ + public function testDecorate_ClosureReturnsSomethingOtherThanArray_ShouldThrowException() + { + $criteria = function (RemoteWebElement $element) { + return $element->isDisplayed() === true; + }; + + $targetClosure = function () { + return 1; + }; + + $assertion = new AtLeastOneElementAppliesToAssertion($criteria); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Browser/Page/Find/Assertion/ElementExistsAtLeastOnceAssertionTest.php b/tests/unit/Browser/Page/Find/Assertion/ElementExistsAtLeastOnceAssertionTest.php new file mode 100644 index 0000000..feb14dc --- /dev/null +++ b/tests/unit/Browser/Page/Find/Assertion/ElementExistsAtLeastOnceAssertionTest.php @@ -0,0 +1,43 @@ +getAttribute('age')->thenReturn(20); + \Phake::when($elem2)->getAttribute('age')->thenReturn(20); + \Phake::when($elem3)->getAttribute('age')->thenReturn(20); + + return [$elem1, $elem2, $elem3]; + }; + + $assertion = new ElementExistsAtLeastOnceAssertion(); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\NoSuchElementException + */ + public function testDecorate_ElementCountIsZero_ShouldThrowNoSuchElementException() + { + $targetClosure = function () { + return []; + }; + + $assertion = new ElementExistsAtLeastOnceAssertion(); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Browser/Page/Find/Assertion/ElementExistsOnlyOnceAssertionTest.php b/tests/unit/Browser/Page/Find/Assertion/ElementExistsOnlyOnceAssertionTest.php new file mode 100644 index 0000000..6b1b3d1 --- /dev/null +++ b/tests/unit/Browser/Page/Find/Assertion/ElementExistsOnlyOnceAssertionTest.php @@ -0,0 +1,46 @@ +decorate($closure, $locator); + } + + /** + * @dataProvider closuresWhichReturnArrayOfLengthOne + */ + public function testAssertion_ClosureWhichReturnsArrayOfLengthOne_ShouldSucceed($closure) + { + $this->runAssertion($closure, []); + } + + /** + * @dataProvider closuresWhichReturnArrayOfLengthOtherThanOne + * @expectedException \Exception + */ + public function testAssertion_ClosureWhichReturnsArrayOfLengthOtherThanOne_ShouldFail($closure) + { + $this->runAssertion($closure, []); + } + + public function closuresWhichReturnArrayOfLengthOne() + { + return [ + [ function() { return ['foo']; } ], + [ function() { return ['bar']; } ], + ]; + } + + public function closuresWhichReturnArrayOfLengthOtherThanOne() + { + return [ + [ function() { return []; } ], + [ function() { return ['foo', 'bar']; } ], + ]; + } +} diff --git a/tests/unit/Browser/Page/Find/Assertion/ElementShouldNotExistAssertionTest.php b/tests/unit/Browser/Page/Find/Assertion/ElementShouldNotExistAssertionTest.php new file mode 100644 index 0000000..f7878f4 --- /dev/null +++ b/tests/unit/Browser/Page/Find/Assertion/ElementShouldNotExistAssertionTest.php @@ -0,0 +1,41 @@ +getAttribute('age')->thenReturn(20); + + return [$elem1]; + }; + + $assertion = new ElementShouldNotExistAssertion(); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\StopChainException + */ + public function testDecorate_NoSuchElementExceptionIsThrown_ShouldThrowStopChainException() + { + $targetClosure = function () { + throw new NoSuchElementException('abcd'); + }; + + $assertion = new ElementShouldNotExistAssertion(); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Browser/Page/Find/Assertion/ElementTextEqualsAssertionTest.php b/tests/unit/Browser/Page/Find/Assertion/ElementTextEqualsAssertionTest.php new file mode 100644 index 0000000..ea8e96a --- /dev/null +++ b/tests/unit/Browser/Page/Find/Assertion/ElementTextEqualsAssertionTest.php @@ -0,0 +1,42 @@ +getText()->thenReturn('Ipsos Lorem'); + return $elem1; + }; + + $assertion = new ElementTextEqualsAssertion('Ipsos Lorem'); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\UnexpectedValueException + */ + public function testDecorate_GetTextReturnsDifferentValueThanExpected_ShouldThrowUnexpectedValueException() + { + $targetClosure = function () { + $elem1 = \Phake::mock(RemoteWebElement::class); + \Phake::when($elem1)->getText()->thenReturn('Ipsos Lorem'); + return $elem1; + }; + + $assertion = new ElementTextEqualsAssertion('not what i am expecting'); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Browser/Page/Find/Assertion/ElementValueEqualsAssertionTest.php b/tests/unit/Browser/Page/Find/Assertion/ElementValueEqualsAssertionTest.php new file mode 100644 index 0000000..bd5586d --- /dev/null +++ b/tests/unit/Browser/Page/Find/Assertion/ElementValueEqualsAssertionTest.php @@ -0,0 +1,57 @@ +getAttribute('value')->thenReturn('Ipsos Lorem'); + return $elem1; + }; + + $assertion = new ElementValueEqualsAssertion('Ipsos Lorem'); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\UnexpectedValueException + */ + public function testDecorate_GetAttributetReturnsDifferentValueThanExpected_ShouldThrowUnexpectedValueException() + { + $targetClosure = function () { + $elem1 = \Phake::mock(RemoteWebElement::class); + \Phake::when($elem1)->getAttribute('value')->thenReturn('Ipsos Lorem'); + return $elem1; + }; + + $assertion = new ElementValueEqualsAssertion('not what i am expecting'); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \Exception + */ + public function testDecorate_ClosureReturnsArray_ShouldThrowException() + { + $targetClosure = function () { + return []; + }; + + $assertion = new ElementValueEqualsAssertion('not what i am expecting'); + $this->assertTrue($assertion->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Browser/Page/Find/Decorator/AbstractPageFinderDecoratorTest.php b/tests/unit/Browser/Page/Find/Decorator/AbstractPageFinderDecoratorTest.php new file mode 100644 index 0000000..ed0a0cb --- /dev/null +++ b/tests/unit/Browser/Page/Find/Decorator/AbstractPageFinderDecoratorTest.php @@ -0,0 +1,56 @@ +publicRegisterDecorator($fakeTargetDecorator); + + $this->assertTrue($pageFinder->publicDecorate(function() { return true; }, null)); + \Phake::verify($fakeTargetDecorator)->decorate(\Phake::anyParameters()); + } + + public function testDecorate_OneWrappedDecoratorThrowsStopChainException_ShouldReturnNull() + { + $pageFinder = new PageFinderDecoratorWithPublicDecorate(\Phake::mock(PageFinderInterface::class)); + $pageFinder->publicRegisterDecorator(new ExceptionThrowerTargetDecorator()); + + $this->assertNull($pageFinder->publicDecorate(function() { }, null)); + } +} + +class PageFinderDecoratorWithPublicDecorate extends AbstractPageFinderDecorator +{ + public function getBrowser() + { + // ... + } + + public function publicRegisterDecorator(TargetDecoratorInterface $decorator) + { + $this->registerDecorator($decorator); + } + + public function publicDecorate($targetClosure, $locator) + { + return $this->decorate($targetClosure, $locator); + } +} + +class ExceptionThrowerTargetDecorator implements TargetDecoratorInterface +{ + public function decorate($targetClosure, $locator) + { + throw new StopChainException(); + } +} diff --git a/tests/unit/Browser/Page/Find/Decorator/CachedPageFinderDecoratorTest.php b/tests/unit/Browser/Page/Find/Decorator/CachedPageFinderDecoratorTest.php new file mode 100644 index 0000000..75d5552 --- /dev/null +++ b/tests/unit/Browser/Page/Find/Decorator/CachedPageFinderDecoratorTest.php @@ -0,0 +1,60 @@ +publicGet($method, $identifier, $bucket); + \Phake::verify($fakePageFinder)->elementWithName($identifier); + } + + public function testGet_WarmCache_ShouldReturnItemFromCache() + { + $method = 'elementWithName'; + $identifier = 'some-name'; + $bucket = 'the-bucket'; + + $fakePageFinder = \Phake::mock(PageFinderInterface::class); + $pageFinder = new CachedPageFinderDecoratorWithWarmCache($fakePageFinder); + + $pageFinder->publicGet($method, $identifier, $bucket); + \Phake::verify($fakePageFinder, \Phake::never())->elementWithName($identifier); + } +} + +class CachedPageFinderDecoratorWithFreshCache extends CachedPageFinderDecorator +{ + public function publicGet($method, $identifier, $bucket) + { + $this->cache = [ + $bucket => [ + ], + ]; + return $this->get($method, $identifier, $bucket); + } +} + +class CachedPageFinderDecoratorWithWarmCache extends CachedPageFinderDecorator +{ + public function publicGet($method, $identifier, $bucket) + { + $this->cache = [ + $bucket => [ + "{$method}_{$identifier}" => \Phake::mock(RemoteWebElement::class), + ], + ]; + return $this->get($method, $identifier, $bucket); + } +} diff --git a/tests/unit/Browser/Page/Find/Decorator/ClosureHolderTest.php b/tests/unit/Browser/Page/Find/Decorator/ClosureHolderTest.php new file mode 100644 index 0000000..0a5d10e --- /dev/null +++ b/tests/unit/Browser/Page/Find/Decorator/ClosureHolderTest.php @@ -0,0 +1,73 @@ +value = $value; + } + + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * @param mixed $value + */ + public function setValue($value) + { + $this->value = $value; + } +} + +class ClosureHolderTest extends \PHPUnit_Framework_TestCase +{ + + public function testGet_ClosureWasNotInvoked_ShouldInvokeAndReturnValue() + { + $countHolder = new ValueHolder(0); + $wasInvokedHolder = new ValueHolder(false); + $closure = function () use ($countHolder, $wasInvokedHolder) { + $wasInvokedHolder->setValue(true); + $countHolder->setValue($countHolder->getValue()+1); + return $countHolder->getValue(); + }; + + $closureHolder = new ClosureHolder($closure); + + $this->assertEquals(1, $closureHolder->get()); + $this->assertTrue($wasInvokedHolder->getValue()); + } + + public function testGet_ClosureWasAlreadyInvoked_ShouldNotInvokeAndReturnSameValue() + { + $countHolder = new ValueHolder(0); + $wasInvokedHolder = new ValueHolder(false); + $closure = function () use ($countHolder, $wasInvokedHolder) { + $wasInvokedHolder->setValue(true); + $countHolder->setValue($countHolder->getValue()+1); + return $countHolder->getValue(); + }; + + $closureHolder = new ClosureHolder($closure); + $closureHolder(); + $wasInvokedHolder->setValue(false); + + $this->assertEquals(1, $closureHolder->get()); + $this->assertFalse($wasInvokedHolder->getValue()); + } +} diff --git a/tests/unit/Browser/Page/Find/PageFinderBuilderTest.php b/tests/unit/Browser/Page/Find/PageFinderBuilderTest.php new file mode 100644 index 0000000..84d8605 --- /dev/null +++ b/tests/unit/Browser/Page/Find/PageFinderBuilderTest.php @@ -0,0 +1,36 @@ +pageFinderBuilder = new PageFinderBuilder(\Phake::mock(BrowserInterface::class)); + } + + public function testBuild_WhenNeitherAssertionNorWaitsAreEnabled_ShouldReturnPageFinder() + { + $this->assertInstanceOf(PageFinder::class, $this->pageFinderBuilder->build()); + } + + public function testBuild_WhenWithAssertions_ShouldReturnPageFinderWithAssertions() + { + $this->pageFinderBuilder->withAssertions(); + $this->assertInstanceOf(PageFinderWithAssertions::class, $this->pageFinderBuilder->build()); + } + + public function testBuild_WhenWithWaits_ShouldReturnPageFinderWithWaits() + { + $this->pageFinderBuilder->withWaits(1); + $this->assertInstanceOf(PageFinderWithWaits::class, $this->pageFinderBuilder->build()); + } +} diff --git a/tests/unit/Browser/Page/Find/Wait/WaitUntilAbsenceTest.php b/tests/unit/Browser/Page/Find/Wait/WaitUntilAbsenceTest.php new file mode 100644 index 0000000..08ace68 --- /dev/null +++ b/tests/unit/Browser/Page/Find/Wait/WaitUntilAbsenceTest.php @@ -0,0 +1,73 @@ +until(Phake::anyParameters())->thenReturn(true); + Phake::when($fakeBrowser)->wait(Phake::anyParameters())->thenReturn($fakeDriverWait); + + $wait = new WaitUntilAbsence($fakeBrowser, 10); + $this->assertTrue($wait->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\CriteriaNotMetException + */ + public function testDecorate_ExceptionIsThrownByWait_ShouldThrowCriteriaNotMetException() + { + $targetClosure = function () { + return []; + }; + + $fakeBrowser = Phake::mock(BrowserInterface::class); + $fakeDriverWait = Phake::mock(WebDriverWait::class); + + Phake::when($fakeDriverWait)->until(Phake::anyParameters())->thenThrow(new Exception('Something thown by wait')); + Phake::when($fakeBrowser)->wait(Phake::anyParameters())->thenReturn($fakeDriverWait); + + $wait = new WaitUntilAbsence($fakeBrowser, 10); + $this->assertTrue($wait->decorate($targetClosure, null)); + } + + /** + * @expectedException \OLX\FluentWebDriverClient\Exception\ElementNotExpectedException + */ + public function testValidation_WhenCallbackReturnsANonEmptyArray_ShouldThrowElementNotExpectedException() + { + $wait = new WaitUntilAbsenceWithPublicValidation(Phake::mock(BrowserInterface::class), 10); + $wait->publicValidate(function() { return ['foo']; }, null); + } + + public function testValidation_WhenCallbackReturnsAnEmptyArray_ShouldReturnTrue() + { + $wait = new WaitUntilAbsenceWithPublicValidation(Phake::mock(BrowserInterface::class), 10); + $this->assertTrue($wait->publicValidate(function() { return []; }, null)); + } +} + +class WaitUntilAbsenceWithPublicValidation extends WaitUntilAbsence +{ + public function publicValidate($targetClosure, $locator = null) + { + return $this->validate($targetClosure, $locator); + } +} diff --git a/tests/unit/Browser/Page/Find/Wait/WaitUntilClickableTest.php b/tests/unit/Browser/Page/Find/Wait/WaitUntilClickableTest.php new file mode 100644 index 0000000..5135114 --- /dev/null +++ b/tests/unit/Browser/Page/Find/Wait/WaitUntilClickableTest.php @@ -0,0 +1,53 @@ +validate(Phake::anyParameters())->thenReturn(true); + Phake::when($fakeDriverWait)->until(Phake::anyParameters())->thenReturn(true); + Phake::when($fakeBrowser)->wait(Phake::anyParameters())->thenReturn($fakeDriverWait); + + $this->assertTrue($fakeWaitUntil->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\CriteriaNotMetException + */ + public function testDecorate_ExceptionIsThrownByWait_ShouldThrowCriteriaNotMetException() + { + $targetClosure = function () { + return []; + }; + + $fakeBrowser = Phake::mock(BrowserInterface::class); + $fakeDriverWait = Phake::mock(WebDriverWait::class); + + Phake::when($fakeDriverWait)->until(Phake::anyParameters())->thenThrow(new Exception('Something thown by wait')); + Phake::when($fakeBrowser)->wait(Phake::anyParameters())->thenReturn($fakeDriverWait); + + $wait = new WaitUntilClickable($fakeBrowser, 10); + $this->assertTrue($wait->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Browser/Page/Find/Wait/WaitUntilPresenceTest.php b/tests/unit/Browser/Page/Find/Wait/WaitUntilPresenceTest.php new file mode 100644 index 0000000..02b3025 --- /dev/null +++ b/tests/unit/Browser/Page/Find/Wait/WaitUntilPresenceTest.php @@ -0,0 +1,76 @@ +until(Phake::anyParameters())->thenReturn(true); + Phake::when($fakeBrowser)->wait(Phake::anyParameters())->thenReturn($fakeDriverWait); + + $wait = new WaitUntilPresence($fakeBrowser, 10); + $this->assertTrue($wait->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\CriteriaNotMetException + */ + public function testDecorate_ExceptionIsThrownByWait_ShouldThrowCriteriaNotMetException() + { + $targetClosure = function () { + return []; + }; + + $fakeBrowser = Phake::mock(BrowserInterface::class); + $fakeDriverWait = Phake::mock(WebDriverWait::class); + + Phake::when($fakeDriverWait)->until(Phake::anyParameters())->thenThrow(new Exception('Something thown by wait')); + Phake::when($fakeBrowser)->wait(Phake::anyParameters())->thenReturn($fakeDriverWait); + + $wait = new WaitUntilPresence($fakeBrowser, 10); + $this->assertTrue($wait->decorate($targetClosure, null)); + } + + /** + * @expectedException \OLX\FluentWebDriverClient\Exception\NoSuchElementException + */ + public function testValidation_CallbackReturnsAnEmptyArray_ShouldThrowNoSuchElementException() + { + $wait = new WaitUntilPresenceWithPublicValidation(Phake::mock(BrowserInterface::class), 10); + $wait->publicValidate(function() { return []; }, null); + } + + public function testValidation_CallbackReturnsANonEmptyArray_ShouldReturnTrue() + { + $wait = new WaitUntilPresenceWithPublicValidation(Phake::mock(BrowserInterface::class), 10); + $this->assertTrue($wait->publicValidate(function() { return ['foo']; }, null)); + } +} + +class WaitUntilPresenceWithPublicValidation extends WaitUntilPresence +{ + public function publicValidate($targetClosure, $locator = null) + { + return $this->validate($targetClosure, $locator); + } +} diff --git a/tests/unit/Browser/Page/Find/Wait/WaitUntilVisibilityTest.php b/tests/unit/Browser/Page/Find/Wait/WaitUntilVisibilityTest.php new file mode 100644 index 0000000..2afd992 --- /dev/null +++ b/tests/unit/Browser/Page/Find/Wait/WaitUntilVisibilityTest.php @@ -0,0 +1,53 @@ +validate(Phake::anyParameters())->thenReturn(true); + Phake::when($fakeDriverWait)->until(Phake::anyParameters())->thenReturn(true); + Phake::when($fakeBrowser)->wait(Phake::anyParameters())->thenReturn($fakeDriverWait); + + $this->assertTrue($fakeWaitUntil->decorate($targetClosure, null)); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\CriteriaNotMetException + */ + public function testDecorate_ExceptionIsThrownByWait_ShouldThrowCriteriaNotMetException() + { + $targetClosure = function () { + return []; + }; + + $fakeBrowser = Phake::mock(BrowserInterface::class); + $fakeDriverWait = Phake::mock(WebDriverWait::class); + + Phake::when($fakeDriverWait)->until(Phake::anyParameters())->thenThrow(new Exception('Something thown by wait')); + Phake::when($fakeBrowser)->wait(Phake::anyParameters())->thenReturn($fakeDriverWait); + + $waitUntil = new WaitUntilVisibility($fakeBrowser, 10); + $this->assertTrue($waitUntil->decorate($targetClosure, null)); + } +} diff --git a/tests/unit/Translator/UrlTranslatorTest.php b/tests/unit/Translator/UrlTranslatorTest.php new file mode 100644 index 0000000..717d097 --- /dev/null +++ b/tests/unit/Translator/UrlTranslatorTest.php @@ -0,0 +1,74 @@ +assertEquals('http://google.pt', $urlTranslator->get('http://google.pt')); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\InvalidUrlException + */ + public function testGet_UrlIsKeyAndNoKeyExistsInContainer_ShouldThrowInvalidUrlException() + { + $urlTranslator = new UrlTranslator([], null); + $urlTranslator->get('home'); + } + + /** + * @test + */ + public function testGet_UrlIsKeyAndKeyExistsInContainer_ShouldReturnFullUrl() + { + $urlTranslator = new UrlTranslator(['myaccount' => '/account'], 'http://google.pt'); + $this->assertEquals('http://google.pt/account', $urlTranslator->get('myaccount')); + } + + /** + * @test + */ + public function testGet_UrlIsKeyAndKeyExistsInContainer_ShouldReturnFullUrlFromCache() + { + $urlTranslator = new UrlTranslator(['myaccount' => '/account'], 'http://google.pt'); + $urlTranslator->get('myaccount'); + + $this->assertEquals('http://google.pt/account', $urlTranslator->get('myaccount')); + } + + /** + * @test + */ + public function testGet_UrlIsDash_ShouldReturnBaseUrl() + { + $urlTranslator = new UrlTranslator([], 'http://google.pt'); + $this->assertEquals('http://google.pt', $urlTranslator->get('/')); + } + + /** + * @test + */ + public function testGet_UrlStartsWithDashAndIsValid_ShouldReturnFullUrl() + { + $urlTranslator = new UrlTranslator(['home' => 'http://google.pt'], 'http://google.pt'); + $this->assertEquals('http://google.pt/account', $urlTranslator->get('/account')); + } + + /** + * @test + * @expectedException \OLX\FluentWebDriverClient\Exception\InvalidUrlException + */ + public function testGet_UrlStartsWithDashAndBaseUrlIsNotValid_ShouldThrowInvalidUrlException() + { + $urlTranslator = new UrlTranslator([], 'google.pt'); + $urlTranslator->get('/'); + } +}