-
Notifications
You must be signed in to change notification settings - Fork 13
Add Playwright-based YouTube UI tests using Optics #227
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
Open
sheshnath1st
wants to merge
31
commits into
mozarkai:main
Choose a base branch
from
sheshnath1st:feat_playwright
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
62e5d7d
implemented using asyncapi
sheshnathmozark c0823be
added yaml
sheshnathmozark 88d364d
working all functionality
sheshnathmozark aaac0f9
feat: enhance async handling and element extraction in Playwright int…
malto101 e9c11e9
test(playwright): add unit test for app launch
sheshnathmozark 270690a
test(playwright): verify youtube search box is clickable
sheshnathmozark 7353be0
test(playwright): verify youtube search box click with delay
sheshnathmozark 6111047
test(playwright): change config sample
sheshnathmozark ee2a608
test(playwright): add youtube search box click and sleep test
sheshnathmozark dd635a3
test(playwright): verify search text entry and enter key submission
sheshnathmozark b158339
# This is a combination of 6 commits.
sheshnathmozark d50aead
fix(playwright): handle Locator input in clear_text_element
sheshnathmozark 6d5774e
fix(action-keyword): make get_text work for non-appium drivers
sheshnathmozark a457b2b
fix(playwright): handle locator safely in get_text_element
sheshnathmozark 0b40bf8
test: refactor login helper and fix lint issues
sheshnathmozark 7397778
test: refactor login helper and fix lint issues
sheshnathmozark 22eb817
fix: rollback strategies
sheshnathmozark bf6f6d1
chore: fix EOF newline issues
sheshnathmozark 097fa70
chore: remove commented-out scroll code
sheshnathmozark d942f13
refactor: reuse locator_exists to eliminate duplication
sheshnathmozark 99f6c1d
refactor: extract assert retry loop to eliminate duplication
sheshnathmozark 88b0329
chore: fix end-of-file newline in requirements.txt
sheshnathmozark 92ee4fa
chore: normalize eof newline in requirements.txt
sheshnathmozark f4398a9
326c42a refactor(playwright): centralize locator resolution and retry…
sheshnathmozark c591cf5
refactor(playwright): align get_interactive_elements signature and re…
sheshnathmozark 44a2baa
refactor(playwright): decompose xpath and text extraction to reduce c…
sheshnathmozark f01fe63
refactor: address SonarQube issues by reducing complexity and duplica…
sheshnathmozark f6ec6aa
refactor: align get_interactive_elements signature with interface
sheshnathmozark 3937379
refactor: test load_config return type
sheshnathmozark d21491e
chore(async-utils): add detailed documentation for persistent async l…
sheshnathmozark f22edd3
chore(playwright): document intentional exception handling in retry loop
sheshnathmozark File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,184 @@ | ||||||||||||||||||
| import asyncio | ||||||||||||||||||
| import threading | ||||||||||||||||||
| from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError | ||||||||||||||||||
| from typing import Any, Coroutine, Optional | ||||||||||||||||||
|
|
||||||||||||||||||
| from optics_framework.common.logging_config import internal_logger | ||||||||||||||||||
| from optics_framework.common.error import OpticsError, Code | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| """ | ||||||||||||||||||
| ================================================================================ | ||||||||||||||||||
| Async Utilities Module | ||||||||||||||||||
| ================================================================================ | ||||||||||||||||||
|
|
||||||||||||||||||
| Design intent: | ||||||||||||||||||
| - Provide a safe, deterministic way to execute async coroutines from | ||||||||||||||||||
| synchronous code paths. | ||||||||||||||||||
| - Support environments where an event loop may or may not already exist | ||||||||||||||||||
| (pytest, Playwright, FastAPI, CLI tools). | ||||||||||||||||||
| - Avoid deadlocks caused by blocking calls on the currently running loop. | ||||||||||||||||||
|
|
||||||||||||||||||
| Key architectural decisions: | ||||||||||||||||||
| - A SINGLE persistent background event loop is created and reused. | ||||||||||||||||||
| - All async coroutines are executed on that loop via | ||||||||||||||||||
| `asyncio.run_coroutine_threadsafe`. | ||||||||||||||||||
| - This avoids nested-loop errors and Playwright deadlocks. | ||||||||||||||||||
|
|
||||||||||||||||||
| Important constraints: | ||||||||||||||||||
| - This module is intentionally conservative and defensive. | ||||||||||||||||||
| - Stability and predictability are prioritized over performance. | ||||||||||||||||||
| ================================================================================ | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| # --------------------------------------------------------------------- | ||||||||||||||||||
| # Persistent background event loop state | ||||||||||||||||||
| # --------------------------------------------------------------------- | ||||||||||||||||||
| # NOTE: | ||||||||||||||||||
| # - These are module-level globals by design. | ||||||||||||||||||
| # - Access is guarded by `_loop_lock` to ensure thread safety. | ||||||||||||||||||
| # - Optional[...] is used instead of Python 3.10 `| None` | ||||||||||||||||||
| # for backward compatibility. | ||||||||||||||||||
| # --------------------------------------------------------------------- | ||||||||||||||||||
| _persistent_loop: Optional[asyncio.AbstractEventLoop] = None | ||||||||||||||||||
| _loop_thread: Optional[threading.Thread] = None | ||||||||||||||||||
| _loop_lock = threading.Lock() | ||||||||||||||||||
|
|
||||||||||||||||||
| # --------------------------------------------------------------------- | ||||||||||||||||||
| # Shared executor | ||||||||||||||||||
| # --------------------------------------------------------------------- | ||||||||||||||||||
| # Design note: | ||||||||||||||||||
| # - A single-thread executor is sufficient because actual async execution | ||||||||||||||||||
| # happens inside the event loop. | ||||||||||||||||||
| # - This avoids thread churn and resource exhaustion. | ||||||||||||||||||
| # --------------------------------------------------------------------- | ||||||||||||||||||
| _executor = ThreadPoolExecutor(max_workers=1) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def _start_loop(loop: asyncio.AbstractEventLoop): | ||||||||||||||||||
| """ | ||||||||||||||||||
| Entry point for the background thread. | ||||||||||||||||||
|
|
||||||||||||||||||
| Responsibilities: | ||||||||||||||||||
| - Bind the provided event loop to the current thread. | ||||||||||||||||||
| - Run the loop indefinitely. | ||||||||||||||||||
|
|
||||||||||||||||||
| This function never returns unless the loop is explicitly stopped. | ||||||||||||||||||
| """ | ||||||||||||||||||
| asyncio.set_event_loop(loop) | ||||||||||||||||||
| loop.run_forever() | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def _get_or_create_persistent_loop() -> asyncio.AbstractEventLoop: | ||||||||||||||||||
| """ | ||||||||||||||||||
| Retrieve the shared persistent event loop, creating it if necessary. | ||||||||||||||||||
|
|
||||||||||||||||||
| Design considerations: | ||||||||||||||||||
| - Ensures exactly ONE background event loop exists. | ||||||||||||||||||
| - Safe for concurrent access via `_loop_lock`. | ||||||||||||||||||
| - Automatically recreates the loop if it was closed. | ||||||||||||||||||
|
|
||||||||||||||||||
| Returns: | ||||||||||||||||||
| asyncio.AbstractEventLoop: | ||||||||||||||||||
| A running, reusable event loop suitable for scheduling coroutines. | ||||||||||||||||||
| """ | ||||||||||||||||||
| global _persistent_loop, _loop_thread | ||||||||||||||||||
|
|
||||||||||||||||||
| with _loop_lock: | ||||||||||||||||||
| if _persistent_loop is None or _persistent_loop.is_closed(): | ||||||||||||||||||
| internal_logger.info("[AsyncUtils] Creating persistent event loop") | ||||||||||||||||||
|
|
||||||||||||||||||
| _persistent_loop = asyncio.new_event_loop() | ||||||||||||||||||
| _loop_thread = threading.Thread( | ||||||||||||||||||
| target=_start_loop, | ||||||||||||||||||
| args=(_persistent_loop,), | ||||||||||||||||||
| daemon=True, | ||||||||||||||||||
| name="optics-async-loop", | ||||||||||||||||||
| ) | ||||||||||||||||||
| _loop_thread.start() | ||||||||||||||||||
|
|
||||||||||||||||||
| return _persistent_loop | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def run_async(coro: Coroutine[Any, Any, Any]): | ||||||||||||||||||
| """ | ||||||||||||||||||
| Execute an async coroutine safely from synchronous code. | ||||||||||||||||||
|
|
||||||||||||||||||
| Why this exists: | ||||||||||||||||||
| - `asyncio.run()` cannot be used when an event loop is already running. | ||||||||||||||||||
| - Directly awaiting coroutines is impossible from sync code. | ||||||||||||||||||
| - Playwright + FastAPI frequently run inside existing loops. | ||||||||||||||||||
|
|
||||||||||||||||||
| How it works: | ||||||||||||||||||
| 1. Detect whether a loop is already running (for awareness only). | ||||||||||||||||||
| 2. Always schedule the coroutine on a dedicated background loop. | ||||||||||||||||||
| 3. Block synchronously until the result is available. | ||||||||||||||||||
|
|
||||||||||||||||||
| Error handling: | ||||||||||||||||||
| - Timeouts are converted into OpticsError with a clear message. | ||||||||||||||||||
| - Pending coroutines are cancelled to prevent runaway execution. | ||||||||||||||||||
|
|
||||||||||||||||||
| Args: | ||||||||||||||||||
| coro (Coroutine): | ||||||||||||||||||
| The async coroutine to execute. | ||||||||||||||||||
|
|
||||||||||||||||||
| Returns: | ||||||||||||||||||
| Any: | ||||||||||||||||||
| The result returned by the coroutine. | ||||||||||||||||||
|
|
||||||||||||||||||
| Raises: | ||||||||||||||||||
| OpticsError: | ||||||||||||||||||
| If execution times out or fails unexpectedly. | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| # ----------------------------------------------------------------- | ||||||||||||||||||
| # Detect existing event loop (informational only) | ||||||||||||||||||
| # ----------------------------------------------------------------- | ||||||||||||||||||
| # IMPORTANT: | ||||||||||||||||||
| # - We do NOT use the running loop even if one exists. | ||||||||||||||||||
| # - Blocking on the same loop would cause a deadlock. | ||||||||||||||||||
| # - This try/except is intentional and expected. | ||||||||||||||||||
| # ----------------------------------------------------------------- | ||||||||||||||||||
| try: | ||||||||||||||||||
| asyncio.get_running_loop() | ||||||||||||||||||
| except RuntimeError: | ||||||||||||||||||
|
||||||||||||||||||
| except RuntimeError: | |
| except RuntimeError: | |
| # No running event loop in this thread; this is expected and safe to ignore |
Copilot
AI
Jan 13, 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.
'except' clause does nothing but pass and there is no explanatory comment.
Suggested change
| except RuntimeError: | |
| except RuntimeError: | |
| # It's safe to ignore this: a RuntimeError here just means there is no | |
| # currently running event loop in this thread. We always use our own | |
| # persistent background loop below, so this probe is purely informational. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 change to get_text implementation adds fallback logic to use element_source.locate and driver.get_text_element. However, this code path lacks error handling. If either locate or get_text_element fails, it could raise an unhandled exception. Consider adding try-except blocks or null checks to handle cases where the element cannot be located or text cannot be retrieved.