diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 44e740c106..42da8c341f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -137,7 +137,7 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(needs.setup.outputs.matrix) }} - timeout-minutes: 90 + timeout-minutes: 30 steps: - uses: holoviz-dev/holoviz_tasks/pixi_install@v0 with: diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index 94a868ae87..d1796f1d36 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -1172,6 +1172,7 @@ def callback(contents, user, instance): feed = ChatFeed(callback=callback) feed.send("Message", respond=True) + await async_wait_until(lambda: len(feed.objects) == 2) await async_wait_until(lambda: feed.objects[-1].object == "helloooo") assert chat_feed._placeholder not in chat_feed._chat_log diff --git a/panel/tests/command/test_compile.py b/panel/tests/command/test_compile.py index b562b790f6..60ae44755a 100644 --- a/panel/tests/command/test_compile.py +++ b/panel/tests/command/test_compile.py @@ -23,3 +23,4 @@ def test_compile_component(py_file): assert 'function render() {\n console.log("foo");\n}' in bundle.read_text() finally: bundle.unlink() + p.kill() diff --git a/panel/tests/ui/chat/test_chat_interface_ui.py b/panel/tests/ui/chat/test_chat_interface_ui.py index b601323be5..0cc009b720 100644 --- a/panel/tests/ui/chat/test_chat_interface_ui.py +++ b/panel/tests/ui/chat/test_chat_interface_ui.py @@ -91,6 +91,7 @@ def edit_callback(content, index, instance): # find the input field and type new message chat_input = page.locator(".bk-input").first + page.wait_for_timeout(200) chat_input.fill("Edited") # click enter diff --git a/panel/tests/ui/io/test_convert.py b/panel/tests/ui/io/test_convert.py index 41df409ed5..c2abd2e8f5 100644 --- a/panel/tests/ui/io/test_convert.py +++ b/panel/tests/ui/io/test_convert.py @@ -23,11 +23,11 @@ allow_module_level=True ) -pytestmark = pytest.mark.ui +pytestmark = [pytest.mark.ui, pytest.mark.flaky(max_runs=3)] -if os.name == "wt": - TIMEOUT = 150_000 +if os.name == "nt": + TIMEOUT = 200_000 else: TIMEOUT = 90_000 diff --git a/panel/tests/ui/io/test_jupyter_server_extension.py b/panel/tests/ui/io/test_jupyter_server_extension.py index 36494154a5..93d7c48624 100644 --- a/panel/tests/ui/io/test_jupyter_server_extension.py +++ b/panel/tests/ui/io/test_jupyter_server_extension.py @@ -10,8 +10,7 @@ from panel.tests.util import wait_until -pytestmark = pytest.mark.jupyter - +pytestmark = [pytest.mark.jupyter, pytest.mark.flaky(max_runs=3)] def test_jupyter_server(page, jupyter_preview): diff --git a/panel/tests/ui/io/test_reload.py b/panel/tests/ui/io/test_reload.py index 09d2fa5a43..c30b5e3abf 100644 --- a/panel/tests/ui/io/test_reload.py +++ b/panel/tests/ui/io/test_reload.py @@ -7,9 +7,9 @@ try: from playwright.sync_api import expect - pytestmark = pytest.mark.ui + pytestmark = [pytest.mark.ui, pytest.mark.flaky(reruns=3, reason="Writing files can sometimes be unpredictable")] except ImportError: - pytestmark = pytest.mark.skip("playwright not available") + pytestmark = [pytest.mark.skip("playwright not available")] from panel.io.state import state from panel.tests.util import serve_component, wait_until @@ -80,7 +80,6 @@ def test_load_app_with_no_content(page, autoreload, py_file): expect(page.locator('.alert')).to_have_count(1) -@pytest.mark.flaky(reruns=3, reason="Writing files can sometimes be unpredictable") def test_reload_app_on_local_module_change(page, autoreload, py_files): py_file, module = py_files import_name = pathlib.Path(module.name).stem diff --git a/panel/tests/ui/layout/test_column.py b/panel/tests/ui/layout/test_column.py index 5ecd26156f..ce8ba6b129 100644 --- a/panel/tests/ui/layout/test_column.py +++ b/panel/tests/ui/layout/test_column.py @@ -238,6 +238,7 @@ def test_column_scroll_position_param_updated(page): expect(column).to_have_js_property('scrollTop', 175) +@pytest.mark.flaky(reruns=3) def test_column_scroll_to(page): col = Column( *list(range(100)), diff --git a/panel/tests/ui/layout/test_feed.py b/panel/tests/ui/layout/test_feed.py index ae62b4980f..4e6e6fe1ff 100644 --- a/panel/tests/ui/layout/test_feed.py +++ b/panel/tests/ui/layout/test_feed.py @@ -8,7 +8,7 @@ from panel.layout.spacer import Spacer from panel.tests.util import serve_component, wait_until -pytestmark = pytest.mark.ui +pytestmark = [pytest.mark.ui, pytest.mark.flaky(max_runs=3)] ITEMS = 100 # 1000 items make the CI flaky @@ -49,7 +49,7 @@ def test_feed_view_latest(page): # Assert scroll is not at 0 (view_latest) wait_until(lambda: feed_el.evaluate('(el) => el.scrollTop') > 0, page) - wait_until(lambda: int(page.locator('pre').last.inner_text()) > 0.9 * ITEMS, page) + wait_until(lambda: int(page.locator('pre').last.inner_text() or 0) > 0.9 * ITEMS, page) def test_feed_view_scroll_to_latest(page): @@ -113,25 +113,25 @@ def test_feed_scroll_to_latest_within_limit(page): feed.scroll_to_latest(scroll_limit=100) - # assert scroll location is still at top feed.append(Spacer(styles=dict(background='yellow'), width=200, height=200)) - page.wait_for_timeout(500) - + # assert scroll location is still at top expect(feed_el.locator('div')).to_have_count(5) expect(feed_el).to_have_js_property('scrollTop', 0) # scroll to close to bottom - feed_el.evaluate('(el) => el.scrollTo({top: el.scrollHeight})') + feed_el.evaluate('(el) => el.scrollTo({top: 200})') + expect(feed_el).to_have_js_property('scrollTop', 200) # assert auto scroll works; i.e. distance from bottom is 0 feed.append(Spacer(styles=dict(background='yellow'), width=200, height=200)) + feed.scroll_to_latest(scroll_limit=1000) - feed.scroll_to_latest(scroll_limit=100) - - wait_until(lambda: feed_el.evaluate( - '(el) => el.scrollHeight - el.scrollTop - el.clientHeight' - ) == 0, page) + def assert_at_bottom(): + assert feed_el.evaluate( + '(el) => el.scrollHeight - el.scrollTop - el.clientHeight' + ) == 0 + wait_until(assert_at_bottom, page) def test_feed_view_scroll_button(page): @@ -150,7 +150,7 @@ def test_feed_view_scroll_button(page): # Assert scroll is not at 0 (view_latest) wait_until(lambda: feed_el.evaluate('(el) => el.scrollTop') > 0, page) - wait_until(lambda: int(page.locator('pre').last.inner_text()) > 50, page) + wait_until(lambda: int(page.locator('pre').last.inner_text() or 0) > 50, page) def test_feed_dynamic_objects(page): feed = Feed(height=250, load_buffer=10) diff --git a/panel/tests/ui/pane/test_image.py b/panel/tests/ui/pane/test_image.py index 887e11ea22..8a723a9f3f 100644 --- a/panel/tests/ui/pane/test_image.py +++ b/panel/tests/ui/pane/test_image.py @@ -25,7 +25,13 @@ def get_bbox(page, obj): with page.expect_response(obj.object): page.goto(f"http://localhost:{port}") wait_until(lambda: page.locator("img") is not None, page) - return page.locator("img").bounding_box() + for _ in range(5): + bbox = page.locator("img").bounding_box() + if bbox["width"] and bbox["height"]: + return bbox + page.wait_for_timeout(100) + + raise TimeoutError("Image has not been loaded") @pytest.mark.parametrize('embed', [False, True]) def test_png_native_size(embed, page): diff --git a/panel/tests/ui/pane/test_markup.py b/panel/tests/ui/pane/test_markup.py index 71399b79d0..cc4cbb3d86 100644 --- a/panel/tests/ui/pane/test_markup.py +++ b/panel/tests/ui/pane/test_markup.py @@ -114,7 +114,7 @@ def test_html_model_no_stylesheet(page): serve_component(page, html) header_element = page.locator('h1:has-text("Header")') - assert header_element.is_visible() + expect(header_element).to_be_visible() assert header_element.text_content() == "Header" def test_anchor_scroll(page): diff --git a/panel/tests/ui/test_custom.py b/panel/tests/ui/test_custom.py index 06cf4b64be..bc37cf005e 100644 --- a/panel/tests/ui/test_custom.py +++ b/panel/tests/ui/test_custom.py @@ -20,6 +20,16 @@ pytestmark = pytest.mark.ui +@pytest.fixture(scope="module", autouse=True) +def set_expect_timeout(): + timeout = expect._timeout + expect.set_options(timeout=10_000) + try: + yield + finally: + expect.set_options(timeout=timeout) + + class JSUpdate(JSComponent): text = param.String() diff --git a/panel/tests/ui/widgets/test_icon.py b/panel/tests/ui/widgets/test_icon.py index 8ef0bfe8a1..448401d0a9 100644 --- a/panel/tests/ui/widgets/test_icon.py +++ b/panel/tests/ui/widgets/test_icon.py @@ -126,9 +126,12 @@ def cb(event): # update size icon.size = "8em" - assert page.locator(".icon-tabler-ad-filled").bounding_box()["width"] >= 96 + wait_until(lambda: page.locator(".icon-tabler-ad-filled").bounding_box() is not None, page) + wait_until(lambda: page.locator(".icon-tabler-ad-filled").bounding_box()["width"] > 96, page) + icon.size = "1em" - wait_until(lambda: page.locator(".icon-tabler-ad-filled").bounding_box()["width"] <= 24, page) + wait_until(lambda: page.locator(".icon-tabler-ad-filled").bounding_box() is not None, page) + wait_until(lambda: page.locator(".icon-tabler-ad-filled").bounding_box()["width"] < 24, page) def test_toggle_icon_svg(page): diff --git a/panel/tests/ui/widgets/test_misc.py b/panel/tests/ui/widgets/test_misc.py index f8807c907a..30a87475e5 100644 --- a/panel/tests/ui/widgets/test_misc.py +++ b/panel/tests/ui/widgets/test_misc.py @@ -55,7 +55,10 @@ def create_file(value): download = download_info.value tmp = tempfile.NamedTemporaryFile(suffix='.txt') download.save_as(tmp.name) - assert tmp.file.read().decode('utf-8') == 'abc' + try: + assert tmp.file.read().decode('utf-8') == 'abc' + finally: + tmp.close() page.click('.bk-tab:not(.bk-active)') page.click('.bk-tab:not(.bk-active)') @@ -71,4 +74,7 @@ def create_file(value): download = download_info.value tmp = tempfile.NamedTemporaryFile(suffix='.txt') download.save_as(tmp.name) - assert tmp.file.read().decode('utf-8') == 'abcdef' + try: + assert tmp.file.read().decode('utf-8') == 'abcdef' + finally: + tmp.close() diff --git a/panel/tests/ui/widgets/test_player.py b/panel/tests/ui/widgets/test_player.py index 44616607e9..790bb99109 100644 --- a/panel/tests/ui/widgets/test_player.py +++ b/panel/tests/ui/widgets/test_player.py @@ -56,7 +56,7 @@ def test_name(page): player = Player(name='test') serve_component(page, player) - assert page.is_visible('label') + expect(page.locator('label')).to_have_count(3) assert page.query_selector('.pn-player-value') is None name = page.locator('.pn-player-title:has-text("test")') @@ -75,16 +75,17 @@ def test_name_and_show_value(page): player = Player(name='test', show_value=True) serve_component(page, player) - assert page.is_visible('label') + expect(page.locator('label')).to_have_count(3) assert page.query_selector('.pn-player-value') is not None name = page.locator('.pn-player-title:has-text("test")') expect(name).to_have_count(1) + def test_player_visible_buttons(page): player = Player(visible_buttons=["play", "pause"]) serve_component(page, player) - assert page.is_visible(".play") + expect(page.locator(".play")).to_be_visible() assert page.is_visible(".pause") assert not page.is_visible(".reverse") assert not page.is_visible(".first") @@ -104,14 +105,14 @@ def test_player_visible_loop_options(page): player = Player(visible_loop_options=["loop", "once"]) serve_component(page, player) - assert page.is_visible(".loop") - assert page.is_visible(".once") - assert not page.is_visible(".reflect") + expect(page.locator(".loop")).to_be_visible() + expect(page.locator(".once")).to_be_visible() + expect(page.locator(".reflect")).to_be_hidden() player.visible_loop_options = ["reflect"] expect(page.locator(".reflect")).to_be_visible() - assert not page.is_visible(".loop") - assert not page.is_visible(".once") + expect(page.locator(".loop")).to_be_hidden() + expect(page.locator(".once")).to_be_hidden() def test_player_scale_buttons(page): diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 18f51f6ae6..87d41e0f65 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -586,6 +586,7 @@ def test_tabulator_editors_panel_date(page, df_mixed): cell_edit = page.locator('input[type="date"]') new_date = "1980-01-01" cell_edit.fill(new_date) + page.wait_for_timeout(100) # Need to Enter to validate the change page.locator('input[type="date"]').press('Enter') expect(page.locator(f'text="{new_date}"')).to_have_count(1) @@ -597,6 +598,7 @@ def test_tabulator_editors_panel_date(page, df_mixed): cell_edit = page.locator('input[type="date"]') new_date2 = "1990-01-01" cell_edit.fill(new_date2) + page.wait_for_timeout(100) # Escape invalidates the change page.locator('input[type="date"]').press('Escape') expect(page.locator(f'text="{new_date2}"')).to_have_count(0) @@ -1081,11 +1083,11 @@ def test_tabulator_patch_no_horizontal_rescroll(page, df_mixed): widths = 100 width = int(((df_mixed.shape[1] + 1) * widths) / 2) df_mixed['tomodify'] = 'target' - widget = Tabulator(df_mixed, width=width, widths=widths) + widget = Tabulator(df_mixed.iloc[:1], width=width, widths=widths) serve_component(page, widget) - cell = page.locator('text="target"').first + cell = page.locator('text="target"') # Scroll to the right cell.scroll_into_view_if_needed() page.wait_for_timeout(200) @@ -2432,6 +2434,8 @@ def test_tabulator_patching_and_styling(page, df_mixed): serve_component(page, widget) + expect(page.locator('.tabulator-cell')).not_to_have_count(0) + # Changing the highest value in the int column should # update the style so that this cell gets a yellow background widget.patch({'int': [(0, 100)]}, as_index=False) @@ -2869,6 +2873,7 @@ def test_tabulator_edit_event_and_header_filters_same_column(page, show_index, i assert len(widget.current_view) == 2 +@pytest.mark.flaky(max_runs=3) @pytest.mark.parametrize('pagination', ['remote', 'local']) def test_tabulator_edit_event_and_header_filters_same_column_pagination(page, pagination): df = pd.DataFrame({ @@ -2907,7 +2912,6 @@ def test_tabulator_edit_event_and_header_filters_same_column_pagination(page, pa assert len(widget.current_view) == 4 page.locator('text="Last"').click() - page.wait_for_timeout(200) # Check the table has the right number of rows expect(page.locator('.tabulator-row')).to_have_count(2) @@ -3494,6 +3498,7 @@ def test_tabulator_sorter_default_number(page): widget = Tabulator(df, sorters=[{"field": "x", "dir": "desc"}]) serve_component(page, widget) + expect(page.locator('.tabulator-cell')).to_have_count(0) df2 = pd.DataFrame({'x': [0, 96, 116]}) widget.value = df2 diff --git a/pixi.toml b/pixi.toml index 8c36fb47b7..9bf73b7dfe 100644 --- a/pixi.toml +++ b/pixi.toml @@ -180,7 +180,7 @@ packaging = "*" _install-ui = 'playwright install chromium' [feature.test-ui.tasks.test-ui] -cmd = 'pytest panel/tests/ui --ui --browser chromium -n logical --dist loadgroup --reruns 3 --reruns-delay 10' +cmd = 'pytest panel/tests/ui --ui --browser chromium -n logical --dist loadgroup' depends-on = ["_install-ui"] # =============================================