Compare commits

..

5 Commits

Author SHA1 Message Date
Lendemor
d2b183e3f9 Merge branch 'main' into lendemor/fix_duplicate_tab_issue 2025-02-20 15:12:04 +01:00
Lendemor
2f215a350e fix issue 2025-01-09 16:21:35 +01:00
Lendemor
2825500e26 review changes 2025-01-09 11:22:40 +01:00
Lendemor
41d8cfae57 Merge branch 'main' into lendemor/fix_duplicate_tab_issue 2025-01-08 17:20:06 +01:00
Lendemor
b11328160a fix duplicate tab issue 2025-01-08 17:17:34 +01:00
14 changed files with 188 additions and 130 deletions

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "reflex"
version = "0.7.2dev1"
version = "0.7.1dev1"
description = "Web apps in pure Python."
license = "Apache-2.0"
authors = [

View File

@ -15,13 +15,7 @@
"devDependencies": {
{% for package, version in dev_dependencies.items() %}
"{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %}
{% endfor %}
},
"overrides": {
{% for package, version in overrides.items() %}
"{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %}
{% endfor %}
}
}

View File

@ -78,9 +78,9 @@ export function UploadFilesProvider({ children }) {
return newFilesById
})
return (
<UploadFilesContext value={[filesById, setFilesById]}>
<UploadFilesContext.Provider value={[filesById, setFilesById]}>
{children}
</UploadFilesContext>
</UploadFilesContext.Provider>
)
}
@ -92,9 +92,9 @@ export function EventLoopProvider({ children }) {
clientStorage,
)
return (
<EventLoopContext value={[addEvents, connectErrors]}>
<EventLoopContext.Provider value={[addEvents, connectErrors]}>
{children}
</EventLoopContext>
</EventLoopContext.Provider>
)
}
@ -112,13 +112,13 @@ export function StateProvider({ children }) {
return (
{% for state_name in initial_state %}
<StateContexts.{{state_name|var_name}} value={ {{state_name|var_name}} }>
<StateContexts.{{state_name|var_name}}.Provider value={ {{state_name|var_name}} }>
{% endfor %}
<DispatchContext value={dispatchers}>
<DispatchContext.Provider value={dispatchers}>
{children}
</DispatchContext>
</DispatchContext.Provider>
{% for state_name in initial_state|reverse %}
</StateContexts.{{state_name|var_name}}>
</StateContexts.{{state_name|var_name}}.Provider>
{% endfor %}
)
}

View File

@ -36,17 +36,17 @@ export default function RadixThemesColorModeProvider({ children }) {
const allowedModes = ["light", "dark", "system"];
if (!allowedModes.includes(mode)) {
console.error(
`Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`,
`Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`
);
mode = defaultColorMode;
}
setTheme(mode);
};
return (
<ColorModeContext
<ColorModeContext.Provider
value={{ rawColorMode, resolvedColorMode, toggleColorMode, setColorMode }}
>
{children}
</ColorModeContext>
</ColorModeContext.Provider>
);
}

View File

