project.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import { login } from './login'
  2. import { openEmail } from './email'
  3. import { v4 as uuid } from 'uuid'
  4. export function createProject(
  5. name: string,
  6. {
  7. type = 'Blank project',
  8. newProjectButtonMatcher = /new project/i,
  9. open = true,
  10. }: {
  11. type?: 'Blank project' | 'Example project'
  12. newProjectButtonMatcher?: RegExp
  13. open?: boolean
  14. } = {}
  15. ): Cypress.Chainable<string> {
  16. cy.url().then(url => {
  17. if (!url.endsWith('/project')) {
  18. cy.visit('/project')
  19. }
  20. })
  21. const interceptId = uuid()
  22. let projectId = ''
  23. if (!open) {
  24. cy.then(() => {
  25. // Register intercept just before creating the project, otherwise we might
  26. // intercept a request from a prior createProject invocation.
  27. cy.intercept(
  28. { method: 'GET', url: /\/project\/[a-fA-F0-9]{24}$/, times: 1 },
  29. req => {
  30. projectId = req.url.split('/').pop()!
  31. // Redirect back to the project dashboard, effectively reload the page.
  32. req.redirect('/project')
  33. }
  34. ).as(interceptId)
  35. })
  36. }
  37. cy.findAllByRole('button').contains(newProjectButtonMatcher).click()
  38. // FIXME: This should only look in the left menu
  39. // The upgrading tests create projects in older versions of Server Pro which used different casing of the project type. Use case-insensitive match.
  40. cy.findAllByText(type, { exact: false }).first().click()
  41. cy.findByRole('dialog').within(() => {
  42. cy.get('input').type(name)
  43. cy.findByText('Create').click()
  44. })
  45. if (open) {
  46. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  47. waitForMainDocToLoad()
  48. return cy
  49. .url()
  50. .should('match', /\/project\/[a-fA-F0-9]{24}/)
  51. .then(url => url.split('/').pop())
  52. } else {
  53. const alias = `@${interceptId}` // IDEs do not like computed values in cy.wait().
  54. cy.wait(alias)
  55. return cy.then(() => projectId)
  56. }
  57. }
  58. export function openProjectByName(projectName: string) {
  59. cy.visit('/project')
  60. cy.findByText(projectName).click()
  61. waitForMainDocToLoad()
  62. }
  63. export function openProjectById(projectId: string) {
  64. cy.visit(`/project/${projectId}`)
  65. waitForMainDocToLoad()
  66. }
  67. export function openProjectViaLinkSharingAsAnon(url: string) {
  68. cy.visit(url)
  69. waitForMainDocToLoad()
  70. }
  71. export function openProjectViaLinkSharingAsUser(
  72. url: string,
  73. projectName: string,
  74. email: string
  75. ) {
  76. cy.visit(url)
  77. cy.findByText(projectName) // wait for lazy loading
  78. cy.contains(`as ${email}`)
  79. cy.findByText('OK, join project').click()
  80. waitForMainDocToLoad()
  81. }
  82. export function openProjectViaInviteNotification(projectName: string) {
  83. cy.visit('/project')
  84. cy.findByText(projectName)
  85. .parent()
  86. .parent()
  87. .within(() => {
  88. cy.findByText('Join Project').click()
  89. })
  90. cy.findByText('Open Project').click()
  91. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  92. waitForMainDocToLoad()
  93. }
  94. function shareProjectByEmail(
  95. projectName: string,
  96. email: string,
  97. level: 'Viewer' | 'Editor'
  98. ) {
  99. openProjectByName(projectName)
  100. cy.findByText('Share').click()
  101. cy.findByRole('dialog').within(() => {
  102. cy.findByLabelText('Add people', { selector: 'input' }).type(`${email},`)
  103. cy.findByLabelText('Add people', { selector: 'input' })
  104. .parents('form')
  105. .within(() => {
  106. cy.findByTestId('add-collaborator-select')
  107. .click()
  108. .then(() => {
  109. cy.findByText(level).click()
  110. })
  111. })
  112. cy.findByText('Invite').click({ force: true })
  113. cy.findByText('Invite not yet accepted.')
  114. })
  115. }
  116. export function shareProjectByEmailAndAcceptInviteViaDash(
  117. projectName: string,
  118. email: string,
  119. level: 'Viewer' | 'Editor'
  120. ) {
  121. shareProjectByEmail(projectName, email, level)
  122. login(email)
  123. openProjectViaInviteNotification(projectName)
  124. }
  125. export function getSpamSafeProjectName() {
  126. while (true) {
  127. // Move from hex/16 to base64/64 possible characters per char in string
  128. const name = Buffer.from(uuid().replaceAll('-', ''), 'hex')
  129. .toString('base64')
  130. .slice(0, 10)
  131. const nDigits = (name.match(/\d/g) || []).length
  132. if (nDigits < 6) return name
  133. }
  134. }
  135. export function shareProjectByEmailAndAcceptInviteViaEmail(
  136. projectName: string,
  137. email: string,
  138. level: 'Viewer' | 'Editor'
  139. ) {
  140. shareProjectByEmail(projectName, email, level)
  141. login(email)
  142. openEmail(projectName, frame => {
  143. frame.contains('View project').then(a => {
  144. cy.log(
  145. 'bypass target=_blank and navigate current browser tab/cypress-iframe to project invite'
  146. )
  147. cy.visit(a.attr('href')!)
  148. })
  149. })
  150. cy.url().should('match', /\/project\/[a-f0-9]+\/invite\/token\/[a-f0-9]+/)
  151. cy.findByText(/user would like you to join/)
  152. cy.contains(new RegExp(`You are accepting this invite as ${email}`))
  153. cy.findByText('Join Project').click()
  154. waitForMainDocToLoad()
  155. }
  156. export function enableLinkSharing() {
  157. let linkSharingReadOnly: string
  158. let linkSharingReadAndWrite: string
  159. const origin = new URL(Cypress.config().baseUrl!).origin
  160. waitForMainDocToLoad()
  161. cy.findByText('Share').click()
  162. cy.findByText('Turn on link sharing').click()
  163. cy.findByText('Anyone with this link can view this project')
  164. .next()
  165. .should('contain.text', origin + '/read')
  166. .then(el => {
  167. linkSharingReadOnly = el.text()
  168. })
  169. cy.findByText('Anyone with this link can edit this project')
  170. .next()
  171. .should('contain.text', origin + '/')
  172. .then(el => {
  173. linkSharingReadAndWrite = el.text()
  174. })
  175. return cy.then(() => {
  176. return { linkSharingReadOnly, linkSharingReadAndWrite }
  177. })
  178. }
  179. export function waitForMainDocToLoad() {
  180. cy.log('Wait for main doc to load; it will steal the focus after loading')
  181. cy.get('.cm-content').should('contain.text', 'Introduction')
  182. }
  183. export function openFile(fileName: string, waitFor: string) {
  184. // force: The file-tree pane is too narrow to display the full name.
  185. cy.findByTestId('file-tree').findByText(fileName).click({ force: true })
  186. // wait until we've switched to the selected file
  187. cy.findByText('Loading…').should('not.exist')
  188. cy.findByText(waitFor)
  189. }
  190. export function createNewFile() {
  191. const fileName = `${uuid()}.tex`
  192. cy.log('create new project file')
  193. cy.get('button').contains('New file').click({ force: true })
  194. cy.findByRole('dialog').within(() => {
  195. cy.get('input').clear()
  196. cy.get('input').type(fileName)
  197. cy.findByText('Create').click()
  198. })
  199. // force: The file-tree pane is too narrow to display the full name.
  200. cy.findByTestId('file-tree').findByText(fileName).click({ force: true })
  201. // wait until we've switched to the newly created empty file
  202. cy.findByText('Loading…').should('not.exist')
  203. cy.get('.cm-line').should('have.length', 1)
  204. return fileName
  205. }
  206. export function expectFileExists(
  207. name: string,
  208. binary: boolean,
  209. content: string
  210. ) {
  211. cy.findByRole('treeitem', { name }).click()
  212. if (binary) {
  213. cy.findByText(content).should('not.have.class', 'cm-line')
  214. } else {
  215. cy.findByText(content).should('have.class', 'cm-line')
  216. }
  217. }
  218. export function prepareFileUploadTest(binary = false) {
  219. const name = `${uuid()}.txt`
  220. const content = `Test File Content ${name}${binary ? ' \x00' : ''}`
  221. cy.get('button').contains('Upload').click({ force: true })
  222. cy.get('input[type=file]')
  223. .first()
  224. .selectFile(
  225. {
  226. contents: Cypress.Buffer.from(content),
  227. fileName: name,
  228. lastModified: Date.now(),
  229. },
  230. { force: true }
  231. )
  232. // wait for the upload to finish
  233. cy.findByRole('treeitem', { name })
  234. return () => expectFileExists(name, binary, content)
  235. }
  236. export function testNewFileUpload() {
  237. it('can upload text file', () => {
  238. const check = prepareFileUploadTest(false)
  239. check()
  240. })
  241. it('can upload binary file', () => {
  242. const check = prepareFileUploadTest(true)
  243. check()
  244. })
  245. }