Skip to content

Commit

Permalink
Add Playwright end-to-end test example
Browse files Browse the repository at this point in the history
  • Loading branch information
ihalaij1 committed May 3, 2024
1 parent 6082268 commit c060f31
Show file tree
Hide file tree
Showing 15 changed files with 316 additions and 6 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/lint.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM python:3.10-alpine
FROM python:3.10-bookworm

WORKDIR /app

RUN apk update && apk add git freetype-dev gcc musl-dev libffi-dev
RUN apt update && apt install -y git gcc musl-dev libffi-dev

RUN adduser --disabled-password prospector prospector \
RUN adduser --disabled-password prospector \
&& chown -R prospector:prospector /app \
&& rm -rf ${HOME}/.cache/ ${HOME}/.local/bin/__pycache__/
USER prospector
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,25 @@ jobs:
key: v2-${{ hashFiles('.github/workflows/Dockerfile', 'requirements_testing.txt', 'requirements.txt') }}
- run: docker load -i .docker-img.tar
- run: docker run -v ${{ github.workspace }}:${{ github.workspace }} -w ${{ github.workspace }} testimg bash -c 'python3 manage.py compilemessages && selenium_test/run_servers_and_tests.sh'
playwright-tests:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements_testing.txt
- name: Ensure browsers are installed
run: python -m playwright install --with-deps
- name: Run tests
run: pytest e2e_tests --tracing=retain-on-failure
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-traces
path: test_results/
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ venv/

/assets/sass/vendor/
/node_cache/
__pycache__/
.pytest_cache/
node_modules/

test.db
Expand Down
2 changes: 1 addition & 1 deletion aplus/local_settings.example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#DEBUG = False
#SECRET_KEY = '' # will be autogenerated in secret_key.py if not specified here
#BASE_URL = 'http://localhost:8000/' # required!
BASE_URL = 'http://localhost:8000/' # required!
#SERVICE_BASE_URL = 'http://plus:8000'
#GITMANAGER_URL = 'https://gitmanager.cs.aalto.fi' or 'https://<gitmanager_host>'
#SERVER_EMAIL = 'your_email@domain.com'
Expand Down
2 changes: 1 addition & 1 deletion aplus/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@
TEST_RUNNER = "xmlrunner.extra.djangotestrunner.XMLTestRunner"
TEST_OUTPUT_VERBOSE = True
TEST_OUTPUT_DESCRIPTIONS = True
TEST_OUTPUT_DIR = "test_results"
TEST_OUTPUT_DIR = join(dirname(dirname(abspath(__file__))), "test_results")

# Logging
# https://docs.djangoproject.com/en/1.7/topics/logging/
Expand Down
24 changes: 24 additions & 0 deletions e2e_tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Creating tests

