editor.spec.ts 7.6 KB

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