editor.spec.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import {
  2. createNewFile,
  3. createProject,
  4. openProjectById,
  5. testNewFileUpload,
  6. } from './helpers/project'
  7. import { isExcludedBySharding, startWith } from './helpers/config'
  8. import { ensureUserExists, login } from './helpers/login'
  9. import { v4 as uuid } from 'uuid'
  10. import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
  11. import { prepareWaitForNextCompileSlot } from './helpers/compile'
  12. describe('editor', () => {
  13. if (isExcludedBySharding('PRO_DEFAULT_1')) return
  14. startWith({ pro: true })
  15. ensureUserExists({ email: 'user@example.com' })
  16. ensureUserExists({ email: 'collaborator@example.com' })
  17. let projectName: string
  18. let projectId: string
  19. let recompile: () => void
  20. let waitForCompileRateLimitCoolOff: (fn: () => void) => void
  21. beforeWithReRunOnTestRetry(function () {
  22. projectName = `project-${uuid()}`
  23. login('user@example.com')
  24. createProject(projectName, { type: 'Example project', open: false }).then(
  25. id => (projectId = id)
  26. )
  27. ;({ recompile, waitForCompileRateLimitCoolOff } =
  28. prepareWaitForNextCompileSlot())
  29. })
  30. beforeEach(() => {
  31. login('user@example.com')
  32. waitForCompileRateLimitCoolOff(() => {
  33. openProjectById(projectId)
  34. })
  35. })
  36. describe('spelling', function () {
  37. function changeSpellCheckLanguageTo(lng: string) {
  38. cy.log(`change project language to '${lng}'`)
  39. cy.get('button').contains('Menu').click()
  40. cy.get('select[id=settings-menu-spellCheckLanguage]').select(lng)
  41. cy.get('[id="left-menu"]').type('{esc}') // close left menu
  42. }
  43. afterEach(function () {
  44. changeSpellCheckLanguageTo('Off')
  45. })
  46. it('word dictionary and spelling', () => {
  47. changeSpellCheckLanguageTo('English (American)')
  48. createNewFile()
  49. const word = createRandomLetterString()
  50. cy.log('edit project file')
  51. cy.get('.cm-line').type(word)
  52. cy.get('.ol-cm-spelling-error').should('exist')
  53. changeSpellCheckLanguageTo('Spanish')
  54. cy.log('add word to dictionary')
  55. cy.get('.ol-cm-spelling-error').contains(word).rightclick()
  56. cy.findByText('Add to dictionary').click()
  57. cy.get('.ol-cm-spelling-error').should('not.exist')
  58. cy.log('remove word from dictionary')
  59. cy.get('button').contains('Menu').click()
  60. cy.get('button#dictionary-settings').contains('Edit').click()
  61. cy.get('[id="dictionary-modal"]').within(() => {
  62. cy.findByText(word)
  63. .parent()
  64. .within(() => cy.get('button').click())
  65. // the modal has 2 close buttons, this ensures the one with the visible label is
  66. // clicked, otherwise it would need `force: true`
  67. cy.get('.btn').contains('Close').click()
  68. })
  69. cy.log('close left panel')
  70. cy.get('[id="left-menu"]').type('{esc}')
  71. cy.log('rewrite word to force spelling error')
  72. cy.get('.cm-line').type('{selectAll}{del}' + word + '{enter}')
  73. cy.get('.ol-cm-spelling-error').should('contain.text', word)
  74. })
  75. })
  76. describe('editor', () => {
  77. it('renders jpg', () => {
  78. cy.findByTestId('file-tree').findByText('frog.jpg').click()
  79. cy.get('[alt="frog.jpg"]')
  80. .should('be.visible')
  81. .and('have.prop', 'naturalWidth')
  82. .should('be.greaterThan', 0)
  83. })
  84. it('symbol palette', () => {
  85. createNewFile()
  86. cy.get('button[aria-label="Toggle Symbol Palette"]').click({
  87. force: true,
  88. })
  89. cy.get('button').contains('𝜉').click()
  90. cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
  91. 'contain.text',
  92. '\\xi'
  93. )
  94. cy.log('recompile to force flush and avoid "unsaved changes" prompt')
  95. recompile()
  96. })
  97. })
  98. describe('add new file to project', () => {
  99. beforeEach(() => {
  100. cy.get('button').contains('New file').click({ force: true })
  101. })
  102. testNewFileUpload()
  103. it('should not display import from URL', () => {
  104. cy.findByText('From external URL').should('not.exist')
  105. })
  106. })
  107. describe('left menu', () => {
  108. beforeEach(() => {
  109. cy.get('button').contains('Menu').click()
  110. })
  111. it('can download project sources', () => {
  112. cy.get('a').contains('Source').click()
  113. const zipName = projectName.replaceAll('-', '_')
  114. cy.task('readFileInZip', {
  115. pathToZip: `cypress/downloads/${zipName}.zip`,
  116. fileToRead: 'main.tex',
  117. }).should('contain', 'Your introduction goes here')
  118. })
  119. it('can download project PDF', () => {
  120. cy.log('ensure project is compiled')
  121. cy.get('.pdf-viewer').should('contain.text', 'Your Paper')
  122. cy.get('.nav-downloads').within(() => {
  123. cy.findByText('PDF').click()
  124. const pdfName = projectName.replaceAll('-', '_')
  125. cy.task('readPdf', `cypress/downloads/${pdfName}.pdf`).should(
  126. 'contain',
  127. 'Your introduction goes here'
  128. )
  129. })
  130. })
  131. it('word count', () => {
  132. cy.log('ensure project is compiled')
  133. cy.get('.pdf-viewer').should('contain.text', 'Your Paper')
  134. cy.findByText('Word Count').click()
  135. cy.get('#word-count-modal').within(() => {
  136. cy.findByText('Total Words:')
  137. cy.findByText('607')
  138. cy.findByText('Headers:')
  139. cy.findByText('14')
  140. cy.findByText('Math Inline:')
  141. cy.findByText('6')
  142. cy.findByText('Math Display:')
  143. cy.findByText('1')
  144. })
  145. })
  146. })
  147. describe('layout selector', () => {
  148. it('show editor only and switch between editor and pdf', () => {
  149. cy.get('.pdf-viewer').should('be.visible')
  150. cy.get('.cm-editor').should('be.visible')
  151. cy.findByText('Layout').click()
  152. cy.findByText('Editor only').click()
  153. cy.get('.pdf-viewer').should('not.be.visible')
  154. cy.get('.cm-editor').should('be.visible')
  155. cy.findByText('Switch to PDF').click()
  156. cy.get('.pdf-viewer').should('be.visible')
  157. cy.get('.cm-editor').should('not.be.visible')
  158. cy.findByText('Switch to editor').click()
  159. cy.get('.pdf-viewer').should('not.be.visible')
  160. cy.get('.cm-editor').should('be.visible')
  161. })
  162. it('show PDF only and go back to Editor & PDF', () => {
  163. cy.get('.pdf-viewer').should('be.visible')
  164. cy.get('.cm-editor').should('be.visible')
  165. cy.findByText('Layout').click()
  166. cy.findByText('PDF only').click()
  167. cy.get('.pdf-viewer').should('be.visible')
  168. cy.get('.cm-editor').should('not.be.visible')
  169. cy.findByText('Layout').click()
  170. cy.findByText('Editor & PDF').click()
  171. cy.get('.pdf-viewer').should('be.visible')
  172. cy.get('.cm-editor').should('be.visible')
  173. })
  174. it('PDF in a separate tab (tests editor only)', () => {
  175. cy.get('.pdf-viewer').should('be.visible')
  176. cy.get('.cm-editor').should('be.visible')
  177. cy.findByText('Layout').click()
  178. cy.findByText('PDF in separate tab').click()
  179. cy.get('.pdf-viewer').should('not.exist')
  180. cy.get('.cm-editor').should('be.visible')
  181. })
  182. })
  183. })
  184. function createRandomLetterString() {
  185. const chars = 'abcdefghijklmnopqrstuvwxyz'
  186. let result = ''
  187. for (let i = 0; i < 12; i++) {
  188. result += chars.charAt(Math.floor(Math.random() * chars.length))
  189. }
  190. return result
  191. }