1. Run A+ locally using the [develop-aplus](https://github.com/apluslms/develop-aplus) repository
```
source .venv/bin/activate
./docker-up.sh
```

2. Begin "recording" a test in the browser
```
playwright codegen --target python-pytest localhost:8000
```

3. Copy the generated test code to a Python file

## Documentation

[Generating tests](https://playwright.dev/python/docs/codegen-intro)

[Writing tests](https://playwright.dev/python/docs/writing-tests)

[Running and debugging tests](https://playwright.dev/python/docs/running-tests)

[Trace viewer](https://playwright.dev/python/docs/trace-viewer-intro)
Empty file added e2e_tests/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions e2e_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os
import subprocess
import time

import pytest
import requests


@pytest.fixture(scope="session", autouse=True)
def django_server():
# Start containers
path = os.path.dirname(os.path.abspath(__file__))
# pylint: disable-next=consider-using-with
server_process = subprocess.Popen([os.path.join(path, "run_servers.sh")], stdin=subprocess.PIPE)

# Wait 60 seconds for the server to be ready
max_retries = 60
retries = 0
while retries < max_retries:
try:
response = requests.get("http://localhost:8000/") # pylint: disable=missing-timeout
if response.status_code == 200:
break
except requests.ConnectionError:
pass

retries += 1
time.sleep(1)

# Wait a bit more to let the server settle (tests may fail otherwise)
time.sleep(10)

# Run tests
yield

# Stop containers and remove data
server_process.communicate(input=b'q')
28 changes: 28 additions & 0 deletions e2e_tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
version: '3'

volumes:
data:
services:
grader:
image: apluslms/run-mooc-grader
command: "python3 manage.py runserver 0.0.0.0:8080 --noreload"
volumes:
- data:/data
- /var/run/docker.sock:/var/run/docker.sock
- /tmp/aplus:/tmp/aplus
- ../../aplus-manual/:/srv/courses/default:ro
ports:
- "8080:8080"
plus:
image: apluslms/run-aplus-front
command: "python3 manage.py runserver 0.0.0.0:8000 --noreload"
environment:
APLUS_BASE_URL: 'http://localhost:8000/'
USE_GITMANAGER: 'false'
volumes:
- data:/data
- ../:/src/aplus/:ro
ports:
- "8000:8000"
depends_on:
- grader
134 changes: 134 additions & 0 deletions e2e_tests/docker-up.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/bin/bash

OS=$(uname -s)
COMPOSE_PROJECT_NAME=aplus
if [ -z "$COMPOSE_FILE" ]; then
COMPOSE_FILE="docker-compose.yml"
fi
DOCKER_SOCK=/var/run/docker.sock
[ -e "$DOCKER_SOCK" ] || { echo "ERROR: docker socket $DOCKER_SOCK doesn't exists. Do you have docker-ce installed?." >&2; exit 1; }
USER_ID=$(id -u)
USER_GID=$(id -g)

if [ $USER_ID -eq 0 ] || [ "$OS" = 'Darwin' ]; then
DOCKER_GID=0
if ! [ -e $DOCKER_SOCK ]; then
echo "No docker socket detected in $DOCKER_SOCK. Is docker installed and active?" >&2
fi
else
DOCKER_GID=$(stat -c '%g' $DOCKER_SOCK)
if ! [ -w $DOCKER_SOCK ]; then
echo "Your user does not have write access to docker." >&2
echo "It is recommended that you add yourself to that group (sudo adduser $USER docker; and then logout and login again)." >&2
echo "Alternatively, you can execute this script as sudo." >&2
exit 1
fi
fi

if [[ $(docker compose version) != *"version"* ]]; then
echo "ERROR: Unable to find docker compose plugin. Are you sure it is installed?" >&2
exit 1
fi

DATA_PATH=_data
has_data=$(grep -F "$DATA_PATH" "$COMPOSE_FILE"|grep -vE '^\s*#')
[ "$has_data" ] && mkdir -p "$DATA_PATH"

ACOS_LOG_PATH=_data/acos
has_acos_log=$(grep -F "$ACOS_LOG_PATH" "$COMPOSE_FILE"|grep -vE '^\s*#')
[ "$has_acos_log" ] && mkdir -p "$ACOS_LOG_PATH"

export COMPOSE_PROJECT_NAME USER_ID USER_GID DOCKER_GID

pid=
keep=
onexit() {
trap - INT
# Send SIGHUP to the childs of docker compose to silence their output (detach them from controlling tty)
# and then stop containers.
[ "$pid" ] && { pkill -SIGHUP -P $pid; docker compose stop; } || true
wait
if [ "$keep" = "" ]; then
clean
else
echo "Data was not removed. You can remove it with: $0 --clean"
fi
rm -rf /tmp/aplus || true
if [ -t 0 ]; then
stty sane
fi
exit 0
}

clean() {
echo " !! Removing persistent data !! "
docker compose down --volumes --remove-orphans
if [ "$DATA_PATH" -a -e "$DATA_PATH" ]; then
echo "Removing $DATA_PATH"
rm -rf "$DATA_PATH" || true
fi
}

update() {
docker compose pull
res=$?
[ $res -eq 0 ] && touch "$COMPOSE_FILE"
return $res
}

while [ "$1" ]; do
case "$1" in
-c|--clean)
clean
exit 0
;;
-u|--update)
update
exit $?
;;
*)
echo "Invalid option $1" >&2
exit 1
;;
esac
shift
done

docker compose version

if [ $(($(date +%s) - $(date -r "$COMPOSE_FILE" +%s))) -gt 604800 ]; then
# Pull updates weekly
echo "Checking for updates to the service images..."
update
echo
fi

mkdir -p /tmp/aplus
trap onexit INT
docker compose up & pid=$!

help_n=4 # Show first info after 24 seconds
while kill -0 $pid 2>/dev/null; do
while read -rs -t 0; do read -rs -t 0.1; done # Flush input
read -rsn1 -t 6 i # Read a byte
# (1 or 142) -> timeout (6s). Show help every 50 times (every 5 minutes)
[[ $? != 0 ]] && { ((--help_n > 0)) && continue || help_n=50; }
case "$i" in
q|Q) break ;;
s|S) keep="x" ; break ;;
$'\e') # Escape (ESC or ANSI code)
read -rsn1 -t 0.01 i # Try to read a second byte
[ $? -eq 142 ] && { keep="x"; break; } # Timeout -> no second byte -> plain ESC
;;
esac

