Skip to content

Commit 85407b0

Browse files
committed
Improve the integration tests
- separate the integration tests into smaller scripts - add a new integration test
1 parent 223ce97 commit 85407b0

5 files changed

Lines changed: 174 additions & 40 deletions

File tree

.github/workflows/integration_tests.yml

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,7 @@ jobs:
8888
if: steps.cache-playwright.outputs.cache-hit != 'true'
8989
run: poetry run python -m playwright install chromium --with-deps
9090
- name: Restore the min DB for testing
91-
run: |
92-
wget https://github.com/Metaculus/metaculus/releases/download/v0.0.1-alpha/test_metaculus.sql.zip
93-
unzip test_metaculus.sql.zip
94-
psql $DATABASE_URL -c "CREATE EXTENSION vector;"
95-
pg_restore --dbname=$DATABASE_URL \
96-
--clean \
97-
--no-privileges \
98-
--no-owner \
99-
--if-exists \
100-
--schema=public \
101-
test_metaculus.sql
91+
run: scripts/tests/setup_test_db.sh "$DATABASE_URL"
10292
- name: "Install Node"
10393
uses: actions/setup-node@v4
10494
with:
@@ -118,7 +108,6 @@ jobs:
118108
run: cd front_end && npm run build
119109
- name: Run the integration tests
120110
run: |
121-
poetry run ./manage.py migrate
122111
PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH} scripts/tests/run_integration_tests.sh
123112
- name: Create trace.zip
124113
if: ${{ !cancelled() }}

scripts/tests/run_all_services.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/bash
2+
set -x
3+
set -e
4+
set -o pipefail
5+
6+
cleanup() {
7+
for process in "next-server" "gunicorn" "dramatiq"; do
8+
PID=$(ps aux | awk -v process=$process '$0 ~ process && $0 !~ /awk/ {print $2}')
9+
kill -s SIGTERM $PID || true
10+
done
11+
}
12+
13+
trap cleanup EXIT
14+
trap "exit 0" INT TERM
15+
trap "exit 1" ERR
16+
17+
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/test_metaculus
18+
19+
# Start the frontend and backend processes in the background,
20+
# and use sed to add a prefix to each of their stdout and stderr printed lines
21+
poetry run ./manage.py collectstatic --noinput
22+
(poetry run gunicorn metaculus_web.wsgi:application --log-level=debug --bind 0.0.0.0:8000 2>&1 | sed 's/^/[Backend]: /') &
23+
(poetry run ./manage.py rundramatiq --processes 1 --threads 1 2>&1 | sed 's/^/[Dramatiq]: /') &
24+
(npm run --prefix front_end start 2>&1 | sed 's/^/[Frontend]: /') &
25+
26+
wait

scripts/tests/run_integration_tests.sh

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,22 @@ set -x
33
set -e
44
set -o pipefail
55

6+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
"$SCRIPT_DIR/run_all_services.sh" &
8+
SERVICE_PID=$!
9+
610
cleanup() {
7-
for process in "next-server" "gunicorn" "dramatiq"; do
8-
PID=$(ps aux | awk -v process=$process '$0 ~ process && $0 !~ /awk/ {print $2}')
9-
kill -s SIGTERM $PID || true
10-
done
11+
exit_code=$?
12+
if [ -n "${SERVICE_PID:-}" ]; then
13+
kill $SERVICE_PID 2>/dev/null || true
14+
wait $SERVICE_PID 2>/dev/null || true
15+
fi
16+
exit $exit_code
1117
}
12-
1318
trap cleanup EXIT
14-
trap "exit" INT TERM ERR
15-
16-
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/test_metaculus
17-
18-
# Start the frontend and backend processes in the background,
19-
# and use sed to add a prefix to each of their stdout and stderr printed lines
20-
poetry run ./manage.py collectstatic --noinput
21-
(poetry run gunicorn metaculus_web.wsgi:application --log-level=debug --bind 0.0.0.0:8000 2>&1 | sed 's/^/[Backend]: /') &
22-
(poetry run ./manage.py rundramatiq --processes 1 --threads 1 2>&1 | sed 's/^/[Dramatiq]: /') &
23-
(npm run --prefix front_end start 2>&1 | sed 's/^/[Frontend]: /') &
2419

