editor-floating-menu.spec.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import CodeMirrorEditor from '../../../../frontend/js/features/source-editor/components/codemirror-editor'
  2. import { EditorProviders, USER_ID } from '../../helpers/editor-providers'
  3. import { mockScope } from '../source-editor/helpers/mock-scope'
  4. import { TestContainer } from '../source-editor/helpers/test-container'
  5. import { docId } from '../source-editor/helpers/mock-doc'
  6. import { useUserSettingsContext } from '@/shared/context/user-settings-context'
  7. function FloatingMenuToggle() {
  8. const { setUserSettings } = useUserSettingsContext()
  9. return (
  10. <button
  11. onClick={() =>
  12. setUserSettings(settings => ({
  13. ...settings,
  14. floatingMenu: !settings.floatingMenu,
  15. }))
  16. }
  17. >
  18. Toggle floating menu
  19. </button>
  20. )
  21. }
  22. describe('<EditorFloatingMenu />', function () {
  23. function mountEditor({
  24. migrationEnabled,
  25. trackChangesVisible = true,
  26. floatingMenu = true,
  27. withSettingsToggle = false,
  28. }: {
  29. migrationEnabled: boolean
  30. trackChangesVisible?: boolean
  31. floatingMenu?: boolean
  32. withSettingsToggle?: boolean
  33. }) {
  34. window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
  35. window.metaAttributesCache.set('ol-splitTestVariants', {
  36. 'writefull-toolbar-migration': migrationEnabled ? 'enabled' : 'default',
  37. })
  38. cy.interceptEvents()
  39. const scope = mockScope()
  40. cy.mount(
  41. <TestContainer>
  42. <EditorProviders
  43. scope={scope}
  44. features={{ trackChangesVisible }}
  45. userSettings={{ floatingMenu }}
  46. >
  47. <CodeMirrorEditor />
  48. {withSettingsToggle && <FloatingMenuToggle />}
  49. </EditorProviders>
  50. </TestContainer>
  51. )
  52. // Create a non-empty selection so the selection tooltip is shown.
  53. cy.findByText('contentLine 12').type(
  54. '{home}{shift}' + '{rightArrow}'.repeat(6),
  55. {
  56. scrollBehavior: false,
  57. }
  58. )
  59. }
  60. function mountEditorWithChanges() {
  61. window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
  62. window.metaAttributesCache.set('ol-splitTestVariants', {
  63. 'writefull-toolbar-migration': 'enabled',
  64. })
  65. cy.interceptEvents()
  66. cy.intercept('POST', `/project/*/doc/${docId}/changes/accept`, {}).as(
  67. 'acceptChange'
  68. )
  69. const changes = [
  70. {
  71. metadata: {
  72. user_id: USER_ID,
  73. ts: new Date('2025-01-01T00:00:00.000Z'),
  74. },
  75. id: 'inserted-op-id',
  76. op: { p: 166, t: 'inserted-op-id', i: 'introduction' },
  77. },
  78. {
  79. metadata: {
  80. user_id: USER_ID,
  81. ts: new Date('2025-01-01T01:00:00.000Z'),
  82. },
  83. id: 'deleted-op-id',
  84. op: { p: 110, t: 'deleted-op-id', d: 'beautiful ' },
  85. },
  86. ]
  87. const getChanges = cy.stub().as('getChanges').returns([])
  88. const removeChangeIds = cy.stub().as('removeChangeIds')
  89. const scope = mockScope(undefined, {
  90. docOptions: {
  91. rangesOptions: { changes, getChanges, removeChangeIds },
  92. },
  93. })
  94. cy.mount(
  95. <TestContainer>
  96. <EditorProviders scope={scope} features={{ trackChangesVisible: true }}>
  97. <CodeMirrorEditor />
  98. </EditorProviders>
  99. </TestContainer>
  100. )
  101. // Select a deletion and an insertion so the bulk-action controls appear.
  102. cy.findByText('\\maketitle').type(
  103. '{home}{shift}' + '{downArrow}'.repeat(10),
  104. { scrollBehavior: false }
  105. )
  106. }
  107. describe('when the migration split test is enabled', function () {
  108. it('shows the unified floating menu with Add comment, not the legacy tooltip', function () {
  109. mountEditor({ migrationEnabled: true })
  110. cy.get('.editor-floating-menu').within(() => {
  111. cy.findByLabelText('Add comment').should('exist')
  112. })
  113. // Legacy tooltip is replaced.
  114. cy.get('.review-tooltip-menu').should('not.exist')
  115. })
  116. it('clicking Add comment dispatches the add-new-review-comment event', function () {
  117. mountEditor({ migrationEnabled: true })
  118. cy.window().then(win => {
  119. win.addEventListener(
  120. 'add-new-review-comment',
  121. cy.stub().as('addComment')
  122. )
  123. })
  124. cy.get('.editor-floating-menu').within(() => {
  125. cy.findByLabelText('Add comment').click({ scrollBehavior: false })
  126. })
  127. cy.get('@addComment').should('have.been.called')
  128. })
  129. })
  130. describe('when the migration split test is disabled (control)', function () {
  131. it('keeps the legacy review tooltip and does not render the unified menu', function () {
  132. mountEditor({ migrationEnabled: false })
  133. cy.get('.review-tooltip-menu').should('exist')
  134. cy.get('.editor-floating-menu').should('not.exist')
  135. })
  136. })
  137. describe('floating menu setting', function () {
  138. it('does not show the unified floating menu when the migration split test is enabled', function () {
  139. mountEditor({ migrationEnabled: true, floatingMenu: false })
  140. cy.get('.editor-floating-menu').should('not.exist')
  141. })
  142. it('does not show the legacy review tooltip when the migration split test is disabled', function () {
  143. mountEditor({ migrationEnabled: false, floatingMenu: false })
  144. cy.get('.review-tooltip-menu').should('not.exist')
  145. })
  146. it('removes the menu and its tooltip when the setting is turned off', function () {
  147. mountEditor({
  148. migrationEnabled: true,
  149. floatingMenu: true,
  150. withSettingsToggle: true,
  151. })
  152. cy.get('.review-tooltip-menu-container').should('exist')
  153. cy.get('.editor-floating-menu').should('exist')
  154. // force: the visible menu can overlap this test-only toggle button
  155. cy.findByRole('button', { name: 'Toggle floating menu' }).click({
  156. scrollBehavior: false,
  157. force: true,
  158. })
  159. cy.get('.editor-floating-menu').should('not.exist')
  160. cy.get('.review-tooltip-menu-container').should('not.exist')
  161. })
  162. it('restores the menu when the setting is turned back on', function () {
  163. mountEditor({
  164. migrationEnabled: true,
  165. floatingMenu: false,
  166. withSettingsToggle: true,
  167. })
  168. cy.get('.editor-floating-menu').should('not.exist')
  169. cy.findByRole('button', { name: 'Toggle floating menu' }).click({
  170. scrollBehavior: false,
  171. force: true,
  172. })
  173. // delay: the extension reconfigures on a deferred dispatch, so spaced-out
  174. // keystrokes ensure a selection event lands after it to rebuild the menu
  175. cy.findByText('contentLine 12').type(
  176. '{home}{shift}' + '{rightArrow}'.repeat(6),
  177. { scrollBehavior: false, delay: 20 }
  178. )
  179. cy.get('.editor-floating-menu').should('exist')
  180. })
  181. })
  182. describe('bulk tracked-change actions', function () {
  183. beforeEach(function () {
  184. mountEditorWithChanges()
  185. cy.findByLabelText('Accept selected changes').as(
  186. 'accept-selected-changes'
  187. )
  188. cy.findByLabelText('Reject selected changes').as(
  189. 'reject-selected-changes'
  190. )
  191. })
  192. it('renders the accept and reject controls in the unified menu', function () {
  193. cy.get('.editor-floating-menu').should('exist')
  194. cy.get('@accept-selected-changes').should('exist')
  195. cy.get('@reject-selected-changes').should('exist')
  196. })
  197. it('accepts the selected changes', function () {
  198. cy.get('@accept-selected-changes').click({ scrollBehavior: false })
  199. cy.findByRole('dialog').within(() => {
  200. cy.findByText(
  201. 'Are you sure you want to accept the selected 2 changes?'
  202. ).should('exist')
  203. cy.findByRole('button', { name: 'OK' }).click({ scrollBehavior: false })
  204. })
  205. cy.wait('@acceptChange')
  206. cy.get('@removeChangeIds').should('have.been.calledWith', [
  207. 'inserted-op-id',
  208. 'deleted-op-id',
  209. ])
  210. })
  211. it('rejects the selected changes', function () {
  212. cy.get('@reject-selected-changes').click({ scrollBehavior: false })
  213. cy.findByRole('dialog').within(() => {
  214. cy.findByText(
  215. 'Are you sure you want to reject the selected 2 changes?'
  216. ).should('exist')
  217. cy.findByRole('button', { name: 'OK' }).click({ scrollBehavior: false })
  218. })
  219. cy.get('@getChanges').should('have.been.calledWith', [
  220. 'inserted-op-id',
  221. 'deleted-op-id',
  222. ])
  223. })
  224. it('keeps the menu visible when cancelling the confirmation modal', function () {
  225. cy.get('@accept-selected-changes').click({ scrollBehavior: false })
  226. cy.findByRole('dialog').within(() => {
  227. cy.findByRole('button', { name: 'Cancel' }).click({
  228. scrollBehavior: false,
  229. })
  230. })
  231. cy.get('.editor-floating-menu').should('exist')
  232. })
  233. })
  234. })