@ -227,8 +227,8 @@ export const applyEvent = async (event, socket) => {
a.href = eval?.(
event.payload.url.replace(
"getBackendURL(env.UPLOAD)",
`"${getBackendURL(env.UPLOAD)}"`,
),
`"${getBackendURL(env.UPLOAD)}"`
)
);
}
a.download = event.payload.filename;
@ -341,7 +341,7 @@ export const applyRestEvent = async (event, socket) => {
event.payload.files,
event.payload.upload_id,
event.payload.on_upload_progress,
socket,
socket
);
return false;
}
@ -408,7 +408,7 @@ export const connect = async (
dispatch,
transports,
setConnectErrors,
client_storage = {},
client_storage = {}
) => {
// Get backend URL object from the endpoint.
const endpoint = getBackendURL(EVENTURL);
@ -419,6 +419,7 @@ export const connect = async (
transports: transports,
protocols: [reflexEnvironment.version],
autoUnref: false,
query: { token: getToken() },
});
// Ensure undefined fields in events are sent as null instead of removed
socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v);
@ -479,6 +480,10 @@ export const connect = async (
event_processing = false;
queueEvents([...initialEvents(), event], socket);
});
socket.current.on("new_token", async (new_token) => {
token = new_token;
window.sessionStorage.setItem(TOKEN_KEY, new_token);
});
document.addEventListener("visibilitychange", checkVisibility);
};
@ -499,7 +504,7 @@ export const uploadFiles = async (
files,
upload_id,
on_upload_progress,
socket,
socket
) => {
// return if there's no file to upload
if (files === undefined || files.length === 0) {
@ -604,7 +609,7 @@ export const Event = (
name,
payload = {},
event_actions = {},
handler = null,
handler = null
) => {
return { name, payload, handler, event_actions };
};
@ -631,7 +636,7 @@ export const hydrateClientStorage = (client_storage) => {
for (const state_key in client_storage.local_storage) {
const options = client_storage.local_storage[state_key];
const local_storage_value = localStorage.getItem(
options.name || state_key,
options.name || state_key
);
if (local_storage_value !== null) {
client_storage_values[state_key] = local_storage_value;
@ -642,7 +647,7 @@ export const hydrateClientStorage = (client_storage) => {
for (const state_key in client_storage.session_storage) {
const session_options = client_storage.session_storage[state_key];
const session_storage_value = sessionStorage.getItem(
session_options.name || state_key,
session_options.name || state_key
);
if (session_storage_value != null) {
client_storage_values[state_key] = session_storage_value;
@ -667,7 +672,7 @@ export const hydrateClientStorage = (client_storage) => {
const applyClientStorageDelta = (client_storage, delta) => {
// find the main state and check for is_hydrated
const unqualified_states = Object.keys(delta).filter(
(key) => key.split(".").length === 1,
(key) => key.split(".").length === 1
);
if (unqualified_states.length === 1) {
const main_state = delta[unqualified_states[0]];
@ -701,7 +706,7 @@ const applyClientStorageDelta = (client_storage, delta) => {
const session_options = client_storage.session_storage[state_key];
sessionStorage.setItem(
session_options.name || state_key,
delta[substate][key],
delta[substate][key]
);
}
}
@ -721,7 +726,7 @@ const applyClientStorageDelta = (client_storage, delta) => {
export const useEventLoop = (
dispatch,
initial_events = () => [],
client_storage = {},
client_storage = {}
) => {
const socket = useRef(null);
const router = useRouter();
@ -735,7 +740,7 @@ export const useEventLoop = (
event_actions = events.reduce(
(acc, e) => ({ ...acc, ...e.event_actions }),
event_actions ?? {},
event_actions ?? {}
);
const _e = args.filter((o) => o?.preventDefault !== undefined)[0];
@ -763,7 +768,7 @@ export const useEventLoop = (
debounce(
combined_name,
() => queueEvents(events, socket),
event_actions.debounce,
event_actions.debounce
);
} else {
queueEvents(events, socket);
@ -782,7 +787,7 @@ export const useEventLoop = (
query,
asPath,
}))(router),
})),
}))
);
sentHydrate.current = true;
}
@ -828,7 +833,7 @@ export const useEventLoop = (
dispatch,
["websocket"],
setConnectErrors,
client_storage,
client_storage
);
}
}
@ -876,7 +881,7 @@ export const useEventLoop = (
vars[storage_to_state_map[e.key]] = e.newValue;
const event = Event(
`${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`,
{ vars: vars },
{ vars: vars }
);
addEvents([event], e);
}
@ -969,7 +974,7 @@ export const getRefValues = (refs) => {
return refs.map((ref) =>
ref.current
? ref.current.value || ref.current.getAttribute("aria-valuenow")
: null,
: null
);
};

View File

@ -13,6 +13,8 @@ import io
import json
import sys
import traceback
import urllib.parse
import uuid
from datetime import datetime
from pathlib import Path
from timeit import default_timer as timer
@ -75,7 +77,6 @@ from reflex.components.core.client_side_routing import (
from reflex.components.core.sticky import sticky
from reflex.components.core.upload import Upload, get_upload_dir
from reflex.components.radix import themes
from reflex.components.sonner.toast import toast
from reflex.config import ExecutorType, environment, get_config
from reflex.event import (
_EVENT_FIELDS,
@ -85,6 +86,7 @@ from reflex.event import (
EventType,
IndividualEventType,
get_hydrate_event,
window_alert,
)
from reflex.model import Model, get_db_status
from reflex.page import DECORATED_PAGES
@ -144,7 +146,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
EventSpec: The window alert event.
"""
from reflex.components.sonner.toast import toast
from reflex.components.sonner.toast import Toaster, toast
error = traceback.format_exc()
@ -155,16 +157,18 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
if is_prod_mode()
else [f"{type(exception).__name__}: {exception}.", "See logs for details."]
)
return toast(
"An error occurred.",
level="error",
fallback_to_alert=True,
description="<br/>".join(error_message),
position="top-center",
id="backend_error",
style={"width": "500px"},
)
if Toaster.is_used:
return toast(
"An error occurred.",
level="error",
description="<br/>".join(error_message),
position="top-center",
id="backend_error",
style={"width": "500px"},
)
else:
error_message.insert(0, "An error occurred.")
return window_alert("\n".join(error_message))
def extra_overlay_function() -> Optional[Component]:
@ -412,7 +416,7 @@ class App(MiddlewareMixin, LifespanMixin):
] = default_backend_exception_handler
# Put the toast provider in the app wrap.
toaster: Component | None = dataclasses.field(default_factory=toast.provider)
bundle_toaster: bool = True
@property
def api(self) -> FastAPI | None:
@ -1098,6 +1102,10 @@ class App(MiddlewareMixin, LifespanMixin):
should_compile = self._should_compile()
if not should_compile:
if self.bundle_toaster:
from reflex.components.sonner.toast import Toaster
Toaster.is_used = True
with console.timing("Evaluate Pages (Backend)"):
for route in self._unevaluated_pages:
console.debug(f"Evaluating page: {route}")
@ -1127,6 +1135,20 @@ class App(MiddlewareMixin, LifespanMixin):
+ adhoc_steps_without_executor,
)
if self.bundle_toaster:
from reflex.components.component import memo
from reflex.components.sonner.toast import toast
internal_toast_provider = toast.provider()
@memo
def memoized_toast_provider():
return internal_toast_provider
toast_provider = Fragment.create(memoized_toast_provider())
app_wrappers[(1, "ToasterProvider")] = toast_provider
with console.timing("Evaluate Pages (Frontend)"):
performance_metrics: list[tuple[str, float]] = []
for route in self._unevaluated_pages:
@ -1187,17 +1209,6 @@ class App(MiddlewareMixin, LifespanMixin):
# Add the custom components from the page to the set.
custom_components |= component._get_all_custom_components()
if (toaster := self.toaster) is not None:
from reflex.components.component import memo
@memo
def memoized_toast_provider():
return toaster
toast_provider = Fragment.create(memoized_toast_provider())
app_wrappers[(1, "ToasterProvider")] = toast_provider
# Add the app wraps to the app.
for key, app_wrap in self.app_wraps.items():
component = app_wrap(self._state is not None)
@ -1825,13 +1836,16 @@ class EventNamespace(AsyncNamespace):
self.sid_to_token = {}
self.app = app
def on_connect(self, sid: str, environ: dict):
async def on_connect(self, sid: str, environ: dict):
"""Event for when the websocket is connected.
Args:
sid: The Socket.IO session id.
environ: The request information, including HTTP headers.
"""
query_params = urllib.parse.parse_qs(environ.get("QUERY_STRING"))
await self.link_token_to_sid(sid, query_params.get("token", [])[0])
subprotocol = environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL")
if subprotocol and subprotocol != constants.Reflex.VERSION:
console.warn(
@ -1900,9 +1914,6 @@ class EventNamespace(AsyncNamespace):
f"Failed to deserialize event data: {fields}."
) from ex
self.token_to_sid[event.token] = sid
self.sid_to_token[sid] = event.token
# Get the event environment.
if self.app.sio is None:
raise RuntimeError("Socket.IO is not initialized.")
@ -1935,3 +1946,17 @@ class EventNamespace(AsyncNamespace):
"""
# Emit the test event.
await self.emit(str(constants.SocketEvent.PING), "pong", to=sid)
async def link_token_to_sid(self, sid: str, token: str):
"""Link a token to a session id.
Args:
sid: The Socket.IO session id.
token: The client token.
"""
if token in self.sid_to_token.values() and sid != self.token_to_sid.get(token):
token = str(uuid.uuid4())
await self.emit("new_token", token, to=sid)
self.token_to_sid[token] = sid
self.sid_to_token[sid] = token

View File

@ -11,9 +11,7 @@ from reflex.vars.base import Var, get_unique_variable_name
class AutoScroll(Div):
"""A div that automatically scrolls to the bottom when new content is added."""
_memoization_mode = MemoizationMode(
disposition=MemoizationDisposition.ALWAYS, recursive=False
)
_memoization_mode = MemoizationMode(disposition=MemoizationDisposition.ALWAYS)
@classmethod
def create(cls, *children, **props):
@ -46,6 +44,7 @@ class AutoScroll(Div):
"""
ref_name = self.get_ref()
return [
"const containerRef = useRef(null);",
"const wasNearBottom = useRef(false);",
"const hadScrollbar = useRef(false);",
f"""
@ -86,8 +85,6 @@ useEffect(() => {{
const container = {ref_name}.current;
if (!container) return;
scrollToBottomIfNeeded();
// Create ResizeObserver to detect height changes
const resizeObserver = new ResizeObserver(() => {{
scrollToBottomIfNeeded();

View File

@ -5,7 +5,6 @@ from __future__ import annotations
from typing import Optional
from reflex import constants
from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.core.cond import cond
from reflex.components.el.elements.typography import Div
@ -17,7 +16,7 @@ from reflex.components.radix.themes.components.dialog import (
)
from reflex.components.radix.themes.layout.flex import Flex
from reflex.components.radix.themes.typography.text import Text
from reflex.components.sonner.toast import ToastProps, toast_ref
from reflex.components.sonner.toast import Toaster, ToastProps
from reflex.config import environment
from reflex.constants import Dirs, Hooks, Imports
from reflex.constants.compiler import CompileVars
@ -91,7 +90,7 @@ def default_connection_error() -> list[str | Var | Component]:
]
class ConnectionToaster(Fragment):
class ConnectionToaster(Toaster):
"""A connection toaster component."""
def add_hooks(self) -> list[str | Var]:
@ -114,11 +113,11 @@ class ConnectionToaster(Fragment):
if environment.REFLEX_DOES_BACKEND_COLD_START.get():
loading_message = Var.create("Backend is starting.")
backend_is_loading_toast_var = Var(
f"toast?.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
f"toast.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
)
backend_is_not_responding = Var.create("Backend is not responding.")
backend_is_down_toast_var = Var(
f"toast?.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
f"toast.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
)
toast_var = Var(
f"""
@ -139,11 +138,10 @@ setTimeout(() => {{
f"Cannot connect to server: {connection_error}."
)
toast_var = Var(
f"toast?.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
f"toast.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
)
individual_hooks = [
Var(f"const toast = {toast_ref};"),
f"const toast_props = {LiteralVar.create(props)!s};",
"const [userDismissed, setUserDismissed] = useState(false);",
"const [waitedForBackend, setWaitedForBackend] = useState(false);",
@ -165,7 +163,7 @@ setTimeout(() => {{
{toast_var!s}
}}
}} else {{
toast?.dismiss("{toast_id}");
toast.dismiss("{toast_id}");
setUserDismissed(false); // after reconnection reset dismissed state
}}
}}
@ -191,6 +189,7 @@ setTimeout(() => {{
Returns:
The connection toaster component.
"""
Toaster.is_used = True
return super().create(*children, **props)

View File

@ -5,10 +5,10 @@
# ------------------------------------------------------
from typing import Any, Dict, Literal, Optional, Union, overload
from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.el.elements.typography import Div
from reflex.components.lucide.icon import Icon
from reflex.components.sonner.toast import Toaster, ToastProps
from reflex.constants.compiler import CompileVars
from reflex.event import EventType
from reflex.style import Style
@ -41,13 +41,48 @@ class WebsocketTargetURL(Var):
def default_connection_error() -> list[str | Var | Component]: ...
class ConnectionToaster(Fragment):
class ConnectionToaster(Toaster):
def add_hooks(self) -> list[str | Var]: ...
@overload
@classmethod
def create( # type: ignore
cls,
*children,
theme: Optional[Union[Var[str], str]] = None,
rich_colors: Optional[Union[Var[bool], bool]] = None,
expand: Optional[Union[Var[bool], bool]] = None,
visible_toasts: Optional[Union[Var[int], int]] = None,
position: Optional[
Union[
Literal[
"bottom-center",
"bottom-left",
"bottom-right",
"top-center",
"top-left",
"top-right",
],
Var[
Literal[
"bottom-center",
"bottom-left",
"bottom-right",
"top-center",
"top-left",
"top-right",
]
],
]
] = None,
close_button: Optional[Union[Var[bool], bool]] = None,
offset: Optional[Union[Var[str], str]] = None,
dir: Optional[Union[Var[str], str]] = None,
hotkey: Optional[Union[Var[str], str]] = None,
invert: Optional[Union[Var[bool], bool]] = None,
toast_options: Optional[Union[ToastProps, Var[ToastProps]]] = None,
gap: Optional[Union[Var[int], int]] = None,
loading_icon: Optional[Union[Icon, Var[Icon]]] = None,
pause_when_page_is_hidden: Optional[Union[Var[bool], bool]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
@ -75,6 +110,20 @@ class ConnectionToaster(Fragment):
Args:
*children: The children of the component.
theme: the theme of the toast
rich_colors: whether to show rich colors
expand: whether to expand the toast
visible_toasts: the number of toasts that are currently visible
position: the position of the toast
close_button: whether to show the close button
offset: offset of the toast
dir: directionality of the toast (default: ltr)
hotkey: Keyboard shortcut that will move focus to the toaster area.
invert: Dark toasts in light mode and vice versa.
toast_options: These will act as default options for all toasts. See toast() for all available options.
gap: Gap between toasts when expanded
loading_icon: Changes the default loading icon
pause_when_page_is_hidden: Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from typing import Any, Literal, Optional, Union
from typing import Any, ClassVar, Literal, Optional, Union
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
@ -17,7 +17,6 @@ from reflex.utils.serializers import serializer
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
from reflex.vars.function import FunctionVar
from reflex.vars.number import ternary_operation
from reflex.vars.object import ObjectVar
LiteralPosition = Literal[
@ -218,6 +217,9 @@ class Toaster(Component):
# Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
pause_when_page_is_hidden: Var[bool]
# Marked True when any Toast component is created.
is_used: ClassVar[bool] = False
def add_hooks(self) -> list[Var | str]:
"""Add hooks for the toaster component.
@ -239,17 +241,13 @@ class Toaster(Component):
@staticmethod
def send_toast(
message: str | Var = "",
level: str | None = None,
fallback_to_alert: bool = False,
**props,
message: str | Var = "", level: str | None = None, **props
) -> EventSpec:
"""Send a toast message.
Args:
message: The message to display.
level: The level of the toast.
fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
**props: The options for the toast.
Raises:
@ -258,6 +256,11 @@ class Toaster(Component):
Returns:
The toast event.
"""
if not Toaster.is_used:
raise ValueError(
"Toaster component must be created before sending a toast. (use `rx.toast.provider()`)"
)
toast_command = (
ObjectVar.__getattr__(toast_ref.to(dict), level) if level else toast_ref
).to(FunctionVar)
@ -274,21 +277,6 @@ class Toaster(Component):
else:
toast = toast_command.call(message)
if fallback_to_alert:
toast = ternary_operation(
toast_ref.bool(),
toast,
FunctionVar("window.alert").call(
Var.create(
message
if isinstance(message, str) and message
else props.get("title", props.get("description", ""))
)
.to(str)
.replace("<br/>", "\n")
),
)
return run_script(toast)
@staticmethod
@ -391,6 +379,7 @@ class Toaster(Component):
Returns:
The toaster component.
"""
cls.is_used = True
return super().create(*children, **props)

View File

@ -3,7 +3,7 @@
# ------------------- DO NOT EDIT ----------------------
# This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------
from typing import Any, Dict, Literal, Optional, Union, overload
from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
@ -60,13 +60,12 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
class Toaster(Component):
is_used: ClassVar[bool] = False
def add_hooks(self) -> list[Var | str]: ...
@staticmethod
def send_toast(
message: str | Var = "",
level: str | None = None,
fallback_to_alert: bool = False,
**props,
message: str | Var = "", level: str | None = None, **props
) -> EventSpec: ...
@staticmethod
def toast_info(message: str | Var = "", **kwargs: Any): ...
@ -186,17 +185,13 @@ class ToastNamespace(ComponentNamespace):
@staticmethod
def __call__(
message: Union[str, Var] = "",
level: Optional[str] = None,
fallback_to_alert: bool = False,
**props,
message: Union[str, Var] = "", level: Optional[str] = None, **props
) -> "EventSpec":
"""Send a toast message.
Args:
message: The message to display.
level: The level of the toast.
fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
**props: The options for the toast.
Raises:

View File

@ -195,7 +195,3 @@ class PackageJson(SimpleNamespace):
"postcss": "8.5.1",
"postcss-import": "16.1.0",
}
OVERRIDES = {
# This should always match the `react` version in DEPENDENCIES for recharts compatibility.
"react-is": "19.0.0"
}

View File

@ -846,7 +846,6 @@ def _compile_package_json():
},
dependencies=constants.PackageJson.DEPENDENCIES,
dev_dependencies=constants.PackageJson.DEV_DEPENDENCIES,
overrides=constants.PackageJson.OVERRIDES,
)

View File

@ -35,6 +35,7 @@ import reflex.config
from reflex import constants
from reflex.app import App
from reflex.base import Base
from reflex.components.sonner.toast import Toaster
from reflex.constants import CompileVars, RouteVar, SocketEvent
from reflex.event import Event, EventHandler
from reflex.state import (
@ -1612,20 +1613,29 @@ async def test_state_with_invalid_yield(capsys, mock_app):
rx.event.Event(token="fake_token", name="invalid_handler")
):
assert not update.delta
assert update.events == rx.event.fix_events(
[
rx.toast(
"An error occurred.",
level="error",
fallback_to_alert=True,
description="TypeError: Your handler test_state_with_invalid_yield.<locals>.StateWithInvalidYield.invalid_handler must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`).<br/>See logs for details.",
id="backend_error",
position="top-center",
style={"width": "500px"},
)
],
token="",
)
if Toaster.is_used:
assert update.events == rx.event.fix_events(
[
rx.toast(
"An error occurred.",
description="TypeError: Your handler test_state_with_invalid_yield.<locals>.StateWithInvalidYield.invalid_handler must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`).<br/>See logs for details.",
level="error",
id="backend_error",
position="top-center",
style={"width": "500px"},
)
],
token="",
)
else:
assert update.events == rx.event.fix_events(
[
rx.window_alert(
"An error occurred.\nContact the website administrator."
)
],
token="",
)
captured = capsys.readouterr()
assert "must only return/yield: None, Events or other EventHandlers" in captured.out