20+
sleep 2
2521
npx wait-on http://localhost:8000/api/healthcheck/ --timeout 30000
2622
npx wait-on http://localhost:3000 --timeout 30000
2723

28-
# Run pytest without Django plugins or the conf test, as the global DB setup/parmas
29-
# interfere with running the backend in prod "mode"
3024
poetry run pytest -s -p no:django -c - --noconftest --log-cli-level=INFO tests/integration/*.py | sed 's/^/[Tests] /'

scripts/tests/setup_test_db.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
export DATABASE_URL="${1:-postgres://postgres:postgres@localhost:5432/test_metaculus}"
5+
DB_NAME="${DATABASE_URL##*/}"
6+
ADMIN_DB_URL="${DATABASE_URL%/*}/postgres"
7+
8+
rm -f test_metaculus.sql.zip
9+
wget https://github.com/Metaculus/metaculus/releases/download/v0.0.1-alpha/test_metaculus.sql.zip -O test_metaculus.sql.zip
10+
unzip test_metaculus.sql.zip
11+
12+
psql "$ADMIN_DB_URL" -c "DROP DATABASE IF EXISTS $DB_NAME;"
13+
psql "$ADMIN_DB_URL" -c "CREATE DATABASE $DB_NAME;"
14+
psql "$DATABASE_URL" -c "CREATE EXTENSION IF NOT EXISTS vector;"
15+
pg_restore --dbname="$DATABASE_URL" \
16+
--clean \
17+
--no-privileges \
18+
--no-owner \
19+
--if-exists \
20+
--schema=public \
21+
test_metaculus.sql
22+
23+
poetry run ./manage.py migrate
24+
25+
# Make a binary question open for prediction so integration tests can use it
26+
psql "$DATABASE_URL" <<'SQL'
27+
WITH target AS (
28+
SELECT q.id AS question_id, p.id AS post_id
29+
FROM questions_question q
30+
JOIN posts_post p ON p.question_id = q.id
31+
WHERE q.type = 'binary'
32+
AND q.resolution IS NULL
33+
ORDER BY q.id
34+
LIMIT 1
35+
),
36+
update_question AS (
37+
UPDATE questions_question
38+
SET open_time = NOW() - INTERVAL '30 days',
39+
scheduled_close_time = NOW() + INTERVAL '365 days',
40+
scheduled_resolve_time = NOW() + INTERVAL '395 days',
41+
actual_close_time = NULL,
42+
actual_resolve_time = NULL,
43+
resolution = NULL
44+
FROM target
45+
WHERE questions_question.id = target.question_id
46+
)
47+
UPDATE posts_post
48+
SET published_at = NOW() - INTERVAL '30 days',
49+
open_time = NOW() - INTERVAL '30 days'
50+
FROM target
51+
WHERE posts_post.id = target.post_id;
52+
SQL

tests/integration/test_simple.py

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import re
12
import time
3+
from typing import Any, cast
24

35
from playwright.sync_api import expect, sync_playwright
46

@@ -12,7 +14,7 @@ def setup_all(cls):
1214
cls.context = cls.browser.new_context()
1315
cls.context.tracing.start(screenshots=True, snapshots=True, sources=True)
1416

15-
expect.set_options(timeout=2)
17+
expect.set_options(timeout=200)
1618

1719

1820
def teardown_all(cls):
@@ -21,7 +23,35 @@ def teardown_all(cls):
2123
cls.playwright.stop()
2224

2325

