test_table.py 12 KB

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