pycascadeui 3.3.3__tar.gz → 3.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pycascadeui-3.3.3/pycascadeui.egg-info → pycascadeui-3.3.4}/PKG-INFO +1 -1
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/__init__.py +1 -1
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/_navigation.py +33 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/base.py +12 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4/pycascadeui.egg-info}/PKG-INFO +1 -1
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/pyproject.toml +1 -1
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_layout_view.py +70 -1
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_navigation.py +63 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/LICENSE +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/README.md +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/__main__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/base.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/buttons.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/inputs.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/patterns/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/patterns/v1.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/patterns/v2.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/selects.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/types.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/v1_composition.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/components/wrappers.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/devtools.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/exceptions.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/backends/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/backends/memory.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/backends/postgres.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/backends/sqlite.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/config.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/manager.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/migrations.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/protocols.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/schema.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/persistence/schema_postgres.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/py.typed +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/setup.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/actions.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/computed.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/middleware/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/middleware/logging.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/middleware/persistence.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/middleware/undo.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/reducers.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/singleton.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/slots.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/store.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/state/types.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/theming/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/theming/context.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/theming/core.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/theming/themes.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/tracing.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/utils/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/utils/coercion.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/utils/decorators.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/utils/errors.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/utils/fetch.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/utils/logging.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/utils/strings.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/utils/tasks.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/validation.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/_interaction.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/_placement.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/layout.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/patterns/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/patterns/form.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/patterns/leaderboard.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/patterns/menu.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/patterns/paginated.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/patterns/roles.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/patterns/tabs.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/patterns/types.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/patterns/wizard.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/persistent.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/cascadeui/views/view.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/pycascadeui.egg-info/SOURCES.txt +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/pycascadeui.egg-info/dependency_links.txt +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/pycascadeui.egg-info/requires.txt +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/pycascadeui.egg-info/top_level.txt +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/setup.cfg +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_auto_defer.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_backends.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_backends_postgres.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_backends_raw_sql.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_batching.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_button_grid.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_components.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_computed.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_devtools.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_dynamic_persistent_button.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_emoji_grid.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_ephemeral_refresh.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_fetch.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_five_pillars.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_form_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_hooks.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_input_coercion.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_inputs.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_instance_limit.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_layout_pagination.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_layout_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_layout_persistent.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_leaderboard_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_menu_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_message_cleanup.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_middleware.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_nav_version.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_navigation_artifact_restore.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_owner_only.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_paginated_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_pagination.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_persistence.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_persistence_middleware.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_persistent_views.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_pillar_isolation.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_placement.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_rebuild_callback.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_reducers.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_roles_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_scoping.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_selects.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_state_slots.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_state_store.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_tab_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_theming.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_undo.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_update_coalescing.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_v2_helpers.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_validation.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_view_init.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_wizard_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.3.4}/tests/test_wrappers.py +0 -0
|
@@ -395,6 +395,39 @@ class _NavigationMixin:
|
|
|
395
395
|
# populates the tree post-init.
|
|
396
396
|
new_view._check_placement()
|
|
397
397
|
|
|
398
|
+
# Push/pop reuse the parent's Discord message, carried onto
|
|
399
|
+
# new_view._message in the navigation batch above. That message --
|
|
400
|
+
# not the acting interaction's -- is the navigation edit target.
|
|
401
|
+
target_message = new_view._message or self._message
|
|
402
|
+
|
|
403
|
+
# The acting interaction normally IS a click on target_message. A
|
|
404
|
+
# with_confirmation wrapper, though, runs the original callback with
|
|
405
|
+
# the confirm-button interaction, whose message is the ephemeral
|
|
406
|
+
# prompt -- a different message. Both interaction-coupled paths below
|
|
407
|
+
# (response.edit_message and edit_original_response) would then render
|
|
408
|
+
# the new view onto the prompt and leave target_message stale. When
|
|
409
|
+
# the interaction carries a message that differs from the view's,
|
|
410
|
+
# edit target_message directly through the channel endpoint instead.
|
|
411
|
+
# Interactions with no message (slash commands) keep the
|
|
412
|
+
# edit_original_response path -- their original response IS the view's
|
|
413
|
+
# message.
|
|
414
|
+
interaction_msg_id = getattr(getattr(current_interaction, "message", None), "id", None)
|
|
415
|
+
if (
|
|
416
|
+
target_message is not None
|
|
417
|
+
and interaction_msg_id is not None
|
|
418
|
+
and interaction_msg_id != target_message.id
|
|
419
|
+
):
|
|
420
|
+
# Ack the foreign interaction so the click does not error
|
|
421
|
+
# (_safe_defer no-ops when a wrapper already consumed the slot),
|
|
422
|
+
# then edit the view's own message. refresh() carries the
|
|
423
|
+
# cooldown/backoff/digest machinery and its own acting-fast-path
|
|
424
|
+
# gate re-confirms the message match, so it cannot misfire here.
|
|
425
|
+
await self._safe_defer(current_interaction)
|
|
426
|
+
if new_view._message is None:
|
|
427
|
+
new_view._message = target_message
|
|
428
|
+
await new_view.refresh(**edit_kwargs)
|
|
429
|
+
return
|
|
430
|
+
|
|
398
431
|
# Fast path: a component/modal interaction whose response slot is still
|
|
399
432
|
# open edits + acks in one request -- no separate defer round-trip, which
|
|
400
433
|
# was the source of the navigation pause. Bounded at the ack deadline via
|
|
@@ -1452,6 +1452,17 @@ class _StatefulMixin(_InteractionMixin, _NavigationMixin):
|
|
|
1452
1452
|
parts.append(("t", item.content))
|
|
1453
1453
|
# Buttons and selects: record the wire-visible attributes.
|
|
1454
1454
|
elif hasattr(item, "custom_id"):
|
|
1455
|
+
# A select's rendered selection lives in opt.default, which
|
|
1456
|
+
# none of the scalar attributes above capture. set_selected()
|
|
1457
|
+
# mutates exactly that field, so a selection-only rebuild
|
|
1458
|
+
# would otherwise hash identical and refresh() would drop the
|
|
1459
|
+
# re-render. Walk options (string selects only -- auto-populated
|
|
1460
|
+
# UserSelect/RoleSelect/etc. expose no .options, so the getattr
|
|
1461
|
+
# returns None for them and for buttons).
|
|
1462
|
+
options = getattr(item, "options", None)
|
|
1463
|
+
option_state = (
|
|
1464
|
+
tuple((opt.value, opt.default) for opt in options) if options else None
|
|
1465
|
+
)
|
|
1455
1466
|
parts.append(
|
|
1456
1467
|
(
|
|
1457
1468
|
"i",
|
|
@@ -1463,6 +1474,7 @@ class _StatefulMixin(_InteractionMixin, _NavigationMixin):
|
|
|
1463
1474
|
getattr(item, "url", None),
|
|
1464
1475
|
getattr(item, "placeholder", None),
|
|
1465
1476
|
str(getattr(item, "emoji", None)) if getattr(item, "emoji", None) else None,
|
|
1477
|
+
option_state,
|
|
1466
1478
|
)
|
|
1467
1479
|
)
|
|
1468
1480
|
# Other layout items (Separator, MediaGallery, Thumbnail) do
|
|
@@ -10,7 +10,7 @@ import pytest
|
|
|
10
10
|
from discord.ui import ActionRow, LayoutView, TextDisplay
|
|
11
11
|
from helpers import make_interaction as _make_interaction
|
|
12
12
|
|
|
13
|
-
from cascadeui.components.base import StatefulButton
|
|
13
|
+
from cascadeui.components.base import StatefulButton, StatefulSelect
|
|
14
14
|
from cascadeui.state.singleton import get_store
|
|
15
15
|
from cascadeui.state.store import _CURRENT_INTERACTION
|
|
16
16
|
from cascadeui.views.base import _StatefulMixin, _view_class_registry
|
|
@@ -764,6 +764,75 @@ class TestRenderHashShortCircuit:
|
|
|
764
764
|
after = view._compute_tree_digest()
|
|
765
765
|
assert before != after
|
|
766
766
|
|
|
767
|
+
def test_digest_reflects_select_selection(self):
|
|
768
|
+
"""A select's rendered selection lives in opt.default, which the
|
|
769
|
+
scalar wire attributes (placeholder/disabled/...) do not capture.
|
|
770
|
+
A selection-only rebuild must change the digest, or refresh()
|
|
771
|
+
short-circuits and the re-render is silently dropped -- the client
|
|
772
|
+
keeps the stale selection and the next interaction submits stale
|
|
773
|
+
values.
|
|
774
|
+
"""
|
|
775
|
+
selected = ["a"]
|
|
776
|
+
|
|
777
|
+
def build(view):
|
|
778
|
+
view.clear_items()
|
|
779
|
+
view.add_item(
|
|
780
|
+
ActionRow(
|
|
781
|
+
StatefulSelect(
|
|
782
|
+
placeholder="Pick",
|
|
783
|
+
options=[
|
|
784
|
+
discord.SelectOption(
|
|
785
|
+
label="A", value="a", default=(selected[0] == "a")
|
|
786
|
+
),
|
|
787
|
+
discord.SelectOption(
|
|
788
|
+
label="B", value="b", default=(selected[0] == "b")
|
|
789
|
+
),
|
|
790
|
+
],
|
|
791
|
+
min_values=0,
|
|
792
|
+
max_values=1,
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
view = self._make_view_with_build(build)
|
|
798
|
+
view.build_ui()
|
|
799
|
+
before = view._compute_tree_digest()
|
|
800
|
+
|
|
801
|
+
selected[0] = "b"
|
|
802
|
+
view.build_ui()
|
|
803
|
+
after = view._compute_tree_digest()
|
|
804
|
+
assert before != after
|
|
805
|
+
|
|
806
|
+
def test_digest_reflects_set_selected(self):
|
|
807
|
+
"""set_selected() is the canonical selection-mutation API; it
|
|
808
|
+
rewrites opt.default in place without rebuilding the component.
|
|
809
|
+
The digest must reflect the change so the same select object
|
|
810
|
+
flipped to a new selection re-renders.
|
|
811
|
+
"""
|
|
812
|
+
select = StatefulSelect(
|
|
813
|
+
placeholder="Pick",
|
|
814
|
+
options=[
|
|
815
|
+
discord.SelectOption(label="A", value="a"),
|
|
816
|
+
discord.SelectOption(label="B", value="b"),
|
|
817
|
+
],
|
|
818
|
+
min_values=0,
|
|
819
|
+
max_values=1,
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
def build(view):
|
|
823
|
+
view.clear_items()
|
|
824
|
+
view.add_item(ActionRow(select))
|
|
825
|
+
|
|
826
|
+
view = self._make_view_with_build(build)
|
|
827
|
+
select.set_selected("a")
|
|
828
|
+
view.build_ui()
|
|
829
|
+
before = view._compute_tree_digest()
|
|
830
|
+
|
|
831
|
+
select.set_selected("b")
|
|
832
|
+
view.build_ui()
|
|
833
|
+
after = view._compute_tree_digest()
|
|
834
|
+
assert before != after
|
|
835
|
+
|
|
767
836
|
async def test_refresh_skips_edit_when_digest_matches(self):
|
|
768
837
|
def build(view):
|
|
769
838
|
view.clear_items()
|
|
@@ -538,6 +538,69 @@ class TestNavigationFastPath:
|
|
|
538
538
|
nav.edit_original_response.assert_awaited_once()
|
|
539
539
|
|
|
540
540
|
|
|
541
|
+
class TestNavigationForeignInteraction:
|
|
542
|
+
"""A with_confirmation wrapper runs the navigation callback with the
|
|
543
|
+
confirm-button interaction, whose message is the ephemeral prompt --
|
|
544
|
+
a different message than the view's dashboard. Navigation must edit the
|
|
545
|
+
view's own message via the channel endpoint, not the foreign prompt."""
|
|
546
|
+
|
|
547
|
+
async def test_push_with_foreign_interaction_edits_view_message(self):
|
|
548
|
+
class _Root(StatefulView):
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
class _Sub(StatefulView):
|
|
552
|
+
async def on_state_changed(self, state):
|
|
553
|
+
pass
|
|
554
|
+
|
|
555
|
+
root = _Root(interaction=_make_interaction(user_id=1, guild_id=100))
|
|
556
|
+
await root.send()
|
|
557
|
+
# Pin a concrete dashboard message id for the foreign comparison.
|
|
558
|
+
dashboard = MagicMock(id=555, channel=MagicMock(id=2))
|
|
559
|
+
dashboard.edit = AsyncMock()
|
|
560
|
+
root._message = dashboard
|
|
561
|
+
|
|
562
|
+
# Confirm-button click on the ephemeral prompt (different message id),
|
|
563
|
+
# slot already consumed by the prompt edit -- the with_confirmation shape.
|
|
564
|
+
prompt = MagicMock(id=999)
|
|
565
|
+
confirm = _make_interaction(user_id=1, guild_id=100, is_done=True, message=prompt)
|
|
566
|
+
confirm.edit_original_response = AsyncMock()
|
|
567
|
+
|
|
568
|
+
sub = await root.push(_Sub, interaction=confirm)
|
|
569
|
+
|
|
570
|
+
# The view's own message is edited; the foreign prompt is untouched.
|
|
571
|
+
dashboard.edit.assert_awaited()
|
|
572
|
+
confirm.edit_original_response.assert_not_called()
|
|
573
|
+
confirm.response.edit_message.assert_not_called()
|
|
574
|
+
assert sub._message is dashboard
|
|
575
|
+
|
|
576
|
+
async def test_pop_with_foreign_interaction_edits_view_message(self):
|
|
577
|
+
class _Root(StatefulView):
|
|
578
|
+
pass
|
|
579
|
+
|
|
580
|
+
class _Sub(StatefulView):
|
|
581
|
+
async def on_state_changed(self, state):
|
|
582
|
+
pass
|
|
583
|
+
|
|
584
|
+
root = _Root(interaction=_make_interaction(user_id=1, guild_id=100))
|
|
585
|
+
await root.send()
|
|
586
|
+
dashboard = MagicMock(id=555, channel=MagicMock(id=2))
|
|
587
|
+
dashboard.edit = AsyncMock()
|
|
588
|
+
root._message = dashboard
|
|
589
|
+
|
|
590
|
+
sub = await root.push(_Sub)
|
|
591
|
+
|
|
592
|
+
prompt = MagicMock(id=999)
|
|
593
|
+
confirm = _make_interaction(user_id=1, guild_id=100, is_done=True, message=prompt)
|
|
594
|
+
confirm.edit_original_response = AsyncMock()
|
|
595
|
+
|
|
596
|
+
restored = await sub.pop(confirm)
|
|
597
|
+
|
|
598
|
+
assert restored is not None
|
|
599
|
+
dashboard.edit.assert_awaited()
|
|
600
|
+
confirm.edit_original_response.assert_not_called()
|
|
601
|
+
confirm.response.edit_message.assert_not_called()
|
|
602
|
+
|
|
603
|
+
|
|
541
604
|
class TestNavigationUndoTransfer:
|
|
542
605
|
"""Push/pop forward-transfer the undo/redo stacks to the new view's state
|
|
543
606
|
row so the undo timeline stays continuous across navigation. The transfer
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|