Merge pull request #800 from jrbenny35/add_initial_ui_tests
Initial user integration tests.
This commit is contained in:
commit
21f7fd7dbc
@ -1,5 +1,6 @@
|
||||
node_modules
|
||||
.git
|
||||
.tox
|
||||
.DS_Store
|
||||
firefox
|
||||
assets
|
||||
@ -7,4 +8,4 @@ docs
|
||||
public
|
||||
test
|
||||
coverage
|
||||
.nyc_output
|
||||
.nyc_output
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,4 +3,6 @@ coverage
|
||||
dist
|
||||
.idea
|
||||
.DS_Store
|
||||
.nyc_output
|
||||
.nyc_output
|
||||
.tox
|
||||
.pytest_cache
|
||||
|
45
circle.yml
45
circle.yml
@ -31,9 +31,49 @@ jobs:
|
||||
- node_modules
|
||||
- run: npm run check
|
||||
- run: npm run lint
|
||||
- run: npm test
|
||||
- run: npm run test
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
integration_tests:
|
||||
working_directory: ~/send
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- uitest-cache-{{ checksum "test/integration/Pipfile" }}
|
||||
- uitest-cache-{{ checksum "test/integration/pipenv.txt" }}
|
||||
- run:
|
||||
name: Install Docker Compose
|
||||
command: |
|
||||
set -x
|
||||
pip install docker-compose>=1.18
|
||||
docker-compose --version
|
||||
- run:
|
||||
name: Install Tox
|
||||
command: |
|
||||
set -x
|
||||
pip install tox
|
||||
- run:
|
||||
name: Start docker container
|
||||
command: docker-compose up -d
|
||||
- run:
|
||||
name: Run User Integration Tests
|
||||
command: |
|
||||
npm run start:integration-docker
|
||||
npm run test-integration-docker
|
||||
environment:
|
||||
MOZ_HEADLESS: 1
|
||||
- store_artifacts:
|
||||
path: send-test.html
|
||||
- save_cache:
|
||||
key: uitest-cache-{{ checksum "test/integration/Pipfile" }}
|
||||
paths:
|
||||
- test/integration/.tox
|
||||
- save_cache:
|
||||
key: uitest-cache-{{ checksum "test/integration/pipenv.txt" }}
|
||||
paths:
|
||||
- test/integration/.tox
|
||||
deploy_dev:
|
||||
machine: true
|
||||
steps:
|
||||
@ -58,6 +98,7 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
ignore: master
|
||||
- integration_tests
|
||||
build_and_deploy_dev:
|
||||
jobs:
|
||||
- build:
|
||||
@ -96,4 +137,4 @@ workflows:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /^v.*/
|
||||
only: /^v.*/
|
||||
|
@ -10,3 +10,16 @@ services:
|
||||
- REDIS_HOST=redis
|
||||
redis:
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
selenium-firefox:
|
||||
image: b4handjr/selenium-firefox
|
||||
volumes:
|
||||
- .:/send
|
||||
working_dir: /send
|
||||
expose:
|
||||
- "4444"
|
||||
ports:
|
||||
- "5900"
|
||||
- "4444:4444"
|
||||
shm_size: 2g
|
||||
|
@ -28,6 +28,9 @@
|
||||
"test": "npm-run-all test:*",
|
||||
"test:backend": "nyc mocha --reporter=min test/backend",
|
||||
"test:frontend": "cross-env NODE_ENV=development node test/frontend/runner.js && nyc report --reporter=html",
|
||||
"test-integration-local": "tox -c test/integration/tox.ini",
|
||||
"test-integration-docker": "docker-compose exec -T --user root selenium-firefox tox -c test/integration/tox.ini",
|
||||
"start:integration-docker": "docker-compose exec -T --user root selenium-firefox ./test/integration/scripts/start-docker.sh &",
|
||||
"start": "npm run clean && cross-env NODE_ENV=development webpack-dev-server",
|
||||
"prod": "node server/prod.js"
|
||||
},
|
||||
|
17
test/integration/Pipfile
Normal file
17
test/integration/Pipfile
Normal file
@ -0,0 +1,17 @@
|
||||
[[source]]
|
||||
|
||||
url = "https://pypi.python.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
|
||||
[packages]
|
||||
|
||||
selenium = "==3.11.0"
|
||||
flake8 = "==3.5.0"
|
||||
flake8-isort = "==2.5"
|
||||
PyPOM = "==1.3.0"
|
||||
pytest = "==3.5.0"
|
||||
pytest-html = "==1.16.1"
|
||||
pytest-selenium = "==1.12.0"
|
||||
pytest-xdist = "==1.22.2"
|
89
test/integration/README.md
Normal file
89
test/integration/README.md
Normal file
@ -0,0 +1,89 @@
|
||||
# Integration Tests for [Firefox Send](https://send.firefox.com/).
|
||||
## How to run the tests locally
|
||||
### Clone the repository
|
||||
|
||||
If you have cloned this project already then you can skip this, otherwise you'll
|
||||
need to clone this repo using Git. If you do not know how to clone a GitHub
|
||||
repository, check out this [help page][git-clone] from GitHub.
|
||||
|
||||
If you think you would like to contribute to the tests by writing or maintaining
|
||||
them in the future, it would be a good idea to create a fork of this repository
|
||||
first, and then clone that. GitHub also has great instructions for
|
||||
[forking a repository][git-fork].
|
||||
|
||||
### App Setup
|
||||
|
||||
Please view the README at the root directory of the project.
|
||||
|
||||
### Run the tests
|
||||
|
||||
Included in the docker-compose file is an image containing Firefox Nightly.
|
||||
[tox][Tox] is our test environment manager and [pytest][pytest] is the test runner.
|
||||
|
||||
To run the tests, execute the command below:
|
||||
1. Make sure all of the images are running:
|
||||
```sh
|
||||
docker-compose ps
|
||||
```
|
||||
If not start them detached:
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
2. Start the tests within the docker container
|
||||
```sh
|
||||
npm run test:integration-docker
|
||||
```
|
||||
|
||||
If you have [geckodriver][geckodriver] installed you can use these steps:
|
||||
```sh
|
||||
npm start &
|
||||
npm run test:integration
|
||||
```
|
||||
This will use your local Firefox installation.
|
||||
|
||||
### Adding a test
|
||||
|
||||
The tests are written in Python using a POM, or Page Object Model. The plugin we use for this is called [pypom][pypom]. Please read the documentation there for good examples on how to use the Page Object Model when writing tests.
|
||||
|
||||
The pytest plugin that we use for running tests has a number of advanced command line options available too. The full documentation for the plugin can be found [here][pytest-selenium].
|
||||
|
||||
### Watching a test run (within the docker container)
|
||||
|
||||
The tests are run on a live version of Firefox, but they are run headless. To access the container where the tests are run to view them follow these steps:
|
||||
|
||||
1. Make sure all of the containers are running:
|
||||
```sh
|
||||
docker-compose ps
|
||||
```
|
||||
If not start them detached:
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. Copy the port that is forwarded for the ```selenium-firefox``` image:
|
||||
```sh
|
||||
0.0.0.0:32771->5900/tcp
|
||||
```
|
||||
Note: Your port may not match what is seen here.
|
||||
|
||||
You will want to copy what ever IP address and port is before the ```->5900/tcp```.
|
||||
|
||||
3. Open your favorite VNC viewer and type in, or paste that address.
|
||||
4. The password is ```secret```.
|
||||
5. The viewer should open a window with a Ubuntu logo. If that happens you are connected to the ```selenium-firefox``` image and if you start the test, you should see a Firefox window open and the tests running.
|
||||
|
||||
### Debugging a failure
|
||||
|
||||
Whether a test passes or fails will result in a HTML report being created. This report will have detailed information of the test run and if a test does fail, it will provide geckodriver logs, terminal logs, as well as a screenshot of the browser when the test failed. We use a pytest plugin called [pytest-html][pytest-html] to create this report. The report can be found within the root directory of the project and is named ```send-test.html```. It should be viewed within a browser.
|
||||
|
||||
[flake8]: http://flake8.pycqa.org/en/latest/
|
||||
[git-clone]: https://help.github.com/articles/cloning-a-repository/
|
||||
[git-fork]: https://help.github.com/articles/fork-a-repo/
|
||||
[geckodriver]: https://github.com/mozilla/geckodriver/releases/tag/v0.19.1
|
||||
[pypom]: http://pypom.readthedocs.io/en/latest/
|
||||
[pytest]: https://docs.pytest.org/en/latest/
|
||||
[pytest-html]: https://github.com/pytest-dev/pytest-html
|
||||
[pytest-selenium]: http://pytest-selenium.readthedocs.org/
|
||||
[Selenium]: http://selenium-python.readthedocs.io/index.html
|
||||
[selenium-api]: http://selenium-python.readthedocs.io/locating-elements.html
|
||||
[Tox]: http://tox.readthedocs.io/
|
76
test/integration/conftest.py
Normal file
76
test/integration/conftest.py
Normal file
@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
"""Configuration files for pytest."""
|
||||
import pytest
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
|
||||
from pages.desktop.download import Download
|
||||
from pages.desktop.home import Home
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def firefox_options(firefox_options, download_location_dir):
|
||||
"""Firefox options."""
|
||||
firefox_options.set_preference("browser.download.panel.shown", False)
|
||||
firefox_options.set_preference(
|
||||
"browser.helperApps.neverAsk.openFile", "text/plain")
|
||||
firefox_options.set_preference(
|
||||
"browser.helperApps.neverAsk.saveToDisk", "text/plain")
|
||||
firefox_options.set_preference("browser.download.folderList", 2)
|
||||
firefox_options.set_preference(
|
||||
"browser.download.dir", "{0}".format(download_location_dir))
|
||||
firefox_options.add_argument('-foreground')
|
||||
firefox_options.log.level = 'trace'
|
||||
return firefox_options
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def _verify_url(request, base_url):
|
||||
"""Verifies the base URL"""
|
||||
verify = request.config.option.verify_base_url
|
||||
if base_url and verify:
|
||||
session = requests.Session()
|
||||
retries = Retry(backoff_factor=0.1,
|
||||
status_forcelist=[500, 502, 503, 504])
|
||||
session.mount(base_url, HTTPAdapter(max_retries=retries))
|
||||
session.get(base_url, verify=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def download_location_dir(tmpdir):
|
||||
"""Directory for downloading sample file."""
|
||||
return tmpdir.mkdir('test_download')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def upload_location_dir(tmpdir):
|
||||
"""Directory for uploading sample file."""
|
||||
return tmpdir.mkdir('test_upload')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_file(upload_location_dir):
|
||||
"""Create test upload/download file."""
|
||||
setattr(test_file, 'name', 'sample.txt')
|
||||
setattr(test_file, 'location', upload_location_dir.join(test_file.name))
|
||||
return test_file
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def download_file(upload_file):
|
||||
"""Uploads and downloads a file"""
|
||||
download = Download(upload_file.selenium, upload_file.file_url).open()
|
||||
download.download_btn.click()
|
||||
return download
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def upload_file(selenium, base_url, download_location_dir, test_file):
|
||||
"""Upload file fixture."""
|
||||
home = Home(selenium, base_url).open()
|
||||
test_file.location.write('This is a test! This is a test!')
|
||||
return home.upload_area("{0}".format(test_file.location.realpath()))
|
0
test/integration/pages/__init__.py
Normal file
0
test/integration/pages/__init__.py
Normal file
0
test/integration/pages/desktop/__init__.py
Normal file
0
test/integration/pages/desktop/__init__.py
Normal file
19
test/integration/pages/desktop/base.py
Normal file
19
test/integration/pages/desktop/base.py
Normal file
@ -0,0 +1,19 @@
|
||||
from pypom import Page
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
|
||||
class Base(Page):
|
||||
"""Base object model."""
|
||||
|
||||
_url = '{base_url}'
|
||||
_send_logo_locator = (By.CLASS_NAME, 'logo')
|
||||
|
||||
def __init__(self, selenium, base_url, locale='en-US', **kwargs):
|
||||
super(Base, self).__init__(
|
||||
selenium, base_url, locale=locale, timeout=10, **kwargs)
|
||||
|
||||
def wait_for_page_to_load(self):
|
||||
self.wait.until(
|
||||
lambda _: self.find_element(
|
||||
*self._send_logo_locator).is_displayed())
|
||||
return self
|
17
test/integration/pages/desktop/download.py
Normal file
17
test/integration/pages/desktop/download.py
Normal file
@ -0,0 +1,17 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from pages.desktop.base import Base
|
||||
|
||||
|
||||
class Download(Base):
|
||||
"""Download page object model."""
|
||||
|
||||
_download_button_locator = (By.CLASS_NAME, 'btn--download')
|
||||
|
||||
def wait_for_page_to_load(self):
|
||||
self.wait.until(lambda _: self.download_btn.is_displayed())
|
||||
|
||||
@property
|
||||
def download_btn(self):
|
||||
"""Download button."""
|
||||
return self.find_element(*self._download_button_locator)
|
26
test/integration/pages/desktop/home.py
Normal file
26
test/integration/pages/desktop/home.py
Normal file
@ -0,0 +1,26 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from pages.desktop.base import Base
|
||||
|
||||
|
||||
class Home(Base):
|
||||
"""Firefox Send Home page object model."""
|
||||
|
||||
_upload_area_locator = (By.ID, 'file-upload')
|
||||
_upload_button_locator = (By.CLASS_NAME, 'btn--file')
|
||||
|
||||
@property
|
||||
def upload_btn(self):
|
||||
"""Upload button."""
|
||||
return self.find_element(*self._upload_button_locator)
|
||||
|
||||
def upload_area(self, path, cancel=False):
|
||||
"""Area that allows for drag and drop uploading.
|
||||
|
||||
Returns Progress Object.
|
||||
"""
|
||||
self.find_element(*self._upload_area_locator).send_keys(path)
|
||||
from pages.desktop.progress import Progress
|
||||
return Progress(
|
||||
self.selenium, self.base_url).wait_for_page_to_load(
|
||||
cancel_after_load=cancel)
|
25
test/integration/pages/desktop/progress.py
Normal file
25
test/integration/pages/desktop/progress.py
Normal file
@ -0,0 +1,25 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from pages.desktop.base import Base
|
||||
|
||||
|
||||
class Progress(Base):
|
||||
"""Progress page object model."""
|
||||
|
||||
_cancel_button = (By.ID, 'cancel-upload')
|
||||
_progress_icon_locator = (By.CLASS_NAME, 'progress__bar')
|
||||
|
||||
def wait_for_page_to_load(self, cancel_after_load=False):
|
||||
self.wait.until(
|
||||
lambda _: self.find_element(
|
||||
*self._progress_icon_locator).is_displayed())
|
||||
if cancel_after_load:
|
||||
self.cancel_btn.click()
|
||||
return
|
||||
from pages.desktop.share import Share
|
||||
return Share(self.selenium, self.base_url).wait_for_page_to_load()
|
||||
|
||||
@property
|
||||
def cancel_btn(self):
|
||||
"""Cancel upload button."""
|
||||
return self.find_element(*self._cancel_button)
|
22
test/integration/pages/desktop/share.py
Normal file
22
test/integration/pages/desktop/share.py
Normal file
@ -0,0 +1,22 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from pages.desktop.base import Base
|
||||
|
||||
|
||||
class Share(Base):
|
||||
"""SHare page object model."""
|
||||
|
||||
_share_page_locator = (By.CLASS_NAME, 'sharePage')
|
||||
_share_url_locator = (By.ID, 'fileUrl')
|
||||
|
||||
def wait_for_page_to_load(self):
|
||||
self.wait.until(
|
||||
lambda _: self.find_element(
|
||||
*self._share_page_locator).is_displayed())
|
||||
return self
|
||||
|
||||
@property
|
||||
def file_url(self):
|
||||
"""File uploaded URL."""
|
||||
return self.find_element(
|
||||
*self._share_url_locator).get_property('value')
|
1
test/integration/pipenv.txt
Normal file
1
test/integration/pipenv.txt
Normal file
@ -0,0 +1 @@
|
||||
pipenv==11.9.0
|
4
test/integration/scripts/start-docker.sh
Executable file
4
test/integration/scripts/start-docker.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
# piping to dev/null for starting the server within the firefox docker image
|
||||
npm install > "/dev/null" 2>&1
|
||||
npm start > "/dev/null" 2>&1 &
|
6
test/integration/test_download.py
Normal file
6
test/integration/test_download.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Test files regarding downloads."""
|
||||
|
||||
|
||||
def test_download(download_file, download_location_dir, test_file):
|
||||
"""Test downloaded file matches uploaded file."""
|
||||
assert download_location_dir.ensure(test_file.name)
|
6
test/integration/test_progress.py
Normal file
6
test/integration/test_progress.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Test files regarding the upload progress pages."""
|
||||
|
||||
|
||||
def test_progress(upload_file):
|
||||
"""Test progress icon shows while uploading."""
|
||||
assert upload_file
|
6
test/integration/test_upload.py
Normal file
6
test/integration/test_upload.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Test files regarding uploading."""
|
||||
|
||||
|
||||
def test_upload(upload_file):
|
||||
"""Test file upload and creates URL."""
|
||||
assert upload_file.file_url is not None
|
24
test/integration/tox.ini
Executable file
24
test/integration/tox.ini
Executable file
@ -0,0 +1,24 @@
|
||||
[tox]
|
||||
envlist = integration-tests, flake8
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
recreate=True
|
||||
skip_install = True
|
||||
passenv = DISPLAY MOZ_HEADLESS
|
||||
deps = -rpipenv.txt
|
||||
commands =
|
||||
pipenv install --skip-lock
|
||||
pipenv run pytest -v --verify-base-url --driver Firefox --html=send-test.html --self-contained-html {posargs}
|
||||
|
||||
[testenv:flake8]
|
||||
commands =
|
||||
pipenv install --skip-lock
|
||||
pipenv run flake8 {posargs:.}
|
||||
|
||||
[flake8]
|
||||
exclude = .eggs,.tox,docs,node_modules
|
||||
|
||||
[pytest]
|
||||
base_url = http://localhost:8080
|
||||
sensitive_url = mozilla\.(com|org)
|
Loading…
Reference in New Issue
Block a user