Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Playwright E2E Tests
on:
push:
branches:
- "main"
- 'main'
pull_request:
types: [opened, synchronize, reopened]
# Allows workflow to be called from other workflows
Expand All @@ -21,6 +21,10 @@ concurrency:
jobs:
playwright-e2e-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
streamlit: ['1.50.0', 'latest']

defaults:
run:
Expand All @@ -32,12 +36,12 @@ jobs:
with:
ref: ${{ inputs.ref }}
persist-credentials: false
submodules: "recursive"
submodules: 'recursive'
fetch-depth: 2
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: '3.12'
- name: Enable and Prepare Latest Yarn
run: |
corepack enable
Expand All @@ -46,9 +50,9 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "yarn"
cache-dependency-path: "**/yarn.lock"
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: '**/yarn.lock'
- name: Make frontend
run: |
cd streamlit_bokeh/frontend
Expand Down Expand Up @@ -88,6 +92,11 @@ jobs:
source venv/bin/activate
pip install --upgrade pip
pip install -r e2e_playwright/test-requirements.txt
if [[ "${{ matrix.streamlit }}" == "latest" ]]; then
pip install streamlit
else
pip install "streamlit==${{ matrix.streamlit }}"
fi
python -m playwright install --with-deps
- name: Run playwright tests
run: |
Expand All @@ -98,5 +107,5 @@ jobs:
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright_test_results
name: playwright_test_results-${{ matrix.streamlit }}
path: e2e_playwright/test-results
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 27 additions & 11 deletions e2e_playwright/bokeh_chart_basics_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,46 @@
# limitations under the License.

import pytest
from conftest import ImageCompareFunction, wait_for_app_run
from chart_types import CHART_TYPES
from conftest import ImageCompareFunction, wait_for_app_run
from playwright.sync_api import Page, expect


@pytest.mark.parametrize("chart", CHART_TYPES)
def test_bokeh_chart(
themed_app: Page, assert_snapshot: ImageCompareFunction, chart: str
themed_app: Page, assert_snapshot: ImageCompareFunction, chart: str, is_v2: bool
):
"""Test that st.bokeh_chart renders correctly."""
themed_app.get_by_test_id("stSelectbox").locator("input").click()

# Take a snapshot of the selection dropdown:
selection_dropdown = themed_app.locator('[data-baseweb="popover"]').first
selection_dropdown.get_by_text(chart).scroll_into_view_if_needed()
selection_dropdown.get_by_text(chart).click()
# Make sure the dropdown closed before we continue to snapshots.
expect(selection_dropdown).to_be_hidden()

wait_for_app_run(themed_app)
iframes = themed_app.locator("iframe")
IFRAME_COUNT = 2
expect(iframes).to_have_count(IFRAME_COUNT)

for idx in range(IFRAME_COUNT):
label = "use-container-width" if idx == 1 else "standard-width"
iframe = iframes.nth(idx)
canvas = iframe.content_frame.locator("div.bk-Canvas")
expect(canvas).to_be_visible()
assert_snapshot(iframe, name=f"bokeh_chart-{chart}-{label}")
if is_v2:
# Custom Component v2 renders inline without iframes
instances = themed_app.locator("[data-testid=stBidiComponentRegular]")
EXPECTED_INSTANCE_COUNT = 2
expect(instances).to_have_count(EXPECTED_INSTANCE_COUNT)
for idx in range(EXPECTED_INSTANCE_COUNT):
label = "use-container-width" if idx == 1 else "standard-width"
instance = instances.nth(idx)
canvas = instance.locator("div.bk-Canvas")
expect(canvas).to_be_visible()
assert_snapshot(instance, name=f"bokeh_chart-{chart}-{label}")
else:
# Custom Component v1 renders inside iframes
iframes = themed_app.locator("iframe")
IFRAME_COUNT = 2
expect(iframes).to_have_count(IFRAME_COUNT)
for idx in range(IFRAME_COUNT):
label = "use-container-width" if idx == 1 else "standard-width"
iframe = iframes.nth(idx)
canvas = iframe.content_frame.locator("div.bk-Canvas")
expect(canvas).to_be_visible()
assert_snapshot(iframe, name=f"bokeh_chart-{chart}-{label}")
24 changes: 22 additions & 2 deletions e2e_playwright/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

