Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/reflex-components-code/news/6520.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Track state vars referenced in `rx.code_block`'s `custom_style` dict so styles bound to state re-render on updates, and make `wrap_long_lines=True` apply `whiteSpace: pre-wrap` to the code tag even when `code_tag_props` is also provided.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import dataclasses
from collections.abc import Iterator
from typing import ClassVar, Literal

from reflex_base.components.component import Component, ComponentNamespace, field
Expand Down Expand Up @@ -409,7 +410,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,
Comment thread
FarhanAliRaza marked this conversation as resolved.
)
Comment thread
FarhanAliRaza marked this conversation as resolved.

code_tag_props: Var[dict[str, str | dict[str, str]]] = field(
Expand Down Expand Up @@ -459,6 +459,25 @@ def create(
dark=Theme.one_dark,
)

if props.get("wrap_long_lines") is True:
Comment thread
FarhanAliRaza marked this conversation as resolved.
code_tag_props = props.get("code_tag_props")
Comment thread
FarhanAliRaza marked this conversation as resolved.
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 = (
Expand Down Expand Up @@ -499,6 +518,30 @@ def add_style(self):
"""Add style to the component."""
self.custom_style.update(self.style)

def _get_vars(
self, include_children: bool = False, ignore_ids: set[int] | None = None
) -> Iterator[Var]:
"""Walk all Vars used in this component.

Args:
include_children: Whether to include Vars from children.
ignore_ids: The ids to ignore.

Yields:
Each var referenced by the component, including any embedded in
the custom_style dict, which is not a Var-typed prop.
"""
yield from super()._get_vars(
include_children=include_children, ignore_ids=ignore_ids
)
custom_style = self.custom_style
if isinstance(custom_style, Style) and custom_style._var_data:
yield Var(
_js_expr="custom_style",
_var_type=str,
_var_data=custom_style._var_data,
)

def _render(self):
out = super()._render()

Expand Down
223 changes: 223 additions & 0 deletions tests/integration/tests_playwright/test_code_block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
"""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

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")

# PrismAsyncLight loads the language grammar asynchronously, so token
# spans appear after the initial plain-text render; wait for one.
expect(code_tag.get_by_text("def", exact=True)).to_be_visible()

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,
)
20 changes: 20 additions & 0 deletions tests/units/components/datadisplay/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ def test_code_block_rejects_string_theme():
CodeBlock.create("print('Hello')", theme="one_dark") # pyright: ignore[reportArgumentType]


def test_code_block_collects_custom_style_state_vars():
class CustomStyleState(rx.State):
color: str = "red"

code_block = CodeBlock.create(
"x = 1", custom_style={"color": CustomStyleState.color}
)

rendered = str(code_block.render())
assert CustomStyleState.color._js_expr in rendered # pyright: ignore [reportAttributeAccessIssue]

var_data = CustomStyleState.color._get_all_var_data() # pyright: ignore [reportAttributeAccessIssue]
assert var_data is not None
assert var_data.hooks
collected_hooks = code_block._get_all_hooks()
assert all(hook in collected_hooks for hook in var_data.hooks), (
"state hook for custom_style var was not collected"
)


def test_code_block_accepts_color_mode_cond_theme():
theme = rx.color_mode_cond(
light=rx.code_block.themes.one_light,
Expand Down
Loading