diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..712b117 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,75 @@ +name: Test Workflow + +on: + push: + branches: + - master + pull_request: + +jobs: + unit-tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python: ["3.10"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Install dependencies + run: | + uv pip install ".[test]" --system + + - name: Run unit tests + run: | + pytest tests/unit + + integration-tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python: ["3.10"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Install dependencies + run: | + uv pip install ".[test]" --system + + - name: Install Playwright + run: | + playwright install + + - name: Run integration tests + run: | + pytest tests/integration --video=retain-on-failure --output=test-results + + - name: upload test artifacts + uses: actions/upload-artifact@v4 + with: + name: test-results-unit-os${{ matrix.os }}-python${{ matrix.python }}-ipywidgets${{ matrix.ipywidgets }} + path: test-results + include-hidden-files: true diff --git a/glue_solara/hooks.py b/glue_solara/hooks.py index 02b79cb..3ccacab 100644 --- a/glue_solara/hooks.py +++ b/glue_solara/hooks.py @@ -40,10 +40,12 @@ def connect(): solara.use_effect(connect, [id(hub)]) -def use_glue_watch_close(app: glue_jupyter.JupyterApplication): +def use_glue_watch_close(app: glue_jupyter.JupyterApplication, on_msg=None): counter, set_counter = solara.use_state(0) def remove_viewer(msg): + if on_msg is not None: + on_msg(msg) viewers = app._viewer_refs for _viewer in viewers: viewer = _viewer() diff --git a/pyproject.toml b/pyproject.toml index 7538763..15925ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,10 @@ dev = [ "mypy", ] +test = [ + "pytest-ipywidgets", +] + [tool.ruff] ignore-init-module-imports = true fix = true diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/basics_test.py b/tests/integration/basics_test.py new file mode 100644 index 0000000..bae49cd --- /dev/null +++ b/tests/integration/basics_test.py @@ -0,0 +1,94 @@ +import playwright.sync_api +from playwright.sync_api import expect + + +def test_add_data_current_viewer( + page_session: playwright.sync_api.Page, solara_server, solara_app, assert_solara_snapshot +): + with solara_app("glue_solara.app"): + page_session.goto(solara_server.base_url) + add_data = page_session.locator("button", has_text="load data") + add_data.wait_for() + add_data.click() + expect(page_session.locator("button")).to_have_count(4) + page_session.locator("button", has_text="add w5 data").click() + page_session.locator("button", has_text="a 2d image").click() + page_session.locator("div.bqplot.figure").wait_for() + add_to_figure = page_session.locator( + "button.v-btn--icon:not(.v-btn--disabled)", has=page_session.locator("i.mdi-tab-plus") + ) + expect(add_to_figure).to_have_count(1) + add_to_figure.click() + # Tried to assert with screenshot, but seems the graph is not consistent + # assert_solara_snapshot(page_session.locator("div.bqplot.figure").screenshot()) + expect(page_session.get_by_role("button", disabled=True)).to_have_count(2) + + +def test_tabbed_viewers(page_session: playwright.sync_api.Page, solara_server, solara_app): + with solara_app("glue_solara.app"): + page_session.goto(solara_server.base_url) + add_data = page_session.locator("button", has_text="load data") + add_data.wait_for() + add_data.click() + page_session.locator("button", has_text="add w5 data").click() + page_session.locator("button", has_text="a 2d image").click() + page_session.locator("div.bqplot.figure").wait_for() + add_figure = page_session.locator("button", has=page_session.locator("i.mdi-tab")).nth(1) + expect(add_figure).to_be_attached() + add_figure.click() + page_session.locator("button", has_text="add").click() + expect(page_session.locator("div.v-tab")).to_have_count(2) + + +def test_tabbed_viewer_close(page_session: playwright.sync_api.Page, solara_server, solara_app): + with solara_app("glue_solara.app"): + page_session.goto(solara_server.base_url) + add_data = page_session.get_by_role("button", name="load data") + add_data.wait_for() + add_data.click() + page_session.locator("button", has_text="add w5 data").click() + page_session.locator("button", has_text="a 2d image").click() + page_session.locator("div.bqplot.figure").wait_for() + close_viewer = page_session.locator("button", has=page_session.locator("i.mdi-close")) + expect(close_viewer).to_have_count(1) + close_viewer.click() + expect(page_session.locator("div.bqplot.figure")).not_to_be_attached() + expect(page_session.get_by_text("What do you want to visualize")).to_be_visible() + + +def test_mdi_viewers(page_session: playwright.sync_api.Page, solara_server, solara_app): + with solara_app("glue_solara.app"): + page_session.goto(solara_server.base_url) + add_data = page_session.locator("button", has_text="load data") + add_data.wait_for() + add_data.click() + page_session.locator("button", has_text="add w5 data").click() + page_session.get_by_role("button", name="mdi").click() + page_session.locator("button", has_text="a 2d image").click() + page_session.locator("div.glue-solara__window").wait_for() + expect(page_session.locator("div.glue-solara__window")).to_have_count(1) + add_figure = page_session.locator("button", has=page_session.locator("i.mdi-tab")).nth(1) + expect(add_figure).to_be_attached() + add_figure.click() + page_session.locator("button", has_text="add").click() + expect(page_session.locator("div.glue-solara__window")).to_have_count(2) + + +def test_mdi_viewers_close(page_session: playwright.sync_api.Page, solara_server, solara_app): + with solara_app("glue_solara.app"): + page_session.goto(solara_server.base_url) + add_data = page_session.get_by_role("button", name="load data") + add_data.wait_for() + add_data.click() + page_session.locator("button", has_text="add w5 data").click() + page_session.get_by_role("button", name="mdi").click() + page_session.locator("button", has_text="a 2d image").click() + page_session.locator("div.glue-solara__window").wait_for() + expect(page_session.locator("div.glue-solara__window")).to_have_count(1) + close_button = page_session.locator( + "button", has=page_session.locator("i.mdi-close-circle-outline") + ) + expect(close_button).to_have_count(1) + close_button.click() + expect(page_session.locator("div.glue-solara__window")).not_to_be_attached() + expect(page_session.get_by_text("What do you want to visualize")).to_be_visible() diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..e72287c --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,9 @@ +from pathlib import Path + +from glue_jupyter.data import require_data + + +def pytest_sessionstart(session): + # Ensure that required example data is available + if not Path("w5.fits").exists(): + require_data("Astronomy/W5/w5.fits") diff --git a/tests/unit/hooks_test.py b/tests/unit/hooks_test.py new file mode 100644 index 0000000..fb1d430 --- /dev/null +++ b/tests/unit/hooks_test.py @@ -0,0 +1,62 @@ +from unittest.mock import MagicMock + +import ipyvuetify as v +import solara +from glue.core.message import Message +from glue_jupyter.app import JupyterApplication + +from glue_solara.hooks import ClosedMessage, use_glue_watch, use_glue_watch_close + + +def test_use_glue_watch(): + glue_app = JupyterApplication() + handler = MagicMock() + + class CustomMessage(Message): + def __init__(self): + super().__init__(MagicMock()) + + @solara.component + def TestComponent(): + use_glue_watch(glue_app.session.hub, CustomMessage, on_msg=handler) + + box, rc = solara.render(TestComponent(), handle_error=False) + assert len(glue_app.session.hub._subscriptions) == 5 + assert handler.call_count == 0 + glue_app.session.hub.broadcast(CustomMessage()) + assert handler.call_count == 1 + box.close() + + +def test_use_glue_watch_close(): + glue_app = JupyterApplication() + handler = MagicMock() + + @solara.component + def TestComponent(): + use_glue_watch_close(glue_app, on_msg=handler) + + def create_viewer(): + # There has to be some data added before we can create a viewer + glue_app.load_data("w5.fits") + glue_app.scatter2d(show=False) + + def close_viewer(): + msg = ClosedMessage(glue_app.viewers[0]) + glue_app.session.hub.broadcast(msg) + + solara.Button("open", on_click=create_viewer) + solara.Button("close", on_click=close_viewer) + + box, rc = solara.render(TestComponent(), handle_error=False) + assert len(glue_app.session.hub._subscriptions) == 5 + assert len(glue_app._viewer_refs) == 0 + assert handler.call_count == 0 + open_button, close_button = rc.find(v.Btn) + open_button.widget.click() + assert len(glue_app._viewer_refs) == 1 + assert handler.call_count == 0 + close_button.widget.click() + assert len(glue_app._viewer_refs) == 0 + assert handler.call_count == 1 + box.close()