editor-floating-menu.spec.tsx 9.1 KB

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