From 1f7765020c4b7e7b8bb21f027158e997282928b5 Mon Sep 17 00:00:00 2001 From: Karan Date: Tue, 16 Jan 2024 12:59:54 -0800 Subject: [PATCH] Truncate the requirements.txt file before deploys (#998) Co-authored-by: Barret Schloerke --- .../apps/plotly_app/app_requirements.txt | 1 + .../app_requirements.txt | 1 + .../app_requirements.txt | 1 + .../shiny-express-folium/app_requirements.txt | 1 + .../app_requirements.txt | 1 + .../app_requirements.txt | 1 + .../app_requirements.txt | 1 + .../app_requirements.txt | 1 + tests/playwright/examples/example_apps.py | 234 +++++++++++++++++ .../playwright/examples/test_api_examples.py | 10 + .../examples/test_deploy_examples.py | 9 + tests/playwright/examples/test_examples.py | 237 +----------------- .../playwright/examples/test_shiny_create.py | 75 ++++++ tests/playwright/utils/deploy_utils.py | 2 +- tests/pytest/test_shiny_create.py | 52 ---- 15 files changed, 341 insertions(+), 286 deletions(-) create mode 100644 tests/playwright/examples/example_apps.py create mode 100644 tests/playwright/examples/test_api_examples.py create mode 100644 tests/playwright/examples/test_deploy_examples.py create mode 100644 tests/playwright/examples/test_shiny_create.py delete mode 100644 tests/pytest/test_shiny_create.py diff --git a/tests/playwright/deploys/apps/plotly_app/app_requirements.txt b/tests/playwright/deploys/apps/plotly_app/app_requirements.txt index 5ad561067..5238309ef 100644 --- a/tests/playwright/deploys/apps/plotly_app/app_requirements.txt +++ b/tests/playwright/deploys/apps/plotly_app/app_requirements.txt @@ -2,3 +2,4 @@ pandas plotly git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools git+https://github.com/posit-dev/py-shinywidgets.git#egg=shinywidgets +fastapi==0.108.0 diff --git a/tests/playwright/deploys/apps/shiny-express-accordion/app_requirements.txt b/tests/playwright/deploys/apps/shiny-express-accordion/app_requirements.txt index 87a253e09..b20ad55dd 100644 --- a/tests/playwright/deploys/apps/shiny-express-accordion/app_requirements.txt +++ b/tests/playwright/deploys/apps/shiny-express-accordion/app_requirements.txt @@ -1 +1,2 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools +fastapi==0.108.0 diff --git a/tests/playwright/deploys/apps/shiny-express-dataframe/app_requirements.txt b/tests/playwright/deploys/apps/shiny-express-dataframe/app_requirements.txt index 720b713c0..2399add11 100644 --- a/tests/playwright/deploys/apps/shiny-express-dataframe/app_requirements.txt +++ b/tests/playwright/deploys/apps/shiny-express-dataframe/app_requirements.txt @@ -1,2 +1,3 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools pandas +fastapi==0.108.0 diff --git a/tests/playwright/deploys/apps/shiny-express-folium/app_requirements.txt b/tests/playwright/deploys/apps/shiny-express-folium/app_requirements.txt index a64e03b44..52e2dfa93 100644 --- a/tests/playwright/deploys/apps/shiny-express-folium/app_requirements.txt +++ b/tests/playwright/deploys/apps/shiny-express-folium/app_requirements.txt @@ -1,2 +1,3 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools folium +fastapi==0.108.0 diff --git a/tests/playwright/deploys/apps/shiny-express-page-default/app_requirements.txt b/tests/playwright/deploys/apps/shiny-express-page-default/app_requirements.txt index 87a253e09..b20ad55dd 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-default/app_requirements.txt +++ b/tests/playwright/deploys/apps/shiny-express-page-default/app_requirements.txt @@ -1 +1,2 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools +fastapi==0.108.0 diff --git a/tests/playwright/deploys/apps/shiny-express-page-fillable/app_requirements.txt b/tests/playwright/deploys/apps/shiny-express-page-fillable/app_requirements.txt index 87a253e09..b20ad55dd 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fillable/app_requirements.txt +++ b/tests/playwright/deploys/apps/shiny-express-page-fillable/app_requirements.txt @@ -1 +1,2 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools +fastapi==0.108.0 diff --git a/tests/playwright/deploys/apps/shiny-express-page-fluid/app_requirements.txt b/tests/playwright/deploys/apps/shiny-express-page-fluid/app_requirements.txt index 87a253e09..b20ad55dd 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fluid/app_requirements.txt +++ b/tests/playwright/deploys/apps/shiny-express-page-fluid/app_requirements.txt @@ -1 +1,2 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools +fastapi==0.108.0 diff --git a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app_requirements.txt b/tests/playwright/deploys/apps/shiny-express-page-sidebar/app_requirements.txt index 87a253e09..b20ad55dd 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app_requirements.txt +++ b/tests/playwright/deploys/apps/shiny-express-page-sidebar/app_requirements.txt @@ -1 +1,2 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools +fastapi==0.108.0 diff --git a/tests/playwright/examples/example_apps.py b/tests/playwright/examples/example_apps.py new file mode 100644 index 000000000..6fff36ad4 --- /dev/null +++ b/tests/playwright/examples/example_apps.py @@ -0,0 +1,234 @@ +import os +import sys +import typing +from pathlib import PurePath +from typing import Literal + +from conftest import run_shiny_app +from playwright.sync_api import ConsoleMessage, Page + +here_tests_e2e_examples = PurePath(__file__).parent +pyshiny_root = here_tests_e2e_examples.parent.parent.parent + +is_interactive = hasattr(sys, "ps1") +reruns = 1 if is_interactive else 3 + + +def get_apps(path: str) -> typing.List[str]: + full_path = pyshiny_root / path + app_paths: typing.List[str] = [] + for folder in os.listdir(full_path): + folder_path = os.path.join(full_path, folder) + if os.path.isdir(folder_path): + app_path = os.path.join(folder_path, "app.py") + if os.path.isfile(app_path): + # Return relative app path + app_paths.append(os.path.join(path, folder, "app.py")) + return app_paths + + +example_apps: typing.List[str] = [ + *get_apps("examples"), + *get_apps("shiny/api-examples"), + *get_apps("shiny/templates/app-templates"), + *get_apps("tests/playwright/deploys"), +] + +app_idle_wait = {"duration": 300, "timeout": 5 * 1000} +app_hard_wait: typing.Dict[str, int] = { + "brownian": 250, + "ui-func": 250, +} +output_transformer_errors = [ + "ShinyDeprecationWarning: `shiny.render.transformer.output_transformer()`", + " return OutputRenderer", + # brownian example app + "ShinyDeprecationWarning:", + "shiny.render.transformer.output_transformer()", +] +express_warnings = ["Detected Shiny Express app. "] +app_allow_shiny_errors: typing.Dict[ + str, typing.Union[Literal[True], typing.List[str]] +] = { + "SafeException": True, + "global_pyplot": True, + "static_plots": [ + # acceptable warning + "PlotnineWarning: Smoothing requires 2 or more points", + "RuntimeWarning: divide by zero encountered", + "UserWarning: This figure includes Axes that are not compatible with tight_layout", + ], + # Remove after shinywidgets accepts `Renderer` PR + "airmass": [*output_transformer_errors], + "brownian": [*output_transformer_errors], + "multi-page": [*output_transformer_errors], + "model-score": [*output_transformer_errors], + "data_frame": [*output_transformer_errors], + "output_transformer": [*output_transformer_errors], + "render_display": [*express_warnings], +} +app_allow_external_errors: typing.List[str] = [ + # if shiny express app detected + "Detected Shiny Express app", + # plotnine: https://github.com/has2k1/plotnine/issues/713 + # mizani: https://github.com/has2k1/mizani/issues/34 + # seaborn: https://github.com/mwaskom/seaborn/issues/3457 + "FutureWarning: is_categorical_dtype is deprecated", + "if pd.api.types.is_categorical_dtype(vector", # continutation of line above + # plotnine: https://github.com/has2k1/plotnine/issues/713#issuecomment-1701363058 + "FutureWarning: The default of observed=False is deprecated", + # seaborn: https://github.com/mwaskom/seaborn/pull/3355 + "FutureWarning: use_inf_as_na option is deprecated", + "pd.option_context('mode.use_inf_as_na", # continutation of line above +] +app_allow_js_errors: typing.Dict[str, typing.List[str]] = { + "brownian": ["Failed to acquire camera feed:"], +} + + +# Altered from `shinytest2:::app_wait_for_idle()` +# https://github.com/rstudio/shinytest2/blob/b8fdce681597e9610fc078aa6e376134c404f3bd/R/app-driver-wait.R +def wait_for_idle_js(duration: int = 500, timeout: int = 30 * 1000) -> str: + return """ + let duration = %s; // time needed to be idle + let timeout = %s; // max total time + console.log("Making promise to run"); + new Promise((resolve, reject) => { + console.log('Waiting for Shiny to be stable'); + const cleanup = () => { + $(document).off('shiny:busy', busyFn); + $(document).off('shiny:idle', idleFn); + clearTimeout(timeoutId); + clearTimeout(idleId); + } + let timeoutId = setTimeout(() => { + cleanup(); + reject('Shiny did not become stable within ' + timeout + 'ms'); + }, +timeout); // make sure timeout is number + let idleId = null; + const busyFn = () => { + // clear timeout. Calling with `null` is ok. + clearTimeout(idleId); + }; + const idleFn = () => { + const fn = () => { + // Made it through the required duration + // Remove event listeners + cleanup(); + console.log('Shiny has been idle for ' + duration + 'ms'); + // Resolve the promise + resolve(); + }; + // delay the callback wrapper function + idleId = setTimeout(fn, +duration); + }; + // set up individual listeners for this function. + $(document).on('shiny:busy', busyFn); + $(document).on('shiny:idle', idleFn); + // if already idle, call `idleFn` to kick things off. + if (! $("html").hasClass("shiny-busy")) { + idleFn(); + } + }) + """ % ( + duration, + timeout, + ) + + +def wait_for_idle_app( + page: Page, duration: int = 500, timeout: int = 30 * 1000 +) -> None: + page.evaluate( + wait_for_idle_js(duration, timeout), + ) + + +def validate_example(page: Page, ex_app_path: str) -> None: + app = run_shiny_app(pyshiny_root / ex_app_path, wait_for_start=True) + + console_errors: typing.List[str] = [] + + def on_console_msg(msg: ConsoleMessage) -> None: + if msg.type == "error": + console_errors.append(msg.text) + + page.on("console", on_console_msg) + + # Makes sure the app is closed when exiting the code block + with app: + page.goto(app.url) + + app_name = os.path.basename(os.path.dirname(ex_app_path)) + + if app_name in app_hard_wait.keys(): + # Apps are constantly invalidating and will not stabilize + # Instead, wait for specific amount of time + page.wait_for_timeout(app_hard_wait[app_name]) + else: + # Wait for app to stop updating + wait_for_idle_app( + page, + duration=app_idle_wait["duration"], + timeout=app_idle_wait["timeout"], + ) + + # Check for py-shiny errors + error_lines = str(app.stderr).splitlines() + + # Remove any errors that are allowed + error_lines = [ + line + for line in error_lines + if not any([error_txt in line for error_txt in app_allow_external_errors]) + ] + + # Remove any app specific errors that are allowed + if app_name in app_allow_shiny_errors: + app_allowable_errors = app_allow_shiny_errors[app_name] + else: + app_allowable_errors = [] + + # If all errors are not allowed, check for unexpected errors + if app_allowable_errors is not True: + if isinstance(app_allowable_errors, str): + app_allowable_errors = [app_allowable_errors] + app_allowable_errors = ( + # Remove ^INFO lines + ["INFO:"] + # Remove any known errors caused by external packages + + app_allow_external_errors + # Remove any known errors allowed by the app + + app_allowable_errors + ) + + # If there is an array of allowable errors, remove them from errors. Ex: `PlotnineWarning` + error_lines = [ + line + for line in error_lines + if len(line.strip()) > 0 + and not any([error_txt in line for error_txt in app_allowable_errors]) + ] + if len(error_lines) > 0: + print("\napp_allowable_errors :") + print("\n".join(app_allowable_errors)) + print("\nError lines remaining:") + print("\n".join(error_lines)) + assert len(error_lines) == 0 + + # Check for JavaScript errors + if app_name in app_allow_js_errors: + # Remove any errors that are allowed + console_errors = [ + line + for line in console_errors + if not any( + [error_txt in line for error_txt in app_allow_js_errors[app_name]] + ) + ] + assert len(console_errors) == 0, ( + "In app " + + ex_app_path + + " had JavaScript console errors!\n" + + "* ".join(console_errors) + ) diff --git a/tests/playwright/examples/test_api_examples.py b/tests/playwright/examples/test_api_examples.py new file mode 100644 index 000000000..2ba9fac06 --- /dev/null +++ b/tests/playwright/examples/test_api_examples.py @@ -0,0 +1,10 @@ +import pytest +from example_apps import get_apps, reruns, validate_example +from playwright.sync_api import Page + + +@pytest.mark.examples +@pytest.mark.flaky(reruns=reruns, reruns_delay=1) +@pytest.mark.parametrize("ex_app_path", get_apps("shiny/api-examples")) +def test_api_examples(page: Page, ex_app_path: str) -> None: + validate_example(page, ex_app_path) diff --git a/tests/playwright/examples/test_deploy_examples.py b/tests/playwright/examples/test_deploy_examples.py new file mode 100644 index 000000000..767ad5b98 --- /dev/null +++ b/tests/playwright/examples/test_deploy_examples.py @@ -0,0 +1,9 @@ +import pytest +from example_apps import get_apps, reruns, validate_example +from playwright.sync_api import Page + + +@pytest.mark.flaky(reruns=reruns, reruns_delay=1) +@pytest.mark.parametrize("ex_app_path", get_apps("tests/playwright/deploys")) +def test_deploy_examples(page: Page, ex_app_path: str) -> None: + validate_example(page, ex_app_path) diff --git a/tests/playwright/examples/test_examples.py b/tests/playwright/examples/test_examples.py index e4178ed8e..4c3942d36 100644 --- a/tests/playwright/examples/test_examples.py +++ b/tests/playwright/examples/test_examples.py @@ -1,238 +1,9 @@ -import os -import sys -import typing -from pathlib import PurePath -from typing import Literal - import pytest -from conftest import run_shiny_app -from playwright.sync_api import ConsoleMessage, Page - -here_tests_e2e_examples = PurePath(__file__).parent -pyshiny_root = here_tests_e2e_examples.parent.parent.parent - -is_interactive = hasattr(sys, "ps1") -reruns = 1 if is_interactive else 3 - - -def get_apps(path: str) -> typing.List[str]: - full_path = pyshiny_root / path - app_paths: typing.List[str] = [] - for folder in os.listdir(full_path): - folder_path = os.path.join(full_path, folder) - if os.path.isdir(folder_path): - app_path = os.path.join(folder_path, "app.py") - if os.path.isfile(app_path): - # Return relative app path - app_paths.append(os.path.join(path, folder, "app.py")) - return app_paths - - -example_apps: typing.List[str] = [ - *get_apps("examples"), - *get_apps("shiny/api-examples"), - *get_apps("shiny/templates/app-templates"), -] - -app_idle_wait = {"duration": 300, "timeout": 5 * 1000} -app_hard_wait: typing.Dict[str, int] = { - "brownian": 250, - "ui-func": 250, -} -output_transformer_errors = [ - "ShinyDeprecationWarning: `shiny.render.transformer.output_transformer()`", - " return OutputRenderer", - # brownian example app - "ShinyDeprecationWarning:", - "shiny.render.transformer.output_transformer()", -] -express_warnings = ["Detected Shiny Express app. "] -app_allow_shiny_errors: typing.Dict[ - str, typing.Union[Literal[True], typing.List[str]] -] = { - "SafeException": True, - "global_pyplot": True, - "static_plots": [ - # acceptable warning - "PlotnineWarning: Smoothing requires 2 or more points", - "RuntimeWarning: divide by zero encountered", - "UserWarning: This figure includes Axes that are not compatible with tight_layout", - ], - # Remove after shinywidgets accepts `Renderer` PR - "airmass": [*output_transformer_errors], - "brownian": [*output_transformer_errors], - "multi-page": [*output_transformer_errors], - "model-score": [*output_transformer_errors], - "data_frame": [*output_transformer_errors], - "output_transformer": [*output_transformer_errors], - "render_display": [*express_warnings], -} -app_allow_external_errors: typing.List[str] = [ - # if shiny express app detected - "Detected Shiny Express app", - # plotnine: https://github.com/has2k1/plotnine/issues/713 - # mizani: https://github.com/has2k1/mizani/issues/34 - # seaborn: https://github.com/mwaskom/seaborn/issues/3457 - "FutureWarning: is_categorical_dtype is deprecated", - "if pd.api.types.is_categorical_dtype(vector", # continutation of line above - # plotnine: https://github.com/has2k1/plotnine/issues/713#issuecomment-1701363058 - "FutureWarning: The default of observed=False is deprecated", - # seaborn: https://github.com/mwaskom/seaborn/pull/3355 - "FutureWarning: use_inf_as_na option is deprecated", - "pd.option_context('mode.use_inf_as_na", # continutation of line above -] -app_allow_js_errors: typing.Dict[str, typing.List[str]] = { - "brownian": ["Failed to acquire camera feed:"], -} - - -# Altered from `shinytest2:::app_wait_for_idle()` -# https://github.com/rstudio/shinytest2/blob/b8fdce681597e9610fc078aa6e376134c404f3bd/R/app-driver-wait.R -def wait_for_idle_js(duration: int = 500, timeout: int = 30 * 1000) -> str: - return """ - let duration = %s; // time needed to be idle - let timeout = %s; // max total time - console.log("Making promise to run"); - new Promise((resolve, reject) => { - console.log('Waiting for Shiny to be stable'); - const cleanup = () => { - $(document).off('shiny:busy', busyFn); - $(document).off('shiny:idle', idleFn); - clearTimeout(timeoutId); - clearTimeout(idleId); - } - let timeoutId = setTimeout(() => { - cleanup(); - reject('Shiny did not become stable within ' + timeout + 'ms'); - }, +timeout); // make sure timeout is number - let idleId = null; - const busyFn = () => { - // clear timeout. Calling with `null` is ok. - clearTimeout(idleId); - }; - const idleFn = () => { - const fn = () => { - // Made it through the required duration - // Remove event listeners - cleanup(); - console.log('Shiny has been idle for ' + duration + 'ms'); - // Resolve the promise - resolve(); - }; - // delay the callback wrapper function - idleId = setTimeout(fn, +duration); - }; - // set up individual listeners for this function. - $(document).on('shiny:busy', busyFn); - $(document).on('shiny:idle', idleFn); - // if already idle, call `idleFn` to kick things off. - if (! $("html").hasClass("shiny-busy")) { - idleFn(); - } - }) - """ % ( - duration, - timeout, - ) - - -def wait_for_idle_app( - page: Page, duration: int = 500, timeout: int = 30 * 1000 -) -> None: - page.evaluate( - wait_for_idle_js(duration, timeout), - ) +from example_apps import get_apps, reruns, validate_example +from playwright.sync_api import Page -# Run this test for each example app -@pytest.mark.examples -@pytest.mark.parametrize("ex_app_path", example_apps) @pytest.mark.flaky(reruns=reruns, reruns_delay=1) +@pytest.mark.parametrize("ex_app_path", get_apps("examples")) def test_examples(page: Page, ex_app_path: str) -> None: - app = run_shiny_app(pyshiny_root / ex_app_path, wait_for_start=True) - - console_errors: typing.List[str] = [] - - def on_console_msg(msg: ConsoleMessage) -> None: - if msg.type == "error": - console_errors.append(msg.text) - - page.on("console", on_console_msg) - - # Makes sure the app is closed when exiting the code block - with app: - page.goto(app.url) - - app_name = os.path.basename(os.path.dirname(ex_app_path)) - - if app_name in app_hard_wait.keys(): - # Apps are constantly invalidating and will not stabilize - # Instead, wait for specific amount of time - page.wait_for_timeout(app_hard_wait[app_name]) - else: - # Wait for app to stop updating - wait_for_idle_app( - page, - duration=app_idle_wait["duration"], - timeout=app_idle_wait["timeout"], - ) - - # Check for py-shiny errors - error_lines = str(app.stderr).splitlines() - - # Remove any errors that are allowed - error_lines = [ - line - for line in error_lines - if not any([error_txt in line for error_txt in app_allow_external_errors]) - ] - - # Remove any app specific errors that are allowed - if app_name in app_allow_shiny_errors: - app_allowable_errors = app_allow_shiny_errors[app_name] - else: - app_allowable_errors = [] - - # If all errors are not allowed, check for unexpected errors - if app_allowable_errors is not True: - if isinstance(app_allowable_errors, str): - app_allowable_errors = [app_allowable_errors] - app_allowable_errors = ( - # Remove ^INFO lines - ["INFO:"] - # Remove any known errors caused by external packages - + app_allow_external_errors - # Remove any known errors allowed by the app - + app_allowable_errors - ) - - # If there is an array of allowable errors, remove them from errors. Ex: `PlotnineWarning` - error_lines = [ - line - for line in error_lines - if len(line.strip()) > 0 - and not any([error_txt in line for error_txt in app_allowable_errors]) - ] - if len(error_lines) > 0: - print("\napp_allowable_errors :") - print("\n".join(app_allowable_errors)) - print("\nError lines remaining:") - print("\n".join(error_lines)) - assert len(error_lines) == 0 - - # Check for JavaScript errors - if app_name in app_allow_js_errors: - # Remove any errors that are allowed - console_errors = [ - line - for line in console_errors - if not any( - [error_txt in line for error_txt in app_allow_js_errors[app_name]] - ) - ] - assert len(console_errors) == 0, ( - "In app " - + ex_app_path - + " had JavaScript console errors!\n" - + "* ".join(console_errors) - ) + validate_example(page, ex_app_path) diff --git a/tests/playwright/examples/test_shiny_create.py b/tests/playwright/examples/test_shiny_create.py new file mode 100644 index 000000000..550cec0af --- /dev/null +++ b/tests/playwright/examples/test_shiny_create.py @@ -0,0 +1,75 @@ +import os +import subprocess +import tempfile + +import pytest +from example_apps import get_apps, reruns, validate_example +from playwright.sync_api import Page + + +def subprocess_create( + app_template: str, + dest_dir: str = "", + *, + mode: str = "core", + package_name: str = "", +): + cmd = [ + "shiny", + "create", + "--template", + app_template, + "--mode", + mode, + "--dir", + dest_dir, + "--package-name", + package_name, + ] + + subprocess.run(cmd) + assert os.path.isdir(dest_dir) + + # Package templates don't have a top-level app.py file + if package_name == "": + assert os.path.isfile(f"{dest_dir}/app.py") + else: + assert os.path.isfile(f"{dest_dir}/pyproject.toml") + + print("\nCheck for duplicate files") + with pytest.raises(subprocess.CalledProcessError): + subprocess.run(cmd, check=True) + + +@pytest.mark.examples +@pytest.mark.flaky(reruns=reruns, reruns_delay=1) +@pytest.mark.parametrize("ex_app_path", get_apps("shiny/templates/app-templates")) +def test_template_examples(page: Page, ex_app_path: str) -> None: + validate_example(page, ex_app_path) + + +@pytest.mark.examples +@pytest.mark.flaky(reruns=reruns, reruns_delay=1) +@pytest.mark.parametrize("app_template", ["basic-app", "dashboard", "multi-page"]) +def test_create_core(app_template: str, page: Page): + with tempfile.TemporaryDirectory("example_apps") as tmpdir: + subprocess_create(app_template, dest_dir=tmpdir) + validate_example(page, f"{tmpdir}/app.py") + + +@pytest.mark.examples +@pytest.mark.flaky(reruns=reruns, reruns_delay=1) +@pytest.mark.parametrize("app_template", ["basic-app"]) +def test_create_express(app_template: str, page: Page): + with tempfile.TemporaryDirectory("example_apps") as tmpdir: + subprocess_create(app_template, dest_dir=tmpdir, mode="express") + validate_example(page, f"{tmpdir}/app.py") + + +@pytest.mark.examples +@pytest.mark.flaky(reruns=reruns, reruns_delay=1) +@pytest.mark.parametrize("app_template", ["js-input", "js-output", "js-react"]) +def test_create_js(app_template: str): + with tempfile.TemporaryDirectory("example_apps") as tmpdir: + subprocess_create(app_template, dest_dir=tmpdir, package_name="my_component") + # TODO-Karan: Add test to validate once flag to install packages is implemented diff --git a/tests/playwright/utils/deploy_utils.py b/tests/playwright/utils/deploy_utils.py index f416757ae..423c6bc57 100644 --- a/tests/playwright/utils/deploy_utils.py +++ b/tests/playwright/utils/deploy_utils.py @@ -110,6 +110,6 @@ def write_requirements_txt(app_file_path: str) -> None: git_hash = git_cmd.stdout.decode("utf-8").strip() with open(app_requirements_file_path) as f: requirements = f.read() - with open(requirements_file_path, "a") as f: + with open(requirements_file_path, "w") as f: f.write(f"{requirements}\n") f.write(f"git+https://github.com/posit-dev/py-shiny.git@{git_hash}\n") diff --git a/tests/pytest/test_shiny_create.py b/tests/pytest/test_shiny_create.py deleted file mode 100644 index a777f5022..000000000 --- a/tests/pytest/test_shiny_create.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import shutil -import subprocess -import tempfile - -import pytest - - -def subprocess_create(app_template: str, mode: str = "core", package_name: str = ""): - dest_dir = tempfile.mkdtemp() - cmd = [ - "shiny", - "create", - "--template", - app_template, - "--mode", - mode, - "--dir", - dest_dir, - "--package-name", - package_name, - ] - - subprocess.run(cmd) - assert os.path.isdir(dest_dir) - - # Package templates don't have a top-level app.py file - if package_name == "": - assert os.path.isfile(f"{dest_dir}/app.py") - else: - assert os.path.isfile(f"{dest_dir}/pyproject.toml") - - with pytest.raises(subprocess.CalledProcessError): - subprocess.run(cmd, check=True) - - shutil.rmtree(dest_dir) - - -# TODO-karan; Integrate all tests below with _examples_ testing and check for JS errors / output errors. -@pytest.mark.parametrize("app_template", ["basic-app", "dashboard", "multi-page"]) -def test_create_core(app_template: str): - subprocess_create(app_template) - - -@pytest.mark.parametrize("app_template", ["basic-app"]) -def test_create_express(app_template: str): - subprocess_create(app_template, "express") - - -@pytest.mark.parametrize("app_template", ["js-input", "js-output", "js-react"]) -def test_create_js(app_template: str): - subprocess_create("js-input", package_name="my_component")