diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml new file mode 100644 index 0000000..4fa2d1e --- /dev/null +++ b/.github/workflows/test-python.yml @@ -0,0 +1,38 @@ +name: Run tests + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + build: + runs-on: 'ubuntu-latest' + strategy: + fail-fast: false + matrix: + python-version: ['3.12'] + + steps: + - uses: actions/checkout@v4 + - name: 'Setup Chrome and chromedriver' + uses: nanasess/setup-chromedriver@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Setup uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + uv venv + - name: Install dependencies + run: | + source .venv/bin/activate + uv pip install --upgrade pip + uv pip install -r requirements.txt + uv pip install -r requirements-dev.txt + - name: Run unit tests + run: | + source .venv/bin/activate + python -m pytest tests/ diff --git a/nanodash/graph.py b/nanodash/graph.py index b20eb1e..421d8fa 100644 --- a/nanodash/graph.py +++ b/nanodash/graph.py @@ -13,6 +13,6 @@ def html(self): var data = {self._graph_obj['data']}; var layout = {self._graph_obj['layout']}; var config = {self._graph_obj['config']}; - Plotly.newPlot('graph', data, layout, config); + Plotly.newPlot('{self._attributes['id']}', data, layout, config); ''' \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..8d7a528 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest +ruff +selenium diff --git a/requirements.txt b/requirements.txt index 411267e..52884cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ flask flask-cors -ruff +plotly diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8ea60e7 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,101 @@ +import plotly.graph_objects as go +import pytest +import threading + +from nanodash.nanodash import NanoDash +from nanodash.components import Component +from nanodash.graph import Graph + + +@pytest.fixture +def sample_app(): + + # Create a new Flask web server + app = NanoDash(debug=True) + + # Create a header component + header = Component( + tag="h1", + children="Hello, world!" + ) + + ################### + # COMPONENTS + ################### + input = Component( + tag="input", + attributes={"id": "input_sample"} + ) + output = Component( + tag="input", + attributes={"id": "output_sample"} + ) + button = Component( + tag="button", + children="Click me!" + ) + slider = Component( + tag="input", + attributes={ + "id": "slider_sample", + "type": "range", + "min": 0, + "max": 100, + "step": 1 + } + ) + graph_component = Graph( + graph_obj={ + "data": [], + "layout": {}, + "config": {} + }, + attributes={ + "id": "graph-component-sample" + } + ) + all_components = Component( + "div", + children = [ + header, + input, + button, + slider, + output, + graph_component + ] + ) + + # Add layout to the app + app.set_layout(all_components) + + ################### + # CALLBACKS + ################### + def slider_callback(inputs): + return [inputs[0]] + + app.add_callback( + inputs=[("slider_sample", "value")], + outputs=[("output_sample", "value")], + function=slider_callback, + ) + + def sample_callback(inputs): + fig = go.Figure(go.Scatter(x=[1, 2, 3], y=[1, 2, 3])) + fig.layout.title = inputs[0] + "!" + return [fig] + + app.add_callback( + inputs=[("input_sample", "value")], + outputs=[("graph-component-sample", "value")], + function=sample_callback, + ) + + return app + +@pytest.fixture +def start_app(): + def _start_app(app): + threading.Thread(target=lambda: app._app.run(), daemon=True).start() + return _start_app diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py new file mode 100644 index 0000000..4578c35 --- /dev/null +++ b/tests/test_callbacks.py @@ -0,0 +1,29 @@ +import time +import threading + +from selenium import webdriver +from selenium.webdriver.common.by import By + + +def test_callback(sample_app, start_app): + # Start the app in another thread + start_app(sample_app) + + options = webdriver.ChromeOptions() + options.add_argument("--headless") + driver = webdriver.Chrome(options=options) + driver.get("http://127.0.0.1:5000") + + # Set text in the input with id "input_sample" + driver.find_element(By.ID, "input_sample").send_keys("Hello, world!") + + time.sleep(2) + + # Check that it is showing up as the graph title + graph = driver.find_element(By.ID, "graph-component-sample").find_element( + By.CLASS_NAME, "g-gtitle" + ) + assert graph.text == "Hello, world!!" + + # Close the driver + driver.quit()