left-sidebar.spec.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. import {
  3. fireEvent,
  4. getAllByRole,
  5. getByLabelText,
  6. getByRole,
  7. } from '@testing-library/vue'
  8. import { flushPromises } from '@vue/test-utils'
  9. import { visitView } from '#tests/support/components/visitView.ts'
  10. import { mockPermissions } from '#tests/support/mock-permissions.ts'
  11. import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'
  12. import { mockLogoutMutation } from '#shared/graphql/mutations/logout.mocks.ts'
  13. describe('Left sidebar', () => {
  14. beforeEach(() => {
  15. mockUserCurrent({
  16. id: 'gid://zammad/User/999',
  17. firstname: 'Nicole',
  18. lastname: 'Braun',
  19. fullname: 'Nicole Braun',
  20. preferences: {},
  21. })
  22. })
  23. afterEach(() => {
  24. localStorage.clear()
  25. })
  26. describe('width handling', () => {
  27. it('renders initially with the default width', async () => {
  28. const view = await visitView('/')
  29. const aside = view.getByRole('complementary')
  30. expect(aside.parentElement).toHaveStyle({
  31. gridTemplateColumns: '260px 1fr',
  32. })
  33. })
  34. it('restores stored width', async () => {
  35. localStorage.setItem('gid://zammad/User/999-left-sidebar-width', '216')
  36. const view = await visitView('/')
  37. const aside = view.getByRole('complementary')
  38. expect(aside.parentElement).toHaveStyle({
  39. gridTemplateColumns: '216px 1fr',
  40. })
  41. })
  42. it('supports collapsing/expanding', async () => {
  43. const view = await visitView('/')
  44. const aside = view.getByRole('complementary')
  45. const collapseButton = getByRole(aside, 'button', {
  46. name: 'Collapse sidebar',
  47. })
  48. await view.events.click(collapseButton)
  49. expect(aside.parentElement).toHaveStyle({
  50. gridTemplateColumns: '56px 1fr',
  51. })
  52. const expandButton = getByRole(aside, 'button', {
  53. name: 'Expand sidebar',
  54. })
  55. await view.events.click(expandButton)
  56. expect(aside.parentElement).toHaveStyle({
  57. gridTemplateColumns: '260px 1fr',
  58. })
  59. })
  60. it('restores collapsed state width', async () => {
  61. localStorage.setItem(
  62. 'gid://zammad/User/999-left-sidebar-collapsed',
  63. 'true',
  64. )
  65. const view = await visitView('/')
  66. const aside = view.getByRole('complementary')
  67. expect(aside.parentElement).toHaveStyle({
  68. gridTemplateColumns: '56px 1fr',
  69. })
  70. })
  71. it('supports resizing', async () => {
  72. const view = await visitView('/')
  73. const aside = view.getByRole('complementary')
  74. const resizeHandle = getByLabelText(aside, 'Resize sidebar')
  75. await fireEvent.mouseDown(resizeHandle, { clientX: 260 })
  76. await fireEvent.mouseMove(document, { clientX: 216 })
  77. await fireEvent.mouseUp(document, { clientX: 216 })
  78. expect(aside.parentElement).toHaveStyle({
  79. gridTemplateColumns: '216px 1fr',
  80. })
  81. })
  82. it('supports resetting', async () => {
  83. localStorage.setItem('gid://zammad/User/999-left-sidebar-width', '216')
  84. const view = await visitView('/')
  85. const aside = view.getByRole('complementary')
  86. const resizeHandle = getByLabelText(aside, 'Resize sidebar')
  87. await view.events.dblClick(resizeHandle)
  88. expect(aside.parentElement).toHaveStyle({
  89. gridTemplateColumns: '260px 1fr',
  90. })
  91. })
  92. })
  93. describe('User menu', () => {
  94. afterEach(() => {
  95. vi.clearAllMocks()
  96. })
  97. it.each([{ collapsed: false }, { collapsed: true }])(
  98. 'shows menu popover on click (collapsed: $collapsed)',
  99. async ({ collapsed }) => {
  100. localStorage.setItem(
  101. 'gid://zammad/User/999-left-sidebar-collapsed',
  102. String(collapsed),
  103. )
  104. const view = await visitView('/')
  105. const aside = view.getByRole('complementary')
  106. const avatarButton = getByRole(aside, 'button', {
  107. name: 'Nicole Braun',
  108. })
  109. expect(avatarButton).toHaveTextContent('NB')
  110. await view.events.click(avatarButton)
  111. const popover = view.getByRole('region', { name: 'Nicole Braun' })
  112. expect(popover).toHaveTextContent('Nicole Braun')
  113. const menu = getByRole(popover, 'menu')
  114. const menuItems = getAllByRole(menu, 'menuitem')
  115. expect(menuItems).toHaveLength(4)
  116. },
  117. )
  118. it('supports cycling appearance state', async () => {
  119. mockPermissions(['user_preferences.appearance'])
  120. const view = await visitView('/')
  121. const aside = view.getByRole('complementary')
  122. const avatarButton = getByRole(aside, 'button', { name: 'Nicole Braun' })
  123. await view.events.click(avatarButton)
  124. const appearanceButton = view.getByRole('button', { name: 'Appearance' })
  125. const appearanceSwitch = view.getByRole('checkbox', { name: 'Dark Mode' })
  126. expect(appearanceSwitch).toBePartiallyChecked()
  127. await view.events.click(appearanceSwitch)
  128. expect(appearanceSwitch).toBeChecked()
  129. await view.events.click(appearanceButton)
  130. expect(appearanceSwitch).not.toBeChecked()
  131. await view.events.click(appearanceSwitch)
  132. expect(appearanceSwitch).toBePartiallyChecked()
  133. })
  134. it('supports navigating to playground', async () => {
  135. const view = await visitView('/')
  136. const aside = view.getByRole('complementary')
  137. const avatarButton = getByRole(aside, 'button', { name: 'Nicole Braun' })
  138. await view.events.click(avatarButton)
  139. const playgroundLink = view.getByRole('link', {
  140. name: 'Playground',
  141. })
  142. await view.events.click(playgroundLink)
  143. await vi.waitFor(() => {
  144. expect(view, 'correctly redirects to playground page').toHaveCurrentUrl(
  145. '/playground',
  146. )
  147. })
  148. expect(
  149. view.queryByRole('region', { name: 'User menu' }),
  150. ).not.toBeInTheDocument()
  151. })
  152. // TODO: Cover keyboard shortcuts menu item when ready.
  153. it('supports navigating to personal settings', async () => {
  154. const view = await visitView('/')
  155. const aside = view.getByRole('complementary')
  156. const avatarButton = getByRole(aside, 'button', { name: 'Nicole Braun' })
  157. await view.events.click(avatarButton)
  158. const personalSettingsLink = view.getByRole('link', {
  159. name: 'Profile settings',
  160. })
  161. await view.events.click(personalSettingsLink)
  162. await vi.waitFor(() => {
  163. expect(
  164. view,
  165. 'correctly redirects to personal settings page',
  166. ).toHaveCurrentUrl('/personal-setting/appearance')
  167. })
  168. expect(
  169. view.queryByRole('region', { name: 'User menu' }),
  170. ).not.toBeInTheDocument()
  171. })
  172. it('supports signing out', async () => {
  173. const view = await visitView('/')
  174. const aside = view.getByRole('complementary')
  175. const avatarButton = getByRole(aside, 'button', { name: 'Nicole Braun' })
  176. await view.events.click(avatarButton)
  177. const logoutLink = view.getByRole('link', { name: 'Sign out' })
  178. mockLogoutMutation({
  179. logout: {
  180. success: true,
  181. externalLogoutUrl: null,
  182. },
  183. })
  184. await view.events.click(logoutLink)
  185. await flushPromises()
  186. await vi.waitFor(() => {
  187. expect(view, 'correctly redirects to login page').toHaveCurrentUrl(
  188. '/login',
  189. )
  190. })
  191. expect(
  192. view.queryByRole('region', { name: 'User menu' }),
  193. ).not.toBeInTheDocument()
  194. })
  195. })
  196. })