review-panel.spec.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942
  1. import CodeMirrorEditor from '../../../../frontend/js/features/source-editor/components/codemirror-editor'
  2. import {
  3. EditorProviders,
  4. makeProjectProvider,
  5. USER_EMAIL,
  6. USER_ID,
  7. } from '../../helpers/editor-providers'
  8. import { mockScope } from '../source-editor/helpers/mock-scope'
  9. import { TestContainer } from '../source-editor/helpers/test-container'
  10. import { docId } from '../source-editor/helpers/mock-doc'
  11. import { mockProject } from '../source-editor/helpers/mock-project'
  12. import { UserId } from '@ol-types/user'
  13. const userData = {
  14. avatar_text: 'User',
  15. email: USER_EMAIL,
  16. hue: 180,
  17. id: USER_ID,
  18. isSelf: true,
  19. first_name: 'Test',
  20. last_name: 'User',
  21. }
  22. const resolvedThreadId = 'resolved-thread-id'
  23. const unresolvedThreadId = 'unresolved-thread-id'
  24. describe('<ReviewPanel />', function () {
  25. beforeEach(function () {
  26. window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
  27. cy.interceptEvents()
  28. cy.intercept('GET', '/project/*/changes/users', [
  29. {
  30. id: USER_ID,
  31. email: USER_EMAIL,
  32. first_name: 'Test',
  33. last_name: 'User',
  34. },
  35. ])
  36. cy.intercept('GET', '/project/*/threads', {
  37. // Resolved comment thread
  38. [resolvedThreadId]: {
  39. messages: [
  40. {
  41. content: 'comment text',
  42. id: `${resolvedThreadId}-1`,
  43. timestamp: new Date('2025-01-01T00:00:00.000Z'),
  44. user: userData,
  45. user_id: USER_ID,
  46. },
  47. ],
  48. resolved: true,
  49. resolved_at: new Date('2025-01-02T00:00:00.000Z').toISOString(),
  50. resolved_by_user_id: USER_ID,
  51. resolved_by_user: userData,
  52. },
  53. // Unresolved comment thread
  54. [unresolvedThreadId]: {
  55. messages: [
  56. {
  57. content: 'unresolved comment text',
  58. id: `${unresolvedThreadId}-1`,
  59. timestamp: new Date('2025-01-01T00:00:00.000Z'),
  60. user: userData,
  61. user_id: USER_ID,
  62. },
  63. {
  64. content: 'reply to thread',
  65. id: `${unresolvedThreadId}-2`,
  66. timestamp: new Date('2025-01-01T01:00:00.000Z'),
  67. user: userData,
  68. user_id: USER_ID,
  69. },
  70. ],
  71. },
  72. })
  73. const commentOps = [
  74. {
  75. id: 'resolved-op-id',
  76. op: { p: 161, c: 'Your introduction', t: resolvedThreadId },
  77. },
  78. {
  79. id: 'unresolved-op-id',
  80. op: { p: 210, c: 'Your results', t: unresolvedThreadId },
  81. },
  82. ]
  83. const changesOps = [
  84. {
  85. metadata: {
  86. user_id: USER_ID,
  87. ts: new Date('2025-01-01T00:00:00.000Z'),
  88. },
  89. id: 'inserted-op-id',
  90. op: { p: 166, t: 'inserted-op-id', i: 'introduction' },
  91. },
  92. {
  93. metadata: {
  94. user_id: USER_ID,
  95. ts: new Date('2025-01-01T01:00:00.000Z'),
  96. },
  97. id: 'deleted-op-id',
  98. op: { p: 110, t: 'deleted-op-id', d: 'beautiful ' },
  99. },
  100. ]
  101. cy.intercept('GET', '/project/*/ranges', [
  102. {
  103. id: docId,
  104. ranges: {
  105. changes: changesOps,
  106. comments: commentOps,
  107. docId,
  108. },
  109. },
  110. ])
  111. cy.intercept(
  112. 'POST',
  113. `/project/*/doc/${docId}/thread/${resolvedThreadId}/reopen`,
  114. {}
  115. ).as('reopenThread')
  116. cy.intercept(
  117. 'POST',
  118. `/project/*/doc/${docId}/thread/${unresolvedThreadId}/resolve`,
  119. {}
  120. ).as('resolveThreadId')
  121. cy.intercept(
  122. 'POST',
  123. `/project/*/thread/${unresolvedThreadId}/messages/${unresolvedThreadId}-1/edit`,
  124. {}
  125. ).as('editComment')
  126. cy.intercept(
  127. 'POST',
  128. `/project/*/thread/${unresolvedThreadId}/messages`,
  129. {}
  130. ).as('addReply')
  131. cy.intercept(
  132. 'POST',
  133. /\/project\/.*\/thread\/[a-z0-9]{24}\/messages/,
  134. {}
  135. ).as('addNewComment')
  136. cy.intercept(
  137. 'DELETE',
  138. `/project/*/doc/${docId}/thread/${resolvedThreadId}`,
  139. {}
  140. ).as('deleteResolvedThread')
  141. cy.intercept(
  142. 'DELETE',
  143. `/project/*/thread/${unresolvedThreadId}/messages/${unresolvedThreadId}-2`,
  144. {}
  145. ).as('deleteComment')
  146. cy.intercept(
  147. 'DELETE',
  148. `/project/*/doc/${docId}/thread/${unresolvedThreadId}`,
  149. {}
  150. ).as('deleteThread')
  151. cy.intercept('POST', `/project/*/doc/${docId}/changes/accept`, {}).as(
  152. 'acceptChange'
  153. )
  154. cy.intercept('POST', `/project/*/doc/${docId}/metadata`, {})
  155. const getChanges = cy.stub().as('getChanges').returns([])
  156. const removeChangeIds = cy.stub().as('removeChangeIds')
  157. const scope = mockScope(undefined, {
  158. docOptions: {
  159. rangesOptions: {
  160. comments: commentOps,
  161. changes: changesOps,
  162. getChanges,
  163. removeChangeIds,
  164. },
  165. },
  166. })
  167. const project = mockProject({
  168. projectOwner: {
  169. _id: USER_ID,
  170. },
  171. projectFeatures: { trackChanges: false, trackChangesVisible: true },
  172. })
  173. cy.wrap(scope).as('scope')
  174. cy.mount(
  175. <TestContainer className="rp-size-expanded">
  176. <EditorProviders
  177. scope={scope}
  178. providers={{ ProjectProvider: makeProjectProvider(project) }}
  179. >
  180. <CodeMirrorEditor />
  181. </EditorProviders>
  182. </TestContainer>
  183. )
  184. // Wait for the editor to be ready before interacting with it
  185. cy.get('.cm-content').should('have.css', 'opacity', '1')
  186. // Open the review panel with keyboard shortcut
  187. cy.findByText('contentLine 0').type('{command}j', { scrollBehavior: false })
  188. cy.findByText('contentLine 1').type('{ctrl}j', { scrollBehavior: false })
  189. cy.findByTestId('review-panel').as('review-panel')
  190. })
  191. describe('toolbar', function () {
  192. describe('resolved comments dropdown', function () {
  193. it('renders a dropdown of resolved comments', function () {
  194. // The dropdown button should be visible
  195. cy.findByLabelText('Resolved comments').click()
  196. // It should open the dropdown
  197. cy.findByRole('tooltip')
  198. .should('exist')
  199. .within(() => {
  200. // TODO: Fix selector
  201. cy.get(
  202. '.review-panel-resolved-comments-header .badge-content'
  203. ).should('contain.text', '1')
  204. // Should name the document with the comment
  205. cy.findByText('test.tex').should('exist')
  206. // Should show the comment text
  207. cy.findByText('comment text').should('exist')
  208. // Should show the author name
  209. // TODO: Fix selector
  210. cy.get('.review-panel-entry-user').should(
  211. 'contain.text',
  212. 'Test User'
  213. )
  214. })
  215. })
  216. it('reopens resolved comment', function () {
  217. cy.findByLabelText('Resolved comments').click()
  218. cy.findByRole('tooltip').within(() => {
  219. // Find the re-open icon button using the hidden label
  220. cy.findByText('Re-open').click({ force: true })
  221. // verify the reopen thread API call
  222. cy.wait('@reopenThread')
  223. // TODO: Figure out a way to plumb the websocket response back through
  224. // to the test so we can verify the comment is no longer resolved
  225. // cy.get(
  226. // '.review-panel-resolved-comments-header .badge-content'
  227. // ).should('contain.text', '0')
  228. })
  229. })
  230. it('deletes resolved comment', function () {
  231. cy.findByLabelText('Resolved comments').click()
  232. cy.findByRole('tooltip').within(() => {
  233. // Find the Delete icon button using the hidden label
  234. cy.findByText('Delete').click({ force: true })
  235. // verify the delete thread API call
  236. cy.wait('@deleteResolvedThread')
  237. // TODO: Figure out a way to plumb the websocket response back through
  238. // to the test so we can verify the comment is no longer there
  239. // cy.get(
  240. // '.review-panel-resolved-comments-header .badge-content'
  241. // ).should('contain.text', '0')
  242. })
  243. })
  244. })
  245. })
  246. describe('toggler', function () {
  247. it('should close panel when pressing close button', function () {
  248. cy.findByLabelText('Close').click({ scrollBehavior: false })
  249. // We should collapse to the mini state
  250. cy.get('.review-panel-mini').should('exist')
  251. })
  252. })
  253. describe('navigation', function () {
  254. it('renders navigation', function () {
  255. cy.get('@review-panel').within(() => {
  256. cy.findByRole('tab', { name: /current file/i })
  257. cy.findByRole('tab', { name: /overview/i })
  258. })
  259. })
  260. it('selects the active tab', function () {
  261. cy.get('@review-panel').within(() => {
  262. cy.findByRole('tab', { name: /current file/i }).should(
  263. 'have.attr',
  264. 'aria-selected',
  265. 'true'
  266. )
  267. cy.findByRole('tab', { name: /overview/i }).should(
  268. 'have.attr',
  269. 'aria-selected',
  270. 'false'
  271. )
  272. cy.findByRole('tab', { name: /overview/i }).click()
  273. cy.findByRole('tab', { name: /current file/i }).should(
  274. 'have.attr',
  275. 'aria-selected',
  276. 'false'
  277. )
  278. cy.findByRole('tab', { name: /overview/i }).should(
  279. 'have.attr',
  280. 'aria-selected',
  281. 'true'
  282. )
  283. })
  284. })
  285. })
  286. describe('comment entries', function () {
  287. it('shows threads and comments', function () {
  288. cy.get('@review-panel').within(() => {
  289. cy.findByText('unresolved comment text').should('exist')
  290. cy.findByText('reply to thread').should('exist')
  291. })
  292. })
  293. it('edits comment', function () {
  294. cy.get('@review-panel').within(() => {
  295. // TODO: Fix selector
  296. cy.get('.review-panel-comment-wrapper')
  297. .first()
  298. .within(() => {
  299. // Find the options icon button using the hidden label
  300. cy.findByText('More options')
  301. .first()
  302. .click({ force: true, scrollBehavior: false })
  303. cy.findByRole('menu').within(() => {
  304. cy.findByText('Edit').click({ scrollBehavior: false })
  305. })
  306. cy.findByRole('textbox').type(
  307. '{selectAll}edited comment text{enter}',
  308. { scrollBehavior: false }
  309. )
  310. cy.wait('@editComment')
  311. // TODO: Figure out a way to plumb the websocket response back through
  312. // to the test so we can verify the comment is resolved
  313. // cy.findByText('edited comment text').should('exist')
  314. })
  315. })
  316. })
  317. it('deletes thread', function () {
  318. cy.get('@review-panel').within(() => {
  319. // TODO: Fix selector
  320. cy.get('.review-panel-comment-wrapper')
  321. .first()
  322. .within(() => {
  323. // Find the options icon button using the hidden label
  324. cy.findByText('More options')
  325. .first()
  326. .click({ force: true, scrollBehavior: false })
  327. cy.findByRole('menu').within(() => {
  328. cy.findByText('Delete').click({ scrollBehavior: false })
  329. })
  330. })
  331. })
  332. cy.findByRole('dialog').within(() => {
  333. cy.findByRole('button', { name: 'Delete' }).click()
  334. })
  335. cy.wait('@deleteThread')
  336. // TODO: Figure out a way to plumb the websocket response back through
  337. // to the test so we can verify the thread is deleted
  338. // cy.findByText('unresolved comment text').should('not.exist')
  339. })
  340. it('deletes reply', function () {
  341. cy.get('@review-panel').within(() => {
  342. // TODO: Fix selector
  343. cy.get('.review-panel-comment-wrapper')
  344. .eq(1)
  345. .within(() => {
  346. // Find the options icon button using the hidden label
  347. cy.findByText('More options')
  348. .first()
  349. .click({ force: true, scrollBehavior: false })
  350. cy.findByRole('menu').within(() => {
  351. cy.findByText('Delete').click({ scrollBehavior: false })
  352. })
  353. })
  354. })
  355. cy.findByRole('dialog').within(() => {
  356. cy.findByRole('button', { name: 'Delete' }).click()
  357. })
  358. cy.wait('@deleteComment')
  359. // TODO: Figure out a way to plumb the websocket response back through
  360. // to the test so we can verify the reply is deleted
  361. // cy.findByText('reply to thread').should('not.exist')
  362. })
  363. it('cancels comment deletion', function () {
  364. cy.get('@review-panel').within(() => {
  365. // TODO: Fix selector
  366. cy.get('.review-panel-comment-wrapper')
  367. .eq(1)
  368. .within(() => {
  369. // Find the options icon button using the hidden label
  370. cy.findByText('More options')
  371. .first()
  372. .click({ force: true, scrollBehavior: false })
  373. cy.findByRole('menu').within(() => {
  374. cy.findByText('Delete').click({ scrollBehavior: false })
  375. })
  376. })
  377. })
  378. cy.findByRole('dialog').within(() => {
  379. cy.findByRole('button', { name: 'Cancel' }).click()
  380. })
  381. cy.findByText('unresolved comment text').should('exist')
  382. })
  383. it('adds new comment (replies) to a thread', function () {
  384. cy.get('@review-panel').within(() => {
  385. cy.findByRole('textbox').type('a new reply{enter}', {
  386. scrollBehavior: false,
  387. })
  388. })
  389. cy.wait('@addReply')
  390. })
  391. it('resolves comment', function () {
  392. cy.get('@review-panel').within(() => {
  393. // Find the resolve icon button using the hidden label
  394. cy.findByText('Resolve comment').click({ force: true })
  395. cy.wait('@resolveThreadId')
  396. // TODO: Figure out a way to plumb the websocket response back through
  397. // to the test so we can verify the comment is resolved
  398. // cy.findByText('unresolved comment text').should('not.exist')
  399. })
  400. })
  401. })
  402. describe('change entries', function () {
  403. it('renders inserted entries in current file mode', function () {
  404. cy.get('@review-panel').within(() => {
  405. cy.findByText('Added:').should('exist')
  406. cy.findByText('introduction').should('exist')
  407. })
  408. })
  409. it('renders deleted entries in current file mode', function () {
  410. cy.get('@review-panel').within(() => {
  411. cy.findByText('Deleted:').should('exist')
  412. cy.findByText('beautiful').should('exist')
  413. })
  414. })
  415. it('accepts change', function () {
  416. cy.get('@review-panel').within(() => {
  417. // TODO: Fix selector
  418. cy.get('.review-panel-entry-insert').within(() => {
  419. // Find the accept icon button using the hidden label
  420. cy.findByText('Accept change').click({ force: true })
  421. cy.wait('@acceptChange')
  422. })
  423. // TODO: Fix selector
  424. cy.get('.review-panel-entry-delete').within(() => {
  425. // Find the accept icon button using the hidden label
  426. cy.findByText('Accept change').click({ force: true })
  427. cy.wait('@acceptChange')
  428. })
  429. })
  430. })
  431. it('rejects change', function () {
  432. cy.get('@review-panel').within(() => {
  433. // TODO: Fix selector
  434. cy.get('.review-panel-entry-insert').within(() => {
  435. // Find the reject icon button using the hidden label
  436. cy.findByText('Reject change').click({ force: true })
  437. cy.get('@getChanges').should('be.calledOnce')
  438. })
  439. // TODO: Fix selector
  440. cy.get('.review-panel-entry-delete').within(() => {
  441. // Find the reject icon button using the hidden label
  442. cy.findByText('Reject change').click({ force: true })
  443. cy.get('@getChanges').should('be.calledTwice')
  444. })
  445. })
  446. })
  447. })
  448. describe('aggregate change entries', function () {
  449. // eslint-disable-next-line mocha/no-skipped-tests
  450. it.skip('renders changed entries in current file mode', function () {})
  451. // eslint-disable-next-line mocha/no-skipped-tests
  452. it.skip('renders changed entries in overview mode', function () {})
  453. })
  454. describe('add comment entry', function () {
  455. beforeEach(function () {
  456. cy.findByText('contentLine 12').type(
  457. '{home}{shift}' + '{rightArrow}'.repeat(6),
  458. { scrollBehavior: false }
  459. )
  460. // TODO: Fix selector
  461. cy.get('.review-tooltip-add-comment-button').as('add-comment-button')
  462. })
  463. it('renders floating `add comment button`', function () {
  464. cy.get('@add-comment-button').should('exist')
  465. })
  466. it('can add comment', function () {
  467. cy.get('@add-comment-button').click({ scrollBehavior: false })
  468. cy.get('@review-panel').within(() => {
  469. // TODO: Fix selector
  470. cy.get('.review-panel-add-comment-textarea').type(
  471. 'a new comment{enter}',
  472. {
  473. scrollBehavior: false,
  474. }
  475. )
  476. })
  477. cy.wait('@addNewComment')
  478. // TODO : Figure out a way to plumb the websocket response back through
  479. // to the test so we can verify the comment is added
  480. // cy.findByText('a new comment').should('exist')
  481. })
  482. it('cancels adding comment', function () {
  483. cy.get('@add-comment-button').click({ scrollBehavior: false })
  484. cy.get('@review-panel').within(() => {
  485. cy.findByRole('button', { name: 'Cancel' }).click({
  486. scrollBehavior: false,
  487. })
  488. })
  489. })
  490. })
  491. describe('add comment tooltip visibility', function () {
  492. beforeEach(function () {
  493. cy.findByText('contentLine 12').type(
  494. '{home}{shift}' + '{rightArrow}'.repeat(6),
  495. { scrollBehavior: false }
  496. )
  497. cy.get('.review-tooltip-menu').should('exist')
  498. })
  499. it('hides the tooltip when clicking a toolbar button', function () {
  500. cy.findByRole('button', { name: 'Undo' }).click({ scrollBehavior: false })
  501. cy.get('.review-tooltip-menu').should('not.exist')
  502. })
  503. it('hides the tooltip when clicking outside the editor', function () {
  504. cy.get('@review-panel').click({ scrollBehavior: false })
  505. cy.get('.review-tooltip-menu').should('not.exist')
  506. })
  507. it('keeps the add comment button functional when clicked', function () {
  508. cy.get('.review-tooltip-add-comment-button').click({
  509. scrollBehavior: false,
  510. })
  511. cy.get('@review-panel').within(() => {
  512. cy.get('.review-panel-add-comment-textarea').should('exist')
  513. })
  514. })
  515. })
  516. describe('bulk actions entry', function () {
  517. beforeEach(function () {
  518. // Select a deletion and an insertion
  519. cy.findByText('\\maketitle').type(
  520. '{home}{shift}' + '{downArrow}'.repeat(10),
  521. { scrollBehavior: false }
  522. )
  523. cy.findByLabelText('Accept selected changes').as(
  524. 'accept-selected-changes'
  525. )
  526. cy.findByLabelText('Reject selected changes').as(
  527. 'reject-selected-changes'
  528. )
  529. })
  530. it('renders the reject and accept all buttons`', function () {
  531. cy.get('@accept-selected-changes').should('exist')
  532. cy.get('@reject-selected-changes').should('exist')
  533. })
  534. it('accepts all changes', function () {
  535. cy.get('@accept-selected-changes').click({ scrollBehavior: false })
  536. cy.findByRole('dialog').within(() => {
  537. cy.findByText(
  538. 'Are you sure you want to accept the selected 2 changes?'
  539. ).should('exist')
  540. cy.findByRole('button', { name: 'OK' }).click({
  541. scrollBehavior: false,
  542. })
  543. cy.wait('@acceptChange')
  544. cy.get('@removeChangeIds').should('have.been.calledWith', [
  545. 'inserted-op-id',
  546. 'deleted-op-id',
  547. ])
  548. })
  549. })
  550. it('rejects all changes', function () {
  551. cy.get('@reject-selected-changes').click({ scrollBehavior: false })
  552. cy.findByRole('dialog').within(() => {
  553. cy.findByText(
  554. 'Are you sure you want to reject the selected 2 changes?'
  555. ).should('exist')
  556. cy.findByRole('button', { name: 'OK' }).click({
  557. scrollBehavior: false,
  558. })
  559. cy.get('@getChanges').should('have.been.calledWith', [
  560. 'inserted-op-id',
  561. 'deleted-op-id',
  562. ])
  563. })
  564. })
  565. it('keeps the tooltip visible when cancelling the accept confirmation modal', function () {
  566. cy.get('@accept-selected-changes').click({ scrollBehavior: false })
  567. cy.findByRole('dialog').within(() => {
  568. cy.findByRole('button', { name: 'Cancel' }).click({
  569. scrollBehavior: false,
  570. })
  571. })
  572. cy.get('.review-tooltip-menu').should('exist')
  573. })
  574. it('keeps the tooltip visible when cancelling the reject confirmation modal', function () {
  575. cy.get('@reject-selected-changes').click({ scrollBehavior: false })
  576. cy.findByRole('dialog').within(() => {
  577. cy.findByRole('button', { name: 'Cancel' }).click({
  578. scrollBehavior: false,
  579. })
  580. })
  581. cy.get('.review-tooltip-menu').should('exist')
  582. })
  583. })
  584. describe('overview mode', function () {
  585. beforeEach(function () {
  586. cy.findByRole('tab', { name: /overview/i }).click()
  587. })
  588. it('shows list of files changed', function () {
  589. // TODO: Fix selector
  590. cy.get('.collapsible-file-header').should('contain.text', 'test.tex')
  591. })
  592. it('renders comments', function () {
  593. cy.get('@review-panel').within(() => {
  594. cy.findByText('unresolved comment text').should('exist')
  595. cy.findByText('reply to thread').should('exist')
  596. })
  597. })
  598. it('renders changes', function () {
  599. cy.get('@review-panel').within(() => {
  600. cy.findByText('Added:').should('exist')
  601. cy.findByText('introduction').should('exist')
  602. cy.findByText('Deleted:').should('exist')
  603. cy.findByText('beautiful').should('exist')
  604. })
  605. })
  606. it('collapses the file entries when clicked', function () {
  607. cy.findByText('test.tex').click()
  608. cy.get('@review-panel').within(() => {
  609. // TODO: Fix selector
  610. cy.get('.review-panel-entry').should('not.exist')
  611. })
  612. cy.findByText('test.tex').click()
  613. cy.get('@review-panel').within(() => {
  614. // TODO: Fix selector
  615. cy.get('.review-panel-entry').should('exist')
  616. })
  617. })
  618. })
  619. })
  620. describe('<ReviewPanel /> in mini mode', function () {
  621. function render({ comments = [], changes = [], threads = {} }: any) {
  622. window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
  623. cy.interceptEvents()
  624. cy.intercept('GET', '/project/*/changes/users', [
  625. {
  626. id: USER_ID,
  627. email: USER_EMAIL,
  628. first_name: 'Test',
  629. last_name: 'User',
  630. },
  631. ])
  632. const getChanges = cy.stub().as('getChanges').returns([])
  633. const removeChangeIds = cy.stub().as('removeChangeIds')
  634. const scope = mockScope(undefined, {
  635. docOptions: {
  636. rangesOptions: {
  637. comments,
  638. changes,
  639. getChanges,
  640. removeChangeIds,
  641. },
  642. },
  643. projectFeatures: { trackChangesVisible: true },
  644. })
  645. const project = mockProject({
  646. projectFeatures: { trackChangesVisible: true },
  647. })
  648. cy.intercept('GET', '/project/*/ranges', [
  649. {
  650. id: docId,
  651. ranges: {
  652. changes,
  653. comments,
  654. docId,
  655. },
  656. },
  657. ])
  658. cy.intercept('GET', '/project/*/threads', threads).as('loadThreads')
  659. cy.intercept('POST', `/project/*/doc/${docId}/metadata`, {})
  660. cy.wrap(scope).as('scope')
  661. cy.mount(
  662. <TestContainer>
  663. <EditorProviders
  664. scope={scope}
  665. providers={{ ProjectProvider: makeProjectProvider(project) }}
  666. >
  667. <CodeMirrorEditor />
  668. </EditorProviders>
  669. </TestContainer>
  670. )
  671. // Wait for editor
  672. cy.get('.cm-content').should('have.css', 'opacity', '1')
  673. // Wait for the threads to load, since mini mode renders conditionally on
  674. // the threads/ranges data being present
  675. cy.wait('@loadThreads')
  676. // Toggle the review panel twice to ensure data is loaded
  677. cy.findByText('contentLine 0').type('{command}jj', {
  678. scrollBehavior: false,
  679. })
  680. cy.findByText('contentLine 1').type('{ctrl}jj', { scrollBehavior: false })
  681. }
  682. it("doesn't render mini when no comments or changes are present in project", function () {
  683. render({
  684. comments: [],
  685. changes: [],
  686. threads: {},
  687. })
  688. cy.get('.review-panel-mini').should('not.exist')
  689. })
  690. it("doesn't render mini when no comments or changes are present in document", function () {
  691. render({
  692. comments: [],
  693. changes: [],
  694. threads: {
  695. 'random-unrelated-thread': {
  696. messages: [
  697. {
  698. content: 'a comment',
  699. id: 'random-unrelated-thread-1',
  700. timestamp: new Date('2025-01-01T01:00:00.000Z'),
  701. user: userData,
  702. user_id: USER_ID,
  703. },
  704. ],
  705. },
  706. },
  707. })
  708. cy.get('.review-panel-mini').should('not.exist')
  709. })
  710. it("doesn't render mini when a resolved comment is present in document", function () {
  711. render({
  712. comments: [
  713. {
  714. id: resolvedThreadId,
  715. op: { p: 161, c: 'Your introduction', t: resolvedThreadId },
  716. },
  717. ],
  718. changes: [],
  719. threads: {
  720. [resolvedThreadId]: {
  721. resolved: true,
  722. resolved_at: new Date('2025-01-02T00:00:00.000Z').toISOString(),
  723. resolved_by_user_id: USER_ID,
  724. resolved_by_user: userData,
  725. messages: [
  726. {
  727. content: 'a comment',
  728. id: `${resolvedThreadId}-1`,
  729. timestamp: new Date('2025-01-01T01:00:00.000Z'),
  730. user: userData,
  731. user_id: USER_ID,
  732. },
  733. ],
  734. },
  735. },
  736. })
  737. cy.get('.review-panel-mini').should('not.exist')
  738. })
  739. it('renders mini when an unresolved comment is present in document', function () {
  740. render({
  741. comments: [
  742. {
  743. id: unresolvedThreadId,
  744. op: { p: 161, c: 'Your introduction', t: unresolvedThreadId },
  745. },
  746. ],
  747. changes: [],
  748. threads: {
  749. [unresolvedThreadId]: {
  750. messages: [
  751. {
  752. content: 'a comment',
  753. id: `${unresolvedThreadId}-1`,
  754. timestamp: new Date('2025-01-01T01:00:00.000Z'),
  755. user: userData,
  756. user_id: USER_ID,
  757. },
  758. ],
  759. },
  760. },
  761. })
  762. cy.get('.review-panel-mini').should('exist')
  763. })
  764. it('renders mini when a tracked change is present in document', function () {
  765. render({
  766. comments: [],
  767. changes: [
  768. {
  769. metadata: {
  770. user_id: USER_ID,
  771. ts: new Date('2025-01-01T00:00:00.000Z'),
  772. },
  773. id: 'inserted-op-id',
  774. op: { p: 166, t: 'inserted-op-id', i: 'introduction' },
  775. },
  776. ],
  777. threads: {},
  778. })
  779. cy.get('.review-panel-mini').should('exist')
  780. })
  781. })
  782. describe('<ReviewPanel /> for free users', function () {
  783. function mountEditor(ownerId = USER_ID) {
  784. const scope = mockScope(undefined, {
  785. permissions: { write: true, trackedWrite: false, comment: true },
  786. })
  787. const project = mockProject({
  788. projectFeatures: { trackChanges: false, trackChangesVisible: true },
  789. projectOwner: {
  790. _id: ownerId,
  791. },
  792. })
  793. cy.wrap(scope).as('scope')
  794. cy.mount(
  795. <TestContainer className="rp-size-expanded">
  796. <EditorProviders
  797. scope={scope}
  798. providers={{ ProjectProvider: makeProjectProvider(project) }}
  799. >
  800. <CodeMirrorEditor />
  801. </EditorProviders>
  802. </TestContainer>
  803. )
  804. // Wait for the editor to be ready before interacting with it
  805. cy.get('.cm-content').should('have.css', 'opacity', '1')
  806. cy.findByLabelText('Editing').click()
  807. cy.findByRole('menu').within(() => {
  808. cy.findByText(/Reviewing/).click()
  809. })
  810. }
  811. beforeEach(function () {
  812. window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
  813. cy.interceptEvents()
  814. cy.intercept('GET', '/project/*/changes/users', [])
  815. cy.intercept('GET', '/project/*/threads', {})
  816. })
  817. it('renders modal', function () {
  818. mountEditor()
  819. cy.findByRole('dialog').within(() => {
  820. cy.findByText('Upgrade to review').should('exist')
  821. })
  822. })
  823. it('closes modal', function () {
  824. mountEditor()
  825. cy.findByRole('dialog').within(() => {
  826. cy.findByText('Close').click()
  827. })
  828. cy.findByRole('dialog').should('not.exist')
  829. })
  830. it('opens subscription page after clicking on `upgrade`', function () {
  831. mountEditor()
  832. cy.findByRole('dialog').within(() => {
  833. // Verify the button exists. Clicking it will open a new window
  834. cy.findByText('Upgrade').should('exist')
  835. })
  836. })
  837. // eslint-disable-next-line mocha/no-skipped-tests
  838. it.skip('opens subscription page after clicking on `try it for free`', function () {})
  839. it('shows `ask project owner to upgrade` message', function () {
  840. mountEditor('other-user-id' as UserId)
  841. cy.findByRole('dialog').within(() => {
  842. cy.findByText(
  843. 'Please ask the project owner to upgrade to use track changes'
  844. ).should('exist')
  845. })
  846. })
  847. })