Run AppHarness selenium integration tests in CI (#1538)
This commit is contained in:
parent
4a658ef9be
commit
544d352e55
.github/workflows
integration
reflex
37
.github/workflows/integration_app_harness.yml
vendored
Normal file
37
.github/workflows/integration_app_harness.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: integration-app-harness
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
integration-app-harness:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/setup_build_env
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
run-poetry-install: true
|
||||||
|
create-venv-at-path: .venv
|
||||||
|
- run: poetry run pip install pyvirtualdisplay pillow
|
||||||
|
- name: Run app harness tests
|
||||||
|
env:
|
||||||
|
SCREENSHOT_DIR: /tmp/screenshots
|
||||||
|
run: |
|
||||||
|
poetry run pytest integration
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
name: Upload failed test screenshots
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: failed_test_screenshots
|
||||||
|
path: /tmp/screenshots
|
56
integration/conftest.py
Normal file
56
integration/conftest.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"""Shared conftest for all integration tests."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
DISPLAY = None
|
||||||
|
XVFB_DIMENSIONS = (800, 600)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
def xvfb():
|
||||||
|
"""Create virtual X display.
|
||||||
|
|
||||||
|
This function is a no-op unless GITHUB_ACTIONS is set in the environment.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
the pyvirtualdisplay object that the browser will be open on
|
||||||
|
"""
|
||||||
|
if os.environ.get("GITHUB_ACTIONS"):
|
||||||
|
from pyvirtualdisplay.smartdisplay import ( # pyright: ignore [reportMissingImports]
|
||||||
|
SmartDisplay,
|
||||||
|
)
|
||||||
|
|
||||||
|
global DISPLAY
|
||||||
|
with SmartDisplay(visible=0, size=XVFB_DIMENSIONS) as DISPLAY:
|
||||||
|
yield DISPLAY
|
||||||
|
DISPLAY = None
|
||||||
|
else:
|
||||||
|
yield None
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_exception_interact(node, call, report):
|
||||||
|
"""Take and upload screenshot when tests fail.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node: The pytest item that failed.
|
||||||
|
call: The pytest call describing when/where the test was invoked.
|
||||||
|
report: The pytest log report object.
|
||||||
|
"""
|
||||||
|
screenshot_dir = os.environ.get("SCREENSHOT_DIR")
|
||||||
|
if DISPLAY is None or screenshot_dir is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
screenshot_dir = Path(screenshot_dir)
|
||||||
|
screenshot_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
safe_filename = re.sub(
|
||||||
|
r"(?u)[^-\w.]",
|
||||||
|
"_",
|
||||||
|
str(node.nodeid).strip().replace(" ", "_"),
|
||||||
|
)
|
||||||
|
|
||||||
|
DISPLAY.waitgrab().save(
|
||||||
|
(Path(screenshot_dir) / safe_filename).with_suffix(".png"),
|
||||||
|
)
|
@ -96,21 +96,21 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
|
|||||||
|
|
||||||
# type more characters
|
# type more characters
|
||||||
debounce_input.send_keys("getting testing done")
|
debounce_input.send_keys("getting testing done")
|
||||||
time.sleep(0.1)
|
time.sleep(0.2)
|
||||||
assert debounce_input.get_attribute("value") == "getting testing done"
|
assert debounce_input.get_attribute("value") == "getting testing done"
|
||||||
assert backend_state.text == "getting testing done"
|
assert backend_state.text == "getting testing done"
|
||||||
assert fully_controlled_input.poll_for_value(value_input) == "getting testing done"
|
assert fully_controlled_input.poll_for_value(value_input) == "getting testing done"
|
||||||
|
|
||||||
# type into the on_change input
|
# type into the on_change input
|
||||||
on_change_input.send_keys("overwrite the state")
|
on_change_input.send_keys("overwrite the state")
|
||||||
time.sleep(0.1)
|
time.sleep(0.2)
|
||||||
assert debounce_input.get_attribute("value") == "overwrite the state"
|
assert debounce_input.get_attribute("value") == "overwrite the state"
|
||||||
assert on_change_input.get_attribute("value") == "overwrite the state"
|
assert on_change_input.get_attribute("value") == "overwrite the state"
|
||||||
assert backend_state.text == "overwrite the state"
|
assert backend_state.text == "overwrite the state"
|
||||||
assert fully_controlled_input.poll_for_value(value_input) == "overwrite the state"
|
assert fully_controlled_input.poll_for_value(value_input) == "overwrite the state"
|
||||||
|
|
||||||
clear_button.click()
|
clear_button.click()
|
||||||
time.sleep(0.1)
|
time.sleep(0.2)
|
||||||
assert on_change_input.get_attribute("value") == ""
|
assert on_change_input.get_attribute("value") == ""
|
||||||
# potential bug: clearing the on_change field doesn't itself trigger on_change
|
# potential bug: clearing the on_change field doesn't itself trigger on_change
|
||||||
# assert backend_state.text == ""
|
# assert backend_state.text == ""
|
||||||
|
@ -261,17 +261,23 @@ class Config(Base):
|
|||||||
return urllib.parse.urlsplit(event_url).path
|
return urllib.parse.urlsplit(event_url).path
|
||||||
|
|
||||||
|
|
||||||
def get_config() -> Config:
|
def get_config(reload: bool = False) -> Config:
|
||||||
"""Get the app config.
|
"""Get the app config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reload: Re-import the rxconfig module from disk
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The app config.
|
The app config.
|
||||||
"""
|
"""
|
||||||
from reflex.config import Config
|
from reflex.config import Config
|
||||||
|
|
||||||
sys.path.append(os.getcwd())
|
sys.path.insert(0, os.getcwd())
|
||||||
try:
|
try:
|
||||||
return __import__(constants.CONFIG_MODULE).config
|
rxconfig = __import__(constants.CONFIG_MODULE)
|
||||||
|
if reload:
|
||||||
|
importlib.reload(rxconfig)
|
||||||
|
return rxconfig.config
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return Config(app_name="") # type: ignore
|
return Config(app_name="") # type: ignore
|
||||||
|
@ -155,6 +155,8 @@ class AppHarness:
|
|||||||
)
|
)
|
||||||
self.app_module_path.write_text(source_code)
|
self.app_module_path.write_text(source_code)
|
||||||
with chdir(self.app_path):
|
with chdir(self.app_path):
|
||||||
|
# ensure config is reloaded when testing different app
|
||||||
|
reflex.config.get_config(reload=True)
|
||||||
self.app_module = reflex.utils.prerequisites.get_app()
|
self.app_module = reflex.utils.prerequisites.get_app()
|
||||||
self.app_instance = self.app_module.app
|
self.app_instance = self.app_module.app
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user