1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
|
"""
Pytest configuration and fixtures for Selenium integration tests.
"""
import pytest
import os
import time
import threading
import socket
from http.server import HTTPServer, SimpleHTTPRequestHandler
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager
def find_free_port():
"""Find a free port to use for the HTTP server."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
s.listen(1)
port = s.getsockname()[1]
return port
@pytest.fixture(scope="session")
def http_server():
"""Start a local HTTP server to serve the application."""
# Check if APP_URL is provided (to skip server for external testing)
if os.getenv("APP_URL"):
yield os.getenv("APP_URL")
return
# Find project root and ggj26 directory
project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
ggj26_dir = os.path.join(project_root, "ggj26")
# Verify directory exists
if not os.path.exists(ggj26_dir):
raise FileNotFoundError(f"ggj26 directory not found at {ggj26_dir}")
# Find a free port
port = find_free_port()
# Change to ggj26 directory for serving
original_dir = os.getcwd()
os.chdir(ggj26_dir)
# Create HTTP server
server = HTTPServer(('localhost', port), SimpleHTTPRequestHandler)
# Start server in background thread
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
server_thread.start()
# Wait a moment for server to start
time.sleep(0.5)
base_url = f"http://localhost:{port}"
print(f"\nStarted HTTP server at {base_url}")
yield base_url
# Cleanup
server.shutdown()
os.chdir(original_dir)
@pytest.fixture(scope="session")
def base_url(http_server):
"""Base URL for the application."""
return http_server
@pytest.fixture(scope="function")
def chrome_options():
"""Chrome options for Selenium WebDriver."""
options = Options()
# Set Chromium binary location
chromium_path = os.getenv("CHROMIUM_PATH", r"D:\Programs\Chromium\chrome-win\chrome.exe")
if os.path.exists(chromium_path):
options.binary_location = chromium_path
# Headless mode - set HEADLESS=1 to run tests without opening browser
if os.getenv("HEADLESS") == "1":
options.add_argument("--headless=new")
# Additional options for stability
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
options.add_argument("--window-size=1920,1080")
# Enable WebGL and canvas
options.add_argument("--enable-webgl")
options.add_argument("--ignore-gpu-blacklist")
# Allow file access (needed for file:// protocol)
options.add_argument("--allow-file-access-from-files")
# Disable unnecessary features
options.add_argument("--disable-extensions")
options.add_experimental_option("excludeSwitches", ["enable-logging"])
return options
@pytest.fixture(scope="session")
def chrome_options_session():
"""Chrome options for Selenium WebDriver (session-scoped)."""
options = Options()
# Set Chromium binary location
chromium_path = os.getenv("CHROMIUM_PATH", r"D:\Programs\Chromium\chrome-win\chrome.exe")
if os.path.exists(chromium_path):
options.binary_location = chromium_path
# Headless mode - set HEADLESS=1 to run tests without opening browser
if os.getenv("HEADLESS") == "1":
options.add_argument("--headless=new")
# Additional options for stability
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
options.add_argument("--window-size=1920,1080")
# Enable WebGL and canvas
options.add_argument("--enable-webgl")
options.add_argument("--ignore-gpu-blacklist")
# Allow file access (needed for file:// protocol)
options.add_argument("--allow-file-access-from-files")
# Disable unnecessary features
options.add_argument("--disable-extensions")
options.add_experimental_option("excludeSwitches", ["enable-logging"])
return options
@pytest.fixture(scope="session")
def driver(chrome_options_session):
"""Selenium WebDriver instance (session-scoped - reused across all tests)."""
# Get the Chrome/Chromium version to download matching ChromeDriver
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.core.os_manager import ChromeType
# Install ChromeDriver that matches the Chromium version
service = Service(ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install())
driver = webdriver.Chrome(service=service, options=chrome_options_session)
driver.implicitly_wait(10)
yield driver
# Teardown - only happens once at end of session
driver.quit()
@pytest.fixture(scope="function")
def wait(driver):
"""WebDriverWait instance with 10 second timeout."""
return WebDriverWait(driver, 10)
@pytest.fixture(scope="function")
def app(driver, base_url):
"""Load the application in the browser (reloads for each test)."""
driver.get(base_url)
# Wait for initial page load
time.sleep(2)
yield driver
# Clear browser logs after each test to avoid cross-contamination
try:
driver.get_log("browser")
except:
pass
def check_for_amulet_error(driver):
"""
Check if Amulet has logged any errors via the log_js_bridge.
Returns tuple (has_error, error_text)
"""
try:
# Check if window.amuletErrors exists and has any errors
amulet_errors = driver.execute_script("""
if (typeof window.amuletErrors !== 'undefined' && window.amuletErrors.length > 0) {
return window.amuletErrors;
}
return null;
""")
if amulet_errors and len(amulet_errors) > 0:
# Take a screenshot for debugging
import datetime
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
screenshot_path = f"error_screenshot_{timestamp}.png"
driver.save_screenshot(screenshot_path)
error_messages = [f"{err['level'].upper()}: {err['message']}" for err in amulet_errors]
return True, f"Amulet errors detected:\n" + "\n".join(error_messages) + f"\n(screenshot: {screenshot_path})"
return False, None
except Exception as e:
# If we can't check, assume no error
return False, None
def check_javascript_errors(driver, ignore_favicon=True):
"""
Check for JavaScript errors in the browser console.
Returns a list of error messages (empty if no errors).
"""
logs = driver.get_log("browser")
errors = [log for log in logs if log["level"] == "SEVERE"]
# Filter out favicon 404 errors if requested
if ignore_favicon:
errors = [log for log in errors if "favicon.ico" not in log.get("message", "")]
return errors
def assert_no_javascript_errors(driver, ignore_favicon=True):
"""
Assert that there are no JavaScript errors in the browser console.
Also checks for Amulet error screens.
Raises AssertionError if errors are found.
"""
# Check for Amulet blue screen errors first
from selenium.webdriver.common.by import By
has_amulet_error, error_msg = check_for_amulet_error(driver)
if has_amulet_error:
pytest.fail(f"Amulet error detected: {error_msg}")
# Then check console logs
errors = check_javascript_errors(driver, ignore_favicon)
assert len(errors) == 0, f"JavaScript errors found: {errors}"
def wait_for_canvas_ready(driver, timeout=30):
"""
Wait for the Amulet canvas to be ready.
Returns True when canvas is visible and status overlay is hidden.
"""
wait = WebDriverWait(driver, timeout)
# Wait for status overlay to disappear (indicates loading is complete)
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
try:
# Wait for status-overlay to have display:none or be removed
wait.until(
EC.invisibility_of_element_located((By.ID, "status-overlay"))
)
return True
except:
return False
@pytest.fixture(scope="function")
def ready_app(app):
"""Load app and wait for it to be ready."""
if wait_for_canvas_ready(app):
return app
else:
pytest.fail("Application did not load within timeout period")
|