├── .dockerignore ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Jenkinsfile ├── LICENSE.txt ├── Pipfile ├── README.md ├── pages ├── __init__.py ├── about.py ├── auth0.py ├── base.py ├── confirm_profile_delete.py ├── create_group_page.py ├── edit_group.py ├── edit_profile.py ├── github.py ├── group_info_page.py ├── groups_page.py ├── home_page.py ├── invite.py ├── invite_success.py ├── link_crawler.py ├── location_search_results.py ├── profile.py ├── register.py ├── search.py └── settings.py ├── pipenv.txt ├── setup.cfg ├── tests ├── __init__.py ├── conftest.py ├── restmail.py ├── test_about_page.py ├── test_account.py ├── test_group.py ├── test_invite.py ├── test_profile.py ├── test_redirects.py ├── test_register.py └── test_search.py └── variables.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .git 3 | **/__pycache__ 4 | **/*.pyc 5 | results 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .pytest_cache 4 | *.pyc 5 | Pipfile.lock 6 | results/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 2.7 3 | install: 4 | - pip install -r pipenv.txt 5 | script: 6 | - pipenv install --system --skip-lock 7 | - pipenv run flake8 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive \ 4 | MOZ_HEADLESS=1 5 | 6 | RUN apt-get update \ 7 | && apt-get install -y software-properties-common \ 8 | && add-apt-repository ppa:deadsnakes/ppa \ 9 | && apt-get update \ 10 | && apt-get install -y bzip2 curl firefox python2.7 \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | WORKDIR /src 14 | COPY pipenv.txt /src 15 | 16 | RUN curl -fsSL https://bootstrap.pypa.io/get-pip.py | python2.7 17 | RUN pip install -r pipenv.txt 18 | 19 | ENV FIREFOX_VERSION=64.0 20 | 21 | RUN curl -fsSLo /tmp/firefox.tar.bz2 https://download-installer.cdn.mozilla.net/pub/firefox/releases/$FIREFOX_VERSION/linux-x86_64/en-US/firefox-$FIREFOX_VERSION.tar.bz2 \ 22 | && apt-get -y purge firefox \ 23 | && rm -rf /opt/firefox \ 24 | && tar -C /opt -xjf /tmp/firefox.tar.bz2 \ 25 | && rm /tmp/firefox.tar.bz2 \ 26 | && mv /opt/firefox /opt/firefox-$FIREFOX_VERSION \ 27 | && ln -fs /opt/firefox-$FIREFOX_VERSION/firefox /usr/bin/firefox 28 | 29 | ENV GECKODRIVER_VERSION=0.24.0 30 | RUN curl -fsSLo /tmp/geckodriver.tar.gz https://github.com/mozilla/geckodriver/releases/download/v$GECKODRIVER_VERSION/geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz \ 31 | && rm -rf /opt/geckodriver \ 32 | && tar -C /opt -zxf /tmp/geckodriver.tar.gz \ 33 | && rm /tmp/geckodriver.tar.gz \ 34 | && mv /opt/geckodriver /opt/geckodriver-$GECKODRIVER_VERSION \ 35 | && chmod 755 /opt/geckodriver-$GECKODRIVER_VERSION \ 36 | && ln -fs /opt/geckodriver-$GECKODRIVER_VERSION /usr/bin/geckodriver 37 | 38 | WORKDIR /src 39 | COPY Pipfile /src/ 40 | RUN pipenv install --system --skip-lock 41 | 42 | COPY . /src 43 | CMD pytest --variables=/variables.json 44 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | def branch = env.BRANCH_NAME ?: 'master' 4 | 5 | /** Desired capabilities */ 6 | def capabilities = [ 7 | browserName: 'Firefox', 8 | version: '65.0', 9 | platform: 'Windows 10' 10 | ] 11 | 12 | pipeline { 13 | agent any 14 | libraries { 15 | lib('fxtest@1.10') 16 | } 17 | triggers { 18 | pollSCM(branch == 'master' ? 'H/5 * * * *' : '') 19 | cron(branch == 'master' ? 'H H * * *' : '') 20 | } 21 | options { 22 | ansiColor('xterm') 23 | timestamps() 24 | timeout(time: 1, unit: 'HOURS') 25 | } 26 | stages { 27 | stage('Lint') { 28 | agent { 29 | dockerfile true 30 | } 31 | steps { 32 | sh "flake8" 33 | } 34 | } 35 | stage('Test') { 36 | agent { 37 | dockerfile true 38 | } 39 | environment { 40 | VARIABLES = credentials('MOZILLIANS_VARIABLES') 41 | PYTEST_PROCESSES = "${PYTEST_PROCESSES ?: "auto"}" 42 | PULSE = credentials('PULSE') 43 | SAUCELABS = credentials('SAUCELABS') 44 | } 45 | steps { 46 | writeCapabilities(capabilities, 'capabilities.json') 47 | sh "pytest " + 48 | "-n=${PYTEST_PROCESSES} " + 49 | "--tb=short " + 50 | "--color=yes " + 51 | "--driver=SauceLabs " + 52 | "--variables=capabilities.json " + 53 | "--variables=${VARIABLES} " + 54 | "--junit-xml=results/junit.xml " + 55 | "--html=results/index.html " + 56 | "--self-contained-html " + 57 | "--log-raw=results/raw.txt " + 58 | "--log-tbpl=results/tbpl.txt" 59 | } 60 | post { 61 | always { 62 | stash includes: 'results/*', name: 'results' 63 | archiveArtifacts 'results/*' 64 | junit 'results/*.xml' 65 | submitToActiveData('results/raw.txt') 66 | submitToTreeherder('mozillians-tests', 'e2e', 'End-to-end integration tests', 'results/*', 'results/tbpl.txt') 67 | } 68 | } 69 | } 70 | } 71 | post { 72 | always { 73 | unstash 'results' 74 | publishHTML(target: [ 75 | allowMissing: false, 76 | alwaysLinkToLastBuild: true, 77 | keepAll: true, 78 | reportDir: 'results', 79 | reportFiles: 'index.html', 80 | reportName: 'HTML Report']) 81 | } 82 | changed { 83 | ircNotification() 84 | } 85 | failure { 86 | emailext( 87 | attachLog: true, 88 | attachmentsPattern: 'results/index.html', 89 | body: '$BUILD_URL\n\n$FAILED_TESTS', 90 | replyTo: '$DEFAULT_REPLYTO', 91 | subject: '$DEFAULT_SUBJECT', 92 | to: '$DEFAULT_RECIPIENTS') 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Unless otherwise stated, this license applies to all files contained 2 | within this source code repository. 3 | 4 | 5 | Mozilla Public License Version 2.0 6 | ================================== 7 | 8 | 1. Definitions 9 | -------------- 10 | 11 | 1.1. "Contributor" 12 | means each individual or legal entity that creates, contributes to 13 | the creation of, or owns Covered Software. 14 | 15 | 1.2. "Contributor Version" 16 | means the combination of the Contributions of others (if any) used 17 | by a Contributor and that particular Contributor's Contribution. 18 | 19 | 1.3. "Contribution" 20 | means Covered Software of a particular Contributor. 21 | 22 | 1.4. "Covered Software" 23 | means Source Code Form to which the initial Contributor has attached 24 | the notice in Exhibit A, the Executable Form of such Source Code 25 | Form, and Modifications of such Source Code Form, in each case 26 | including portions thereof. 27 | 28 | 1.5. "Incompatible With Secondary Licenses" 29 | means 30 | 31 | (a) that the initial Contributor has attached the notice described 32 | in Exhibit B to the Covered Software; or 33 | 34 | (b) that the Covered Software was made available under the terms of 35 | version 1.1 or earlier of the License, but not also under the 36 | terms of a Secondary License. 37 | 38 | 1.6. "Executable Form" 39 | means any form of the work other than Source Code Form. 40 | 41 | 1.7. "Larger Work" 42 | means a work that combines Covered Software with other material, in 43 | a separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | means this document. 47 | 48 | 1.9. "Licensable" 49 | means having the right to grant, to the maximum extent possible, 50 | whether at the time of the initial grant or subsequently, any and 51 | all of the rights conveyed by this License. 52 | 53 | 1.10. "Modifications" 54 | means any of the following: 55 | 56 | (a) any file in Source Code Form that results from an addition to, 57 | deletion from, or modification of the contents of Covered 58 | Software; or 59 | 60 | (b) any new file in Source Code Form that contains any Covered 61 | Software. 62 | 63 | 1.11. "Patent Claims" of a Contributor 64 | means any patent claim(s), including without limitation, method, 65 | process, and apparatus claims, in any patent Licensable by such 66 | Contributor that would be infringed, but for the grant of the 67 | License, by the making, using, selling, offering for sale, having 68 | made, import, or transfer of either its Contributions or its 69 | Contributor Version. 70 | 71 | 1.12. "Secondary License" 72 | means either the GNU General Public License, Version 2.0, the GNU 73 | Lesser General Public License, Version 2.1, the GNU Affero General 74 | Public License, Version 3.0, or any later versions of those 75 | licenses. 76 | 77 | 1.13. "Source Code Form" 78 | means the form of the work preferred for making modifications. 79 | 80 | 1.14. "You" (or "Your") 81 | means an individual or a legal entity exercising rights under this 82 | License. For legal entities, "You" includes any entity that 83 | controls, is controlled by, or is under common control with You. For 84 | purposes of this definition, "control" means (a) the power, direct 85 | or indirect, to cause the direction or management of such entity, 86 | whether by contract or otherwise, or (b) ownership of more than 87 | fifty percent (50%) of the outstanding shares or beneficial 88 | ownership of such entity. 89 | 90 | 2. License Grants and Conditions 91 | -------------------------------- 92 | 93 | 2.1. Grants 94 | 95 | Each Contributor hereby grants You a world-wide, royalty-free, 96 | non-exclusive license: 97 | 98 | (a) under intellectual property rights (other than patent or trademark) 99 | Licensable by such Contributor to use, reproduce, make available, 100 | modify, display, perform, distribute, and otherwise exploit its 101 | Contributions, either on an unmodified basis, with Modifications, or 102 | as part of a Larger Work; and 103 | 104 | (b) under Patent Claims of such Contributor to make, use, sell, offer 105 | for sale, have made, import, and otherwise transfer either its 106 | Contributions or its Contributor Version. 107 | 108 | 2.2. Effective Date 109 | 110 | The licenses granted in Section 2.1 with respect to any Contribution 111 | become effective for each Contribution on the date the Contributor first 112 | distributes such Contribution. 113 | 114 | 2.3. Limitations on Grant Scope 115 | 116 | The licenses granted in this Section 2 are the only rights granted under 117 | this License. No additional rights or licenses will be implied from the 118 | distribution or licensing of Covered Software under this License. 119 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 120 | Contributor: 121 | 122 | (a) for any code that a Contributor has removed from Covered Software; 123 | or 124 | 125 | (b) for infringements caused by: (i) Your and any other third party's 126 | modifications of Covered Software, or (ii) the combination of its 127 | Contributions with other software (except as part of its Contributor 128 | Version); or 129 | 130 | (c) under Patent Claims infringed by Covered Software in the absence of 131 | its Contributions. 132 | 133 | This License does not grant any rights in the trademarks, service marks, 134 | or logos of any Contributor (except as may be necessary to comply with 135 | the notice requirements in Section 3.4). 136 | 137 | 2.4. Subsequent Licenses 138 | 139 | No Contributor makes additional grants as a result of Your choice to 140 | distribute the Covered Software under a subsequent version of this 141 | License (see Section 10.2) or under the terms of a Secondary License (if 142 | permitted under the terms of Section 3.3). 143 | 144 | 2.5. Representation 145 | 146 | Each Contributor represents that the Contributor believes its 147 | Contributions are its original creation(s) or it has sufficient rights 148 | to grant the rights to its Contributions conveyed by this License. 149 | 150 | 2.6. Fair Use 151 | 152 | This License is not intended to limit any rights You have under 153 | applicable copyright doctrines of fair use, fair dealing, or other 154 | equivalents. 155 | 156 | 2.7. Conditions 157 | 158 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 159 | in Section 2.1. 160 | 161 | 3. Responsibilities 162 | ------------------- 163 | 164 | 3.1. Distribution of Source Form 165 | 166 | All distribution of Covered Software in Source Code Form, including any 167 | Modifications that You create or to which You contribute, must be under 168 | the terms of this License. You must inform recipients that the Source 169 | Code Form of the Covered Software is governed by the terms of this 170 | License, and how they can obtain a copy of this License. You may not 171 | attempt to alter or restrict the recipients' rights in the Source Code 172 | Form. 173 | 174 | 3.2. Distribution of Executable Form 175 | 176 | If You distribute Covered Software in Executable Form then: 177 | 178 | (a) such Covered Software must also be made available in Source Code 179 | Form, as described in Section 3.1, and You must inform recipients of 180 | the Executable Form how they can obtain a copy of such Source Code 181 | Form by reasonable means in a timely manner, at a charge no more 182 | than the cost of distribution to the recipient; and 183 | 184 | (b) You may distribute such Executable Form under the terms of this 185 | License, or sublicense it under different terms, provided that the 186 | license for the Executable Form does not attempt to limit or alter 187 | the recipients' rights in the Source Code Form under this License. 188 | 189 | 3.3. Distribution of a Larger Work 190 | 191 | You may create and distribute a Larger Work under terms of Your choice, 192 | provided that You also comply with the requirements of this License for 193 | the Covered Software. If the Larger Work is a combination of Covered 194 | Software with a work governed by one or more Secondary Licenses, and the 195 | Covered Software is not Incompatible With Secondary Licenses, this 196 | License permits You to additionally distribute such Covered Software 197 | under the terms of such Secondary License(s), so that the recipient of 198 | the Larger Work may, at their option, further distribute the Covered 199 | Software under the terms of either this License or such Secondary 200 | License(s). 201 | 202 | 3.4. Notices 203 | 204 | You may not remove or alter the substance of any license notices 205 | (including copyright notices, patent notices, disclaimers of warranty, 206 | or limitations of liability) contained within the Source Code Form of 207 | the Covered Software, except that You may alter any license notices to 208 | the extent required to remedy known factual inaccuracies. 209 | 210 | 3.5. Application of Additional Terms 211 | 212 | You may choose to offer, and to charge a fee for, warranty, support, 213 | indemnity or liability obligations to one or more recipients of Covered 214 | Software. However, You may do so only on Your own behalf, and not on 215 | behalf of any Contributor. You must make it absolutely clear that any 216 | such warranty, support, indemnity, or liability obligation is offered by 217 | You alone, and You hereby agree to indemnify every Contributor for any 218 | liability incurred by such Contributor as a result of warranty, support, 219 | indemnity or liability terms You offer. You may include additional 220 | disclaimers of warranty and limitations of liability specific to any 221 | jurisdiction. 222 | 223 | 4. Inability to Comply Due to Statute or Regulation 224 | --------------------------------------------------- 225 | 226 | If it is impossible for You to comply with any of the terms of this 227 | License with respect to some or all of the Covered Software due to 228 | statute, judicial order, or regulation then You must: (a) comply with 229 | the terms of this License to the maximum extent possible; and (b) 230 | describe the limitations and the code they affect. Such description must 231 | be placed in a text file included with all distributions of the Covered 232 | Software under this License. Except to the extent prohibited by statute 233 | or regulation, such description must be sufficiently detailed for a 234 | recipient of ordinary skill to be able to understand it. 235 | 236 | 5. Termination 237 | -------------- 238 | 239 | 5.1. The rights granted under this License will terminate automatically 240 | if You fail to comply with any of its terms. However, if You become 241 | compliant, then the rights granted under this License from a particular 242 | Contributor are reinstated (a) provisionally, unless and until such 243 | Contributor explicitly and finally terminates Your grants, and (b) on an 244 | ongoing basis, if such Contributor fails to notify You of the 245 | non-compliance by some reasonable means prior to 60 days after You have 246 | come back into compliance. Moreover, Your grants from a particular 247 | Contributor are reinstated on an ongoing basis if such Contributor 248 | notifies You of the non-compliance by some reasonable means, this is the 249 | first time You have received notice of non-compliance with this License 250 | from such Contributor, and You become compliant prior to 30 days after 251 | Your receipt of the notice. 252 | 253 | 5.2. If You initiate litigation against any entity by asserting a patent 254 | infringement claim (excluding declaratory judgment actions, 255 | counter-claims, and cross-claims) alleging that a Contributor Version 256 | directly or indirectly infringes any patent, then the rights granted to 257 | You by any and all Contributors for the Covered Software under Section 258 | 2.1 of this License shall terminate. 259 | 260 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 261 | end user license agreements (excluding distributors and resellers) which 262 | have been validly granted by You or Your distributors under this License 263 | prior to termination shall survive termination. 264 | 265 | ************************************************************************ 266 | * * 267 | * 6. Disclaimer of Warranty * 268 | * ------------------------- * 269 | * * 270 | * Covered Software is provided under this License on an "as is" * 271 | * basis, without warranty of any kind, either expressed, implied, or * 272 | * statutory, including, without limitation, warranties that the * 273 | * Covered Software is free of defects, merchantable, fit for a * 274 | * particular purpose or non-infringing. The entire risk as to the * 275 | * quality and performance of the Covered Software is with You. * 276 | * Should any Covered Software prove defective in any respect, You * 277 | * (not any Contributor) assume the cost of any necessary servicing, * 278 | * repair, or correction. This disclaimer of warranty constitutes an * 279 | * essential part of this License. No use of any Covered Software is * 280 | * authorized under this License except under this disclaimer. * 281 | * * 282 | ************************************************************************ 283 | 284 | ************************************************************************ 285 | * * 286 | * 7. Limitation of Liability * 287 | * -------------------------- * 288 | * * 289 | * Under no circumstances and under no legal theory, whether tort * 290 | * (including negligence), contract, or otherwise, shall any * 291 | * Contributor, or anyone who distributes Covered Software as * 292 | * permitted above, be liable to You for any direct, indirect, * 293 | * special, incidental, or consequential damages of any character * 294 | * including, without limitation, damages for lost profits, loss of * 295 | * goodwill, work stoppage, computer failure or malfunction, or any * 296 | * and all other commercial damages or losses, even if such party * 297 | * shall have been informed of the possibility of such damages. This * 298 | * limitation of liability shall not apply to liability for death or * 299 | * personal injury resulting from such party's negligence to the * 300 | * extent applicable law prohibits such limitation. Some * 301 | * jurisdictions do not allow the exclusion or limitation of * 302 | * incidental or consequential damages, so this exclusion and * 303 | * limitation may not apply to You. * 304 | * * 305 | ************************************************************************ 306 | 307 | 8. Litigation 308 | ------------- 309 | 310 | Any litigation relating to this License may be brought only in the 311 | courts of a jurisdiction where the defendant maintains its principal 312 | place of business and such litigation shall be governed by laws of that 313 | jurisdiction, without reference to its conflict-of-law provisions. 314 | Nothing in this Section shall prevent a party's ability to bring 315 | cross-claims or counter-claims. 316 | 317 | 9. Miscellaneous 318 | ---------------- 319 | 320 | This License represents the complete agreement concerning the subject 321 | matter hereof. If any provision of this License is held to be 322 | unenforceable, such provision shall be reformed only to the extent 323 | necessary to make it enforceable. Any law or regulation which provides 324 | that the language of a contract shall be construed against the drafter 325 | shall not be used to construe this License against a Contributor. 326 | 327 | 10. Versions of the License 328 | --------------------------- 329 | 330 | 10.1. New Versions 331 | 332 | Mozilla Foundation is the license steward. Except as provided in Section 333 | 10.3, no one other than the license steward has the right to modify or 334 | publish new versions of this License. Each version will be given a 335 | distinguishing version number. 336 | 337 | 10.2. Effect of New Versions 338 | 339 | You may distribute the Covered Software under the terms of the version 340 | of the License under which You originally received the Covered Software, 341 | or under the terms of any subsequent version published by the license 342 | steward. 343 | 344 | 10.3. Modified Versions 345 | 346 | If you create software not governed by this License, and you want to 347 | create a new license for such software, you may create and use a 348 | modified version of this License if you rename the license and remove 349 | any references to the name of the license steward (except to note that 350 | such modified license differs from this License). 351 | 352 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 353 | Licenses 354 | 355 | If You choose to distribute Source Code Form that is Incompatible With 356 | Secondary Licenses under the terms of this version of the License, the 357 | notice described in Exhibit B of this License must be attached. 358 | 359 | Exhibit A - Source Code Form License Notice 360 | ------------------------------------------- 361 | 362 | This Source Code Form is subject to the terms of the Mozilla Public 363 | License, v. 2.0. If a copy of the MPL was not distributed with this 364 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 365 | 366 | If it is not possible or desirable to put the notice in a particular 367 | file, then You may include the notice in a location (such as a LICENSE 368 | file in a relevant directory) where a recipient would be likely to look 369 | for such a notice. 370 | 371 | You may add additional accurate notices of copyright ownership. 372 | 373 | Exhibit B - "Incompatible With Secondary Licenses" Notice 374 | --------------------------------------------------------- 375 | 376 | This Source Code Form is "Incompatible With Secondary Licenses", as 377 | defined by the Mozilla Public License, v. 2.0. 378 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [dev-packages] 9 | 10 | 11 | 12 | [packages] 13 | 14 | BeautifulSoup = "==3.2.1" 15 | flake8 = "==3.7.8" 16 | flake8-isort = "==2.7.0" 17 | mozlog = "==4.2.0" 18 | PyPOM = "==2.2.0" 19 | pytest = "==4.4.1" 20 | pytest-metadata = "==1.8.0" 21 | pytest-selenium = "==1.16.0" 22 | pytest-variables = "==1.7.1" 23 | pytest-xdist = "==1.29.0" 24 | requests = "==2.22.0" 25 | pyotp = "==2.2.7" 26 | 27 | 28 | [packages.configparser] 29 | 30 | version = "==3.8.1" 31 | markers = "python_version < '3.5'" 32 | 33 | 34 | [packages.enum34] 35 | 36 | version = "==1.1.6" 37 | markers = "python_version < '3.4'" 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tests for mozillians.org 2 | 3 | Thank you for checking out our Mozillians test suite! This repository contains 4 | tests for [Mozillians](https://mozillians.org/) - a community phonebook for 5 | core contributors. 6 | 7 | [![license](https://img.shields.io/badge/license-MPL%202.0-blue.svg)](https://github.com/mozilla/mozillians-tests/blob/master/LICENSE.txt) 8 | [![travis](https://img.shields.io/travis/mozilla/mozillians-tests.svg?label=travis)](http://travis-ci.org/mozilla/mozillians-tests/) 9 | [![updates](https://api.dependabot.com/badges/status?host=github&repo=mozilla/mozillians-tests)](https://dependabot.com) 10 | 11 | 12 | ## Table of contents: 13 | 14 | * [Getting involved](#getting-involved) 15 | * [How to run the tests](#how-to-run-the-tests) 16 | * [Writing tests](#writing-tests) 17 | 18 | ## Getting involved 19 | 20 | We love working with contributors to improve test coverage our projects, but it 21 | does require a few skills. By contributing to our test suite you will have an 22 | opportunity to learn and/or improve your skills with Python, Selenium 23 | WebDriver, GitHub, virtual environments, the Page Object Model, and more. 24 | 25 | Our [new contributor guide][guide] should help you to get started, and will 26 | also point you in the right direction if you need to ask questions. 27 | 28 | ## How to run the tests 29 | 30 | ### Clone the repository 31 | 32 | If you have cloned this project already, then you can skip this; otherwise 33 | you'll need to clone this repo using Git. If you do not know how to clone a 34 | GitHub repository, check out this [help page][git clone] from GitHub. 35 | 36 | If you think you would like to contribute to the tests by writing or 37 | maintaining them in the future, it would be a good idea to create a fork of 38 | this repository first, and then clone that. GitHub also has great instructions 39 | for [forking a repository][git fork]. 40 | 41 | ### Create test variables files 42 | 43 | Some of the tests require credentials associated with account with specific 44 | access levels. Create at least three users on [staging][]. Vouch at least two 45 | of these users by adding '/vouch' to the end of the profile URL for each user. 46 | In one of the vouched users' profiles, join at least one group and mark groups 47 | as private. 48 | 49 | Create a file outside of the project (to avoid accidentally exposing the 50 | credentials) with the following format. You will reference this file when 51 | running the tests using the `--variables` command line option. 52 | 53 | Note that the `vouched` key is a list. This is so that multiple vouched users 54 | can be used when running the tests in parallel. It's recommended that you have 55 | as many vouched users as you intend to have tests running in parallel. 56 | 57 | ```json 58 | { 59 | "web-mozillians-staging.production.paas.mozilla.community": { 60 | "users": { 61 | "vouched": [ 62 | { 63 | "username": "vouched", 64 | "email": "vouched@example.com", 65 | "name": "Vouched User" 66 | } 67 | ], 68 | "unvouched": { 69 | "username": "unvouched", 70 | "email": "unvouched@example.com", 71 | "name": "Unvouched User" 72 | }, 73 | "private": { 74 | "username": "private", 75 | "email": "private@example.com", 76 | "name": "Private User" 77 | } 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | Then you can run the tests using [Docker][]: 84 | 85 | ```bash 86 | $ docker build -t mozillians-tests . 87 | $ docker run -it \ 88 | --mount type=bind,source=/path/to/variables.json,destination=/variables.json,readonly \ 89 | mozillians-tests 90 | ``` 91 | 92 | ### Run the tests using Sauce Labs 93 | 94 | You will need a [Sauce Labs][] account, with a `.saucelabs` file in your home 95 | directory containing your username and API key, as follows: 96 | 97 | ```ini 98 | [credentials] 99 | username = username 100 | key = secret 101 | ``` 102 | 103 | Then you can run the tests against Sauce Labs using [Docker][] by passing the 104 | `--driver SauceLabs` argument as shown below. The `--mount` argument is 105 | important, as it allows your `.saucelabs` file to be accessed by the Docker 106 | container: 107 | 108 | ```bash 109 | $ docker build -t mozillians-tests . 110 | $ docker run -it \ 111 | --mount type=bind,source=$HOME/.saucelabs,destination=/src/.saucelabs,readonly \ 112 | --mount type=bind,source=/path/to/variables.json,destination=/variables.json,readonly \ 113 | mozillians-tests pytest --variables /variables.json \ 114 | --driver SauceLabs --capability browserName Firefox 115 | ``` 116 | 117 | See the documentation on [specifying capabilities][] and the Sauce Labs 118 | [platform configurator][] for selecting the target platform. 119 | 120 | ## Writing tests 121 | 122 | If you want to get involved and add more tests, then there are just a few 123 | things we'd like to ask you to do: 124 | 125 | 1. Follow our simple [style guide][]. 126 | 2. Fork this project with your own GitHub account. 127 | 3. Make sure all tests are passing, and submit a pull request. 128 | 4. Always feel free to reach out to us and ask questions. 129 | 130 | [sauce labs]: https://saucelabs.com/ 131 | [Docker]: https://www.docker.com 132 | [guide]: http://firefox-test-engineering.readthedocs.io/en/latest/guide/index.html 133 | [git clone]: https://help.github.com/articles/cloning-a-repository/ 134 | [git fork]: https://help.github.com/articles/fork-a-repo/ 135 | [staging]: https://web-mozillians-staging.production.paas.mozilla.community/ 136 | [specifying capabilities]: http://pytest-selenium.readthedocs.io/en/latest/user_guide.html#specifying-capabilities 137 | [platform configurator]: http://pytest-selenium.readthedocs.io/en/latest/user_guide.html#specifying-capabilities 138 | [style guide]: https://wiki.mozilla.org/QA/Execution/Web_Testing/Docs/Automation/StyleGuide 139 | -------------------------------------------------------------------------------- /pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/mozillians-tests/b3bc38f407fe29fbf72b92c10a30d00383671db2/pages/__init__.py -------------------------------------------------------------------------------- /pages/about.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | 7 | from pages.base import Base 8 | 9 | 10 | class About(Base): 11 | 12 | _privacy_section_locator = (By.ID, 'privacy') 13 | _get_involved_section_locator = (By.ID, 'get-involved') 14 | 15 | @property 16 | def is_privacy_section_present(self): 17 | return self.is_element_present(*self._privacy_section_locator) 18 | 19 | @property 20 | def is_get_involved_section_present(self): 21 | return self.is_element_present(*self._get_involved_section_locator) 22 | -------------------------------------------------------------------------------- /pages/auth0.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from pypom import Page 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.support import expected_conditions as expected 8 | 9 | from pages.github import Github 10 | 11 | 12 | class Auth0(Page): 13 | 14 | _email_locator = (By.ID, 'field-email-signup') 15 | _enter_locator = (By.ID, 'enter-initial-signup') 16 | _send_email_locator = (By.CSS_SELECTOR, 'button[data-handler=send-passwordless-link]') 17 | _login_with_github_button_locator = (By.CSS_SELECTOR, '#initial-login-signup button[data-handler="authorise-github"]') 18 | 19 | def request_login_link(self, username): 20 | self.wait.until(expected.visibility_of_element_located( 21 | self._email_locator)).send_keys(username) 22 | self.find_element(*self._enter_locator).click() 23 | self.wait.until(expected.visibility_of_element_located( 24 | self._send_email_locator)).click() 25 | 26 | def click_login_with_github(self): 27 | self.find_element(*self._login_with_github_button_locator).click() 28 | return Github(self.selenium, self.base_url) 29 | -------------------------------------------------------------------------------- /pages/base.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from pypom import Page 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.common.keys import Keys 8 | from selenium.webdriver.support import expected_conditions as expected 9 | from selenium.webdriver.support.select import Select 10 | 11 | from pages.auth0 import Auth0 12 | from tests import restmail 13 | 14 | 15 | class Base(Page): 16 | URL_TEMPLATE = '/{locale}' 17 | 18 | _logout_locator = (By.ID, 'nav-logout') 19 | 20 | _pending_approval_locator = (By.ID, 'pending-approval') 21 | _account_created_successfully_locator = (By.CSS_SELECTOR, 'div.alert:nth-child(2)') 22 | 23 | # Not logged in 24 | _sign_in_button_locator = (By.ID, 'nav-login') 25 | 26 | def __init__(self, selenium, base_url, locale='en-US', **url_kwargs): 27 | super(Base, self).__init__(selenium, base_url, locale=locale, **url_kwargs) 28 | 29 | @property 30 | def page_title(self): 31 | return self.wait.until(lambda s: self.selenium.title) 32 | 33 | @property 34 | def is_pending_approval_visible(self): 35 | return self.is_element_displayed(*self._pending_approval_locator) 36 | 37 | @property 38 | def was_account_created_successfully(self): 39 | return self.is_element_displayed(*self._account_created_successfully_locator) 40 | 41 | # Not logged in 42 | 43 | @property 44 | def is_sign_in_button_present(self): 45 | return self.is_element_present(*self._sign_in_button_locator) 46 | 47 | @property 48 | def is_user_loggedin(self): 49 | return self.is_element_present(*self._logout_locator) 50 | 51 | def click_sign_in_button(self): 52 | self.find_element(*self._sign_in_button_locator).click() 53 | 54 | def login(self, email_address): 55 | self.click_sign_in_button() 56 | auth0 = Auth0(self.selenium, self.base_url) 57 | auth0.request_login_link(email_address) 58 | login_link = restmail.get_mail(email_address) 59 | self.selenium.get(login_link) 60 | self.wait.until(lambda s: self.is_user_loggedin) 61 | 62 | def login_with_github(self, username, password, secret): 63 | self.click_sign_in_button() 64 | auth0 = Auth0(self.selenium, self.base_url) 65 | github = auth0.click_login_with_github() 66 | github.login_with_github(username, password, secret) 67 | 68 | def create_new_user(self, email): 69 | self.login(email) 70 | from pages.register import Register 71 | return Register(self.selenium, self.base_url).wait_for_page_to_load() 72 | 73 | @property 74 | def header(self): 75 | return self.Header(self.selenium, self.base_url) 76 | 77 | @property 78 | def footer(self): 79 | return self.Footer(self.selenium, self.base_url) 80 | 81 | class Header(Page): 82 | 83 | _search_box_locator = (By.CSS_SELECTOR, '.search-query') 84 | _search_box_loggedin_locator = (By.CSS_SELECTOR, '.search-right > form > .search-query') 85 | _profile_menu_locator = (By.CSS_SELECTOR, '#nav-main > a.dropdown-toggle i') 86 | 87 | # menu items 88 | _dropdown_menu_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu') 89 | _view_profile_menu_item_locator = (By.ID, 'nav-profile') 90 | _groups_menu_item_locator = (By.ID, 'nav-groups') 91 | _invite_menu_item_locator = (By.ID, 'nav-invite') 92 | _settings_menu_item_locator = (By.ID, 'nav-edit-profile') 93 | _logout_menu_item_locator = (By.ID, 'nav-logout') 94 | 95 | @property 96 | def is_search_box_present(self): 97 | return self.is_element_present(*self._search_box_locator) 98 | 99 | def search_for(self, search_term, loggedin=False): 100 | if loggedin: 101 | search_field = self.find_element(*self._search_box_loggedin_locator) 102 | else: 103 | search_field = self.find_element(*self._search_box_locator) 104 | search_field.send_keys(search_term) 105 | search_field.send_keys(Keys.RETURN) 106 | from pages.search import Search 107 | return Search(self.selenium, self.base_url).wait_for_page_to_load() 108 | 109 | def click_options(self): 110 | self.wait.until(expected.visibility_of_element_located( 111 | self._profile_menu_locator)).click() 112 | self.wait.until(expected.visibility_of_element_located( 113 | self._dropdown_menu_locator)) 114 | 115 | @property 116 | def is_logout_menu_item_present(self): 117 | return self.is_element_present(*self._logout_menu_item_locator) 118 | 119 | @property 120 | def is_groups_menu_item_present(self): 121 | return self.is_element_present(*self._groups_menu_item_locator) 122 | 123 | # menu items 124 | def click_view_profile_menu_item(self): 125 | self.click_options() 126 | self.find_element(*self._view_profile_menu_item_locator).click() 127 | from pages.profile import Profile 128 | return Profile(self.selenium, self.base_url).wait_for_page_to_load() 129 | 130 | def click_invite_menu_item(self): 131 | self.click_options() 132 | self.find_element(*self._invite_menu_item_locator).click() 133 | from pages.invite import Invite 134 | return Invite(self.selenium, self.base_url) 135 | 136 | def click_settings_menu_item(self): 137 | self.click_options() 138 | self.find_element(*self._settings_menu_item_locator).click() 139 | from pages.settings import Settings 140 | return Settings(self.selenium, self.base_url) 141 | 142 | def click_logout_menu_item(self): 143 | self.click_options() 144 | self.find_element(*self._logout_menu_item_locator).click() 145 | self.wait.until(lambda s: not self.is_logout_menu_item_present) 146 | 147 | def click_groups_menu_item(self): 148 | self.click_options() 149 | self.find_element(*self._groups_menu_item_locator).click() 150 | from pages.groups_page import GroupsPage 151 | return GroupsPage(self.selenium, self.base_url) 152 | 153 | class Footer(Page): 154 | 155 | _about_mozillians_link_locator = (By.CSS_SELECTOR, '.footer-nav.details > li:nth-child(1) > a') 156 | _language_selector_locator = (By.ID, 'language') 157 | _language_selection_ok_button = (By.CSS_SELECTOR, '#language-switcher button') 158 | 159 | def click_about_link(self): 160 | self.find_element(*self._about_mozillians_link_locator).click() 161 | from pages.about import About 162 | return About(self.selenium, self.base_url) 163 | 164 | def select_language(self, lang_code): 165 | element = self.find_element(*self._language_selector_locator) 166 | select = Select(element) 167 | select.select_by_value(lang_code) 168 | -------------------------------------------------------------------------------- /pages/confirm_profile_delete.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | 7 | from pages.base import Base 8 | 9 | 10 | class ConfirmProfileDelete(Base): 11 | 12 | _delete_button_locator = (By.ID, 'delete-action') 13 | _cancel_button_locator = (By.ID, 'cancel-action') 14 | _confirm_profile_delete_text_locator = (By.CSS_SELECTOR, '#main > h1') 15 | 16 | @property 17 | def is_confirm_text_present(self): 18 | return self.is_element_displayed(*self._confirm_profile_delete_text_locator) 19 | 20 | @property 21 | def is_delete_button_present(self): 22 | return self.is_element_present(*self._delete_button_locator) 23 | 24 | @property 25 | def is_cancel_button_present(self): 26 | return self.is_element_present(*self._cancel_button_locator) 27 | -------------------------------------------------------------------------------- /pages/create_group_page.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.support import expected_conditions as expected 7 | 8 | from pages.base import Base 9 | 10 | 11 | class CreateGroupPage(Base): 12 | 13 | _create_group_name = (By.NAME, 'name') 14 | _create_group_form = (By.CSS_SELECTOR, 'form.add-group') 15 | _create_group_submit_button = (By.CSS_SELECTOR, 'form.add-group .btn-primary') 16 | _access_group_radio_button = (By.ID, 'id_is_access_group_0') 17 | 18 | @property 19 | def is_access_group_present(self): 20 | return self.is_element_present(*self._access_group_radio_button) 21 | 22 | def create_group_name(self, group_name): 23 | self.wait.until(expected.visibility_of_element_located( 24 | self._create_group_name)).send_keys(group_name) 25 | 26 | def click_create_group_submit(self): 27 | self.wait.until(expected.visibility_of_element_located(self._create_group_form)) 28 | self.find_element(*self._create_group_submit_button).click() 29 | from pages.edit_group import EditGroupPage 30 | return EditGroupPage(self.selenium, self.base_url) 31 | -------------------------------------------------------------------------------- /pages/edit_group.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from pypom import Region 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.support import expected_conditions as expected 8 | 9 | from pages.base import Base 10 | 11 | 12 | class EditGroupPage(Base): 13 | _description_button_locator = (By.ID, 'description-tab') 14 | _description_tab_locator = (By.ID, 'description') 15 | _access_button_locator = (By.ID, 'access-tab') 16 | _access_tab_locator = (By.ID, 'access') 17 | _invitations_button_locator = (By.ID, 'invitations-tab') 18 | _invitations_tab_locator = (By.ID, 'invitations') 19 | 20 | @property 21 | def loaded(self): 22 | return self.is_element_displayed(*self._description_button_locator) 23 | 24 | @property 25 | def description(self): 26 | self.find_element(*self._description_button_locator).click() 27 | return self.DescriptionTab(self, self.find_element(*self._description_tab_locator)) 28 | 29 | @property 30 | def access(self): 31 | self.find_element(*self._access_button_locator).click() 32 | return self.AccessTab(self, self.find_element(*self._access_tab_locator)) 33 | 34 | @property 35 | def invitations(self): 36 | self.wait.until(expected.visibility_of_element_located( 37 | self._invitations_button_locator)).click() 38 | return self.InvitationsTab(self, self.find_element(*self._invitations_tab_locator)) 39 | 40 | class DescriptionTab(Region): 41 | _description_form_locator = (By.ID, 'description-form') 42 | _delete_panel_locator = (By.CSS_SELECTOR, '.panel-danger') 43 | 44 | @property 45 | def description_info(self): 46 | return self.DescriptionForm(self.page, self.find_element(*self._description_form_locator)) 47 | 48 | @property 49 | def delete_group(self): 50 | return self.DeletePanel(self.page, self.find_element(*self._delete_panel_locator)) 51 | 52 | class DescriptionForm(Region): 53 | _description_locator = (By.ID, 'id_description') 54 | _irc_channel_locator = (By.ID, 'id_irc_channel') 55 | _update_locator = (By.ID, 'form-submit-description') 56 | 57 | def set_description(self, description_text): 58 | element = self.find_element(*self._description_locator) 59 | element.clear() 60 | element.send_keys(description_text) 61 | 62 | def set_irc_channel(self, irc_channel): 63 | element = self.find_element(*self._irc_channel_locator) 64 | element.clear() 65 | element.send_keys(irc_channel) 66 | 67 | def click_update(self): 68 | el = self.find_element(*self._update_locator) 69 | el.click() 70 | self.wait.until(expected.staleness_of(el)) 71 | self.wait.until(expected.presence_of_element_located( 72 | self._update_locator)) 73 | 74 | class DeletePanel(Region): 75 | _delete_acknowledgement_locator = (By.ID, 'delete-checkbox') 76 | _delete_group_button_locator = (By.ID, 'delete-group') 77 | 78 | def check_acknowledgement(self): 79 | self.find_element(*self._delete_acknowledgement_locator).click() 80 | 81 | @property 82 | def is_delete_button_enabled(self): 83 | return 'disabled' not in self.find_element(*self._delete_group_button_locator).get_attribute('class') 84 | 85 | def click_delete_group(self): 86 | self.find_element(*self._delete_group_button_locator).click() 87 | from pages.groups_page import GroupsPage 88 | return GroupsPage(self.page.selenium, self.page.base_url) 89 | 90 | class AccessTab(Region): 91 | _group_type_form_locator = (By.ID, 'grouptype-form') 92 | 93 | @property 94 | def group_type(self): 95 | return self.GroupTypeForm(self.page, self.find_element(*self._group_type_form_locator)) 96 | 97 | class GroupTypeForm(Region): 98 | _reviewed_type_locator = (By.ID, 'id_accepting_new_members_1') 99 | _new_member_criteria_locator = (By.ID, 'id_new_member_criteria_fieldset') 100 | 101 | def set_reviewed_group_type(self): 102 | self.find_element(*self._reviewed_type_locator).click() 103 | 104 | @property 105 | def is_member_criteria_visible(self): 106 | return self.find_element(*self._new_member_criteria_locator).is_displayed() 107 | 108 | class InvitationsTab(Region): 109 | _invite_form_locator = (By.ID, 'invite-form') 110 | _invitations_list_form_locator = (By.ID, 'invitations-form') 111 | 112 | @property 113 | def invitations_list(self): 114 | return self.InvitationsForm(self.page, self.find_element(*self._invitations_list_form_locator)) 115 | 116 | @property 117 | def invite(self): 118 | return self.InviteForm(self.page, self.find_element(*self._invite_form_locator)) 119 | 120 | class InvitationsForm(Region): 121 | _invitatation_list_locator = (By.CSS_SELECTOR, '.invitee') 122 | 123 | @property 124 | def search_invitation_list(self): 125 | return [self.SearchResult(self.page, el) for el in 126 | self.find_elements(*self._invitatation_list_locator)] 127 | 128 | class SearchResult(Region): 129 | _name_locator = (By.CSS_SELECTOR, '.invitee a:nth-child(2)') 130 | 131 | @property 132 | def name(self): 133 | return self.find_element(*self._name_locator).text 134 | 135 | class InviteForm(Region): 136 | _invite_search_locator = (By.CSS_SELECTOR, '.select2-search__field') 137 | _invite_locator = (By.ID, 'form-submit-invite') 138 | 139 | def invite_new_member(self, mozillian): 140 | search = self.find_element(*self._invite_search_locator) 141 | search.send_keys(mozillian) 142 | self.wait.until(expected.visibility_of_element_located(( 143 | By.XPATH, '//li[contains(text(), "{0}")]'.format(mozillian)))).click() 144 | 145 | def click_invite(self): 146 | self.find_element(*self._invite_locator).click() 147 | -------------------------------------------------------------------------------- /pages/edit_profile.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import random 6 | 7 | from selenium.webdriver.common.by import By 8 | from selenium.webdriver.common.keys import Keys 9 | from selenium.webdriver.support.select import Select 10 | 11 | from pages.base import Base 12 | from pages.groups_page import GroupsPage 13 | from pages.profile import Profile 14 | 15 | 16 | class EditProfile(Base): 17 | 18 | _acknowledge_deletion_checkbox_locator = (By.CSS_SELECTOR, '.acknowledge') 19 | _cancel_button_locator = (By.CSS_SELECTOR, 'a.cancel') 20 | _update_button_locator = (By.ID, 'form-submit') 21 | _full_name_field_locator = (By.ID, 'id_full_name') 22 | _website_field_locator = (By.ID, 'id_externalaccount_set-0-identifier') 23 | _bio_field_locator = (By.ID, 'id_bio') 24 | _skills_field_locator = (By.CSS_SELECTOR, '#id_skills + ul input') 25 | _groups_locator = (By.CSS_SELECTOR, "#groups .tagit-label") 26 | _skills_locator = (By.CSS_SELECTOR, "#skills .tagit-label") 27 | _voucher_name_locator = (By.CSS_SELECTOR, '#vouches .vouched') 28 | _username_field_locator = (By.ID, 'id_username') 29 | _delete_profile_button_locator = (By.CSS_SELECTOR, '.delete') 30 | _delete_skill_buttons_locator = (By.CSS_SELECTOR, '#skills .tagit-close') 31 | _select_month_locator = (By.ID, 'id_date_mozillian_month') 32 | _select_year_locator = (By.ID, 'id_date_mozillian_year') 33 | _month_locator = (By.CSS_SELECTOR, '#id_date_mozillian_month > option') 34 | _year_locator = (By.CSS_SELECTOR, '#id_date_mozillian_year > option') 35 | _selected_month_locator = (By.CSS_SELECTOR, '#id_date_mozillian_month > option[selected="selected"]') 36 | _selected_year_locator = (By.CSS_SELECTOR, '#id_date_mozillian_year > option[selected="selected"]') 37 | _find_group_page = (By.PARTIAL_LINK_TEXT, 'find the group') 38 | _services_bugzilla_locator = (By.ID, 'services-bugzilla-url') 39 | _services_mozilla_reps_locator = (By.ID, 'services-mozilla-reps') 40 | 41 | def click_update_button(self): 42 | self.find_element(*self._update_button_locator).click() 43 | return Profile(self.selenium, self.base_url) 44 | 45 | def click_cancel_button(self): 46 | self.find_element(*self._cancel_button_locator).click() 47 | 48 | def click_find_group_link(self): 49 | self.find_element(*self._find_group_page).click() 50 | return GroupsPage(self.selenium, self.base_url) 51 | 52 | def set_full_name(self, full_name): 53 | element = self.find_element(*self._full_name_field_locator) 54 | element.clear() 55 | element.send_keys(full_name) 56 | 57 | def set_website(self, website): 58 | element = self.find_element(*self._website_field_locator) 59 | element.clear() 60 | element.send_keys(website) 61 | 62 | def set_bio(self, biography): 63 | element = self.find_element(*self._bio_field_locator) 64 | element.clear() 65 | element.send_keys(biography) 66 | 67 | def add_skill(self, skill_name): 68 | element = self.find_element(*self._skills_field_locator) 69 | element.send_keys(skill_name) 70 | element.send_keys(Keys.RETURN) 71 | 72 | @property 73 | def vouched_by(self): 74 | return self.find_element(*self._voucher_name_locator).text 75 | 76 | @property 77 | def username(self): 78 | return self.find_element(*self._username_field_locator).text 79 | 80 | def click_delete_profile_button(self): 81 | self.find_element(*self._acknowledge_deletion_checkbox_locator).click() 82 | self.find_element(*self._delete_profile_button_locator).click() 83 | from pages.confirm_profile_delete import ConfirmProfileDelete 84 | return ConfirmProfileDelete(self.selenium, self.base_url) 85 | 86 | def select_month(self, option_month): 87 | element = self.find_element(*self._select_month_locator) 88 | select = Select(element) 89 | select.select_by_value(option_month) 90 | 91 | def select_year(self, option_year): 92 | element = self.find_element(*self._select_year_locator) 93 | select = Select(element) 94 | select.select_by_value(option_year) 95 | 96 | @property 97 | def month(self): 98 | return self.find_element(*self._selected_month_locator).text 99 | 100 | @property 101 | def year(self): 102 | return self.find_element(*self._selected_year_locator).text 103 | 104 | @property 105 | def months_values(self): 106 | return [month.get_attribute('value') for month in self.find_elements(*self._month_locator)] 107 | 108 | def select_random_month(self): 109 | return self.select_month(random.choice(self.months_values[1:])) 110 | 111 | @property 112 | def years_values(self): 113 | return [year.get_attribute('value') for year in self.find_elements(*self._year_locator)] 114 | 115 | @property 116 | def groups(self): 117 | groups = self.find_elements(*self._groups_locator) 118 | return [groups[i].text for i in range(0, len(groups))] 119 | 120 | @property 121 | def skills(self): 122 | skills = self.find_elements(*self._skills_locator) 123 | return [skills[i].text for i in range(0, len(skills))] 124 | 125 | @property 126 | def delete_skill_buttons(self): 127 | return self.find_elements(*self._delete_skill_buttons_locator) 128 | 129 | def select_random_year(self): 130 | return self.select_year(random.choice(self.years_values[1:])) 131 | 132 | def get_services_urls(self): 133 | locs = [self._services_bugzilla_locator, self._services_mozilla_reps_locator] 134 | urls = [] 135 | 136 | for element in locs: 137 | url = self.find_element(*element).get_attribute('href') 138 | urls.append(url) 139 | 140 | return urls 141 | -------------------------------------------------------------------------------- /pages/github.py: -------------------------------------------------------------------------------- 1 | import pyotp 2 | from pypom import Page 3 | from selenium.webdriver.common.by import By 4 | 5 | 6 | class Github(Page): 7 | 8 | _github_username_field_locator = (By.ID, 'login_field') 9 | _github_password_field_locator = (By.ID, 'password') 10 | _github_sign_in_button_locator = (By.CSS_SELECTOR, '.btn.btn-primary.btn-block') 11 | _github_passcode_field_locator = (By.CSS_SELECTOR, 'input[id="otp"]') 12 | _github_enter_passcode_button_locator = (By.CSS_SELECTOR, '.btn-primary') 13 | 14 | def login_with_github(self, username, password, secret): 15 | self.find_element(*self._github_username_field_locator).send_keys(username) 16 | self.find_element(*self._github_password_field_locator).send_keys(password) 17 | self.find_element(*self._github_sign_in_button_locator).click() 18 | passcode = pyotp.TOTP(secret).now() 19 | self.find_element(*self._github_passcode_field_locator).send_keys(passcode) 20 | self.find_element(*self._github_enter_passcode_button_locator).click() 21 | -------------------------------------------------------------------------------- /pages/group_info_page.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.support import expected_conditions as expected 7 | 8 | from pages.base import Base 9 | 10 | 11 | class GroupInfoPage(Base): 12 | 13 | _delete_group_button = (By.CSS_SELECTOR, '.button.delete.right') 14 | _description_locator = (By.CSS_SELECTOR, '.group-description') 15 | _irc_channel_locator = (By.ID, 'group-irc') 16 | 17 | @property 18 | def loaded(self): 19 | return self.is_element_present( 20 | By.CSS_SELECTOR, 'html.js body#group-show') 21 | 22 | def delete_group(self): 23 | self.wait.until(expected.visibility_of_element_located( 24 | self._delete_group_button)).click() 25 | from pages.groups_page import GroupsPage 26 | return GroupsPage(self.selenium, self.base_url) 27 | 28 | @property 29 | def description(self): 30 | return self.find_element(*self._description_locator).text 31 | 32 | @property 33 | def irc_channel(self): 34 | return self.find_element(*self._irc_channel_locator).text 35 | -------------------------------------------------------------------------------- /pages/groups_page.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.support import expected_conditions as expected 7 | 8 | from pages.base import Base 9 | from pages.create_group_page import CreateGroupPage 10 | 11 | 12 | class GroupsPage(Base): 13 | 14 | _create_group_main_button = (By.CLASS_NAME, 'large') 15 | _alert_message_locator = (By.CSS_SELECTOR, '.alert-info') 16 | 17 | def click_create_group_main_button(self): 18 | self.find_element(*self._create_group_main_button).click() 19 | return CreateGroupPage(self.selenium, self.base_url) 20 | 21 | def wait_for_alert_message(self): 22 | self.wait.until(expected.visibility_of_element_located( 23 | self._alert_message_locator)) 24 | 25 | def is_group_deletion_alert_present(self): 26 | return self.is_element_displayed(*self._alert_message_locator) 27 | 28 | def create_group(self, group_name): 29 | create_group = self.click_create_group_main_button() 30 | create_group.create_group_name(group_name) 31 | group = create_group.click_create_group_submit() 32 | return group 33 | -------------------------------------------------------------------------------- /pages/home_page.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | 7 | from pages.base import Base 8 | 9 | 10 | class Home(Base): 11 | 12 | _groups_link_locator = (By.CSS_SELECTOR, 'section.groups > a') 13 | _functional_areas_link_locator = (By.CSS_SELECTOR, 'section.functional-areas > a') 14 | 15 | @property 16 | def is_groups_link_visible(self): 17 | return self.is_element_displayed(*self._groups_link_locator) 18 | 19 | @property 20 | def is_functional_areas_link_visible(self): 21 | return self.is_element_displayed(*self._functional_areas_link_locator) 22 | 23 | def wait_for_user_login(self): 24 | # waits to see if user gets logged back in 25 | # if not then all ok 26 | try: 27 | self.wait.until(lambda s: self.is_user_loggedin) 28 | except Exception: 29 | pass 30 | -------------------------------------------------------------------------------- /pages/invite.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | 7 | from pages.base import Base 8 | 9 | 10 | class Invite(Base): 11 | 12 | _recipient_field_locator = (By.ID, 'id_recipient') 13 | _vouch_reason_field_locator = (By.ID, 'id_description') 14 | _send_invite_button_locator = (By.CSS_SELECTOR, '#main button') 15 | _error_text_locator = (By.CSS_SELECTOR, '.error-message') 16 | 17 | @property 18 | def error_text_message(self): 19 | return self.find_element(*self._error_text_locator).text 20 | 21 | def invite(self, email, reason=''): 22 | email_field = self.find_element(*self._recipient_field_locator) 23 | email_field.send_keys(email) 24 | reason_field = self.find_element(*self._vouch_reason_field_locator) 25 | reason_field.send_keys(reason) 26 | self.find_element(*self._send_invite_button_locator).click() 27 | from pages.invite_success import InviteSuccess 28 | return InviteSuccess(self.selenium, self.base_url) 29 | -------------------------------------------------------------------------------- /pages/invite_success.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | 7 | from pages.base import Base 8 | 9 | 10 | class InviteSuccess(Base): 11 | 12 | _success_message_locator = (By.CSS_SELECTOR, '.alert.alert-success') 13 | 14 | @property 15 | def success_message(self): 16 | return self.find_element(*self._success_message_locator).text 17 | -------------------------------------------------------------------------------- /pages/link_crawler.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import requests 6 | from BeautifulSoup import BeautifulSoup 7 | 8 | 9 | class LinkCrawler(object): 10 | 11 | def __init__(self, base_url): 12 | self.base_url = base_url 13 | 14 | def collect_links(self, url, relative=True, name=True, **kwargs): 15 | """Collects links for given page URL. 16 | 17 | If name is True, then links will be collected for whole page. 18 | Use name argument to pass tag name of element. 19 | Use kwargs to pass id of element or its class name. 20 | Because 'class' is a reserved keyword in Python, 21 | you need to pass class as: **{'class': 'container row'}. 22 | 23 | Read more about searching elements with BeautifulSoup. 24 | See: http://goo.gl/85BuZ 25 | """ 26 | 27 | # support for relative URLs 28 | if relative: 29 | url = '%s%s' % (self.base_url, url) 30 | 31 | # get the page and verify status code is OK 32 | r = requests.get(url) 33 | assert requests.codes.ok == r.status_code 34 | 35 | # collect links 36 | parsed_html = BeautifulSoup(r.text) 37 | urls = [anchor['href'] for anchor in 38 | parsed_html.find(name, attrs=kwargs).findAll('a')] 39 | 40 | # prepend base_url to relative links 41 | return map( 42 | lambda u: u if u.startswith('http') else '%s%s' % (self.base_url, u), urls) 43 | 44 | def verify_status_code_is_ok(self, url): 45 | r = requests.get(url, verify=False) 46 | if not r.status_code == requests.codes.ok: 47 | return u'{0.url} returned: {0.status_code} {0.reason}'.format(r) 48 | else: 49 | return True 50 | -------------------------------------------------------------------------------- /pages/location_search_results.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from random import randrange 6 | 7 | from pypom import Region 8 | from selenium.webdriver.common.by import By 9 | 10 | from pages.base import Base 11 | 12 | 13 | class LocationSearchResults(Base): 14 | 15 | _results_title_locator = (By.CSS_SELECTOR, '#main > h2') 16 | _result_item_locator = (By.CSS_SELECTOR, 'div.row > div.result') 17 | 18 | @property 19 | def title(self): 20 | return self.find_element(*self._results_title_locator).text 21 | 22 | @property 23 | def results_count(self): 24 | return len(self.find_elements(*self._result_item_locator)) 25 | 26 | @property 27 | def search_results(self): 28 | return [self.SearchResult(self, el) for el in self.find_elements(*self._result_item_locator)] 29 | 30 | def get_random_profile(self): 31 | random_index = randrange(self.results_count) 32 | return self.search_results[random_index].open_profile_page() 33 | 34 | class SearchResult(Region): 35 | 36 | _profile_page_link_locator = (By.CSS_SELECTOR, 'img') 37 | 38 | def open_profile_page(self): 39 | self.find_element(*self._profile_page_link_locator).click() 40 | from pages.profile import Profile 41 | return Profile(self.page.selenium, self.page.base_url) 42 | -------------------------------------------------------------------------------- /pages/profile.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.support.select import Select 7 | 8 | from pages.base import Base 9 | 10 | 11 | class Profile(Base): 12 | URL_TEMPLATE = '/{locale}/u/{username}' 13 | 14 | _profile_photo_locator = (By.CSS_SELECTOR, '#profile-stats > div.profile-photo img') 15 | _name_locator = (By.CSS_SELECTOR, 'h1.p-name') 16 | _email_locator = (By.CSS_SELECTOR, '.email') 17 | _irc_nickname_locator = (By.CSS_SELECTOR, '.nickname') 18 | _website_locator = (By.CSS_SELECTOR, '.url') 19 | _vouched_by_locator = (By.CSS_SELECTOR, '#profile-info .vouched') 20 | _biography_locator = (By.CSS_SELECTOR, '#bio > .note > p') 21 | _skills_locator = (By.ID, 'skills') 22 | _groups_locator = (By.CSS_SELECTOR, 'div#groups') 23 | _languages_locator = (By.ID, 'languages') 24 | _location_locator = (By.ID, 'location') 25 | _city_locator = (By.CSS_SELECTOR, '#location .locality') 26 | _region_locator = (By.CSS_SELECTOR, '#location .region') 27 | _country_locator = (By.CSS_SELECTOR, '#location .country-name') 28 | _profile_message_locator = (By.CSS_SELECTOR, '.alert') 29 | _view_as_locator = (By.ID, 'view-privacy-mode') 30 | 31 | @property 32 | def loaded(self): 33 | return self.is_element_present(By.CSS_SELECTOR, 'html.js body#profile') 34 | 35 | def view_profile_as(self, view_as): 36 | element = self.find_element(*self._view_as_locator) 37 | select = Select(element) 38 | select.select_by_visible_text(view_as) 39 | 40 | @property 41 | def name(self): 42 | return self.find_element(*self._name_locator).text 43 | 44 | @property 45 | def biography(self): 46 | return self.find_element(*self._biography_locator).text 47 | 48 | @property 49 | def email(self): 50 | return self.find_element(*self._email_locator).text 51 | 52 | @property 53 | def irc_nickname(self): 54 | return self.find_element(*self._irc_nickname_locator).text 55 | 56 | @property 57 | def website(self): 58 | return self.find_element(*self._website_locator).text 59 | 60 | @property 61 | def vouched_by(self): 62 | return self.find_element(*self._vouched_by_locator).text 63 | 64 | @property 65 | def skills(self): 66 | return self.find_element(*self._skills_locator).text.split('\n')[1] 67 | 68 | @property 69 | def groups(self): 70 | return self.find_element(*self._groups_locator).text.split('\n')[1] 71 | 72 | @property 73 | def location(self): 74 | return self.find_element(*self._location_locator).text 75 | 76 | @property 77 | def city(self): 78 | return self.find_element(*self._city_locator).text 79 | 80 | @property 81 | def region(self): 82 | return self.find_element(*self._region_locator).text 83 | 84 | @property 85 | def country(self): 86 | return self.find_element(*self._country_locator).text 87 | 88 | def click_profile_city_filter(self): 89 | self.find_element(*self._city_locator).click() 90 | from location_search_results import LocationSearchResults 91 | return LocationSearchResults(self.selenium, self.base_url) 92 | 93 | def click_profile_region_filter(self,): 94 | self.find_element(*self._region_locator).click() 95 | from location_search_results import LocationSearchResults 96 | return LocationSearchResults(self.selenium, self.base_url) 97 | 98 | def click_profile_country_filter(self): 99 | self.find_element(*self._country_locator).click() 100 | from location_search_results import LocationSearchResults 101 | return LocationSearchResults(self.selenium, self.base_url) 102 | 103 | @property 104 | def languages(self): 105 | return self.find_element(*self._languages_locator).text.split('\n')[1] 106 | 107 | @property 108 | def profile_message(self): 109 | return self.find_element(*self._profile_message_locator).text 110 | 111 | @property 112 | def is_groups_present(self): 113 | return self.is_element_present(*self._groups_locator) 114 | 115 | @property 116 | def is_skills_present(self): 117 | return self.is_element_present(*self._skills_locator) 118 | -------------------------------------------------------------------------------- /pages/register.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.support import expected_conditions as expected 7 | 8 | from pages.base import Base 9 | 10 | 11 | class Register(Base): 12 | 13 | _full_name_field_locator = (By.ID, 'id_full_name') 14 | _privacy_locator = (By.ID, 'id_optin') 15 | _create_profile_button_locator = (By.CSS_SELECTOR, '#form-submit-registration') 16 | _recaptcha_checkbox_locator = (By.CSS_SELECTOR, '.recaptcha-checkbox-checkmark') 17 | _recaptcha_checkbox_checked = (By.CSS_SELECTOR, '.recaptcha-checkbox-checked') 18 | 19 | _country_countainer_locator = (By.ID, 'select2-id_country-container') 20 | _input_locator = (By.CSS_SELECTOR, '.select2-search__field') 21 | _country_results_list_locator = (By.CSS_SELECTOR, '#select2-id_country-results > li.select2-results__option--highlighted') 22 | _first_country_search_result_locator = (By.CSS_SELECTOR, '#select2-id_country-results > li.select2-results__option--highlighted:first-child') 23 | _region_container_locator = (By.ID, "select2-id_region-container") 24 | _region_results_list_locator = (By.CSS_SELECTOR, '#select2-id_region-results > li.select2-results__option--highlighted') 25 | _first_region_search_result_locator = (By.CSS_SELECTOR, '#select2-id_region-results > li.select2-results__option--highlighted:first-child') 26 | _city_container_locator = (By.ID, "select2-id_city-container") 27 | _city_results_list_locator = (By.CSS_SELECTOR, '#select2-id_city-results > li.select2-results__option--highlighted') 28 | _first_city_search_result_locator = (By.CSS_SELECTOR, '#select2-id_city-results > li.select2-results__option--highlighted:first-child') 29 | 30 | @property 31 | def loaded(self): 32 | return self.is_element_present( 33 | By.CSS_SELECTOR, 'html.js body#edit-profile') 34 | 35 | @property 36 | def privacy_error_message(self): 37 | return self.find_element(*self._privacy_locator).get_attribute('validationMessage') 38 | 39 | def set_full_name(self, full_name): 40 | element = self.find_element(*self._full_name_field_locator) 41 | element.send_keys(full_name) 42 | 43 | def select_country(self, country): 44 | self.find_element(*self._country_countainer_locator).click() 45 | self.find_element(*self._input_locator).send_keys(country) 46 | self.wait.until(expected.presence_of_element_located( 47 | self._first_country_search_result_locator)) 48 | countries_list = self.find_elements(*self._country_results_list_locator) 49 | country_item = next(item for item in countries_list if country == item.text) 50 | country_item.click() 51 | 52 | def select_region(self, region): 53 | self.find_element(*self._region_container_locator).click() 54 | self.find_element(*self._input_locator).send_keys(region) 55 | self.wait.until(expected.presence_of_element_located( 56 | self._first_region_search_result_locator)) 57 | regions_list = self.find_elements(*self._region_results_list_locator) 58 | region_item = next(item for item in regions_list if region in item.text) 59 | region_item.click() 60 | 61 | def select_city(self, city): 62 | self.find_element(*self._city_container_locator).click() 63 | self.find_element(*self._input_locator).send_keys(city) 64 | self.wait.until(expected.presence_of_element_located( 65 | self._first_city_search_result_locator)) 66 | cities_list = self.find_elements(*self._city_results_list_locator) 67 | city_item = next(item for item in cities_list if city in item.text) 68 | city_item.click() 69 | 70 | def check_privacy(self): 71 | self.find_element(*self._privacy_locator).click() 72 | 73 | def check_recaptcha(self): 74 | recaptcha_iframe_locator = (By.CSS_SELECTOR, '.g-recaptcha iframe') 75 | recaptcha_iframe = self.find_element(*recaptcha_iframe_locator) 76 | self.selenium.execute_script("arguments[0].scrollIntoView();", recaptcha_iframe) 77 | # To be removed when bug https://bugzilla.mozilla.org/show_bug.cgi?id=1314462 is fixed 78 | self.selenium.switch_to_frame(recaptcha_iframe) 79 | self.wait.until(expected.visibility_of_element_located( 80 | self._recaptcha_checkbox_locator)).click() 81 | self.wait.until(lambda s: self.find_element(*self._recaptcha_checkbox_checked)) 82 | self.selenium.switch_to_default_content() 83 | 84 | def click_create_profile_button(self, leavepage=True): 85 | self.find_element(*self._create_profile_button_locator).click() 86 | if not leavepage: 87 | return self 88 | else: 89 | from pages.profile import Profile 90 | return Profile(self.selenium, self.base_url) 91 | -------------------------------------------------------------------------------- /pages/search.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from pypom import Region 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.support import expected_conditions as expected 8 | 9 | from pages.base import Base 10 | 11 | 12 | class Search(Base): 13 | 14 | _result_locator = (By.CSS_SELECTOR, '#content-wrapper > #main div.result') 15 | _search_button_locator = (By.CSS_SELECTOR, 'button[type = "submit"]') 16 | _advanced_options_button_locator = (By.CSS_SELECTOR, '.btn.primary:nth-of-type(2)') 17 | _advanced_options_locator = (By.CSS_SELECTOR, '.search-options') 18 | _non_vouched_only_checkbox_locator = (By.ID, 'id_nonvouched_only') 19 | _with_photos_only_checkbox_locator = (By.ID, 'id_picture_only') 20 | _no_results_locator_head = (By.ID, 'not-found') 21 | _no_results_locator_body = (By.CSS_SELECTOR, 'div.well > p:nth-of-type(2)') 22 | _last_page_number_locator = (By.CSS_SELECTOR, '#pagination-form select option:last-child') 23 | _group_name_locator = (By.CSS_SELECTOR, '.group-name') 24 | 25 | @property 26 | def loaded(self): 27 | return self.is_element_present(By.CSS_SELECTOR, 'html.js body#search') 28 | 29 | @property 30 | def results_count(self): 31 | return len(self.find_elements(*self._result_locator)) 32 | 33 | @property 34 | def number_of_pages(self): 35 | element = self.find_element(*self._last_page_number_locator) 36 | return element.get_attribute('text') 37 | 38 | @property 39 | def no_results_message_head(self): 40 | return self.find_element(*self._no_results_locator_head).text 41 | 42 | @property 43 | def no_results_message_body(self): 44 | return self.find_element(*self._no_results_locator_body).text 45 | 46 | @property 47 | def advanced_options_shown(self): 48 | return self.is_element_displayed(*self._advanced_options_locator) 49 | 50 | def toggle_advanced_options(self): 51 | self.find_element(*self._advanced_options_button_locator).click() 52 | 53 | def check_non_vouched_only(self): 54 | self.find_element(*self._non_vouched_only_checkbox_locator).click() 55 | 56 | def check_with_photos_only(self): 57 | self.find_element(*self._with_photos_only_checkbox_locator).click() 58 | 59 | @property 60 | def search_results(self): 61 | return [self.SearchResult(self, el) for el in self.find_elements(*self._result_locator)] 62 | 63 | def open_group(self, name): 64 | self.wait.until(expected.visibility_of_element_located( 65 | (By.CSS_SELECTOR, '.group-name[title="{}"]'.format(name)))).click() 66 | from pages.group_info_page import GroupInfoPage 67 | return GroupInfoPage(self.selenium, self.base_url).wait_for_page_to_load() 68 | 69 | class SearchResult(Region): 70 | 71 | _profile_page_link_locator = (By.CSS_SELECTOR, 'li a') 72 | _name_locator = (By.CSS_SELECTOR, '.result .details h2') 73 | 74 | def open_profile_page(self): 75 | self.find_element(*self._profile_page_link_locator).click() 76 | from pages.profile import Profile 77 | return Profile(self.page.selenium, self.page.base_url).wait_for_page_to_load() 78 | 79 | @property 80 | def name(self): 81 | return self.find_element(*self._name_locator).text 82 | -------------------------------------------------------------------------------- /pages/settings.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import random 6 | 7 | from pypom import Region 8 | from selenium.webdriver.common.by import By 9 | from selenium.webdriver.common.keys import Keys 10 | from selenium.webdriver.support import expected_conditions as expected 11 | from selenium.webdriver.support.select import Select 12 | 13 | from pages.base import Base 14 | from pages.groups_page import GroupsPage 15 | 16 | 17 | class Settings(Base): 18 | URL_TEMPLATE = '{locale}/user/edit' 19 | 20 | _profile_tab_locator = (By.ID, 'profile') 21 | _profile_button_locator = (By.CSS_SELECTOR, '#profile-tab > a') 22 | 23 | _you_and_mozilla_tab_locator = (By.ID, 'youandmozilla') 24 | _you_and_mozilla_button_locator = (By.CSS_SELECTOR, '#youandmozilla-tab > a') 25 | 26 | _groups_tab_locator = (By.ID, 'mygroups') 27 | _groups_button_locator = (By.CSS_SELECTOR, '#mygroups-tab > a') 28 | 29 | _external_accounts_tab_locator = (By.ID, 'extaccounts') 30 | _external_accounts_button_locator = (By.CSS_SELECTOR, '#extaccounts-tab > a') 31 | 32 | _developer_tab_locator = (By.ID, 'developer') 33 | _developer_button_locator = (By.CSS_SELECTOR, '#developer-tab > a') 34 | 35 | @property 36 | def profile(self): 37 | self.wait.until(expected.presence_of_element_located( 38 | self._profile_button_locator)).click() 39 | return self.ProfileTab(self, self.find_element(*self._profile_tab_locator)) 40 | 41 | @property 42 | def you_and_mozilla(self): 43 | self.find_element(*self._you_and_mozilla_button_locator).click() 44 | return self.YouAndMozilla(self, self.find_element(*self._you_and_mozilla_tab_locator)) 45 | 46 | @property 47 | def groups(self): 48 | self.find_element(*self._groups_button_locator).click() 49 | return self.Groups(self, self.find_element(*self._groups_tab_locator)) 50 | 51 | @property 52 | def external_accounts(self): 53 | self.find_element(*self._external_accounts_button_locator).click() 54 | return self.ExternalAccountsTab(self, self.find_element(*self._external_accounts_tab_locator)) 55 | 56 | @property 57 | def developer(self): 58 | self.find_element(*self._developer_button_locator).click() 59 | return self.DeveloperTab(self, self.find_element(*self._developer_tab_locator)) 60 | 61 | def create_group(self, group_name): 62 | groups = self.groups.click_find_group_link() 63 | create_group = groups.click_create_group_main_button() 64 | create_group.create_group_name(group_name) 65 | group = create_group.click_create_group_submit() 66 | return group 67 | 68 | class ProfileTab(Region): 69 | _basic_info_form_locator = (By.CSS_SELECTOR, 'form.edit-profile:nth-child(1)') 70 | _skills_form_locator = (By.CSS_SELECTOR, 'form.edit-profile:nth-child(3)') 71 | _delete_account_form_locator = (By.CSS_SELECTOR, 'div.panel-danger') 72 | 73 | @property 74 | def basic_information(self): 75 | return self.EditProfileForm(self.page, self.find_element(*self._basic_info_form_locator)) 76 | 77 | @property 78 | def skills(self): 79 | return self.SkillsForm(self.page, self.find_element(*self._skills_form_locator)) 80 | 81 | @property 82 | def delete_account(self): 83 | return self.DeleteAccount(self.page, self.find_element(*self._delete_account_form_locator)) 84 | 85 | class EditProfileForm (Region): 86 | 87 | _full_name_field_locator = (By.ID, 'id_full_name') 88 | _bio_field_locator = (By.ID, 'id_bio') 89 | _update_locator = (By.ID, 'form-submit-basic') 90 | 91 | def set_full_name(self, full_name): 92 | element = self.find_element(*self._full_name_field_locator) 93 | element.clear() 94 | element.send_keys(full_name) 95 | 96 | def set_bio(self, biography): 97 | element = self.find_element(*self._bio_field_locator) 98 | element.clear() 99 | element.send_keys(biography) 100 | 101 | def click_update(self): 102 | el = self.find_element(*self._update_locator) 103 | el.click() 104 | self.wait.until(expected.staleness_of(el)) 105 | self.wait.until(expected.presence_of_element_located( 106 | self._update_locator)) 107 | 108 | class DeleteAccount(Region): 109 | 110 | _delete_acknowledgement_locator = (By.CSS_SELECTOR, '#delete-checkbox') 111 | _delete_profile_button_locator = (By.ID, 'delete-profile') 112 | 113 | def check_acknowledgement(self): 114 | self.find_element(*self._delete_acknowledgement_locator).click() 115 | 116 | @property 117 | def is_delete_button_enabled(self): 118 | return 'disabled' not in self.find_element(*self._delete_profile_button_locator).get_attribute('class') 119 | 120 | def click_delete_profile(self): 121 | self.find_element(*self._delete_profile_button_locator).click() 122 | from pages.confirm_profile_delete import ConfirmProfileDelete 123 | return ConfirmProfileDelete(self.page.selenium, self.page.base_url) 124 | 125 | class SkillsForm(Region): 126 | _skills_locator = (By.CSS_SELECTOR, '#skills .select2-selection__choice') 127 | _skills_field_locator = (By.CSS_SELECTOR, '#skills input') 128 | _delete_skill_buttons_locator = (By.CSS_SELECTOR, '#skills .select2-selection__choice__remove') 129 | _skills_first_result_locator = (By.CSS_SELECTOR, '.select2-results li:not(.loading-results):first-child') 130 | _update_locator = (By.ID, 'form-submit-skills') 131 | 132 | @property 133 | def skills(self): 134 | # Return skills list with leading `x` button stripped 135 | skills = self.find_elements(*self._skills_locator) 136 | return [skills[i].text[1:] for i in range(0, len(skills))] 137 | 138 | def add_skill(self, skill_name): 139 | element = self.find_element(*self._skills_field_locator) 140 | element.send_keys(skill_name) 141 | self.wait.until(expected.presence_of_element_located( 142 | self._skills_first_result_locator)) 143 | element.send_keys(Keys.RETURN) 144 | 145 | @property 146 | def delete_skill_buttons(self): 147 | return self.find_elements(*self._delete_skill_buttons_locator) 148 | 149 | def delete_skill(self, skill): 150 | skill_index = self.skills.index(skill) 151 | self.delete_skill_buttons[skill_index].click() 152 | 153 | def click_update(self): 154 | self.find_element(*self._update_locator).click() 155 | 156 | class YouAndMozilla(Region): 157 | 158 | _contributions_form_locator = (By.CSS_SELECTOR, 'form.edit-profile:nth-child(1)') 159 | 160 | @property 161 | def contributions(self): 162 | return self.Contributions(self.page, self.find_element(*self._contributions_form_locator)) 163 | 164 | class Contributions(Region): 165 | 166 | _select_month_locator = (By.ID, 'id_date_mozillian_month') 167 | _select_year_locator = (By.ID, 'id_date_mozillian_year') 168 | _month_locator = (By.CSS_SELECTOR, '#id_date_mozillian_month > option') 169 | _year_locator = (By.CSS_SELECTOR, '#id_date_mozillian_year > option') 170 | _update_locator = (By.ID, 'form-submit-contribution') 171 | 172 | def select_month(self, option_month): 173 | element = self.find_element(*self._select_month_locator) 174 | select = Select(element) 175 | select.select_by_visible_text(option_month) 176 | 177 | def select_year(self, option_year): 178 | element = self.find_element(*self._select_year_locator) 179 | select = Select(element) 180 | select.select_by_visible_text(option_year) 181 | 182 | @property 183 | def month(self): 184 | # Return selected month text 185 | return [month.text for month in self.find_elements(*self._month_locator) if month.get_property('selected')] 186 | 187 | @property 188 | def year(self): 189 | # Return selected year text 190 | return [year.text for year in self.find_elements(*self._year_locator) if year.get_property('selected')] 191 | 192 | @property 193 | def months_values(self): 194 | # Return all month values 195 | return [month.text for month in self.find_elements(*self._month_locator)] 196 | 197 | @property 198 | def years_values(self): 199 | # Return all year values 200 | return [year.text for year in self.find_elements(*self._year_locator)] 201 | 202 | def select_random_month(self): 203 | return self.select_month(random.choice(self.months_values[1:])) 204 | 205 | def select_random_year(self): 206 | return self.select_year(random.choice(self.years_values[1:])) 207 | 208 | def click_update(self): 209 | el = self.find_element(*self._update_locator) 210 | el.click() 211 | self.wait.until(expected.staleness_of(el)) 212 | self.wait.until(expected.presence_of_element_located( 213 | self._update_locator)) 214 | 215 | class Groups(Region): 216 | 217 | _find_group_page = (By.PARTIAL_LINK_TEXT, 'find the group') 218 | 219 | @property 220 | def is_find_group_link_visible(self): 221 | return self.is_element_displayed(*self._find_group_page) 222 | 223 | def click_find_group_link(self): 224 | self.find_element(*self._find_group_page).click() 225 | return GroupsPage(self.page.selenium, self.page.base_url) 226 | 227 | class ExternalAccountsTab(Region): 228 | 229 | _external_accounts_form_locator = (By.CSS_SELECTOR, '#extaccounts > form > div:nth-child(2)') 230 | _irc_form_locator = (By.CSS_SELECTOR, '#extaccounts > form > div:nth-child(3)') 231 | 232 | @property 233 | def external_accounts_form(self): 234 | return self.ExternalAccounts(self.page, self.find_element(*self._external_accounts_form_locator)) 235 | 236 | @property 237 | def irc_form(self): 238 | return self.Irc(self.page, self.find_element(*self._irc_form_locator)) 239 | 240 | class ExternalAccounts(Region): 241 | _add_account_locator = (By.ID, 'accounts-addfield') 242 | _account_row_locator = (By.CSS_SELECTOR, 'div.externalaccount-fieldrow') 243 | 244 | @property 245 | def is_displayed(self): 246 | return self.root.is_displayed() 247 | 248 | def count_external_accounts(self): 249 | return len(self.find_elements(*self._account_row_locator)) 250 | 251 | def click_add_account(self): 252 | self.find_element(*self._add_account_locator).click() 253 | 254 | class Irc(Region): 255 | _irc_nickname_locator = (By.ID, 'id_ircname') 256 | _update_locator = (By.ID, 'form-submit-irc') 257 | 258 | @property 259 | def nickname(self): 260 | return self.find_element(*self._irc_nickname_locator).get_attribute('value') 261 | 262 | @property 263 | def is_displayed(self): 264 | return self.root.is_displayed() 265 | 266 | def update_nickname(self, new_nickname): 267 | element = self.find_element(*self._irc_nickname_locator) 268 | element.clear() 269 | element.send_keys(new_nickname) 270 | 271 | def click_update(self): 272 | el = self.find_element(*self._update_locator) 273 | el.click() 274 | self.wait.until(expected.staleness_of(el)) 275 | self.wait.until(expected.presence_of_element_located( 276 | self._update_locator)) 277 | 278 | class DeveloperTab(Region): 279 | 280 | _services_bugzilla_locator = (By.ID, 'services-bugzilla-url') 281 | _services_mozilla_reps_locator = (By.ID, 'services-mozilla-reps') 282 | 283 | def get_services_urls(self): 284 | locs = [self._services_bugzilla_locator, self._services_mozilla_reps_locator] 285 | urls = [] 286 | 287 | for element in locs: 288 | url = self.find_element(*element).get_attribute('href') 289 | urls.append(url) 290 | 291 | return urls 292 | -------------------------------------------------------------------------------- /pipenv.txt: -------------------------------------------------------------------------------- 1 | pipenv==2018.11.26 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501 3 | 4 | [isort] 5 | default_section = THIRDPARTY 6 | known_first_party = pages, tests 7 | 8 | [tool:pytest] 9 | addopts = -n=auto --verbose -r=a --driver=Firefox 10 | testpaths = tests 11 | xfail_strict = true 12 | base_url = https://web-mozillians-staging.production.paas.mozilla.community 13 | sensitive_url = mozillians\.org 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/mozillians-tests/b3bc38f407fe29fbf72b92c10a30d00383671db2/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import uuid 6 | from urlparse import urlparse 7 | 8 | import pytest 9 | 10 | 11 | @pytest.fixture(scope='session') 12 | def session_capabilities(pytestconfig, session_capabilities): 13 | if pytestconfig.getoption('driver') == 'SauceLabs': 14 | session_capabilities.setdefault('tags', []).append('mozillians') 15 | return session_capabilities 16 | 17 | 18 | @pytest.fixture 19 | def capabilities(request, capabilities): 20 | driver = request.config.getoption('driver') 21 | if capabilities.get('browserName', driver).lower() == 'firefox': 22 | capabilities['marionette'] = True 23 | return capabilities 24 | 25 | 26 | @pytest.fixture 27 | def new_email(): 28 | return 'mozillians_{0}@restmail.net'.format(uuid.uuid1()) 29 | 30 | 31 | @pytest.fixture 32 | def new_user(new_email): 33 | return {'email': new_email} 34 | 35 | 36 | @pytest.fixture(scope='session') 37 | def stored_users(base_url, variables): 38 | return variables[urlparse(base_url).hostname]['users'] 39 | 40 | 41 | @pytest.fixture(scope='function') 42 | def vouched_user(request, stored_users): 43 | slave_id = getattr(request.config, 'slaveinput', {}).get('slaveid', 'gw0') 44 | return stored_users['vouched'][int(slave_id[2:])] 45 | 46 | 47 | @pytest.fixture(scope='session') 48 | def private_user(stored_users): 49 | return stored_users['private'] 50 | 51 | 52 | @pytest.fixture(scope='session') 53 | def unvouched_user(stored_users): 54 | return stored_users['unvouched'] 55 | 56 | 57 | @pytest.fixture(scope='session') 58 | def github_non_nda_user(stored_users): 59 | return stored_users['github_non_nda'] 60 | -------------------------------------------------------------------------------- /tests/restmail.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import json 6 | import time 7 | 8 | import requests 9 | 10 | 11 | def get_mail(username, message_count=1, timeout=60): 12 | username = username.partition('@restmail.net')[0] 13 | end_time = time.time() + timeout 14 | response = requests.delete( 15 | 'https://restmail.net/mail/%s' % username) 16 | response.raise_for_status() 17 | while (True): 18 | response = requests.get( 19 | 'https://restmail.net/mail/%s' % username) 20 | response.raise_for_status() 21 | restmail = json.loads(response.content) 22 | if len(restmail) == message_count: 23 | return parse_email(restmail) 24 | time.sleep(0.5) 25 | if (time.time() > end_time): 26 | break 27 | raise Exception('Timeout after %(TIMEOUT)s seconds getting restmail for ' 28 | '%(USERNAME)s. Expected %(EXPECTED_MESSAGE_COUNT)s ' 29 | 'messages but there were %(ACTUAL_MESSAGE_COUNT)s.' % { 30 | 'TIMEOUT': timeout, 31 | 'USERNAME': username, 32 | 'EXPECTED_MESSAGE_COUNT': message_count, 33 | 'ACTUAL_MESSAGE_COUNT': len(restmail)}) 34 | 35 | 36 | def parse_email(email): 37 | mail_content = email[0]['text'].replace('\n', ' ').replace('amp;', '').split(" ") 38 | for link in mail_content: 39 | if 'passwordless/verify_redirect' in link: 40 | return link 41 | -------------------------------------------------------------------------------- /tests/test_about_page.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import pytest 6 | 7 | from pages.home_page import Home 8 | from pages.link_crawler import LinkCrawler 9 | 10 | 11 | class TestAboutPage: 12 | 13 | @pytest.mark.nondestructive 14 | def test_about_page(self, base_url, selenium): 15 | home_page = Home(selenium, base_url).open() 16 | about_mozillians_page = home_page.footer.click_about_link() 17 | assert about_mozillians_page.is_privacy_section_present 18 | assert about_mozillians_page.is_get_involved_section_present 19 | 20 | @pytest.mark.nondestructive 21 | def test_that_links_in_the_about_page_return_200_code(self, base_url): 22 | crawler = LinkCrawler(base_url) 23 | urls = crawler.collect_links('/about', id='main') 24 | bad_urls = [] 25 | 26 | assert len(urls) > 0 27 | 28 | for url in urls: 29 | check_result = crawler.verify_status_code_is_ok(url) 30 | if check_result is not True: 31 | bad_urls.append(check_result) 32 | 33 | assert 0 == len(bad_urls), u'%s bad links found. ' % len(bad_urls) + ', '.join(bad_urls) 34 | -------------------------------------------------------------------------------- /tests/test_account.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import pytest 6 | 7 | from pages.home_page import Home 8 | from pages.link_crawler import LinkCrawler 9 | 10 | 11 | class TestAccount: 12 | 13 | @pytest.mark.credentials 14 | @pytest.mark.nondestructive 15 | def test_login_logout(self, base_url, selenium, vouched_user): 16 | home_page = Home(selenium, base_url).open() 17 | home_page.login(vouched_user['email']) 18 | assert home_page.header.is_logout_menu_item_present 19 | home_page.header.click_logout_menu_item() 20 | assert home_page.is_sign_in_button_present 21 | 22 | @pytest.mark.credentials 23 | @pytest.mark.nondestructive 24 | def test_logout_verify_bid(self, base_url, selenium, vouched_user): 25 | home_page = Home(selenium, base_url).open() 26 | home_page.login(vouched_user['email']) 27 | assert home_page.header.is_logout_menu_item_present 28 | selenium.get(base_url + '/logout') 29 | 30 | home_page.wait_for_user_login() 31 | assert home_page.is_sign_in_button_present 32 | 33 | @pytest.mark.nondestructive 34 | def test_that_links_in_footer_return_200_code(self, base_url): 35 | crawler = LinkCrawler(base_url) 36 | urls = crawler.collect_links('/', name='footer') 37 | bad_urls = [] 38 | 39 | assert len(urls) > 0 40 | 41 | for url in urls: 42 | check_result = crawler.verify_status_code_is_ok(url) 43 | if check_result is not True: 44 | bad_urls.append(check_result) 45 | 46 | assert 0 == len(bad_urls), u'%s bad links found. ' % len(bad_urls) + ', '.join(bad_urls) 47 | -------------------------------------------------------------------------------- /tests/test_group.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import uuid 6 | from random import randrange 7 | 8 | import pytest 9 | 10 | from pages.home_page import Home 11 | 12 | 13 | class TestGroup: 14 | 15 | @pytest.mark.credentials 16 | def test_group_description_edit(self, base_url, selenium, vouched_user): 17 | home_page = Home(selenium, base_url).open() 18 | home_page.login(vouched_user['email']) 19 | 20 | # Create a new group 21 | group_name = 'moz-group-{0}'.format(uuid.uuid4()) 22 | groups_page = home_page.header.click_groups_menu_item() 23 | group = groups_page.create_group(group_name) 24 | 25 | # New group data 26 | new_group_description = 'This is an automated group.' 27 | new_group_irc_channel = '#testgroup' 28 | 29 | # Update the group description fields 30 | group_description = group.description.description_info 31 | group_description.set_description(new_group_description) 32 | group_description.set_irc_channel(new_group_irc_channel) 33 | group_description.click_update() 34 | 35 | search_listings = home_page.header.search_for(group_name) 36 | group_info = search_listings.open_group(group_name) 37 | 38 | # Check that everything was updated 39 | assert new_group_description == group_info.description 40 | assert new_group_irc_channel == group_info.irc_channel 41 | 42 | @pytest.mark.credentials 43 | def test_group_deletion_confirmation(self, base_url, selenium, vouched_user): 44 | home_page = Home(selenium, base_url).open() 45 | home_page.login(vouched_user['email']) 46 | 47 | # Create a new group 48 | group_name = 'moz-group-{0}'.format(uuid.uuid4()) 49 | groups_page = home_page.header.click_groups_menu_item() 50 | group = groups_page.create_group(group_name) 51 | 52 | # Delete should only work with acknowledgement 53 | delete_form = group.description.delete_group 54 | assert not delete_form.is_delete_button_enabled 55 | delete_form.check_acknowledgement() 56 | assert delete_form.is_delete_button_enabled 57 | groups_page = delete_form.click_delete_group() 58 | assert groups_page.is_group_deletion_alert_present 59 | 60 | @pytest.mark.credentials 61 | def test_group_type_change(self, base_url, selenium, vouched_user): 62 | home_page = Home(selenium, base_url).open() 63 | home_page.login(vouched_user['email']) 64 | 65 | # Create a new group 66 | group_name = 'moz-group-{0}'.format(uuid.uuid4()) 67 | groups_page = home_page.header.click_groups_menu_item() 68 | group = groups_page.create_group(group_name) 69 | 70 | # Change group type to reveal criteria 71 | group_type = group.access.group_type 72 | assert not group_type.is_member_criteria_visible 73 | group_type.set_reviewed_group_type() 74 | assert group_type.is_member_criteria_visible 75 | 76 | @pytest.mark.credentials 77 | def test_group_invitations(self, base_url, selenium, vouched_user): 78 | home_page = Home(selenium, base_url).open() 79 | home_page.login(vouched_user['email']) 80 | 81 | # Create a new group 82 | group_name = 'moz-group-{0}'.format(uuid.uuid4()) 83 | groups_page = home_page.header.click_groups_menu_item() 84 | group = groups_page.create_group(group_name) 85 | 86 | # Invite a new member 87 | invite = group.invitations.invite 88 | new_member = "Test User" 89 | invite.invite_new_member(new_member) 90 | invite.click_invite() 91 | 92 | # Check if the pending invitation exists 93 | invitations = group.invitations.invitations_list 94 | random_profile = randrange(len(invitations.search_invitation_list)) 95 | assert new_member in invitations.search_invitation_list[random_profile].name 96 | 97 | @pytest.mark.credentials 98 | def test_github_non_nda_user_cannot_create_access_group(self, base_url, selenium, github_non_nda_user): 99 | home_page = Home(selenium, base_url).open() 100 | home_page.login_with_github(github_non_nda_user['username'], github_non_nda_user['password'], 101 | github_non_nda_user['secret']) 102 | groups_page = home_page.header.click_groups_menu_item() 103 | create_group_page = groups_page.click_create_group_main_button() 104 | assert not create_group_page.is_access_group_present 105 | -------------------------------------------------------------------------------- /tests/test_invite.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import pytest 6 | 7 | from pages.home_page import Home 8 | 9 | 10 | class TestInvite: 11 | 12 | @pytest.mark.credentials 13 | def test_inviting_an_invalid_email_address(self, base_url, selenium, vouched_user): 14 | home_page = Home(selenium, base_url).open() 15 | home_page.login(vouched_user['email']) 16 | invite_page = home_page.header.click_invite_menu_item() 17 | invite_page.invite("invalidmail") 18 | assert 'Enter a valid email address.' == invite_page.error_text_message 19 | 20 | @pytest.mark.credentials 21 | def test_invite(self, base_url, selenium, vouched_user): 22 | home_page = Home(selenium, base_url).open() 23 | home_page.login(vouched_user['email']) 24 | invite_page = home_page.header.click_invite_menu_item() 25 | email_address = "user@example.com" 26 | invite_success_page = invite_page.invite(email_address, 'Just a bot sending a test invite to a test account.') 27 | assert "%s has been invited to Mozillians. They'll receive an email with instructions on how to join.\ 28 | You can invite another Mozillian if you like." % email_address == invite_success_page.success_message 29 | -------------------------------------------------------------------------------- /tests/test_profile.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import time 6 | 7 | import pytest 8 | from selenium.webdriver.common.by import By 9 | 10 | from pages.home_page import Home 11 | from pages.link_crawler import LinkCrawler 12 | from pages.profile import Profile 13 | from pages.settings import Settings 14 | 15 | 16 | class TestProfile: 17 | 18 | @pytest.mark.credentials 19 | @pytest.mark.nondestructive 20 | def test_profile_deletion_confirmation(self, base_url, selenium, vouched_user): 21 | home_page = Home(selenium, base_url).open() 22 | home_page.login(vouched_user['email']) 23 | settings = home_page.header.click_settings_menu_item() 24 | 25 | delete_form = settings.profile.delete_account 26 | 27 | assert not delete_form.is_delete_button_enabled 28 | 29 | delete_form.check_acknowledgement() 30 | 31 | assert delete_form.is_delete_button_enabled 32 | 33 | confirm_profile_delete_page = delete_form.click_delete_profile() 34 | assert confirm_profile_delete_page.is_confirm_text_present 35 | assert confirm_profile_delete_page.is_cancel_button_present 36 | assert confirm_profile_delete_page.is_delete_button_present 37 | 38 | @pytest.mark.credentials 39 | def test_edit_profile_information(self, base_url, selenium, vouched_user): 40 | home_page = Home(selenium, base_url).open() 41 | home_page.login(vouched_user['email']) 42 | settings = home_page.header.click_settings_menu_item() 43 | current_time = str(time.time()).split('.')[0] 44 | 45 | # New profile data 46 | new_full_name = "Updated Mozillians User %s" % current_time 47 | new_biography = "Hello, I'm new here and trying stuff out. Oh, and by the way: I'm a robot, run in a cronjob, most likely, run at %s" % current_time 48 | 49 | profile_basic_info = settings.profile.basic_information 50 | 51 | # Update the profile fields 52 | profile_basic_info.set_full_name(new_full_name) 53 | profile_basic_info.set_bio(new_biography) 54 | profile_basic_info.click_update() 55 | 56 | profile_page = home_page.header.click_view_profile_menu_item() 57 | 58 | # Check that everything was updated 59 | assert new_full_name == profile_page.name 60 | assert new_biography == profile_page.biography 61 | 62 | @pytest.mark.credentials 63 | def test_skill_addition(self, base_url, selenium, vouched_user): 64 | home_page = Home(selenium, base_url).open() 65 | home_page.login(vouched_user['email']) 66 | 67 | settings = home_page.header.click_settings_menu_item() 68 | skills_form = settings.profile.skills 69 | skills_form.add_skill("Hello World") 70 | skills_form.click_update() 71 | 72 | profile_page = home_page.header.click_view_profile_menu_item() 73 | 74 | assert profile_page.is_skills_present 75 | skills = profile_page.skills 76 | assert skills.find("hello world") >= 0 77 | 78 | @pytest.mark.credentials 79 | def test_skill_deletion(self, base_url, selenium, vouched_user): 80 | home_page = Home(selenium, base_url).open() 81 | home_page.login(vouched_user['email']) 82 | 83 | settings = home_page.header.click_settings_menu_item() 84 | skills_form = settings.profile.skills 85 | skills_form.add_skill("Hello World") 86 | skills_form.click_update() 87 | 88 | settings = home_page.header.click_settings_menu_item() 89 | skills_form = settings.profile.skills 90 | skills_form.delete_skill("hello world") 91 | skills_form.click_update() 92 | 93 | profile_page = home_page.header.click_view_profile_menu_item() 94 | 95 | if profile_page.is_skills_present: 96 | skills = profile_page.skills 97 | assert -1 == skills.find("hello world") 98 | 99 | @pytest.mark.credentials 100 | @pytest.mark.nondestructive 101 | def test_that_filter_by_city_works(self, base_url, selenium, vouched_user): 102 | home_page = Home(selenium, base_url).open() 103 | home_page.login(vouched_user['email']) 104 | 105 | profile_page = home_page.header.click_view_profile_menu_item() 106 | city = profile_page.city 107 | country = profile_page.country 108 | 109 | search_results_page = profile_page.click_profile_city_filter() 110 | expected_results_title = u'Mozillians in %s, %s' % (city, country) 111 | actual_results_title = search_results_page.title 112 | 113 | assert expected_results_title == actual_results_title 114 | 115 | random_profile = search_results_page.get_random_profile() 116 | assert city == random_profile.city 117 | 118 | @pytest.mark.credentials 119 | @pytest.mark.nondestructive 120 | def test_that_filter_by_region_works(self, base_url, selenium, vouched_user): 121 | home_page = Home(selenium, base_url).open() 122 | home_page.login(vouched_user['email']) 123 | 124 | profile_page = home_page.header.click_view_profile_menu_item() 125 | region = profile_page.region 126 | country = profile_page.country 127 | search_results_page = profile_page.click_profile_region_filter() 128 | expected_results_title = u'Mozillians in %s, %s' % (region, country) 129 | actual_results_title = search_results_page.title 130 | 131 | assert expected_results_title == actual_results_title 132 | 133 | random_profile = search_results_page.get_random_profile() 134 | assert region == random_profile.region 135 | 136 | @pytest.mark.credentials 137 | @pytest.mark.nondestructive 138 | def test_that_filter_by_country_works(self, base_url, selenium, vouched_user): 139 | home_page = Home(selenium, base_url).open() 140 | home_page.login(vouched_user['email']) 141 | 142 | profile_page = home_page.header.click_view_profile_menu_item() 143 | country = profile_page.country 144 | search_results_page = profile_page.click_profile_country_filter() 145 | expected_results_title = u'Mozillians in %s' % country 146 | actual_results_title = search_results_page.title 147 | 148 | assert expected_results_title == actual_results_title 149 | 150 | random_profile = search_results_page.get_random_profile() 151 | assert country == random_profile.country 152 | 153 | @pytest.mark.credentials 154 | def test_that_non_us_user_can_set_get_involved_date(self, base_url, selenium, vouched_user): 155 | home_page = Home(selenium, base_url).open() 156 | home_page.login(vouched_user['email']) 157 | settings = Settings(selenium, base_url, locale='es').open() 158 | contributions = settings.you_and_mozilla.contributions 159 | selected_date = contributions.month + contributions.year 160 | contributions.select_random_month() 161 | contributions.select_random_year() 162 | contributions.click_update() 163 | 164 | profile_page = home_page.header.click_view_profile_menu_item() 165 | 166 | assert "Tu perfil" == profile_page.profile_message 167 | settings = home_page.header.click_settings_menu_item() 168 | contributions = settings.you_and_mozilla.contributions 169 | assert selected_date != contributions.month + contributions.year 170 | 171 | @pytest.mark.credentials 172 | def test_that_user_can_create_and_delete_group(self, base_url, selenium, vouched_user): 173 | group_name = (time.strftime('%x-%X')) 174 | 175 | home_page = Home(selenium, base_url).open() 176 | home_page.login(vouched_user['email']) 177 | groups_page = home_page.header.click_groups_menu_item() 178 | edit_group = groups_page.create_group(group_name) 179 | 180 | search_listings = edit_group.header.search_for(group_name) 181 | 182 | assert search_listings.is_element_present(By.LINK_TEXT, group_name) 183 | 184 | group_info = search_listings.open_group(group_name) 185 | groups_page = group_info.delete_group() 186 | groups_page.wait_for_alert_message() 187 | 188 | search_listings = home_page.header.search_for(group_name) 189 | 190 | assert not search_listings.is_element_present(By.LINK_TEXT, group_name) 191 | 192 | @pytest.mark.credentials 193 | @pytest.mark.nondestructive 194 | def test_private_groups_field_as_public_when_logged_in(self, base_url, selenium, private_user): 195 | # User has certain fields preset to values to run the test properly 196 | # groups - private 197 | # belongs to at least one group 198 | home_page = Home(selenium, base_url).open() 199 | home_page.login(private_user['email']) 200 | 201 | profile_page = home_page.header.click_view_profile_menu_item() 202 | profile_page.view_profile_as('Public') 203 | assert not profile_page.is_groups_present 204 | 205 | @pytest.mark.credentials 206 | @pytest.mark.nondestructive 207 | def test_private_groups_field_when_not_logged_in(self, base_url, selenium, private_user): 208 | page = Profile(selenium, base_url, username=private_user['username']).open() 209 | assert not page.is_groups_present 210 | 211 | @pytest.mark.credentials 212 | @pytest.mark.nondestructive 213 | def test_that_links_in_the_services_page_return_200_code(self, base_url, selenium, vouched_user): 214 | home_page = Home(selenium, base_url).open() 215 | home_page.login(vouched_user['email']) 216 | 217 | settings = home_page.header.click_settings_menu_item() 218 | developer = settings.developer 219 | crawler = LinkCrawler(base_url) 220 | urls = developer.get_services_urls() 221 | bad_urls = [] 222 | 223 | assert len(urls) > 0 224 | 225 | for url in urls: 226 | check_result = crawler.verify_status_code_is_ok(url) 227 | if check_result is not True: 228 | bad_urls.append(check_result) 229 | 230 | assert 0 == len(bad_urls), u'%s bad links found. ' % len(bad_urls) + ', '.join(bad_urls) 231 | 232 | @pytest.mark.credentials 233 | @pytest.mark.nondestructive 234 | def test_that_user_can_view_external_accounts(self, base_url, selenium, vouched_user): 235 | home_page = Home(selenium, base_url).open() 236 | home_page.login(vouched_user['email']) 237 | settings = home_page.header.click_settings_menu_item() 238 | 239 | assert settings.external_accounts.irc_form.is_displayed 240 | assert settings.external_accounts.external_accounts_form.is_displayed 241 | 242 | @pytest.mark.credentials 243 | def test_that_user_can_add_external_account(self, base_url, selenium, vouched_user): 244 | home_page = Home(selenium, base_url).open() 245 | home_page.login(vouched_user['email']) 246 | settings = home_page.header.click_settings_menu_item() 247 | 248 | external_accounts_form = settings.external_accounts.external_accounts_form 249 | cnt_external_accounts = external_accounts_form.count_external_accounts() 250 | external_accounts_form.click_add_account() 251 | new_cnt_external_accounts = external_accounts_form.count_external_accounts() 252 | assert (cnt_external_accounts + 1) == new_cnt_external_accounts 253 | 254 | @pytest.mark.credentials 255 | def test_that_user_can_modify_external_accounts_irc_nickname(self, base_url, selenium, vouched_user): 256 | home_page = Home(selenium, base_url).open() 257 | home_page.login(vouched_user['email']) 258 | settings = home_page.header.click_settings_menu_item() 259 | 260 | irc_form = settings.external_accounts.irc_form 261 | old_nickname = irc_form.nickname 262 | new_nickname = old_nickname + '_' 263 | irc_form.update_nickname(new_nickname) 264 | irc_form.click_update() 265 | 266 | profile_page = home_page.header.click_view_profile_menu_item() 267 | assert new_nickname == profile_page.irc_nickname 268 | 269 | settings = home_page.header.click_settings_menu_item() 270 | irc_form = settings.external_accounts.irc_form 271 | irc_form.update_nickname(old_nickname) 272 | irc_form.click_update() 273 | 274 | profile_page = home_page.header.click_view_profile_menu_item() 275 | assert old_nickname == profile_page.irc_nickname 276 | 277 | @pytest.mark.credentials 278 | @pytest.mark.nondestructive 279 | def test_new_user_cannot_see_groups_or_functional_areas(self, base_url, selenium, unvouched_user): 280 | home_page = Home(selenium, base_url).open() 281 | home_page.login(unvouched_user['email']) 282 | 283 | assert not home_page.header.is_groups_menu_item_present 284 | assert not home_page.is_groups_link_visible 285 | assert not home_page.is_functional_areas_link_visible 286 | 287 | settings = home_page.header.click_settings_menu_item() 288 | assert not settings.groups.is_find_group_link_visible 289 | -------------------------------------------------------------------------------- /tests/test_redirects.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import pytest 6 | import requests 7 | 8 | 9 | class TestRedirects: 10 | 11 | @pytest.mark.nondestructive 12 | def test_302_redirect_for_anonymous_users(self, base_url): 13 | paths = ['/es/country/us/', 14 | '/sq/country/doesnotexist/', 15 | '/hu/country/us/region/California/', 16 | '/pl/country/in/city/Gulbarga/', 17 | '/zh-TW/group/webqa/', 18 | '/zh-CN/group/258/join/', 19 | '/sl/group/doesnotexit/', 20 | '/pt-BR/u/moz.mozillians.unvouched/', 21 | '/ca/u/UserDoesNotExist/', 22 | '/nl/logout/', 23 | '/lt/user/edit/', 24 | '/en-US/invite/', 25 | '/fr/register/'] 26 | urls = self.make_absolute_paths(base_url, paths) 27 | error_list = self.verify_http_response_codes(urls, 302) 28 | assert 0 == len(error_list), error_list 29 | 30 | @pytest.mark.nondestructive 31 | def test_200_for_anonymous_users(self, base_url): 32 | paths = ['/pl/opensearch.xml', '/nl/u/Mozillians.User/'] 33 | urls = self.make_absolute_paths(base_url, paths) 34 | error_list = self.verify_http_response_codes(urls, 200) 35 | assert 0 == len(error_list), error_list 36 | 37 | def make_absolute_paths(self, url, paths): 38 | urls = [] 39 | for path in paths: 40 | urls.append(url + path) 41 | return urls 42 | 43 | def verify_http_response_codes(self, urls, expected_http_value): 44 | error_list = [] 45 | for url in urls: 46 | # prevent redirects, we only want the value of the 1st HTTP status 47 | response = requests.get(url, allow_redirects=False) 48 | http_status = response.status_code 49 | if http_status != expected_http_value: 50 | error_list.append('Expected %s but got %s. %s' % 51 | (expected_http_value, http_status, url)) 52 | return error_list 53 | -------------------------------------------------------------------------------- /tests/test_register.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from pages.home_page import Home 6 | 7 | 8 | class TestRegister: 9 | 10 | def test_profile_creation(self, base_url, selenium, new_user): 11 | home_page = Home(selenium, base_url).open() 12 | profile = home_page.create_new_user(new_user['email']) 13 | 14 | # Click recaptcha box 15 | profile.check_recaptcha() 16 | 17 | # Full name 18 | profile.set_full_name("New MozilliansUser") 19 | 20 | # Location 21 | profile.select_country("United States") 22 | profile.select_region("California") 23 | profile.select_city("Mountain View") 24 | 25 | # Agree to privacy policy 26 | profile.check_privacy() 27 | 28 | profile_page = profile.click_create_profile_button() 29 | 30 | assert profile_page.was_account_created_successfully 31 | assert profile_page.is_pending_approval_visible 32 | 33 | assert 'New MozilliansUser' == profile_page.name 34 | assert new_user['email'] == profile_page.email 35 | assert 'Mountain View, California, United States' == profile_page.location 36 | 37 | def test_creating_profile_without_checking_privacy_policy_checkbox(self, base_url, selenium, new_user): 38 | home_page = Home(selenium, base_url).open() 39 | profile = home_page.create_new_user(new_user['email']) 40 | 41 | profile.set_full_name("User that doesn't like policy") 42 | 43 | # Location 44 | profile.select_country("United States") 45 | profile.select_region("Colorado") 46 | profile.select_city("Durango") 47 | 48 | # Click recaptcha box 49 | profile.check_recaptcha() 50 | 51 | profile = profile.click_create_profile_button(leavepage=False) 52 | assert 'Please check this box if you want to proceed.' == profile.privacy_error_message 53 | -------------------------------------------------------------------------------- /tests/test_search.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from random import randrange 6 | 7 | import pytest 8 | 9 | from pages.home_page import Home 10 | 11 | 12 | class TestSearch: 13 | 14 | @pytest.mark.credentials 15 | @pytest.mark.nondestructive 16 | def test_that_search_returns_results_for_email_substring(self, base_url, selenium, vouched_user): 17 | home_page = Home(selenium, base_url).open() 18 | home_page.login(vouched_user['email']) 19 | search_page = home_page.header.search_for(u'@mozilla.com', loggedin=True) 20 | assert search_page.results_count > 0 21 | 22 | @pytest.mark.credentials 23 | @pytest.mark.nondestructive 24 | def test_that_search_returns_results_for_first_name(self, base_url, selenium, vouched_user): 25 | query = u'Matt' 26 | home_page = Home(selenium, base_url).open() 27 | home_page.login(vouched_user['email']) 28 | search_page = home_page.header.search_for(query, loggedin=True) 29 | assert search_page.results_count > 0 30 | # get random index 31 | random_profile = randrange(search_page.results_count) 32 | profile_name = search_page.search_results[random_profile].name 33 | assert query.lower() in profile_name.lower() 34 | 35 | @pytest.mark.credentials 36 | @pytest.mark.nondestructive 37 | def test_that_search_returns_results_for_irc_nickname(self, base_url, selenium, vouched_user): 38 | home_page = Home(selenium, base_url).open() 39 | home_page.login(vouched_user['email']) 40 | search_page = home_page.header.search_for(u'mbrandt', loggedin=True) 41 | assert search_page.results_count > 0 42 | profile = search_page.search_results[0].open_profile_page() 43 | assert u'Matt Brandt' == profile.name 44 | 45 | @pytest.mark.credentials 46 | @pytest.mark.nondestructive 47 | def test_search_for_not_existing_mozillian_when_logged_in(self, base_url, selenium, vouched_user): 48 | query = u'Qwerty' 49 | home_page = Home(selenium, base_url).open() 50 | home_page.login(vouched_user['email']) 51 | search_page = home_page.header.search_for(query, loggedin=True) 52 | assert 0 == search_page.results_count 53 | 54 | @pytest.mark.nondestructive 55 | def test_search_for_not_existing_mozillian_when_not_logged_in(self, base_url, selenium): 56 | query = u'Qwerty' 57 | home_page = Home(selenium, base_url).open() 58 | search_page = home_page.header.search_for(query) 59 | assert 0 == search_page.results_count 60 | 61 | @pytest.mark.nondestructive 62 | def test_search_for_empty_string_redirects_to_search_page(self, base_url, selenium): 63 | # Searching for empty string redirects to the Search page 64 | # with publicly available profiles 65 | query = u'' 66 | home_page = Home(selenium, base_url).open() 67 | search_page = home_page.header.search_for(query) 68 | assert search_page.results_count == 0 69 | -------------------------------------------------------------------------------- /variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "web-mozillians-staging.production.paas.mozilla.community": { 3 | "users": { 4 | "vouched": [ 5 | { 6 | "username": "", 7 | "email": "", 8 | "name": "" 9 | }, 10 | ], 11 | "unvouched": { 12 | "username": "", 13 | "email": "", 14 | "name": "" 15 | }, 16 | "private": { 17 | "username": "", 18 | "email": "", 19 | "name": "" 20 | }, 21 | "github_non_nda_user": { 22 | "username": "", 23 | "password": "", 24 | "secret": "" 25 | } 26 | } 27 | } 28 | } 29 | --------------------------------------------------------------------------------