test_binding.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import copy
  2. import weakref
  3. from typing import Dict, Optional, Tuple
  4. from selenium.webdriver.common.keys import Keys
  5. from nicegui import binding, ui
  6. from nicegui.testing import Screen, User
  7. def test_ui_select_with_tuple_as_key(screen: Screen):
  8. class Model:
  9. selection: Optional[Tuple[int, int]] = None
  10. data = Model()
  11. options = {
  12. (2, 1): 'option A',
  13. (1, 2): 'option B',
  14. }
  15. data.selection = next(iter(options))
  16. ui.select(options).bind_value(data, 'selection')
  17. screen.open('/')
  18. screen.should_not_contain('option B')
  19. element = screen.click('option A')
  20. screen.click_at_position(element, x=20, y=100)
  21. screen.wait(0.3)
  22. screen.should_contain('option B')
  23. screen.should_not_contain('option A')
  24. assert data.selection == (1, 2)
  25. def test_ui_select_with_list_of_tuples(screen: Screen):
  26. class Model:
  27. selection = None
  28. data = Model()
  29. options = [(1, 1), (2, 2), (3, 3)]
  30. data.selection = options[0]
  31. ui.select(options).bind_value(data, 'selection')
  32. screen.open('/')
  33. screen.should_not_contain('2,2')
  34. element = screen.click('1,1')
  35. screen.click_at_position(element, x=20, y=100)
  36. screen.wait(0.3)
  37. screen.should_contain('2,2')
  38. screen.should_not_contain('1,1')
  39. assert data.selection == (2, 2)
  40. def test_ui_select_with_list_of_lists(screen: Screen):
  41. class Model:
  42. selection = None
  43. data = Model()
  44. options = [[1, 1], [2, 2], [3, 3]]
  45. data.selection = options[0]
  46. ui.select(options).bind_value(data, 'selection')
  47. screen.open('/')
  48. screen.should_not_contain('2,2')
  49. element = screen.click('1,1')
  50. screen.click_at_position(element, x=20, y=100)
  51. screen.wait(0.3)
  52. screen.should_contain('2,2')
  53. screen.should_not_contain('1,1')
  54. assert data.selection == [2, 2]
  55. def test_binding_to_input(screen: Screen):
  56. class Model:
  57. text = 'one'
  58. data = Model()
  59. element = ui.input().bind_value(data, 'text')
  60. screen.open('/')
  61. screen.should_contain_input('one')
  62. screen.type(Keys.TAB)
  63. screen.type('two')
  64. screen.should_contain_input('two')
  65. screen.wait(0.1)
  66. assert data.text == 'two'
  67. data.text = 'three'
  68. screen.should_contain_input('three')
  69. element.set_value('four')
  70. screen.should_contain_input('four')
  71. assert data.text == 'four'
  72. element.value = 'five'
  73. screen.should_contain_input('five')
  74. assert data.text == 'five'
  75. def test_binding_refresh_before_page_delivery(screen: Screen):
  76. state = {'count': 0}
  77. @ui.page('/')
  78. def main_page() -> None:
  79. ui.label().bind_text_from(state, 'count')
  80. state['count'] += 1
  81. screen.open('/')
  82. screen.should_contain('1')
  83. def test_missing_target_attribute(screen: Screen):
  84. data: Dict = {}
  85. ui.label('Hello').bind_text_to(data)
  86. ui.label().bind_text_from(data, 'text', lambda text: f'{text=}')
  87. screen.open('/')
  88. screen.should_contain("text='Hello'")
  89. def test_bindable_dataclass(screen: Screen):
  90. @binding.bindable_dataclass(bindable_fields=['bindable'])
  91. class TestClass:
  92. not_bindable: str = 'not_bindable_text'
  93. bindable: str = 'bindable_text'
  94. instance = TestClass()
  95. ui.label().bind_text_from(instance, 'not_bindable')
  96. ui.label().bind_text_from(instance, 'bindable')
  97. screen.open('/')
  98. screen.should_contain('not_bindable_text')
  99. screen.should_contain('bindable_text')
  100. assert len(binding.bindings) == 2
  101. assert len(binding.active_links) == 1
  102. assert binding.active_links[0][1] == 'not_bindable'
  103. async def test_copy_instance_with_bindable_property(user: User):
  104. @binding.bindable_dataclass
  105. class Number:
  106. value: int = 1
  107. x = Number()
  108. y = copy.copy(x)
  109. ui.label().bind_text_from(x, 'value', lambda v: f'x={v}')
  110. assert len(binding.bindings) == 1
  111. assert len(binding.active_links) == 0
  112. ui.label().bind_text_from(y, 'value', lambda v: f'y={v}')
  113. assert len(binding.bindings) == 2
  114. assert len(binding.active_links) == 0
  115. await user.open('/')
  116. await user.should_see('x=1')
  117. await user.should_see('y=1')
  118. y.value = 2
  119. await user.should_see('x=1')
  120. await user.should_see('y=2')
  121. def test_automatic_cleanup(screen: Screen):
  122. class Model:
  123. value = binding.BindableProperty()
  124. def __init__(self, value: str) -> None:
  125. self.value = value
  126. def create_model_and_label(value: str) -> Tuple[Model, weakref.ref, ui.label]:
  127. model = Model(value)
  128. label = ui.label(value).bind_text(model, 'value')
  129. return id(model), weakref.ref(model), label
  130. model_id1, ref1, label1 = create_model_and_label('first label')
  131. model_id2, ref2, _label2 = create_model_and_label('second label')
  132. def is_alive(ref: weakref.ref) -> bool:
  133. return ref() is not None
  134. def has_bindable_property(model_id: int) -> bool:
  135. return any(obj_id == model_id for obj_id, _ in binding.bindable_properties)
  136. screen.open('/')
  137. screen.should_contain('first label')
  138. screen.should_contain('second label')
  139. assert is_alive(ref1) and has_bindable_property(model_id1)
  140. assert is_alive(ref2) and has_bindable_property(model_id2)
  141. binding.remove([label1])
  142. assert not is_alive(ref1) and not has_bindable_property(model_id1)
  143. assert is_alive(ref2) and has_bindable_property(model_id2)