26+
def drag_slider(page, offset_px=80):
27+
"""Drag the slider handle by offset_px (positive = right, negative = left)."""
28+
slider = page.get_by_role("slider")
29+
box = slider.bounding_box()
30+
assert box, "Slider handle should have bounding box"
31+
center_x = box["x"] + box["width"] / 2
32+
center_y = box["y"] + box["height"] / 2
33+
page.mouse.move(center_x, center_y)
34+
page.mouse.down()
35+
page.mouse.move(center_x + offset_px, center_y)
36+
page.mouse.up()
37+
time.sleep(0.3)
38+
39+
40+
def login(page, base_url="http://localhost:3000/"):
41+
"""Log in with username_1 / Test1234 on the given page."""
42+
page.goto(base_url)
43+
page.get_by_role("button", name="Log in").click()
44+
page.get_by_placeholder("username or email").fill("username_1")
45+
page.get_by_placeholder("password").fill("Test1234")
46+
page.get_by_role("button", name="Log in", exact=True).last.click()
47+
time.sleep(1)
48+
49+
2450
class TestSimpleFunctionality:
51+
playwright = None
52+
browser = None
53+
context = None
54+
2555
@classmethod
2656
def setup_class(cls):
2757
setup_all(cls)
@@ -30,23 +60,66 @@ def setup_class(cls):
3060
def teardown_class(cls):
3161
teardown_all(cls)
3262

33-
@classmethod
34-
def test_login(cls):
35-
context = cls.context
63+
def teardown_method(self, method):
64+
context = cast(Any, self.__class__.context)
65+
if context is not None:
66+
for page in context.pages:
67+
page.close()
68+
context.clear_cookies()
69+
70+
def test_login(self):
71+
context = cast(Any, self.__class__.context)
72+
assert context is not None
3673
page = context.new_page()
3774
page.on("console", lambda msg: print("[Browser console] ", msg.text))
3875

39-
url = "http://localhost:3000/"
40-
page.goto(url)
76+
login(page)
4177

42-
page.get_by_role("button", name="Log in").click()
78+
page.get_by_role("banner").get_by_role("link", name="Questions").click()
79+
page.get_by_role("button", name="Filter").click()
80+
page.get_by_role("button", name="Binary").click()
81+
page.get_by_role("button", name="Open").click()
82+
page.get_by_role("button", name="Done").click()
4383

44-
page.get_by_placeholder("username or email").fill("username_1")
45-
page.get_by_placeholder("password").fill("Test1234")
46-
page.get_by_role("button", name="Log in", exact=True).last.click()
47-
time.sleep(1)
84+
def test_create_binary_forecast_via_ui(self):
85+
context = cast(Any, self.__class__.context)
86+
assert context is not None
87+
88+
page = context.new_page()
89+
page.on("console", lambda msg: print("[Browser console] ", msg.text))
90+
91+
login(page)
4892
page.get_by_role("banner").get_by_role("link", name="Questions").click()
4993
page.get_by_role("button", name="Filter").click()
5094
page.get_by_role("button", name="Binary").click()
5195
page.get_by_role("button", name="Open").click()
96+
time.sleep(1)
5297
page.get_by_role("button", name="Done").click()
98+
time.sleep(1)
99+
links = page.locator("a[href^='/questions/']")
100+
for i in range(links.count()):
101+
href = links.nth(i).get_attribute("href") or ""
102+
if re.match(r"/questions/\d+/", href):
103+
links.nth(i).click()
104+
break
105+
106+
time.sleep(1)
107+
108+
drag_slider(page, offset_px=80)
109+
110+
slider = page.get_by_role("slider")
111+
value_div = slider.locator("div")
112+
predicted_value = value_div.inner_text()
113+
114+
predict_button = page.get_by_role(
115+
"button", name=re.compile(r"(Predict|Save changes|Reaffirm)", re.IGNORECASE)
116+
).first
117+
predict_button.click()
118+
119+
time.sleep(1)
120+
page.reload()
121+
time.sleep(1)
122+
123+
slider_after = page.get_by_role("slider")
124+
value_div_after = slider_after.locator("div")
125+
expect(value_div_after).to_have_text(predicted_value)

0 commit comments

Comments
 (0)