test_table.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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_selection(screen: Screen):
  85. table = ui.table(columns=columns(), rows=rows(), selection='single')
  86. ui.radio({None: 'none', 'single': 'single', 'multiple': 'multiple'},
  87. on_change=lambda e: table.set_selection(e.value))
  88. screen.open('/')
  89. screen.find('Alice').find_element(By.XPATH, 'preceding-sibling::td').click()
  90. screen.wait(0.5)
  91. screen.should_contain('1 record selected.')
  92. screen.find('Bob').find_element(By.XPATH, 'preceding-sibling::td').click()
  93. screen.wait(0.5)
  94. screen.should_contain('1 record selected.')
  95. screen.click('multiple')
  96. screen.wait(0.5)
  97. screen.find('Lionel').find_element(By.XPATH, 'preceding-sibling::td').click()
  98. screen.wait(0.5)
  99. screen.should_contain('2 records selected.')
  100. assert table.selection == 'multiple'
  101. screen.click('none')
  102. screen.wait(0.5)
  103. screen.should_not_contain('1 record selected.')
  104. assert table.selection is None
  105. def test_dynamic_column_attributes(screen: Screen):
  106. ui.table(columns=[{'name': 'age', 'label': 'Age', 'field': 'age', ':format': 'value => value + " years"'}],
  107. rows=[{'name': 'Alice', 'age': 18}])
  108. screen.open('/')
  109. screen.should_contain('18 years')
  110. def test_remove_selection(screen: Screen):
  111. t = ui.table(columns=columns(), rows=rows(), selection='single')
  112. ui.button('Remove first row', on_click=lambda: t.remove_row(t.rows[0]))
  113. screen.open('/')
  114. screen.find('Alice').find_element(By.XPATH, 'preceding-sibling::td').click()
  115. screen.should_contain('1 record selected.')
  116. screen.click('Remove first row')
  117. screen.wait(0.5)
  118. screen.should_not_contain('Alice')
  119. screen.should_not_contain('1 record selected.')
  120. def test_replace_rows(screen: Screen):
  121. t = ui.table(columns=columns(), rows=rows())
  122. def replace_rows_with_carol():
  123. t.rows = [{'id': 3, 'name': 'Carol', 'age': 32}]
  124. def replace_rows_with_daniel():
  125. t.update_rows([{'id': 4, 'name': 'Daniel', 'age': 33}])
  126. ui.button('Replace rows with C.', on_click=replace_rows_with_carol)
  127. ui.button('Replace rows with D.', on_click=replace_rows_with_daniel)
  128. screen.open('/')
  129. screen.should_contain('Alice')
  130. screen.should_contain('Bob')
  131. screen.should_contain('Lionel')
  132. screen.click('Replace rows with C.')
  133. screen.wait(0.5)
  134. screen.should_not_contain('Alice')
  135. screen.should_not_contain('Bob')
  136. screen.should_not_contain('Lionel')
  137. screen.should_contain('Carol')
  138. screen.click('Replace rows with D.')
  139. screen.wait(0.5)
  140. screen.should_not_contain('Carol')
  141. screen.should_contain('Daniel')
  142. @pytest.mark.parametrize('df_type', ['pandas', 'polars'])
  143. def test_create_and_update_from_df(screen: Screen, df_type: str):
  144. if df_type == 'pandas':
  145. DataFrame = pd.DataFrame
  146. df = DataFrame({'name': ['Alice', 'Bob'], 'age': [18, 21]})
  147. table = ui.table.from_pandas(df)
  148. update_from_df = table.update_from_pandas
  149. else:
  150. DataFrame = pl.DataFrame
  151. df = DataFrame({'name': ['Alice', 'Bob'], 'age': [18, 21]})
  152. table = ui.table.from_polars(df)
  153. update_from_df = table.update_from_polars
  154. ui.button('Update', on_click=lambda: update_from_df(DataFrame({'name': ['Lionel'], 'age': [19]})))
  155. screen.open('/')
  156. screen.should_contain('Alice')
  157. screen.should_contain('Bob')
  158. screen.should_contain('18')
  159. screen.should_contain('21')
  160. screen.click('Update')
  161. screen.should_contain('Lionel')
  162. screen.should_contain('19')
  163. @pytest.mark.parametrize('df_type', ['pandas', 'polars'])
  164. @pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason='Skipping test for Python 3.8')
  165. def test_problematic_datatypes(screen: Screen, df_type: str):
  166. if df_type == 'pandas':
  167. df = pd.DataFrame({
  168. 'Datetime_col': [datetime(2020, 1, 1)],
  169. 'Datetime_col_tz': [datetime(2020, 1, 2, tzinfo=timezone.utc)],
  170. 'Timedelta_col': [timedelta(days=5)],
  171. 'Complex_col': [1 + 2j],
  172. 'Period_col': pd.Series([pd.Period('2021-01')]),
  173. })
  174. ui.table.from_pandas(df)
  175. else:
  176. df = pl.DataFrame({
  177. 'Datetime_col': [datetime(2020, 1, 1)],
  178. 'Datetime_col_tz': [datetime(2020, 1, 2, tzinfo=timezone.utc)],
  179. })
  180. ui.table.from_polars(df)
  181. screen.open('/')
  182. screen.should_contain('Datetime_col')
  183. screen.should_contain('2020-01-01')
  184. screen.should_contain('Datetime_col_tz')
  185. screen.should_contain('2020-01-02')
  186. if df_type == 'pandas':
  187. screen.should_contain('Timedelta_col')
  188. screen.should_contain('5 days')
  189. screen.should_contain('Complex_col')
  190. screen.should_contain('(1+2j)')
  191. screen.should_contain('Period_col')
  192. screen.should_contain('2021-01')
  193. def test_table_computed_props(screen: Screen):
  194. all_rows = rows()
  195. filtered_rows = [row for row in all_rows if 'e' in row['name']]
  196. filtered_sorted_rows = sorted(filtered_rows, key=lambda row: row['age'], reverse=True)
  197. @ui.page('/')
  198. async def page():
  199. table = ui.table(
  200. columns=columns(),
  201. rows=all_rows,
  202. row_key='id',
  203. selection='multiple',
  204. pagination={'rowsPerPage': 1, 'sortBy': 'age', 'descending': True})
  205. table.filter = 'e'
  206. await ui.context.client.connected()
  207. assert filtered_sorted_rows == await table.get_filtered_sorted_rows()
  208. assert filtered_sorted_rows[:1] == await table.get_computed_rows()
  209. assert len(filtered_sorted_rows) == await table.get_computed_rows_number()
  210. screen.open('/')
  211. screen.should_contain('Lionel')
  212. screen.should_not_contain('Alice')
  213. screen.should_not_contain('Bob')
  214. def test_infer_columns(screen: Screen):
  215. ui.table(rows=[
  216. {'name': 'Alice', 'age': 18},
  217. {'name': 'Bob', 'age': 21},
  218. ])
  219. screen.open('/')
  220. screen.should_contain('NAME')
  221. screen.should_contain('AGE')
  222. screen.should_contain('Alice')
  223. screen.should_contain('Bob')
  224. screen.should_contain('18')
  225. screen.should_contain('21')
  226. def test_default_column_parameters(screen: Screen):
  227. ui.table(rows=[
  228. {'name': 'Alice', 'age': 18, 'city': 'London'},
  229. {'name': 'Bob', 'age': 21, 'city': 'Paris'},
  230. ], columns=[
  231. {'name': 'name', 'label': 'Name', 'field': 'name'},
  232. {'name': 'age', 'label': 'Age', 'field': 'age'},
  233. {'name': 'city', 'label': 'City', 'field': 'city', 'sortable': False},
  234. ], column_defaults={'sortable': True})
  235. screen.open('/')
  236. screen.should_contain('Name')
  237. screen.should_contain('Age')
  238. screen.should_contain('Alice')
  239. screen.should_contain('Bob')
  240. screen.should_contain('18')
  241. screen.should_contain('21')
  242. screen.should_contain('London')
  243. screen.should_contain('Paris')
  244. assert len(screen.find_all_by_class('sortable')) == 2
  245. @pytest.mark.parametrize('df_type', ['pandas', 'polars'])
  246. def test_columns_from_df(screen: Screen, df_type: str):
  247. if df_type == 'pandas':
  248. persons = ui.table.from_pandas(pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [18, 21]}))
  249. cars = ui.table.from_pandas(pd.DataFrame({'make': ['Ford', 'Toyota'], 'model': ['Focus', 'Corolla']}),
  250. columns=[{'name': 'make', 'label': 'make', 'field': 'make'}])
  251. DataFrame = pd.DataFrame
  252. update_persons_from_df = persons.update_from_pandas
  253. update_cars_from_df = cars.update_from_pandas
  254. else:
  255. persons = ui.table.from_polars(pl.DataFrame({'name': ['Alice', 'Bob'], 'age': [18, 21]}))
  256. cars = ui.table.from_polars(pl.DataFrame({'make': ['Ford', 'Toyota'], 'model': ['Focus', 'Corolla']}),
  257. columns=[{'name': 'make', 'label': 'make', 'field': 'make'}])
  258. DataFrame = pl.DataFrame
  259. update_persons_from_df = persons.update_from_polars
  260. update_cars_from_df = cars.update_from_polars
  261. ui.button('Update persons without columns',
  262. on_click=lambda: update_persons_from_df(DataFrame({'name': ['Dan'], 'age': [5], 'sex': ['male']})))
  263. ui.button('Update persons with columns',
  264. on_click=lambda: update_persons_from_df(DataFrame({'name': ['Stephen'], 'age': [33]}),
  265. columns=[{'name': 'name', 'label': 'Name', 'field': 'name'}]))
  266. ui.button('Update cars without columns',
  267. on_click=lambda: update_cars_from_df(DataFrame({'make': ['Honda'], 'model': ['Civic']})))
  268. ui.button('Update cars with columns',
  269. on_click=lambda: update_cars_from_df(DataFrame({'make': ['Hyundai'], 'model': ['i30']}),
  270. columns=[{'name': 'make', 'label': 'make', 'field': 'make'},
  271. {'name': 'model', 'label': 'model', 'field': 'model'}]))
  272. screen.open('/')
  273. screen.should_contain('name')
  274. screen.should_contain('age')
  275. screen.should_contain('make')
  276. screen.should_not_contain('model')
  277. screen.click('Update persons without columns') # infer columns (like during instantiation)
  278. screen.should_contain('Dan')
  279. screen.should_contain('5')
  280. screen.should_contain('male')
  281. screen.click('Update persons with columns') # updated columns via parameter
  282. screen.should_contain('Stephen')
  283. screen.should_not_contain('32')
  284. screen.click('Update cars without columns') # don't change columns
  285. screen.should_contain('Honda')
  286. screen.should_not_contain('Civic')
  287. screen.click('Update cars with columns') # updated columns via parameter
  288. screen.should_contain('Hyundai')
  289. screen.should_contain('i30')