-
Notifications
You must be signed in to change notification settings - Fork 0
🧪 [testing improvement description] #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,94 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from playwright.async_api import async_playwright | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import signal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+6
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import time | |
| import subprocess | |
| import os | |
| import signal | |
| import subprocess |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a fixed asyncio.sleep(1) to wait for the server to start can lead to flaky tests, as the server might take more or less time to initialize depending on system load. A more robust approach is to actively poll the server to check if it's ready, for instance by trying to establish a connection in a loop.
# Wait for the server to be ready by polling
for _ in range(20): # Try for 2 seconds
try:
_, writer = await asyncio.open_connection('localhost', 8000)
writer.close()
await writer.wait_closed()
break
except ConnectionRefusedError:
await asyncio.sleep(0.1)
else:
raise RuntimeError("Server did not start in time.")
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The local server is started with a hard-coded command (python3 -m http.server 8000) and a fixed port. This can fail on Windows/CI (no python3) or when port 8000 is already in use, and the failures are hard to debug because stdout/stderr are discarded. Consider using sys.executable, choosing an ephemeral free port, and serving the repo root explicitly (via cwd or --directory) so the script works no matter where it's launched from.
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
page.route handler uses a synchronous lambda that calls route.abort() / route.continue_() without awaiting them. In Playwright's async API these are coroutines; this will raise "coroutine was never awaited" and the routing will not behave reliably. Use an async def route handler and await route.abort() / await route.continue_() (or remove routing if not needed).
| await page.route("**/*", lambda route: route.abort() if route.request.url.startswith("http") and not route.request.url.startswith("http://localhost") else route.continue_()) | |
| async def handle_route(route): | |
| if route.request.url.startswith("http") and not route.request.url.startswith("http://localhost"): | |
| await route.abort() | |
| else: | |
| await route.continue_() | |
| await page.route("**/*", handle_route) |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
page.goto(..., wait_until="commit") can return before DOMContentLoaded and before experiment.js attaches the click handler / defines showScreen. That makes the subsequent page.click('#btn-consent') and page.evaluate("showScreen('trial')") race-prone and can fail nondeterministically. Prefer waiting for domcontentloaded (or load) and/or explicitly wait_for_function that window.showScreen exists before interacting.
| await page.goto("http://localhost:8000/code/index.html?condition=control", wait_until="commit") | |
| await page.goto("http://localhost:8000/code/index.html?condition=control", wait_until="domcontentloaded") | |
| # Ensure the experiment script has defined showScreen before interacting | |
| await page.wait_for_function("window.showScreen !== undefined") |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_attribute('class') can return None when the attribute is missing; 'active' in screen1_classes (and similar checks below) will then raise a TypeError. Consider defaulting to an empty string when the attribute is None, or use evaluate/locator.evaluate to check element.classList.contains('active').
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These initial state assertions can be converted to use expect() for consistency and robustness. expect() will wait for the elements to be in the asserted state, which is safer than asserting immediately after navigation.
# Check classes and visibility using auto-waiting assertions
await expect(screen1).to_have_class(re.compile(r'\bactive\b'))
await expect(screen1).to_be_visible()
await expect(screen2).not_to_be_visible()
await expect(screen2).not_to_have_class(re.compile(r'\bactive\b'))
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fixed asyncio.sleep(0.1) waits for the setTimeout-driven transition are timing-dependent and can be flaky on slower CI machines. It's more reliable to wait on an actual condition (e.g., wait for #screen-2 to become visible / have active, or wait for #screen-1 to become hidden) using wait_for_function or Playwright's locator assertions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using asyncio.sleep() for waiting on UI updates is a common source of test flakiness. The subsequent manual assertions don't wait, so they might run before the UI has finished updating. By using expect(), Playwright will automatically wait for the condition to be true before proceeding, making the test much more reliable. This suggestion replaces the sleep and the manual assertions.
# 3. State Verification: Screen 1 hidden, Screen 2 visible
# Using expect() for robust, auto-waiting assertions.
await expect(screen1).not_to_have_class(re.compile(r'\bactive\b'))
await expect(screen2).to_have_class(re.compile(r'\bactive\b'))
await expect(screen1).not_to_be_visible()
await expect(screen2).to_be_visible()There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the previous transition, this asyncio.sleep() and manual assertion block should be replaced with auto-waiting expect() calls to improve test reliability and prevent flakiness.
screen_trial = page.locator('#screen-trial')
# Check classes and visibility using auto-waiting assertions
await expect(screen2).not_to_have_class(re.compile(r'\bactive\b'))
await expect(screen_trial).to_have_class(re.compile(r'\bactive\b'))
await expect(screen2).not_to_be_visible()
await expect(screen_trial).to_be_visible()
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If any assertion or Playwright call raises inside the async with async_playwright() block, the browser/context may not be closed because await browser.close() is not in a finally. Wrapping browser/context creation in a try/finally (or using async with p.chromium.launch(...) as browser if supported) helps avoid orphaned processes on failure.
| browser = await p.chromium.launch(headless=True) | |
| context = await browser.new_context() | |
| page = await context.new_page() | |
| # Intercept and abort external requests to prevent hanging | |
| await page.route("**/*", lambda route: route.abort() if route.request.url.startswith("http") and not route.request.url.startswith("http://localhost") else route.continue_()) | |
| # Navigate to the experiment | |
| print("Navigating to the experiment...") | |
| await page.goto("http://localhost:8000/code/index.html?condition=control", wait_until="commit") | |
| # Wait for the initial screen to be ready | |
| await page.wait_for_selector('#screen-1', state="attached") | |
| print("Testing initial state...") | |
| # 1. Visibility Assertion: Screen 1 should be visible, others hidden | |
| screen1 = page.locator('#screen-1') | |
| screen2 = page.locator('#screen-2') | |
| # Check classes | |
| screen1_classes = await screen1.get_attribute('class') | |
| assert 'active' in screen1_classes, "Screen 1 should have 'active' class initially" | |
| # Check actual visibility | |
| assert await screen1.is_visible(), "Screen 1 should be visible initially" | |
| assert not await screen2.is_visible(), "Screen 2 should be hidden initially" | |
| print("Testing transition to Screen 2...") | |
| # 2. Transition Assertion: Click consent button | |
| await page.click('#btn-consent') | |
| # Wait for the setTimeout in showScreen to complete (50ms) | |
| await asyncio.sleep(0.1) | |
| # 3. State Verification: Screen 1 hidden, Screen 2 visible | |
| # Check classes | |
| screen1_classes = await screen1.get_attribute('class') | |
| screen2_classes = await screen2.get_attribute('class') | |
| assert 'active' not in screen1_classes, "Screen 1 should NOT have 'active' class after transition" | |
| assert 'active' in screen2_classes, "Screen 2 should have 'active' class after transition" | |
| # Check actual visibility | |
| assert not await screen1.is_visible(), "Screen 1 should be hidden after transition" | |
| assert await screen2.is_visible(), "Screen 2 should be visible after transition" | |
| print("Testing programmatic transition to Screen 'trial'...") | |
| # 4. Trigger a programmatic transition to 'trial' | |
| await page.evaluate("showScreen('trial')") | |
| # Wait for the setTimeout in showScreen to complete | |
| await asyncio.sleep(0.1) | |
| screen_trial = page.locator('#screen-trial') | |
| # Check classes | |
| screen2_classes = await screen2.get_attribute('class') | |
| screen_trial_classes = await screen_trial.get_attribute('class') | |
| assert 'active' not in screen2_classes, "Screen 2 should NOT have 'active' class after transition to trial" | |
| assert 'active' in screen_trial_classes, "Screen trial should have 'active' class after transition" | |
| # Check actual visibility | |
| assert not await screen2.is_visible(), "Screen 2 should be hidden after transition to trial" | |
| assert await screen_trial.is_visible(), "Screen trial should be visible after transition to trial" | |
| print("✅ All navigation and visibility assertions passed successfully.") | |
| await browser.close() | |
| async with p.chromium.launch(headless=True) as browser: | |
| async with browser.new_context() as context: | |
| page = await context.new_page() | |
| # Intercept and abort external requests to prevent hanging | |
| await page.route("**/*", lambda route: route.abort() if route.request.url.startswith("http") and not route.request.url.startswith("http://localhost") else route.continue_()) | |
| # Navigate to the experiment | |
| print("Navigating to the experiment...") | |
| await page.goto("http://localhost:8000/code/index.html?condition=control", wait_until="commit") | |
| # Wait for the initial screen to be ready | |
| await page.wait_for_selector('#screen-1', state="attached") | |
| print("Testing initial state...") | |
| # 1. Visibility Assertion: Screen 1 should be visible, others hidden | |
| screen1 = page.locator('#screen-1') | |
| screen2 = page.locator('#screen-2') | |
| # Check classes | |
| screen1_classes = await screen1.get_attribute('class') | |
| assert 'active' in screen1_classes, "Screen 1 should have 'active' class initially" | |
| # Check actual visibility | |
| assert await screen1.is_visible(), "Screen 1 should be visible initially" | |
| assert not await screen2.is_visible(), "Screen 2 should be hidden initially" | |
| print("Testing transition to Screen 2...") | |
| # 2. Transition Assertion: Click consent button | |
| await page.click('#btn-consent') | |
| # Wait for the setTimeout in showScreen to complete (50ms) | |
| await asyncio.sleep(0.1) | |
| # 3. State Verification: Screen 1 hidden, Screen 2 visible | |
| # Check classes | |
| screen1_classes = await screen1.get_attribute('class') | |
| screen2_classes = await screen2.get_attribute('class') | |
| assert 'active' not in screen1_classes, "Screen 1 should NOT have 'active' class after transition" | |
| assert 'active' in screen2_classes, "Screen 2 should have 'active' class after transition" | |
| # Check actual visibility | |
| assert not await screen1.is_visible(), "Screen 1 should be hidden after transition" | |
| assert await screen2.is_visible(), "Screen 2 should be visible after transition" | |
| print("Testing programmatic transition to Screen 'trial'...") | |
| # 4. Trigger a programmatic transition to 'trial' | |
| await page.evaluate("showScreen('trial')") | |
| # Wait for the setTimeout in showScreen to complete | |
| await asyncio.sleep(0.1) | |
| screen_trial = page.locator('#screen-trial') | |
| # Check classes | |
| screen2_classes = await screen2.get_attribute('class') | |
| screen_trial_classes = await screen_trial.get_attribute('class') | |
| assert 'active' not in screen2_classes, "Screen 2 should NOT have 'active' class after transition to trial" | |
| assert 'active' in screen_trial_classes, "Screen trial should have 'active' class after transition" | |
| # Check actual visibility | |
| assert not await screen2.is_visible(), "Screen 2 should be hidden after transition to trial" | |
| assert await screen_trial.is_visible(), "Screen trial should be visible after transition to trial" | |
| print("✅ All navigation and visibility assertions passed successfully.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To write more robust, non-flaky tests, it's best to use Playwright's auto-waiting assertions via the
expectfunction. This requires importing it, along with theremodule for more precise class name assertions.