-
-
Notifications
You must be signed in to change notification settings - Fork 630
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Provide two additional pytest plugins to make user and screen able to…
… load independently (#3511) * creating two additional plugins to make user and screen able to load independently * fix download tests * update docs to explain new plugins * code review --------- Co-authored-by: Falko Schindler <[email protected]>
- Loading branch information
1 parent
7185449
commit 48e832c
Showing
10 changed files
with
244 additions
and
213 deletions.
There are no files selected for viewing
This file contains 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 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 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 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,80 @@ | ||
import importlib | ||
from typing import Generator, List, Type | ||
|
||
import pytest | ||
from starlette.routing import Route | ||
|
||
import nicegui.storage | ||
from nicegui import Client, app, binding, core, run, ui | ||
from nicegui.page import page | ||
|
||
# pylint: disable=redefined-outer-name | ||
|
||
|
||
def pytest_configure(config: pytest.Config) -> None: | ||
"""Add the "module_under_test" marker to the pytest configuration.""" | ||
config.addinivalue_line('markers', | ||
'module_under_test(module): specify the module under test which then gets automatically reloaded.') | ||
|
||
|
||
@pytest.fixture | ||
def nicegui_reset_globals() -> Generator[None, None, None]: | ||
"""Reset the global state of the NiceGUI package.""" | ||
for route in app.routes: | ||
if isinstance(route, Route) and route.path.startswith('/_nicegui/auto/static/'): | ||
app.remove_route(route.path) | ||
for path in {'/'}.union(Client.page_routes.values()): | ||
app.remove_route(path) | ||
app.openapi_schema = None | ||
app.middleware_stack = None | ||
app.user_middleware.clear() | ||
app.urls.clear() | ||
core.air = None | ||
# NOTE favicon routes must be removed separately because they are not "pages" | ||
for route in app.routes: | ||
if isinstance(route, Route) and route.path.endswith('/favicon.ico'): | ||
app.routes.remove(route) | ||
importlib.reload(core) | ||
importlib.reload(run) | ||
element_classes: List[Type[ui.element]] = [ui.element] | ||
while element_classes: | ||
parent = element_classes.pop() | ||
for cls in parent.__subclasses__(): | ||
cls._default_props = {} # pylint: disable=protected-access | ||
cls._default_style = {} # pylint: disable=protected-access | ||
cls._default_classes = [] # pylint: disable=protected-access | ||
element_classes.append(cls) | ||
Client.instances.clear() | ||
Client.page_routes.clear() | ||
app.reset() | ||
Client.auto_index_client = Client(page('/'), request=None).__enter__() # pylint: disable=unnecessary-dunder-call | ||
# NOTE we need to re-add the auto index route because we removed all routes above | ||
app.get('/')(Client.auto_index_client.build_response) | ||
binding.reset() | ||
yield | ||
|
||
|
||
def prepare_simulation(request: pytest.FixtureRequest) -> None: | ||
"""Prepare a simulation to be started. | ||
By using the "module_under_test" marker you can specify the main entry point of the app. | ||
""" | ||
marker = request.node.get_closest_marker('module_under_test') | ||
if marker is not None: | ||
with Client.auto_index_client: | ||
importlib.reload(marker.args[0]) | ||
|
||
core.app.config.add_run_config( | ||
reload=False, | ||
title='Test App', | ||
viewport='', | ||
favicon=None, | ||
dark=False, | ||
language='en-US', | ||
binding_refresh_interval=0.1, | ||
reconnect_timeout=3.0, | ||
tailwind=True, | ||
prod_js=True, | ||
show_welcome_message=False, | ||
) | ||
nicegui.storage.set_storage_secret('simulated secret') |
This file contains 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 |
---|---|---|
@@ -1,202 +1,4 @@ | ||
import asyncio | ||
import importlib | ||
import os | ||
import shutil | ||
from pathlib import Path | ||
from typing import AsyncGenerator, Callable, Dict, Generator, List, Type | ||
|
||
import httpx | ||
import pytest | ||
from selenium import webdriver | ||
from selenium.webdriver.chrome.service import Service | ||
from starlette.routing import Route | ||
|
||
import nicegui.storage | ||
from nicegui import Client, app, binding, core, run, ui | ||
from nicegui.functions.navigate import Navigate | ||
from nicegui.functions.notify import notify | ||
from nicegui.page import page | ||
|
||
from .screen import Screen | ||
from .user import User | ||
|
||
# pylint: disable=redefined-outer-name | ||
|
||
DOWNLOAD_DIR = Path(__file__).parent / 'download' | ||
|
||
|
||
def pytest_configure(config: pytest.Config) -> None: | ||
"""Add the "module_under_test" marker to the pytest configuration.""" | ||
config.addinivalue_line('markers', | ||
'module_under_test: specify the module under test which then gets automatically reloaded.') | ||
|
||
|
||
@pytest.fixture | ||
def nicegui_chrome_options(chrome_options: webdriver.ChromeOptions) -> webdriver.ChromeOptions: | ||
"""Configure the Chrome options for the NiceGUI tests.""" | ||
chrome_options.add_argument('disable-dev-shm-usage') | ||
chrome_options.add_argument('no-sandbox') | ||
chrome_options.add_argument('headless') | ||
chrome_options.add_argument('disable-gpu' if 'GITHUB_ACTIONS' in os.environ else '--use-gl=angle') | ||
chrome_options.add_argument('window-size=600x600') | ||
chrome_options.add_experimental_option('prefs', { | ||
'download.default_directory': str(DOWNLOAD_DIR), | ||
'download.prompt_for_download': False, # To auto download the file | ||
'download.directory_upgrade': True, | ||
}) | ||
if 'CHROME_BINARY_LOCATION' in os.environ: | ||
chrome_options.binary_location = os.environ['CHROME_BINARY_LOCATION'] | ||
return chrome_options | ||
|
||
|
||
@pytest.fixture | ||
def capabilities(capabilities: Dict) -> Dict: | ||
"""Configure the Chrome driver capabilities.""" | ||
capabilities['goog:loggingPrefs'] = {'browser': 'ALL'} | ||
return capabilities | ||
|
||
|
||
@pytest.fixture | ||
def nicegui_reset_globals() -> Generator[None, None, None]: | ||
"""Reset the global state of the NiceGUI package.""" | ||
for route in app.routes: | ||
if isinstance(route, Route) and route.path.startswith('/_nicegui/auto/static/'): | ||
app.remove_route(route.path) | ||
for path in {'/'}.union(Client.page_routes.values()): | ||
app.remove_route(path) | ||
app.openapi_schema = None | ||
app.middleware_stack = None | ||
app.user_middleware.clear() | ||
app.urls.clear() | ||
core.air = None | ||
# NOTE favicon routes must be removed separately because they are not "pages" | ||
for route in app.routes: | ||
if isinstance(route, Route) and route.path.endswith('/favicon.ico'): | ||
app.routes.remove(route) | ||
importlib.reload(core) | ||
importlib.reload(run) | ||
element_classes: List[Type[ui.element]] = [ui.element] | ||
while element_classes: | ||
parent = element_classes.pop() | ||
for cls in parent.__subclasses__(): | ||
cls._default_props = {} # pylint: disable=protected-access | ||
cls._default_style = {} # pylint: disable=protected-access | ||
cls._default_classes = [] # pylint: disable=protected-access | ||
element_classes.append(cls) | ||
Client.instances.clear() | ||
Client.page_routes.clear() | ||
app.reset() | ||
Client.auto_index_client = Client(page('/'), request=None).__enter__() # pylint: disable=unnecessary-dunder-call | ||
# NOTE we need to re-add the auto index route because we removed all routes above | ||
app.get('/')(Client.auto_index_client.build_response) | ||
binding.reset() | ||
yield | ||
|
||
|
||
@pytest.fixture(scope='session') | ||
def nicegui_remove_all_screenshots() -> None: | ||
"""Remove all screenshots from the screenshot directory before the test session.""" | ||
if os.path.exists(Screen.SCREENSHOT_DIR): | ||
for name in os.listdir(Screen.SCREENSHOT_DIR): | ||
os.remove(os.path.join(Screen.SCREENSHOT_DIR, name)) | ||
|
||
|
||
@pytest.fixture() | ||
def nicegui_driver(nicegui_chrome_options: webdriver.ChromeOptions) -> Generator[webdriver.Chrome, None, None]: | ||
"""Create a new Chrome driver instance.""" | ||
s = Service() | ||
driver_ = webdriver.Chrome(service=s, options=nicegui_chrome_options) | ||
driver_.implicitly_wait(Screen.IMPLICIT_WAIT) | ||
driver_.set_page_load_timeout(4) | ||
yield driver_ | ||
driver_.quit() | ||
|
||
|
||
@pytest.fixture | ||
def screen(nicegui_reset_globals, # pylint: disable=unused-argument | ||
nicegui_remove_all_screenshots, # pylint: disable=unused-argument | ||
nicegui_driver: webdriver.Chrome, | ||
request: pytest.FixtureRequest, | ||
caplog: pytest.LogCaptureFixture, | ||
) -> Generator[Screen, None, None]: | ||
"""Create a new SeleniumScreen fixture.""" | ||
prepare_simulation(request) | ||
screen_ = Screen(nicegui_driver, caplog) | ||
yield screen_ | ||
logs = screen_.caplog.get_records('call') | ||
if screen_.is_open: | ||
screen_.shot(request.node.name) | ||
screen_.stop_server() | ||
if DOWNLOAD_DIR.exists(): | ||
shutil.rmtree(DOWNLOAD_DIR) | ||
if logs: | ||
pytest.fail('There were unexpected logs. See "Captured log call" below.', pytrace=False) | ||
|
||
|
||
@pytest.fixture | ||
async def user(nicegui_reset_globals, # pylint: disable=unused-argument | ||
prepare_simulated_auto_index_client, # pylint: disable=unused-argument | ||
request: pytest.FixtureRequest, | ||
) -> AsyncGenerator[User, None]: | ||
"""Create a new user fixture.""" | ||
prepare_simulation(request) | ||
async with core.app.router.lifespan_context(core.app): | ||
async with httpx.AsyncClient(app=core.app, base_url='http://test') as client: | ||
yield User(client) | ||
ui.navigate = Navigate() | ||
ui.notify = notify | ||
|
||
|
||
@pytest.fixture | ||
async def create_user(nicegui_reset_globals, # pylint: disable=unused-argument | ||
prepare_simulated_auto_index_client, # pylint: disable=unused-argument | ||
request: pytest.FixtureRequest, | ||
) -> AsyncGenerator[Callable[[], User], None]: | ||
"""Create a fixture for building new users.""" | ||
prepare_simulation(request) | ||
async with core.app.router.lifespan_context(core.app): | ||
yield lambda: User(httpx.AsyncClient(app=core.app, base_url='http://test')) | ||
ui.navigate = Navigate() | ||
ui.notify = notify | ||
|
||
|
||
@pytest.fixture() | ||
def prepare_simulated_auto_index_client(request): | ||
"""Prepare the simulated auto index client.""" | ||
original_test = request.node._obj # pylint: disable=protected-access | ||
if asyncio.iscoroutinefunction(original_test): | ||
async def wrapped_test(*args, **kwargs): | ||
with Client.auto_index_client: | ||
return await original_test(*args, **kwargs) | ||
request.node._obj = wrapped_test # pylint: disable=protected-access | ||
else: | ||
def wrapped_test(*args, **kwargs): | ||
Client.auto_index_client.__enter__() # pylint: disable=unnecessary-dunder-call | ||
return original_test(*args, **kwargs) | ||
request.node._obj = wrapped_test # pylint: disable=protected-access | ||
|
||
|
||
def prepare_simulation(request: pytest.FixtureRequest) -> None: | ||
"""Prepare a simulation to be started. | ||
By using the "module_under_test" marker you can specify the main entry point of the app. | ||
""" | ||
marker = request.node.get_closest_marker('module_under_test') | ||
if marker is not None: | ||
with Client.auto_index_client: | ||
importlib.reload(marker.args[0]) | ||
|
||
core.app.config.add_run_config( | ||
reload=False, | ||
title='Test App', | ||
viewport='', | ||
favicon=None, | ||
dark=False, | ||
language='en-US', | ||
binding_refresh_interval=0.1, | ||
reconnect_timeout=3.0, | ||
tailwind=True, | ||
prod_js=True, | ||
show_welcome_message=False, | ||
) | ||
nicegui.storage.set_storage_secret('simulated secret') | ||
# pylint: disable=unused-import | ||
from .general_fixtures import nicegui_reset_globals, pytest_configure # noqa: F401 | ||
from .screen_plugin import nicegui_chrome_options, nicegui_driver, nicegui_remove_all_screenshots, screen # noqa: F401 | ||
from .user_plugin import create_user, prepare_simulated_auto_index_client, user # noqa: F401 |
This file contains 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,84 @@ | ||
import os | ||
import shutil | ||
from pathlib import Path | ||
from typing import Dict, Generator | ||
|
||
import pytest | ||
from selenium import webdriver | ||
from selenium.webdriver.chrome.service import Service | ||
|
||
from .general_fixtures import ( # noqa: F401 # pylint: disable=unused-import | ||
nicegui_reset_globals, | ||
prepare_simulation, | ||
pytest_configure, | ||
) | ||
from .screen import Screen | ||
|
||
# pylint: disable=redefined-outer-name | ||
|
||
DOWNLOAD_DIR = Path(__file__).parent / 'download' | ||
|
||
|
||
@pytest.fixture | ||
def nicegui_chrome_options(chrome_options: webdriver.ChromeOptions) -> webdriver.ChromeOptions: | ||
"""Configure the Chrome options for the NiceGUI tests.""" | ||
chrome_options.add_argument('disable-dev-shm-usage') | ||
chrome_options.add_argument('no-sandbox') | ||
chrome_options.add_argument('headless') | ||
chrome_options.add_argument('disable-gpu' if 'GITHUB_ACTIONS' in os.environ else '--use-gl=angle') | ||
chrome_options.add_argument('window-size=600x600') | ||
chrome_options.add_experimental_option('prefs', { | ||
'download.default_directory': str(DOWNLOAD_DIR), | ||
'download.prompt_for_download': False, # To auto download the file | ||
'download.directory_upgrade': True, | ||
}) | ||
if 'CHROME_BINARY_LOCATION' in os.environ: | ||
chrome_options.binary_location = os.environ['CHROME_BINARY_LOCATION'] | ||
return chrome_options | ||
|
||
|
||
@pytest.fixture | ||
def capabilities(capabilities: Dict) -> Dict: | ||
"""Configure the Chrome driver capabilities.""" | ||
capabilities['goog:loggingPrefs'] = {'browser': 'ALL'} | ||
return capabilities | ||
|
||
|
||
@pytest.fixture(scope='session') | ||
def nicegui_remove_all_screenshots() -> None: | ||
"""Remove all screenshots from the screenshot directory before the test session.""" | ||
if os.path.exists(Screen.SCREENSHOT_DIR): | ||
for name in os.listdir(Screen.SCREENSHOT_DIR): | ||
os.remove(os.path.join(Screen.SCREENSHOT_DIR, name)) | ||
|
||
|
||
@pytest.fixture() | ||
def nicegui_driver(nicegui_chrome_options: webdriver.ChromeOptions) -> Generator[webdriver.Chrome, None, None]: | ||
"""Create a new Chrome driver instance.""" | ||
s = Service() | ||
driver_ = webdriver.Chrome(service=s, options=nicegui_chrome_options) | ||
driver_.implicitly_wait(Screen.IMPLICIT_WAIT) | ||
driver_.set_page_load_timeout(4) | ||
yield driver_ | ||
driver_.quit() | ||
|
||
|
||
@pytest.fixture | ||
def screen(nicegui_reset_globals, # noqa: F811, pylint: disable=unused-argument | ||
nicegui_remove_all_screenshots, # pylint: disable=unused-argument | ||
nicegui_driver: webdriver.Chrome, | ||
request: pytest.FixtureRequest, | ||
caplog: pytest.LogCaptureFixture, | ||
) -> Generator[Screen, None, None]: | ||
"""Create a new SeleniumScreen fixture.""" | ||
prepare_simulation(request) | ||
screen_ = Screen(nicegui_driver, caplog) | ||
yield screen_ | ||
logs = screen_.caplog.get_records('call') | ||
if screen_.is_open: | ||
screen_.shot(request.node.name) | ||
screen_.stop_server() | ||
if DOWNLOAD_DIR.exists(): | ||
shutil.rmtree(DOWNLOAD_DIR) | ||
if logs: | ||
pytest.fail('There were unexpected logs. See "Captured log call" below.', pytrace=False) |
Oops, something went wrong.