personal-setting-avatar.spec.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { within } from '@testing-library/vue'
  3. import * as VueUse from '@vueuse/core'
  4. import { defineComponent, ref } from 'vue'
  5. import { visitView } from '#tests/support/components/visitView.ts'
  6. import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'
  7. import { waitForNextTick } from '#tests/support/utils.ts'
  8. import { mockUserCurrentAvatarAddMutation } from '#shared/entities/user/current/graphql/mutations/userCurrentAvatarAdd.mocks.ts'
  9. import {
  10. mockUserCurrentAvatarDeleteMutation,
  11. waitForUserCurrentAvatarDeleteMutationCalls,
  12. } from '#shared/entities/user/current/graphql/mutations/userCurrentAvatarDelete.mocks.ts'
  13. import {
  14. mockUserCurrentAvatarSelectMutation,
  15. waitForUserCurrentAvatarSelectMutationCalls,
  16. } from '../graphql/mutations/userCurrentAvatarSelect.mocks.ts'
  17. import { mockUserCurrentAvatarListQuery } from '../graphql/queries/userCurrentAvatarList.mocks.ts'
  18. vi.mock('vue-advanced-cropper', () => {
  19. const Cropper = defineComponent({
  20. emits: ['change'],
  21. mounted() {
  22. this.$emit('change', {
  23. canvas: {
  24. toDataURL() {
  25. return 'cropped image url'
  26. },
  27. },
  28. })
  29. },
  30. template: '<div></div>',
  31. })
  32. return {
  33. Cropper,
  34. }
  35. })
  36. describe('avatar personal settings', () => {
  37. beforeEach(() => {
  38. mockUserCurrent({
  39. firstname: 'John',
  40. lastname: 'Doe',
  41. })
  42. })
  43. it('shows all the avatars of the current user', async () => {
  44. mockUserCurrentAvatarListQuery({
  45. userCurrentAvatarList: [
  46. {
  47. default: true,
  48. initial: true,
  49. deletable: false,
  50. },
  51. {
  52. default: false,
  53. initial: false,
  54. deletable: true,
  55. },
  56. {
  57. default: false,
  58. initial: false,
  59. deletable: true,
  60. },
  61. ],
  62. })
  63. const view = await visitView('/personal-setting/avatar')
  64. const mainContent = within(view.getByRole('main'))
  65. const avatars = await mainContent.findAllByTestId('common-avatar')
  66. expect(avatars).toHaveLength(3)
  67. expect(view.getByRole('button', { name: 'Upload' })).toBeInTheDocument()
  68. expect(view.getByRole('button', { name: 'Camera' })).toBeInTheDocument()
  69. })
  70. it('can select an avatar to be the new default one', async () => {
  71. mockUserCurrentAvatarListQuery({
  72. userCurrentAvatarList: [
  73. {
  74. default: true,
  75. initial: true,
  76. deletable: false,
  77. },
  78. {
  79. default: false,
  80. initial: false,
  81. deletable: true,
  82. },
  83. {
  84. default: false,
  85. initial: false,
  86. deletable: true,
  87. },
  88. ],
  89. })
  90. const view = await visitView('/personal-setting/avatar')
  91. const mainContent = within(view.getByRole('main'))
  92. let avatars = await mainContent.findAllByTestId('common-avatar')
  93. expect(avatars[0]).toHaveClass('avatar-selected')
  94. mockUserCurrentAvatarSelectMutation({
  95. userCurrentAvatarSelect: {
  96. success: true,
  97. },
  98. })
  99. await view.events.click(avatars[1])
  100. const calls = await waitForUserCurrentAvatarSelectMutationCalls()
  101. expect(calls).toHaveLength(1)
  102. avatars = await mainContent.findAllByTestId('common-avatar')
  103. expect(avatars[0]).not.toHaveClass('avatar-selected')
  104. expect(avatars[1]).toHaveClass('avatar-selected')
  105. })
  106. it('can delete an avatar', async () => {
  107. mockUserCurrentAvatarListQuery({
  108. userCurrentAvatarList: [
  109. {
  110. default: true,
  111. initial: true,
  112. deletable: false,
  113. },
  114. {
  115. default: false,
  116. initial: false,
  117. deletable: true,
  118. },
  119. ],
  120. })
  121. const view = await visitView('/personal-setting/avatar')
  122. const mainContent = within(view.getByRole('main'))
  123. let avatars = await mainContent.findAllByTestId('common-avatar')
  124. expect(avatars).toHaveLength(2)
  125. const deleteButton = await view.findByRole('button', {
  126. name: 'Delete this avatar',
  127. })
  128. expect(deleteButton).toBeInTheDocument()
  129. mockUserCurrentAvatarDeleteMutation({
  130. userCurrentAvatarDelete: {
  131. success: true,
  132. },
  133. })
  134. await view.events.click(deleteButton)
  135. await waitForNextTick()
  136. expect(
  137. await view.findByRole('dialog', { name: 'Delete Object' }),
  138. ).toBeInTheDocument()
  139. await view.events.click(view.getByRole('button', { name: 'Delete Object' }))
  140. const calls = await waitForUserCurrentAvatarDeleteMutationCalls()
  141. expect(calls).toHaveLength(1)
  142. avatars = await mainContent.findAllByTestId('common-avatar')
  143. expect(avatars).toHaveLength(1)
  144. expect(avatars[0]).toHaveTextContent('JD')
  145. })
  146. it('upload new avatar by file', async () => {
  147. mockUserCurrentAvatarListQuery({
  148. userCurrentAvatarList: [
  149. {
  150. default: true,
  151. initial: true,
  152. deletable: false,
  153. },
  154. ],
  155. })
  156. const view = await visitView('/personal-setting/avatar')
  157. const mainContent = within(view.getByRole('main'))
  158. let avatars = await mainContent.findAllByTestId('common-avatar')
  159. expect(avatars).toHaveLength(1)
  160. expect(avatars[0]).toHaveClass('avatar-selected')
  161. const fileUploadButton = view.getByRole('button', {
  162. name: 'Upload',
  163. })
  164. expect(fileUploadButton).toBeInTheDocument()
  165. const file = new File([], 'test.jpg', { type: 'image/jpeg' })
  166. await view.events.upload(view.getByTestId('fileUploadInput'), file)
  167. await waitForNextTick()
  168. const flyout = await view.findByRole('complementary', {
  169. name: 'Crop Image',
  170. })
  171. expect(flyout).toBeInTheDocument()
  172. const flyoutContent = within(flyout)
  173. expect(
  174. await flyoutContent.findByTestId('common-avatar'),
  175. ).toBeInTheDocument()
  176. mockUserCurrentAvatarAddMutation({
  177. userCurrentAvatarAdd: {
  178. avatar: {
  179. default: true,
  180. initial: true,
  181. deletable: false,
  182. },
  183. },
  184. })
  185. await view.events.click(view.getByRole('button', { name: 'Save' }))
  186. avatars = await mainContent.findAllByTestId('common-avatar')
  187. expect(avatars).toHaveLength(2)
  188. expect(avatars[0]).not.toHaveClass('avatar-selected')
  189. expect(avatars[1]).toHaveClass('avatar-selected')
  190. })
  191. describe('with camera flyout', () => {
  192. let mockPermissionState = 'granted'
  193. let originalMediaDevices: MediaDevices
  194. beforeAll(() => {
  195. originalMediaDevices = navigator.mediaDevices
  196. // Redefine mediaDevices to be writable
  197. Object.defineProperty(navigator, 'mediaDevices', {
  198. writable: true,
  199. value: {
  200. getUserMedia: vi.fn().mockResolvedValue({
  201. getTracks: () => [
  202. {
  203. kind: 'video',
  204. stop: vi.fn(),
  205. },
  206. ],
  207. }),
  208. },
  209. })
  210. vi.spyOn(VueUse, 'usePermission').mockImplementation(() => {
  211. // Return a mock ref object based on the permission you are testing
  212. // You can control the returned value based on the permissionName if needed
  213. return ref(
  214. mockPermissionState,
  215. ) as unknown as VueUse.UsePermissionReturnWithControls
  216. })
  217. })
  218. afterAll(() => {
  219. // Restore original mediaDevices
  220. Object.defineProperty(navigator, 'mediaDevices', {
  221. writable: true,
  222. value: originalMediaDevices,
  223. })
  224. })
  225. beforeEach(() => {
  226. mockUserCurrentAvatarListQuery({
  227. userCurrentAvatarList: [
  228. {
  229. default: true,
  230. initial: true,
  231. deletable: false,
  232. },
  233. ],
  234. })
  235. })
  236. it('upload new avatar by camera', async () => {
  237. const view = await visitView('/personal-setting/avatar')
  238. const mainContent = within(view.getByRole('main'))
  239. let avatars = await mainContent.findAllByTestId('common-avatar')
  240. expect(avatars).toHaveLength(1)
  241. expect(avatars[0]).toHaveClass('avatar-selected')
  242. const cameraButton = view.getByRole('button', {
  243. name: 'Camera',
  244. })
  245. await view.events.click(cameraButton)
  246. const flyout = await view.findByRole('complementary', {
  247. name: 'Camera',
  248. })
  249. expect(flyout).toBeInTheDocument()
  250. expect(
  251. await view.findByLabelText(
  252. 'Use the camera to take a photo for the avatar.',
  253. ),
  254. ).toBeInTheDocument()
  255. const captureButton = view.getByRole('button', {
  256. name: 'Capture From Camera',
  257. })
  258. await view.events.click(captureButton)
  259. mockUserCurrentAvatarAddMutation({
  260. userCurrentAvatarAdd: {
  261. avatar: {
  262. default: true,
  263. initial: false,
  264. deletable: false,
  265. },
  266. },
  267. })
  268. await view.events.click(view.getByRole('button', { name: 'Save' }))
  269. avatars = await mainContent.findAllByTestId('common-avatar')
  270. expect(avatars).toHaveLength(2)
  271. expect(avatars[0]).not.toHaveClass('avatar-selected')
  272. expect(avatars[1]).toHaveClass('avatar-selected')
  273. })
  274. it('should show forbidden access for camera', async () => {
  275. mockPermissionState = 'denied'
  276. const view = await visitView('/personal-setting/avatar')
  277. const cameraButton = view.getByRole('button', {
  278. name: 'Camera',
  279. })
  280. await view.events.click(cameraButton)
  281. const flyout = await view.findByRole('complementary', {
  282. name: 'Camera',
  283. })
  284. expect(flyout).toBeInTheDocument()
  285. expect(
  286. view.getByText(
  287. 'Accessing your camera is forbidden. Please check your settings.',
  288. ),
  289. ).toBeInTheDocument()
  290. })
  291. })
  292. })