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
|
node_modules
|
||||||
.git
|
.git
|
||||||
|
.tox
|
||||||
.DS_Store
|
.DS_Store
|
||||||
firefox
|
firefox
|
||||||
assets
|
assets
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,3 +4,5 @@ dist
|
|||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.nyc_output
|
.nyc_output
|
||||||
|
.tox
|
||||||
|
.pytest_cache
|
||||||
|
43
circle.yml
43
circle.yml
@ -31,9 +31,49 @@ jobs:
|
|||||||
- node_modules
|
- node_modules
|
||||||
- run: npm run check
|
- run: npm run check
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm test
|
- run: npm run test
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: coverage
|
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:
|
deploy_dev:
|
||||||
machine: true
|
machine: true
|
||||||
steps:
|
steps:
|
||||||
@ -58,6 +98,7 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
ignore: master
|
ignore: master
|
||||||
|
- integration_tests
|
||||||
build_and_deploy_dev:
|
build_and_deploy_dev:
|
||||||
jobs:
|
jobs:
|
||||||
- build:
|
- build:
|
||||||
|
@ -10,3 +10,16 @@ services:
|
|||||||
- REDIS_HOST=redis
|
- REDIS_HOST=redis
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
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": "npm-run-all test:*",
|
||||||
"test:backend": "nyc mocha --reporter=min test/backend",
|
"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: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",
|
"start": "npm run clean && cross-env NODE_ENV=development webpack-dev-server",
|
||||||
"prod": "node server/prod.js"
|
"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