From 836335ec90e0d3c0de20be1c1a10da05d6c0f8c3 Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 15 May 2026 18:12:17 -0700 Subject: [PATCH] Add code block integration coverage --- .../src/reflex_components_code/code.py | 20 +- .../tests_playwright/test_code_block.py | 229 ++++++++++++++++++ 2 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 tests/integration/tests_playwright/test_code_block.py diff --git a/packages/reflex-components-code/src/reflex_components_code/code.py b/packages/reflex-components-code/src/reflex_components_code/code.py index a6fec988df6..519ef0c457c 100644 --- a/packages/reflex-components-code/src/reflex_components_code/code.py +++ b/packages/reflex-components-code/src/reflex_components_code/code.py @@ -409,7 +409,6 @@ class CodeBlock(Component, MarkdownComponentMap): custom_style: dict[str, str | Var | Color] = field( doc="A custom style for the code block.", default_factory=dict, - is_javascript_property=False, ) code_tag_props: Var[dict[str, str | dict[str, str]]] = field( @@ -459,6 +458,25 @@ def create( dark=Theme.one_dark, ) + if props.get("wrap_long_lines") is True: + code_tag_props = props.get("code_tag_props") + if code_tag_props is None: + props["code_tag_props"] = {"style": {"whiteSpace": "pre-wrap"}} + elif isinstance(code_tag_props, dict): + code_tag_props = code_tag_props.copy() + code_tag_style = code_tag_props.get("style") + if code_tag_style is None: + code_tag_props["style"] = {"whiteSpace": "pre-wrap"} + elif ( + isinstance(code_tag_style, dict) + and "whiteSpace" not in code_tag_style + ): + code_tag_props["style"] = { + "whiteSpace": "pre-wrap", + **code_tag_style, + } + props["code_tag_props"] = code_tag_props + if can_copy: code = children[0] copy_button = ( diff --git a/tests/integration/tests_playwright/test_code_block.py b/tests/integration/tests_playwright/test_code_block.py new file mode 100644 index 00000000000..2dfdd4daa16 --- /dev/null +++ b/tests/integration/tests_playwright/test_code_block.py @@ -0,0 +1,229 @@ +"""Integration tests for the code block component.""" + +from collections.abc import Generator + +import pytest +from playwright.sync_api import Page, expect + +from reflex.testing import AppHarness + +PRIMARY_CODE = "def add(x, y):\n return x + y" +LONG_LINE_CODE = ( + "message = 'this line is intentionally long so wrap_long_lines changes " + "the rendered whitespace behavior'" +) +DEFAULT_COPY_CODE = "print('copied from default button')" +CUSTOM_COPY_CODE = "print('copied from custom button')" + + +def CodeBlockApp(): + """App exercising code block rendering options.""" + import reflex as rx + + primary_code = "def add(x, y):\n return x + y" + long_line_code = ( + "message = 'this line is intentionally long so wrap_long_lines changes " + "the rendered whitespace behavior'" + ) + default_copy_code = "print('copied from default button')" + custom_copy_code = "print('copied from custom button')" + + def index(): + return rx.vstack( + rx.box( + rx.code_block( + primary_code, + language="python", + theme=rx.code_block.themes.one_light, + show_line_numbers=True, + starting_line_number=41, + custom_style={ + "background_color": "rgb(17, 34, 51)", + "border_radius": "6px", + "padding": "12px", + }, + code_tag_props={ + "data-testid": "primary-code-tag", + "data-code-prop": "tag-prop", + }, + ), + id="primary-code-block", + ), + rx.box( + rx.code_block( + long_line_code, + language="python", + wrap_long_lines=True, + ), + id="wrapped-code-block", + width="220px", + ), + rx.box( + rx.code_block( + "const answer = 42;", + language="javascript", + theme=rx.code_block.themes.one_dark, + ), + id="theme-code-block", + ), + rx.box( + rx.code_block(default_copy_code, language="python", can_copy=True), + id="default-copy-block", + ), + rx.box( + rx.code_block( + custom_copy_code, + language="python", + can_copy=True, + copy_button=rx.button( + "Copy custom", + id="custom-copy", + on_click=rx.set_clipboard(custom_copy_code), + ), + ), + id="custom-copy-block", + ), + align_items="stretch", + spacing="4", + width="480px", + ) + + app = rx.App(enable_state=False) + app.add_page(index) + + +@pytest.fixture(scope="module") +def code_block_app( + tmp_path_factory: pytest.TempPathFactory, +) -> Generator[AppHarness, None, None]: + """Start the code block test app. + + Args: + tmp_path_factory: Pytest tmp path factory. + + Yields: + The running app harness. + """ + with AppHarness.create( + root=tmp_path_factory.mktemp("code_block_app"), + app_source=CodeBlockApp, + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +def _goto_code_block_app(code_block_app: AppHarness, page: Page) -> None: + """Navigate to the code block test app. + + Args: + code_block_app: Running code block app harness. + page: Playwright page. + """ + assert code_block_app.frontend_url is not None + page.goto(code_block_app.frontend_url) + expect(page.locator("#primary-code-block pre")).to_be_visible() + + +def test_code_block_renders_code_language_line_numbers_and_code_tag_props( + code_block_app: AppHarness, page: Page +) -> None: + """The primary code block renders code text and structural props. + + Args: + code_block_app: Running code block app harness. + page: Playwright page. + """ + _goto_code_block_app(code_block_app, page) + + code_tag = page.get_by_test_id("primary-code-tag") + expect(code_tag).to_contain_text("def add") + expect(code_tag).to_contain_text("return x + y") + expect(code_tag).to_have_attribute("data-code-prop", "tag-prop") + + assert code_tag.evaluate( + """el => Array.from(el.querySelectorAll('span')) + .some(span => span.textContent === 'def')""" + ) + + line_numbers = page.locator("#primary-code-block .linenumber") + expect(line_numbers).to_have_count(2) + assert line_numbers.all_inner_texts() == ["41", "42"] + + +def test_code_block_applies_custom_style_and_theme( + code_block_app: AppHarness, page: Page +) -> None: + """Code block styles from custom_style and theme reach the DOM. + + Args: + code_block_app: Running code block app harness. + page: Playwright page. + """ + _goto_code_block_app(code_block_app, page) + + primary = page.locator("#primary-code-block pre") + expect(primary).to_have_css("background-color", "rgb(17, 34, 51)") + expect(primary).to_have_css("border-radius", "6px") + + themed = page.locator("#theme-code-block pre") + expect(themed).to_have_css("background-color", "rgb(40, 44, 52)") + + +def test_code_block_wraps_long_lines(code_block_app: AppHarness, page: Page) -> None: + """wrap_long_lines changes rendered whitespace behavior. + + Args: + code_block_app: Running code block app harness. + page: Playwright page. + """ + _goto_code_block_app(code_block_app, page) + + wrapped = page.locator("#wrapped-code-block") + expect(wrapped.locator("code")).to_contain_text("this line is intentionally long") + assert wrapped.locator("pre").evaluate("el => el.scrollWidth <= el.clientWidth + 1") + + +def test_code_block_default_copy_button(code_block_app: AppHarness, page: Page) -> None: + """The built-in copy button writes the code text to the clipboard. + + Args: + code_block_app: Running code block app harness. + page: Playwright page. + """ + assert code_block_app.frontend_url is not None + page.context.grant_permissions( + ["clipboard-read", "clipboard-write"], origin=code_block_app.frontend_url + ) + _goto_code_block_app(code_block_app, page) + + page.evaluate("navigator.clipboard.writeText('')") + page.locator("#default-copy-block button").click() + page.wait_for_function( + "expected => navigator.clipboard.readText().then(text => text === expected)", + arg=DEFAULT_COPY_CODE, + ) + + +def test_code_block_custom_copy_button(code_block_app: AppHarness, page: Page) -> None: + """A custom copy button replaces the default and can run its own event. + + Args: + code_block_app: Running code block app harness. + page: Playwright page. + """ + assert code_block_app.frontend_url is not None + page.context.grant_permissions( + ["clipboard-read", "clipboard-write"], origin=code_block_app.frontend_url + ) + _goto_code_block_app(code_block_app, page) + + custom_block = page.locator("#custom-copy-block") + expect(custom_block.get_by_role("button")).to_have_text("Copy custom") + expect(custom_block.locator("svg.lucide-copy")).to_have_count(0) + + page.evaluate("navigator.clipboard.writeText('')") + page.locator("#custom-copy").click() + page.wait_for_function( + "expected => navigator.clipboard.readText().then(text => text === expected)", + arg=CUSTOM_COPY_CODE, + )