diff --git a/.changeset/social-dancers-roll.md b/.changeset/social-dancers-roll.md new file mode 100644 index 0000000..bee76a2 --- /dev/null +++ b/.changeset/social-dancers-roll.md @@ -0,0 +1,5 @@ +--- +"trackio": patch +--- + +feat:Adds a basic UI test to `trackio` diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0090cd7..11c98aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,7 +50,18 @@ jobs: run: | uv pip install --system -e .[dev,tensorboard] uv pip install --system pytest + + - name: Install Playwright + run: | + playwright install - - name: Run tests + - name: Run backend unit tests + run: | + pytest --deselect=tests/ui + + - name: Run ui/ux interaction tests + if: matrix.os == 'ubuntu-latest' run: | - pytest \ No newline at end of file + pytest tests/ui + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e02d171..332c1ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,9 +34,11 @@ repository = "https://github.com/gradio-app/trackio" [project.optional-dependencies] dev = [ - "pytest", "ruff==0.9.3", "pyarrow>=21.0", + "pytest>=8.0.0,<9.0.0", + "playwright>=1.40.0,<2.0.0", + "pytest-playwright>=0.7.0,<1.0.0", ] tensorboard = [ "tbparse==0.0.9", diff --git a/tests/test_utils.py b/tests/test_utils.py index d8d5238..c36cd4f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -83,7 +83,7 @@ def test_format_timestamp(): @pytest.mark.parametrize( - "base_url, project, write_token, expected", + "api_url, project, write_token, expected", [ ( "https://example.com", @@ -107,8 +107,8 @@ def test_format_timestamp(): ), ], ) -def test_get_full_url(base_url, project, write_token, expected): - result = utils.get_full_url(base_url, project, write_token) +def test_get_full_url(api_url, project, write_token, expected): + result = utils.get_full_url(api_url, project, write_token) assert result == expected diff --git a/tests/ui/test_ui_display.py b/tests/ui/test_ui_display.py new file mode 100644 index 0000000..57041df --- /dev/null +++ b/tests/ui/test_ui_display.py @@ -0,0 +1,39 @@ +from playwright.sync_api import expect, sync_playwright + +import trackio + + +def test_that_runs_are_displayed(temp_dir): + trackio.init(project="test_project", name="test_run") + trackio.log(metrics={"loss": 0.1}) + trackio.log(metrics={"loss": 0.2, "acc": 0.9}) + trackio.finish() + + app, url, _, _ = trackio.show(block_thread=False, open_browser=False) + + try: + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + page.set_default_timeout(1000) + page.goto(url) + + # The project name and run name should be displayed + locator = page.get_by_label("Project") + expect(locator).to_be_visible() + locator = page.get_by_text("test_run") + expect(locator).to_be_visible() + + # Initially, two line plots should be displayed + locator = page.locator(".vega-embed") + expect(locator).to_have_count(2) + + # But if we uncheck the run, the line plots should be hidden + page.get_by_label("test_run").uncheck() + locator = page.locator(".vega-embed") + expect(locator).to_have_count(0) + + browser.close() + finally: + trackio.delete_project("test_project", force=True) + app.close() diff --git a/trackio/__init__.py b/trackio/__init__.py index e615a51..0f609f0 100644 --- a/trackio/__init__.py +++ b/trackio/__init__.py @@ -10,6 +10,7 @@ from gradio.blocks import BUILT_IN_THEMES from gradio.themes import Default as DefaultTheme from gradio.themes import ThemeClass +from gradio.utils import TupleNoPrint from gradio_client import Client from huggingface_hub import SpaceStorage @@ -41,6 +42,7 @@ "log", "finish", "show", + "delete_project", "import_csv", "import_tf_events", "Image", @@ -261,10 +263,57 @@ def finish(): run.finish() +def delete_project(project: str, force: bool = False) -> bool: + """ + Deletes a project by removing its local SQLite database. + + Args: + project (`str`): + The name of the project to delete. + force (`bool`, *optional*, defaults to `False`): + If `True`, deletes the project without prompting for confirmation. + If `False`, prompts the user to confirm before deleting. + + Returns: + `bool`: `True` if the project was deleted, `False` otherwise. + """ + db_path = SQLiteStorage.get_project_db_path(project) + + if not db_path.exists(): + print(f"* Project '{project}' does not exist.") + return False + + if not force: + response = input( + f"Are you sure you want to delete project '{project}'? " + f"This will permanently delete all runs and metrics. (y/N): " + ) + if response.lower() not in ["y", "yes"]: + print("* Deletion cancelled.") + return False + + try: + db_path.unlink() + + for suffix in ("-wal", "-shm"): + sidecar = Path(str(db_path) + suffix) + if sidecar.exists(): + sidecar.unlink() + + print(f"* Project '{project}' has been deleted.") + return True + except Exception as e: + print(f"* Error deleting project '{project}': {e}") + return False + + def show( project: str | None = None, theme: str | ThemeClass | None = None, mcp_server: bool | None = None, + *, + open_browser: bool = True, + block_thread: bool | None = None, ): """ Launches the Trackio dashboard. @@ -284,6 +333,19 @@ def show( functions will be added as MCP tools. If `None` (default behavior), then the `GRADIO_MCP_SERVER` environment variable will be used to determine if the MCP server should be enabled (which is `"True"` on Hugging Face Spaces). + open_browser (`bool`, *optional*, defaults to `True`): + If `True` and not in a notebook, a new browser tab will be opened with the dashboard. + If `False`, the browser will not be opened. + block_thread (`bool`, *optional*): + If `True`, the main thread will be blocked until the dashboard is closed. + If `None` (default behavior), then the main thread will not be blocked if the + dashboard is launched in a notebook, otherwise the main thread will be blocked. + + Returns: + `app`: The Gradio app object corresponding to the dashboard launched by Trackio. + `url`: The local URL of the dashboard. + `share_url`: The public share URL of the dashboard. + `full_url`: The full URL of the dashboard including the write token (will use the public share URL if launched publicly, otherwise the local URL). """ theme = theme or os.environ.get("TRACKIO_THEME", DEFAULT_THEME) @@ -316,7 +378,7 @@ def show( else os.environ.get("GRADIO_MCP_SERVER", "False") == "True" ) - _, url, share_url = demo.launch( + app, url, share_url = demo.launch( show_api=_mcp_server, quiet=True, inline=False, @@ -333,7 +395,13 @@ def show( if not utils.is_in_notebook(): print(f"* Trackio UI launched at: {full_url}") - webbrowser.open(full_url) - utils.block_main_thread_until_keyboard_interrupt() + if open_browser: + webbrowser.open(full_url) + block_thread = block_thread if block_thread is not None else True else: utils.embed_url_in_notebook(full_url) + block_thread = block_thread if block_thread is not None else False + + if block_thread: + utils.block_main_thread_until_keyboard_interrupt() + return TupleNoPrint((demo, url, share_url, full_url))