project-notifications-setting.test.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import { screen, render, waitFor } from '@testing-library/react'
  2. import { expect } from 'chai'
  3. import fetchMock from 'fetch-mock'
  4. import userEvent from '@testing-library/user-event'
  5. import { EditorProviders, PROJECT_ID } from '../../../helpers/editor-providers'
  6. import { SettingsModalProvider } from '@/features/settings/context/settings-modal-context'
  7. import ProjectNotificationsSetting from '@/features/settings/components/editor-settings/project-notifications-setting'
  8. const preferencesUrl = `/notifications/preferences/project/${PROJECT_ID}`
  9. const allNotificationsOn = {
  10. commentOnOwnProject: true,
  11. commentOnInvitedProject: true,
  12. repliesOnAuthoredThread: true,
  13. repliesOnParticipatingThread: true,
  14. commentResolvedOnAuthoredThread: true,
  15. commentResolvedOnParticipatingThread: true,
  16. commentReopenedOnAuthoredThread: true,
  17. commentReopenedOnParticipatingThread: true,
  18. trackedChangesOnOwnProject: true,
  19. trackedChangesOnInvitedProject: true,
  20. trackChangesAcceptedOnAuthoredChange: true,
  21. trackChangesRejectedOnAuthoredChange: true,
  22. }
  23. const repliesOnlyPreferences = {
  24. commentOnOwnProject: false,
  25. commentOnInvitedProject: false,
  26. repliesOnAuthoredThread: true,
  27. repliesOnParticipatingThread: true,
  28. commentResolvedOnAuthoredThread: true,
  29. commentResolvedOnParticipatingThread: true,
  30. commentReopenedOnAuthoredThread: true,
  31. commentReopenedOnParticipatingThread: true,
  32. trackedChangesOnOwnProject: false,
  33. trackedChangesOnInvitedProject: false,
  34. trackChangesAcceptedOnAuthoredChange: true,
  35. trackChangesRejectedOnAuthoredChange: true,
  36. }
  37. const globallyMutedPreferences = {
  38. trackedChangesOnOwnProject: false,
  39. trackedChangesOnInvitedProject: false,
  40. commentOnOwnProject: false,
  41. commentOnInvitedProject: false,
  42. repliesOnAuthoredThread: false,
  43. repliesOnParticipatingThread: false,
  44. muteAllNotifications: true,
  45. commentResolvedOnAuthoredThread: false,
  46. commentResolvedOnParticipatingThread: false,
  47. commentReopenedOnAuthoredThread: false,
  48. commentReopenedOnParticipatingThread: false,
  49. trackChangesAcceptedOnAuthoredChange: false,
  50. trackChangesRejectedOnAuthoredChange: false,
  51. }
  52. const allNotificationsOff = {
  53. commentOnOwnProject: false,
  54. commentOnInvitedProject: false,
  55. repliesOnAuthoredThread: false,
  56. repliesOnParticipatingThread: false,
  57. commentResolvedOnAuthoredThread: false,
  58. commentResolvedOnParticipatingThread: false,
  59. commentReopenedOnAuthoredThread: false,
  60. commentReopenedOnParticipatingThread: false,
  61. trackedChangesOnOwnProject: false,
  62. trackedChangesOnInvitedProject: false,
  63. trackChangesAcceptedOnAuthoredChange: false,
  64. trackChangesRejectedOnAuthoredChange: false,
  65. }
  66. const defaultPreferences = {
  67. trackedChangesOnOwnProject: true,
  68. trackedChangesOnInvitedProject: false,
  69. commentOnOwnProject: true,
  70. commentOnInvitedProject: false,
  71. repliesOnAuthoredThread: true,
  72. repliesOnParticipatingThread: true,
  73. muteAllNotifications: false,
  74. commentResolvedOnAuthoredThread: true,
  75. commentResolvedOnParticipatingThread: true,
  76. commentReopenedOnAuthoredThread: true,
  77. commentReopenedOnParticipatingThread: true,
  78. trackChangesAcceptedOnAuthoredChange: true,
  79. }
  80. function renderComponent(props: { permissionsLevel?: string } = {}) {
  81. return render(
  82. <EditorProviders permissionsLevel={props.permissionsLevel as any}>
  83. <SettingsModalProvider>
  84. <ProjectNotificationsSetting />
  85. </SettingsModalProvider>
  86. </EditorProviders>
  87. )
  88. }
  89. describe('<ProjectNotificationsSetting />', function () {
  90. afterEach(function () {
  91. fetchMock.removeRoutes().clearHistory()
  92. })
  93. it('shows loading indicator while preferences are loading', async function () {
  94. fetchMock.get(preferencesUrl, new Promise(() => {}))
  95. renderComponent()
  96. expect(await screen.findByRole('status')).to.exist
  97. expect(screen.queryByLabelText('All project activity', { exact: false })).to
  98. .not.exist
  99. })
  100. it('selects "All project activity" when all notifications are on', async function () {
  101. fetchMock.get(preferencesUrl, allNotificationsOn)
  102. renderComponent()
  103. await waitFor(
  104. () =>
  105. expect(
  106. (
  107. screen.getByLabelText('All project activity', {
  108. exact: false,
  109. }) as HTMLInputElement
  110. ).checked
  111. ).to.be.true
  112. )
  113. expect(
  114. (
  115. screen.getByLabelText('Replies to your activity only', {
  116. exact: false,
  117. }) as HTMLInputElement
  118. ).checked
  119. ).to.be.false
  120. expect(
  121. (screen.getByLabelText('Off', { exact: false }) as HTMLInputElement)
  122. .checked
  123. ).to.be.false
  124. })
  125. it('selects "Replies to your activity only" when only reply notifications are on', async function () {
  126. fetchMock.get(preferencesUrl, repliesOnlyPreferences)
  127. renderComponent()
  128. await waitFor(
  129. () =>
  130. expect(
  131. (
  132. screen.getByLabelText('Replies to your activity only', {
  133. exact: false,
  134. }) as HTMLInputElement
  135. ).checked
  136. ).to.be.true
  137. )
  138. expect(
  139. (
  140. screen.getByLabelText('All project activity', {
  141. exact: false,
  142. }) as HTMLInputElement
  143. ).checked
  144. ).to.be.false
  145. expect(
  146. (screen.getByLabelText('Off', { exact: false }) as HTMLInputElement)
  147. .checked
  148. ).to.be.false
  149. })
  150. it('selects "Off" when all notifications are off', async function () {
  151. fetchMock.get(preferencesUrl, allNotificationsOff)
  152. renderComponent()
  153. await waitFor(
  154. () =>
  155. expect(
  156. (screen.getByLabelText('Off', { exact: false }) as HTMLInputElement)
  157. .checked
  158. ).to.be.true
  159. )
  160. expect(
  161. (
  162. screen.getByLabelText('All project activity', {
  163. exact: false,
  164. }) as HTMLInputElement
  165. ).checked
  166. ).to.be.false
  167. expect(
  168. (
  169. screen.getByLabelText('Replies to your activity only', {
  170. exact: false,
  171. }) as HTMLInputElement
  172. ).checked
  173. ).to.be.false
  174. })
  175. it('shows muted message and hides radio buttons when muteAllNotifications is true', async function () {
  176. fetchMock.get(preferencesUrl, globallyMutedPreferences)
  177. renderComponent()
  178. await waitFor(
  179. () =>
  180. expect(
  181. screen.getByText(
  182. 'You are not receiving any notifications, as you have disabled all project notifications.',
  183. { exact: false }
  184. )
  185. ).to.exist
  186. )
  187. expect(
  188. screen.getByRole('link', { name: 'Change settings' }).getAttribute('href')
  189. ).to.equal('/user/notification-preferences')
  190. expect(screen.queryByLabelText('All project activity', { exact: false })).to
  191. .not.exist
  192. expect(
  193. screen.queryByLabelText('Replies to your activity only', { exact: false })
  194. ).to.not.exist
  195. expect(screen.queryByLabelText('Off', { exact: false })).to.not.exist
  196. })
  197. it('POSTs "replies" preferences when "Replies to your activity only" is selected', async function () {
  198. fetchMock.get(preferencesUrl, allNotificationsOn)
  199. const saveMock = fetchMock.post(preferencesUrl, { status: 200 })
  200. renderComponent()
  201. await waitFor(
  202. () =>
  203. expect(
  204. (
  205. screen.getByLabelText('All project activity', {
  206. exact: false,
  207. }) as HTMLInputElement
  208. ).checked
  209. ).to.be.true
  210. )
  211. await userEvent.click(
  212. screen.getByLabelText('Replies to your activity only', { exact: false })
  213. )
  214. expect(
  215. saveMock.callHistory.called(preferencesUrl, {
  216. body: repliesOnlyPreferences,
  217. })
  218. ).to.be.true
  219. })
  220. it('POSTs "off" preferences when "Off" is selected', async function () {
  221. fetchMock.get(preferencesUrl, allNotificationsOn)
  222. const saveMock = fetchMock.post(preferencesUrl, { status: 200 })
  223. renderComponent()
  224. await waitFor(
  225. () =>
  226. expect(
  227. (
  228. screen.getByLabelText('All project activity', {
  229. exact: false,
  230. }) as HTMLInputElement
  231. ).checked
  232. ).to.be.true
  233. )
  234. await userEvent.click(screen.getByLabelText('Off', { exact: false }))
  235. expect(
  236. saveMock.callHistory.called(preferencesUrl, {
  237. body: allNotificationsOff,
  238. })
  239. ).to.be.true
  240. })
  241. it('shows "all" for owner with default preferences', async function () {
  242. fetchMock.get(preferencesUrl, defaultPreferences)
  243. renderComponent({ permissionsLevel: 'owner' })
  244. await waitFor(
  245. () =>
  246. expect(
  247. (
  248. screen.getByLabelText('All project activity', {
  249. exact: false,
  250. }) as HTMLInputElement
  251. ).checked
  252. ).to.be.true
  253. )
  254. })
  255. it('shows "replies" for invitee with default preferences', async function () {
  256. fetchMock.get(preferencesUrl, defaultPreferences)
  257. renderComponent({ permissionsLevel: 'readAndWrite' })
  258. await waitFor(
  259. () =>
  260. expect(
  261. (
  262. screen.getByLabelText('Replies to your activity only', {
  263. exact: false,
  264. }) as HTMLInputElement
  265. ).checked
  266. ).to.be.true
  267. )
  268. })
  269. it('POSTs "all" preferences when "All project activity" is selected', async function () {
  270. fetchMock.get(preferencesUrl, allNotificationsOff)
  271. const saveMock = fetchMock.post(preferencesUrl, { status: 200 })
  272. renderComponent()
  273. await waitFor(
  274. () =>
  275. expect(
  276. (screen.getByLabelText('Off', { exact: false }) as HTMLInputElement)
  277. .checked
  278. ).to.be.true
  279. )
  280. await userEvent.click(
  281. screen.getByLabelText('All project activity', { exact: false })
  282. )
  283. expect(
  284. saveMock.callHistory.called(preferencesUrl, {
  285. body: allNotificationsOn,
  286. })
  287. ).to.be.true
  288. })
  289. })