import pytest
import requests
import streamlit as st
from packaging.version import Version
from PIL import Image
from playwright.sync_api import (
ElementHandle,
Expand All @@ -49,7 +51,6 @@
Page,
)
from pytest import FixtureRequest

from shared.git_utils import get_git_root

if TYPE_CHECKING:
Expand All @@ -76,6 +77,14 @@ def pytest_generate_tests(metafunc: pytest.Metafunc):
reorder_early_fixtures(metafunc)


def _detect_streamlit_mode() -> str:
"""Return 'v1' for Custom Component v1 API, 'v2' for Custom Component v2 API.

Streamlit introduces the Custom Component v2 API starting with version 1.51.0.
"""
return "v2" if Version(st.__version__) >= Version("1.51.0") else "v1"


class AsyncSubprocess:
"""A context manager. Wraps subprocess. Popen to capture output safely."""

Expand Down Expand Up @@ -206,6 +215,12 @@ def wait_for_app_server_to_start(port: int, timeout: int = 5) -> bool:
# region Fixtures


@pytest.fixture(scope="session")
def is_v2() -> bool:
"""True when running with Custom Component v2 API (Streamlit >= 1.51.0)."""
return _detect_streamlit_mode() == "v2"


@pytest.fixture(scope="module")
def app_port(worker_id: str) -> int:
"""Fixture that returns an available port on localhost."""
Expand Down Expand Up @@ -419,7 +434,7 @@ def output_folder(pytestconfig: Any) -> Path:

@pytest.fixture(scope="function")
def assert_snapshot(
request: FixtureRequest, output_folder: Path
request: FixtureRequest, output_folder: Path, is_v2: bool
) -> Generator[ImageCompareFunction, None, None]:
"""Fixture that compares a screenshot with screenshot from a past run."""
root_path = get_git_root()
Expand All @@ -443,6 +458,11 @@ def assert_snapshot(
match = re.search(r"\[(.*?)\]", request.node.name)
if match:
snapshot_file_suffix = f"[{match.group(1)}]"
# Add version suffix for Streamlit v2 to allow parallel baselines
if is_v2:
snapshot_file_suffix = (
f"{snapshot_file_suffix[:-1]}-v2]" if snapshot_file_suffix else "[v2]"
)

snapshot_default_file_name: str = test_function_name + snapshot_file_suffix

Expand Down
36 changes: 2 additions & 34 deletions streamlit_bokeh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
import importlib.metadata
import json
import os
import re
from typing import TYPE_CHECKING

import bokeh
import streamlit as st
from bokeh.embed import json_item
from packaging.version import Version

if TYPE_CHECKING:
from bokeh.plotting.figure import Figure
Expand All @@ -34,43 +34,11 @@
_RELEASE = not _DEV


def _version_ge(a: str, b: str) -> bool:
"""
Return True if version string a is greater than or equal to b.

The comparison extracts up to three numeric components from each version
string (major, minor, patch) and compares them as integer tuples.
Non-numeric suffixes (for example, 'rc1', 'dev') are ignored.

Parameters
----------
a : str
The left-hand version string.
b : str
The right-hand version string to compare against.

Returns
-------
bool
True if a >= b, otherwise False.
"""

def parse(v: str) -> tuple[int, int, int]:
nums = [int(x) for x in re.findall(r"\d+", v)[:3]]
while len(nums) < 3:
nums.append(0)
return nums[0], nums[1], nums[2]

return parse(a) >= parse(b)


_STREAMLIT_VERSION = importlib.metadata.version("streamlit")

# If streamlit version is >= 1.51.0 use Custom Component v2 API, otherwise use
# Custom Component v1 API
# _IS_USING_CCV2 = _version_ge(_STREAMLIT_VERSION, "1.51.0")
# Temporarily setting this to False, will be updated in next PR.
_IS_USING_CCV2 = False
_IS_USING_CCV2 = Version(_STREAMLIT_VERSION) >= Version("1.51.0")

# Version-gated component registration
if _IS_USING_CCV2:
Expand Down
Loading