Skip to content

Commit 64c7666

Browse files
committed
ui: tests for nested components
1 parent 8b15475 commit 64c7666

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed

tests/test_ui_nested.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015-present Rapptz
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a
7+
copy of this software and associated documentation files (the "Software"),
8+
to deal in the Software without restriction, including without limitation
9+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
10+
and/or sell copies of the Software, and to permit persons to whom the
11+
Software is furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22+
DEALINGS IN THE SOFTWARE.
23+
"""
24+
25+
from dataclasses import dataclass
26+
from typing import TYPE_CHECKING, Any
27+
import discord
28+
import pytest
29+
import pytest_asyncio
30+
31+
if TYPE_CHECKING:
32+
from pytest_mock import MockerFixture
33+
34+
35+
@dataclass
36+
class ViewFixture:
37+
layout: discord.ui.LayoutView
38+
container: discord.ui.Container
39+
row: discord.ui.ActionRow
40+
item: discord.ui.Item
41+
42+
43+
@pytest.fixture(params=["button", "select"])
44+
def ui_item(request: pytest.FixtureRequest):
45+
if request.param == "button":
46+
return (discord.ui.Button, discord.ui.button)
47+
elif request.param == "select":
48+
return (discord.ui.Select, discord.ui.select)
49+
50+
51+
@pytest_asyncio.fixture(params=["class", "constructor", "add_item"])
52+
async def view(
53+
request: pytest.FixtureRequest, ui_item: "tuple[type[discord.ui.Item], Any]"
54+
) -> ViewFixture:
55+
item_type, decorator = ui_item
56+
57+
async def on_error(interaction, error: Exception, item):
58+
# do not let errors (especially asserts) be silenced by the default on_error handler
59+
raise error
60+
61+
def check_callback(interaction: discord.Interaction, button: discord.ui.Item):
62+
row = button.parent
63+
assert isinstance(row, discord.ui.ActionRow)
64+
container = row.parent
65+
assert isinstance(container, discord.ui.Container)
66+
assert container.parent is None
67+
assert button.view == row.view == container.view is not None
68+
69+
class Item(item_type):
70+
async def callback(self, interaction):
71+
check_callback(interaction, self)
72+
73+
# return a tuple of layout-container-actionrow-button, pre-configured in different ways
74+
if request.param == "class":
75+
76+
class Row(discord.ui.ActionRow):
77+
@decorator()
78+
async def item(self, interaction, item: discord.ui.Item):
79+
assert item.parent == self
80+
check_callback(interaction, item)
81+
82+
class Container(discord.ui.Container):
83+
myrow = Row()
84+
85+
class Layout(discord.ui.LayoutView):
86+
container = Container()
87+
88+
async def on_error(self, interaction, error: Exception, item) -> None:
89+
await on_error(interaction, error, item)
90+
91+
layout = Layout()
92+
container = layout.container
93+
row = container.myrow
94+
item = row.item
95+
96+
elif request.param == "constructor":
97+
item = Item()
98+
row = discord.ui.ActionRow(item)
99+
container = discord.ui.Container(row)
100+
layout = discord.ui.LayoutView()
101+
layout.on_error = on_error
102+
layout.add_item(container)
103+
104+
elif request.param == "add_item":
105+
item = Item()
106+
row = discord.ui.ActionRow()
107+
row.add_item(item)
108+
container = discord.ui.Container()
109+
container.add_item(row)
110+
layout = discord.ui.LayoutView()
111+
layout.on_error = on_error
112+
layout.add_item(container)
113+
114+
return ViewFixture(layout, container, row, item)
115+
116+
117+
# test that all "parent" attributes are properly set
118+
def test_parent(view: ViewFixture):
119+
assert view.container.parent is None
120+
assert view.row.parent == view.container
121+
assert view.item.parent == view.row
122+
123+
124+
# test that all "view" attributes are properly set
125+
def test_view(view: ViewFixture):
126+
assert view.layout is not None
127+
assert view.container.view == view.layout
128+
assert view.row.view == view.layout
129+
assert view.item.view == view.layout
130+
131+
132+
@pytest.mark.asyncio
133+
async def test_dispatch(view: ViewFixture, mocker: "MockerFixture"):
134+
spy1 = mocker.spy(view.layout, "interaction_check")
135+
spy2 = mocker.spy(view.container, "interaction_check")
136+
spy3 = mocker.spy(view.row, "interaction_check")
137+
spy4 = mocker.spy(view.item, "interaction_check")
138+
139+
interaction = mocker.NonCallableMagicMock(spec=discord.Interaction)
140+
task = view.layout._dispatch_item(view.item, interaction)
141+
assert task is not None
142+
# let the task finish and retrieve any potential exception
143+
await task
144+
exc = task.exception()
145+
if exc:
146+
raise exc
147+
148+
# verify that ALL interaction_check methods are being called
149+
spy1.assert_awaited_once_with(interaction)
150+
spy2.assert_awaited_once_with(interaction)
151+
spy3.assert_awaited_once_with(interaction)
152+
spy4.assert_awaited_once_with(interaction)

0 commit comments

Comments
 (0)