# Print status and help
echo
echo " List of alive containers:"
{ docker container ls --filter "name=^${COMPOSE_PROJECT_NAME}-" --format "{{.ID}}" | xargs docker container inspect --format ' {{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} {{range $p, $conf := .NetworkSettings.Ports}}{{$p}} {{end}}'; } 2>/dev/null || true
echo
echo " Press Q or ^C to stop all and to remove data"
echo " Press S or ESC to stop all and to keep data"
echo
done
onexit
8 changes: 8 additions & 0 deletions e2e_tests/kill_servers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

# This script can be used to stop the containers started by run_servers.sh

export COMPOSE_FILE=docker-compose.yml
export COMPOSE_PROJECT_NAME=aplus

docker compose down --volumes --remove-orphans
26 changes: 26 additions & 0 deletions e2e_tests/run_servers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash

export COMPOSE_FILE=docker-compose.yml

SCRIPTPATH=$(dirname $(realpath "$0"))

# Move to the directory containing this file
cd "$SCRIPTPATH"

# Clone aplus-manual if it hasn't been cloned yet
if ! [ -d ../../aplus-manual ]; then
git clone https://github.com/apluslms/aplus-manual.git ../../aplus-manual
cd ../../aplus-manual
git submodule update --init
cd ../a-plus/e2e_tests
fi

# Move to aplus-manual directory and build the course if it hasn't been built yet
if ! [ -d ../../aplus-manual/_build ]; then
cd ../../aplus-manual
./docker-compile.sh
cd ../a-plus/e2e_tests
fi

# Run the server
./docker-up.sh
13 changes: 13 additions & 0 deletions e2e_tests/test_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import re
from playwright.sync_api import Page, expect


def test_frontpage_has_title(page: Page):
page.goto("http://localhost:8000/")
expect(page).to_have_title(re.compile("A+"))


def test_course_has_heading(page: Page) -> None:
page.goto("http://localhost:8000/")
page.get_by_role("link", name="Def. Course Current DEF000 1.").click()
expect(page.get_by_role("heading", name="A+ Manual")).to_be_visible()
13 changes: 13 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[pytest]
# Run chromium without UI
addopts = --output test_results --browser chromium
python_files = test*.py
testpaths =
.

DJANGO_SETTINGS_MODULE = aplus.settings

# These environment variables are used for the unit tests, but not for the e2e tests
env =
APLUS_BASE_URL=http://localhost
APLUS_LOCAL_SETTINGS=aplus/local_settings.test.py
5 changes: 4 additions & 1 deletion requirements_testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ coverage
selenium
pyvirtualdisplay
django-debug-toolbar >= 3.2.2, < 4
prospector
prospector
pytest-playwright
pytest-django
pytest-env

0 comments on commit c060f31

Please sign in to comment.