test_table.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. from datetime import datetime, timedelta, timezone
  2. from typing import List
  3. import pandas as pd
  4. from selenium.webdriver.common.by import By
  5. from nicegui import ui
  6. from nicegui.testing import Screen
  7. def columns() -> List:
  8. return [
  9. {'name': 'name', 'label': 'Name', 'field': 'name', 'required': True},
  10. {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True},
  11. ]
  12. def rows() -> List:
  13. return [
  14. {'id': 0, 'name': 'Alice', 'age': 18},
  15. {'id': 1, 'name': 'Bob', 'age': 21},
  16. {'id': 2, 'name': 'Lionel', 'age': 19},
  17. ]
  18. def test_table(screen: Screen):
  19. ui.table(title='My Team', columns=columns(), rows=rows())
  20. screen.open('/')
  21. screen.should_contain('My Team')
  22. screen.should_contain('Name')
  23. screen.should_contain('Alice')
  24. screen.should_contain('Bob')
  25. screen.should_contain('Lionel')
  26. def test_pagination_int(screen: Screen):
  27. ui.table(columns=columns(), rows=rows(), pagination=2)
  28. screen.open('/')
  29. screen.should_contain('Alice')
  30. screen.should_contain('Bob')
  31. screen.should_not_contain('Lionel')
  32. screen.should_contain('1-2 of 3')
  33. def test_pagination_dict(screen: Screen):
  34. ui.table(columns=columns(), rows=rows(), pagination={'rowsPerPage': 2})
  35. screen.open('/')
  36. screen.should_contain('Alice')
  37. screen.should_contain('Bob')
  38. screen.should_not_contain('Lionel')
  39. screen.should_contain('1-2 of 3')
  40. def test_filter(screen: Screen):
  41. table = ui.table(columns=columns(), rows=rows())
  42. ui.input('Search by name').bind_value(table, 'filter')
  43. screen.open('/')
  44. screen.should_contain('Alice')
  45. screen.should_contain('Bob')
  46. screen.should_contain('Lionel')
  47. element = screen.selenium.find_element(By.XPATH, '//*[@aria-label="Search by name"]')
  48. element.send_keys('e')
  49. screen.should_contain('Alice')
  50. screen.should_not_contain('Bob')
  51. screen.should_contain('Lionel')
  52. def test_add_remove(screen: Screen):
  53. table = ui.table(columns=columns(), rows=rows())
  54. ui.button('Add', on_click=lambda: table.add_row({'id': 3, 'name': 'Carol', 'age': 32}))
  55. ui.button('Remove', on_click=lambda: table.remove_row(table.rows[0]))
  56. screen.open('/')
  57. screen.click('Add')
  58. screen.should_contain('Carol')
  59. screen.click('Remove')
  60. screen.wait(0.5)
  61. screen.should_not_contain('Alice')
  62. def test_slots(screen: Screen):
  63. with ui.table(columns=columns(), rows=rows()) as table:
  64. with table.add_slot('top-row'):
  65. with table.row():
  66. with table.cell():
  67. ui.label('This is the top slot.')
  68. table.add_slot('body', '''
  69. <q-tr :props="props">
  70. <q-td key="name" :props="props">overridden</q-td>
  71. <q-td key="age" :props="props">
  72. <q-badge color="green">{{ props.row.age }}</q-badge>
  73. </q-td>
  74. </q-tr>
  75. ''')
  76. screen.open('/')
  77. screen.should_contain('This is the top slot.')
  78. screen.should_not_contain('Alice')
  79. screen.should_contain('overridden')
  80. screen.should_contain('21')
  81. def test_single_selection(screen: Screen):
  82. ui.table(columns=columns(), rows=rows(), selection='single')
  83. screen.open('/')
  84. screen.find('Alice').find_element(By.XPATH, 'preceding-sibling::td').click()
  85. screen.wait(0.5)
  86. screen.should_contain('1 record selected.')
  87. screen.find('Bob').find_element(By.XPATH, 'preceding-sibling::td').click()
  88. screen.wait(0.5)
  89. screen.should_contain('1 record selected.')
  90. def test_dynamic_column_attributes(screen: Screen):
  91. ui.table(columns=[{'name': 'age', 'label': 'Age', 'field': 'age', ':format': 'value => value + " years"'}],
  92. rows=[{'name': 'Alice', 'age': 18}])
  93. screen.open('/')
  94. screen.should_contain('18 years')
  95. def test_remove_selection(screen: Screen):
  96. t = ui.table(columns=columns(), rows=rows(), selection='single')
  97. ui.button('Remove first row', on_click=lambda: t.remove_row(t.rows[0]))
  98. screen.open('/')
  99. screen.find('Alice').find_element(By.XPATH, 'preceding-sibling::td').click()
  100. screen.should_contain('1 record selected.')
  101. screen.click('Remove first row')
  102. screen.wait(0.5)
  103. screen.should_not_contain('Alice')
  104. screen.should_not_contain('1 record selected.')
  105. def test_replace_rows(screen: Screen):
  106. t = ui.table(columns=columns(), rows=rows())
  107. def replace_rows_with_carol():
  108. t.rows = [{'id': 3, 'name': 'Carol', 'age': 32}]
  109. def replace_rows_with_daniel():
  110. t.update_rows([{'id': 4, 'name': 'Daniel', 'age': 33}])
  111. ui.button('Replace rows with C.', on_click=replace_rows_with_carol)
  112. ui.button('Replace rows with D.', on_click=replace_rows_with_daniel)
  113. screen.open('/')
  114. screen.should_contain('Alice')
  115. screen.should_contain('Bob')
  116. screen.should_contain('Lionel')
  117. screen.click('Replace rows with C.')
  118. screen.wait(0.5)
  119. screen.should_not_contain('Alice')
  120. screen.should_not_contain('Bob')
  121. screen.should_not_contain('Lionel')
  122. screen.should_contain('Carol')
  123. screen.click('Replace rows with D.')
  124. screen.wait(0.5)
  125. screen.should_not_contain('Carol')
  126. screen.should_contain('Daniel')
  127. def test_create_and_update_from_pandas(screen: Screen):
  128. df = pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [18, 21]})
  129. table = ui.table.from_pandas(df)
  130. def update():
  131. df.loc[2] = ['Lionel', 19]
  132. table.update_from_pandas(df)
  133. ui.button('Update', on_click=update)
  134. screen.open('/')
  135. screen.should_contain('Alice')
  136. screen.should_contain('Bob')
  137. screen.should_contain('18')
  138. screen.should_contain('21')
  139. screen.click('Update')
  140. screen.should_contain('Lionel')
  141. screen.should_contain('19')
  142. def test_problematic_datatypes(screen: Screen):
  143. df = pd.DataFrame({
  144. 'Datetime_col': [datetime(2020, 1, 1)],
  145. 'Datetime_col_tz': [datetime(2020, 1, 1, tzinfo=timezone.utc)],
  146. 'Timedelta_col': [timedelta(days=5)],
  147. 'Complex_col': [1 + 2j],
  148. 'Period_col': pd.Series([pd.Period('2021-01')]),
  149. })
  150. ui.table.from_pandas(df)
  151. screen.open('/')
  152. screen.should_contain('Datetime_col')
  153. screen.should_contain('Datetime_col_tz')
  154. screen.should_contain('Timedelta_col')
  155. screen.should_contain('Complex_col')
  156. screen.should_contain('Period_col')
  157. screen.should_contain('2020-01-01')
  158. screen.should_contain('5 days')
  159. screen.should_contain('(1+2j)')
  160. screen.should_contain('2021-01')
  161. def test_table_computed_props(screen: Screen):
  162. all_rows = rows()
  163. filtered_rows = [row for row in all_rows if 'e' in row['name']]
  164. filtered_sorted_rows = sorted(filtered_rows, key=lambda row: row['age'], reverse=True)
  165. @ui.page('/')
  166. async def page():
  167. table = ui.table(
  168. columns=columns(),
  169. rows=all_rows,
  170. row_key='id',
  171. selection='multiple',
  172. pagination={'rowsPerPage': 1, 'sortBy': 'age', 'descending': True})
  173. table.filter = 'e'
  174. await ui.context.client.connected()
  175. assert filtered_sorted_rows == await table.get_filtered_sorted_rows()
  176. assert filtered_sorted_rows[:1] == await table.get_computed_rows()
  177. assert len(filtered_sorted_rows) == await table.get_computed_rows_number()
  178. screen.open('/')
  179. screen.should_contain('Lionel')
  180. screen.should_not_contain('Alice')
  181. screen.should_not_contain('Bob')
  182. def test_infer_columns(screen: Screen):
  183. ui.table(rows=[
  184. {'name': 'Alice', 'age': 18},
  185. {'name': 'Bob', 'age': 21},
  186. ])
  187. screen.open('/')
  188. screen.should_contain('NAME')
  189. screen.should_contain('AGE')
  190. screen.should_contain('Alice')
  191. screen.should_contain('Bob')
  192. screen.should_contain('18')
  193. screen.should_contain('21')
  194. def test_default_column_parameters(screen: Screen):
  195. ui.table(rows=[
  196. {'name': 'Alice', 'age': 18, 'city': 'London'},
  197. {'name': 'Bob', 'age': 21, 'city': 'Paris'},
  198. ], columns=[
  199. {'name': 'name', 'label': 'Name', 'field': 'name'},
  200. {'name': 'age', 'label': 'Age', 'field': 'age'},
  201. {'name': 'city', 'label': 'City', 'field': 'city', 'sortable': False},
  202. ], column_defaults={'sortable': True})
  203. screen.open('/')
  204. screen.should_contain('Name')
  205. screen.should_contain('Age')
  206. screen.should_contain('Alice')
  207. screen.should_contain('Bob')
  208. screen.should_contain('18')
  209. screen.should_contain('21')
  210. screen.should_contain('London')
  211. screen.should_contain('Paris')
  212. assert len(screen.find_all_by_class('sortable')) == 2
  213. def test_columns_from_df(screen: Screen):
  214. persons = ui.table.from_pandas(pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [18, 21]}))
  215. cars = ui.table.from_pandas(pd.DataFrame({'make': ['Ford', 'Toyota'], 'model': ['Focus', 'Corolla']}),
  216. columns=[{'name': 'make', 'label': 'make', 'field': 'make'}])
  217. ui.button('Update persons without columns',
  218. on_click=lambda: persons.update_from_pandas(pd.DataFrame({'name': ['Dan'], 'age': [5], 'sex': ['male']})))
  219. ui.button('Update persons with columns',
  220. on_click=lambda: persons.update_from_pandas(pd.DataFrame({'name': ['Stephen'], 'age': [33]}),
  221. columns=[{'name': 'name', 'label': 'Name', 'field': 'name'}]))
  222. ui.button('Update cars without columns',
  223. on_click=lambda: cars.update_from_pandas(pd.DataFrame({'make': ['Honda'], 'model': ['Civic']})))
  224. ui.button('Update cars with columns',
  225. on_click=lambda: cars.update_from_pandas(pd.DataFrame({'make': ['Hyundai'], 'model': ['i30']}),
  226. columns=[{'name': 'make', 'label': 'make', 'field': 'make'},
  227. {'name': 'model', 'label': 'model', 'field': 'model'}]))
  228. screen.open('/')
  229. screen.should_contain('name')
  230. screen.should_contain('age')
  231. screen.should_contain('make')
  232. screen.should_not_contain('model')
  233. screen.click('Update persons without columns') # infer columns (like during instantiation)
  234. screen.should_contain('Dan')
  235. screen.should_contain('5')
  236. screen.should_contain('male')
  237. screen.click('Update persons with columns') # updated columns via parameter
  238. screen.should_contain('Stephen')
  239. screen.should_not_contain('32')
  240. screen.click('Update cars without columns') # don't change columns
  241. screen.should_contain('Honda')
  242. screen.should_not_contain('Civic')
  243. screen.click('Update cars with columns') # updated columns via parameter
  244. screen.should_contain('Hyundai')
  245. screen.should_contain('i30')