Remove setup.sh in favor of skyvern CLI (#4737)
This commit is contained in:
224
tests/unit/test_script_skyvern_page.py
Normal file
224
tests/unit/test_script_skyvern_page.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
Unit tests for ScriptSkyvernPage, specifically testing _wait_for_page_ready_before_action.
|
||||
|
||||
This test file exists to prevent regressions like the AttributeError bug where
|
||||
self._page was used instead of self.page (see PR #8425, SKY-7676).
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import re
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from skyvern.config import settings
|
||||
from skyvern.core.script_generations.script_skyvern_page import ScriptSkyvernPage
|
||||
|
||||
|
||||
def create_mock_page():
|
||||
"""Create a mock Playwright Page object with required attributes."""
|
||||
page = MagicMock()
|
||||
page.url = "https://example.com"
|
||||
# Required for Playwright Page base class
|
||||
page._loop = MagicMock()
|
||||
page._impl_obj = page
|
||||
return page
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_scraped_page():
|
||||
"""Create a mock ScrapedPage object."""
|
||||
scraped_page = MagicMock()
|
||||
scraped_page._browser_state = MagicMock()
|
||||
return scraped_page
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ai():
|
||||
"""Create a mock SkyvernPageAi object."""
|
||||
return MagicMock()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_for_page_ready_before_action_calls_skyvern_frame(mock_scraped_page, mock_ai):
|
||||
"""
|
||||
Test that _wait_for_page_ready_before_action correctly calls SkyvernFrame.
|
||||
|
||||
This is a regression test for the bug in PR #8273 where self._page was used
|
||||
instead of self.page, causing AttributeError because SkyvernPage stores the
|
||||
Playwright page in self.page.
|
||||
"""
|
||||
mock_page = create_mock_page()
|
||||
|
||||
# Patch the Page base class to avoid Playwright internals
|
||||
with patch(
|
||||
"skyvern.core.script_generations.skyvern_page.Page.__init__",
|
||||
return_value=None,
|
||||
):
|
||||
# Create ScriptSkyvernPage instance
|
||||
script_page = ScriptSkyvernPage(
|
||||
scraped_page=mock_scraped_page,
|
||||
page=mock_page,
|
||||
ai=mock_ai,
|
||||
)
|
||||
|
||||
# Mock SkyvernFrame to verify it's called with self.page
|
||||
mock_skyvern_frame = MagicMock()
|
||||
mock_skyvern_frame.wait_for_page_ready = AsyncMock()
|
||||
|
||||
with patch(
|
||||
"skyvern.core.script_generations.script_skyvern_page.SkyvernFrame.create_instance",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_skyvern_frame,
|
||||
) as mock_create_instance:
|
||||
await script_page._wait_for_page_ready_before_action()
|
||||
|
||||
# Verify SkyvernFrame.create_instance was called exactly once
|
||||
mock_create_instance.assert_called_once()
|
||||
|
||||
# Get the actual call argument
|
||||
call_kwargs = mock_create_instance.call_args.kwargs
|
||||
assert "frame" in call_kwargs, "create_instance should be called with frame argument"
|
||||
# The frame argument should be a MagicMock (the page object)
|
||||
assert call_kwargs["frame"] is not None, "frame should not be None"
|
||||
|
||||
# Verify wait_for_page_ready was called with correct settings
|
||||
mock_skyvern_frame.wait_for_page_ready.assert_called_once_with(
|
||||
network_idle_timeout_ms=settings.PAGE_READY_NETWORK_IDLE_TIMEOUT_MS,
|
||||
loading_indicator_timeout_ms=settings.PAGE_READY_LOADING_INDICATOR_TIMEOUT_MS,
|
||||
dom_stable_ms=settings.PAGE_READY_DOM_STABLE_MS,
|
||||
dom_stability_timeout_ms=settings.PAGE_READY_DOM_STABILITY_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_for_page_ready_before_action_handles_no_page(mock_scraped_page, mock_ai):
|
||||
"""
|
||||
Test that _wait_for_page_ready_before_action returns early if self.page is None.
|
||||
"""
|
||||
# Patch the Page base class to avoid Playwright internals
|
||||
with patch(
|
||||
"skyvern.core.script_generations.skyvern_page.Page.__init__",
|
||||
return_value=None,
|
||||
):
|
||||
# Create a mock page first, then set page to None after construction
|
||||
mock_page = create_mock_page()
|
||||
script_page = ScriptSkyvernPage(
|
||||
scraped_page=mock_scraped_page,
|
||||
page=mock_page,
|
||||
ai=mock_ai,
|
||||
)
|
||||
# Simulate page being None (e.g., after page was closed)
|
||||
script_page.page = None
|
||||
|
||||
# This should return early without raising an error
|
||||
with patch(
|
||||
"skyvern.core.script_generations.script_skyvern_page.SkyvernFrame.create_instance",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_create_instance:
|
||||
await script_page._wait_for_page_ready_before_action()
|
||||
|
||||
# SkyvernFrame.create_instance should NOT be called
|
||||
mock_create_instance.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_for_page_ready_before_action_catches_exceptions(mock_scraped_page, mock_ai):
|
||||
"""
|
||||
Test that exceptions in _wait_for_page_ready_before_action are caught
|
||||
and don't block action execution.
|
||||
|
||||
This verifies the defensive behavior - page readiness check failures
|
||||
should not prevent actions from executing.
|
||||
"""
|
||||
mock_page = create_mock_page()
|
||||
|
||||
with patch(
|
||||
"skyvern.core.script_generations.skyvern_page.Page.__init__",
|
||||
return_value=None,
|
||||
):
|
||||
script_page = ScriptSkyvernPage(
|
||||
scraped_page=mock_scraped_page,
|
||||
page=mock_page,
|
||||
ai=mock_ai,
|
||||
)
|
||||
|
||||
# Make SkyvernFrame.create_instance raise an exception
|
||||
with patch(
|
||||
"skyvern.core.script_generations.script_skyvern_page.SkyvernFrame.create_instance",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=Exception("Simulated page readiness error"),
|
||||
):
|
||||
# Should NOT raise - exception should be caught
|
||||
await script_page._wait_for_page_ready_before_action()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_for_page_ready_before_action_catches_wait_for_page_ready_exceptions(mock_scraped_page, mock_ai):
|
||||
"""
|
||||
Test that exceptions from wait_for_page_ready are caught and logged.
|
||||
"""
|
||||
mock_page = create_mock_page()
|
||||
|
||||
with patch(
|
||||
"skyvern.core.script_generations.skyvern_page.Page.__init__",
|
||||
return_value=None,
|
||||
):
|
||||
script_page = ScriptSkyvernPage(
|
||||
scraped_page=mock_scraped_page,
|
||||
page=mock_page,
|
||||
ai=mock_ai,
|
||||
)
|
||||
|
||||
# Make wait_for_page_ready raise an exception
|
||||
mock_skyvern_frame = MagicMock()
|
||||
mock_skyvern_frame.wait_for_page_ready = AsyncMock(side_effect=TimeoutError("Page never became idle"))
|
||||
|
||||
with patch(
|
||||
"skyvern.core.script_generations.script_skyvern_page.SkyvernFrame.create_instance",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_skyvern_frame,
|
||||
):
|
||||
# Should NOT raise - exception should be caught
|
||||
await script_page._wait_for_page_ready_before_action()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_for_page_ready_attribute_access_regression():
|
||||
"""
|
||||
Regression test: Verify that the code accesses self.page, not self._page.
|
||||
|
||||
The original bug (fixed in PR #8425) used self._page which caused:
|
||||
AttributeError: 'ScriptSkyvernPage' object has no attribute '_page'
|
||||
|
||||
This test directly inspects the source code to ensure self._page is not used.
|
||||
"""
|
||||
source = inspect.getsource(ScriptSkyvernPage._wait_for_page_ready_before_action)
|
||||
|
||||
# The fixed code should use self.page
|
||||
assert "self.page" in source, "Method should access self.page"
|
||||
|
||||
# The fixed code should NOT use self._page (except in comments)
|
||||
# Remove comments and docstrings first
|
||||
# Remove docstrings
|
||||
source_no_docstrings = re.sub(r'""".*?"""', "", source, flags=re.DOTALL)
|
||||
source_no_docstrings = re.sub(r"'''.*?'''", "", source_no_docstrings, flags=re.DOTALL)
|
||||
# Remove single-line comments
|
||||
source_no_comments = re.sub(r"#.*$", "", source_no_docstrings, flags=re.MULTILINE)
|
||||
|
||||
# Now check - self._page should NOT appear in the actual code
|
||||
# (It may appear in comments explaining the fix, which is fine)
|
||||
lines_with_code = [
|
||||
line for line in source_no_comments.split("\n") if line.strip() and not line.strip().startswith("#")
|
||||
]
|
||||
code_only = "\n".join(lines_with_code)
|
||||
|
||||
# Check for the bug pattern
|
||||
if "self._page" in code_only:
|
||||
# Find the line for better error reporting
|
||||
for i, line in enumerate(source.split("\n"), 1):
|
||||
if "self._page" in line and not line.strip().startswith("#"):
|
||||
pytest.fail(
|
||||
f"Found 'self._page' in code at line {i}: {line.strip()}\n"
|
||||
"This is a regression! SkyvernPage uses self.page, not self._page."
|
||||
)
|
||||
Reference in New Issue
Block a user