editor.spec.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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.findByRole('textbox', { name: /Source Editor editing/i }).should(
  90. 'contain.text',
  91. '\\xi'
  92. )
  93. cy.log('recompile to force flush and avoid "unsaved changes" prompt')
  94. recompile()
  95. })
  96. })
  97. describe('add new file to project', () => {
  98. beforeEach(() => {
  99. cy.get('button').contains('New file').click({ force: true })
  100. })
  101. it('can upload file', () => {
  102. const name = `${uuid()}.txt`
  103. const content = `Test File Content ${name}`
  104. cy.get('button').contains('Upload').click({ force: true })
  105. cy.get('input[type=file]')
  106. .first()
  107. .selectFile(
  108. {
  109. contents: Cypress.Buffer.from(content),
  110. fileName: name,
  111. lastModified: Date.now(),
  112. },
  113. { force: true }
  114. )
  115. // force: The file-tree pane is too narrow to display the full name.
  116. cy.findByTestId('file-tree').findByText(name).click({ force: true })
  117. cy.findByText(content)
  118. })
  119. it('should not display import from URL', () => {
  120. cy.findByText('From external URL').should('not.exist')
  121. })
  122. })
  123. describe('left menu', () => {
  124. beforeEach(() => {
  125. cy.get('button').contains('Menu').click()
  126. })
  127. it('can download project sources', () => {
  128. cy.get('a').contains('Source').click()
  129. const zipName = projectName.replaceAll('-', '_')
  130. cy.task('readFileInZip', {
  131. pathToZip: `cypress/downloads/${zipName}.zip`,
  132. fileToRead: 'main.tex',
  133. }).should('contain', 'Your introduction goes here')
  134. })
  135. it('can download project PDF', () => {
  136. cy.log('ensure project is compiled')
  137. cy.get('.pdf-viewer').should('contain.text', 'Your Paper')
  138. cy.get('.nav-downloads').within(() => {
  139. cy.findByText('PDF').click()
  140. const pdfName = projectName.replaceAll('-', '_')
  141. cy.task('readPdf', `cypress/downloads/${pdfName}.pdf`).should(
  142. 'contain',
  143. 'Your introduction goes here'
  144. )
  145. })
  146. })
  147. it('word count', () => {
  148. cy.log('ensure project is compiled')
  149. cy.get('.pdf-viewer').should('contain.text', 'Your Paper')
  150. cy.findByText('Word Count').click()
  151. cy.get('#word-count-modal').within(() => {
  152. cy.findByText('Total Words:')
  153. cy.findByText('607')
  154. cy.findByText('Headers:')
  155. cy.findByText('14')
  156. cy.findByText('Math Inline:')
  157. cy.findByText('6')
  158. cy.findByText('Math Display:')
  159. cy.findByText('1')
  160. })
  161. })
  162. })
  163. describe('layout selector', () => {
  164. it('show editor only and switch between editor and pdf', () => {
  165. cy.get('.pdf-viewer').should('be.visible')
  166. cy.get('.cm-editor').should('be.visible')
  167. cy.findByText('Layout').click()
  168. cy.findByText('Editor only').click()
  169. cy.get('.pdf-viewer').should('not.be.visible')
  170. cy.get('.cm-editor').should('be.visible')
  171. cy.findByText('Switch to PDF').click()
  172. cy.get('.pdf-viewer').should('be.visible')
  173. cy.get('.cm-editor').should('not.be.visible')
  174. cy.findByText('Switch to editor').click()
  175. cy.get('.pdf-viewer').should('not.be.visible')
  176. cy.get('.cm-editor').should('be.visible')
  177. })
  178. it('show PDF only and go back to Editor & PDF', () => {
  179. cy.get('.pdf-viewer').should('be.visible')
  180. cy.get('.cm-editor').should('be.visible')
  181. cy.findByText('Layout').click()
  182. cy.findByText('PDF only').click()
  183. cy.get('.pdf-viewer').should('be.visible')
  184. cy.get('.cm-editor').should('not.be.visible')
  185. cy.findByText('Layout').click()
  186. cy.findByText('Editor & PDF').click()
  187. cy.get('.pdf-viewer').should('be.visible')
  188. cy.get('.cm-editor').should('be.visible')
  189. })
  190. it('PDF in a separate tab (tests editor only)', () => {
  191. cy.get('.pdf-viewer').should('be.visible')
  192. cy.get('.cm-editor').should('be.visible')
  193. cy.findByText('Layout').click()
  194. cy.findByText('PDF in separate tab').click()
  195. cy.get('.pdf-viewer').should('not.exist')
  196. cy.get('.cm-editor').should('be.visible')
  197. })
  198. })
  199. })
  200. function createRandomLetterString() {
  201. const chars = 'abcdefghijklmnopqrstuvwxyz'
  202. let result = ''
  203. for (let i = 0; i < 12; i++) {
  204. result += chars.charAt(Math.floor(Math.random() * chars.length))
  205. }
  206. return result
  207. }