diff --git a/server-ce/test/admin.spec.ts b/server-ce/test/admin.spec.ts index 944c5d8a8a..15df3d3639 100644 --- a/server-ce/test/admin.spec.ts +++ b/server-ce/test/admin.spec.ts @@ -14,8 +14,10 @@ describe('admin panel', function () { const deletedProjectName = `deleted-project-${uuid()}` let projectToDeleteId = '' - const findProjectRow = (projectName: string) => - cy.findByText(projectName).parent().parent() + const findProjectRow = (projectName: string) => { + cy.log('find project row') + return cy.findByText(projectName).parent().parent() + } startWith({ pro: true, diff --git a/server-ce/test/cypress.config.js b/server-ce/test/cypress.config.js index 050447f4ca..689cf5ce03 100644 --- a/server-ce/test/cypress.config.js +++ b/server-ce/test/cypress.config.js @@ -1,4 +1,5 @@ const { defineConfig } = require('cypress') +const { readPdf, readFileInZip } = require('./helpers/read-file') const specPattern = process.env.SPEC_PATTERN || './**/*.spec.{js,ts,tsx}' @@ -14,7 +15,10 @@ module.exports = defineConfig({ e2e: { baseUrl: 'http://localhost', setupNodeEvents(on, config) { - // implement node event listeners here + on('task', { + readPdf, + readFileInZip, + }) }, specPattern, }, diff --git a/server-ce/test/helpers/read-file.ts b/server-ce/test/helpers/read-file.ts new file mode 100644 index 0000000000..15d032ea4e --- /dev/null +++ b/server-ce/test/helpers/read-file.ts @@ -0,0 +1,52 @@ +import fs from 'fs' +import path from 'path' +import pdf from 'pdf-parse' +import AdmZip from 'adm-zip' +import { promisify } from 'util' + +const sleep = promisify(setTimeout) + +const MAX_ATTEMPTS = 15 +const POLL_INTERVAL = 500 + +type ReadFileInZipArgs = { + pathToZip: string + fileToRead: string +} + +export async function readFileInZip({ + pathToZip, + fileToRead, +}: ReadFileInZipArgs) { + let attempt = 0 + while (attempt < MAX_ATTEMPTS) { + if (fs.existsSync(pathToZip)) { + const zip = new AdmZip(path.resolve(pathToZip)) + const entry = zip + .getEntries() + .find(entry => entry.entryName == fileToRead) + if (entry) { + return entry.getData().toString('utf8') + } else { + throw new Error(`${fileToRead} not found in ${pathToZip}`) + } + } + await sleep(POLL_INTERVAL) + attempt++ + } + throw new Error(`${pathToZip} not found`) +} + +export async function readPdf(file: string) { + let attempt = 0 + while (attempt < MAX_ATTEMPTS) { + if (fs.existsSync(file)) { + const dataBuffer = fs.readFileSync(path.resolve(file)) + const { text } = await pdf(dataBuffer) + return text + } + await sleep(POLL_INTERVAL) + attempt++ + } + throw new Error(`${file} not found`) +} diff --git a/server-ce/test/package-lock.json b/server-ce/test/package-lock.json index 98c499c733..1400f1f87f 100644 --- a/server-ce/test/package-lock.json +++ b/server-ce/test/package-lock.json @@ -8,13 +8,17 @@ "dependencies": { "@isomorphic-git/lightning-fs": "^4.6.0", "@testing-library/cypress": "^10.0.1", + "@types/adm-zip": "^0.5.5", + "@types/pdf-parse": "^1.1.4", "@types/uuid": "^9.0.8", + "adm-zip": "^0.5.12", "body-parser": "^1.20.2", "celebrate": "^15.0.3", "cypress": "13.6.6", "express": "^4.19.2", "isomorphic-git": "^1.25.10", "js-yaml": "^4.1.0", + "pdf-parse": "^1.1.1", "typescript": "^5.0.4", "uuid": "^9.0.1" } @@ -321,6 +325,14 @@ "node": ">=14" } }, + "node_modules/@types/adm-zip": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.5.tgz", + "integrity": "sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/aria-query": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.3.tgz", @@ -330,11 +342,15 @@ "version": "18.18.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.8.tgz", "integrity": "sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ==", - "optional": true, "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/pdf-parse": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.4.tgz", + "integrity": "sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg==" + }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", @@ -371,6 +387,14 @@ "node": ">= 0.6" } }, + "node_modules/adm-zip": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz", + "integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==", + "engines": { + "node": ">=6.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -2427,6 +2451,11 @@ "node": ">= 0.6" } }, + "node_modules/node-ensure": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", + "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==" + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -2572,6 +2601,26 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pdf-parse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz", + "integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==", + "dependencies": { + "debug": "^3.1.0", + "node-ensure": "^0.0.0" + }, + "engines": { + "node": ">=6.8.1" + } + }, + "node_modules/pdf-parse/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -3238,8 +3287,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "optional": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universalify": { "version": "2.0.0", diff --git a/server-ce/test/package.json b/server-ce/test/package.json index 8ed54a100b..fed72133d5 100644 --- a/server-ce/test/package.json +++ b/server-ce/test/package.json @@ -11,13 +11,17 @@ "dependencies": { "@isomorphic-git/lightning-fs": "^4.6.0", "@testing-library/cypress": "^10.0.1", + "@types/adm-zip": "^0.5.5", + "@types/pdf-parse": "^1.1.4", "@types/uuid": "^9.0.8", + "adm-zip": "^0.5.12", "body-parser": "^1.20.2", "celebrate": "^15.0.3", "cypress": "13.6.6", "express": "^4.19.2", "isomorphic-git": "^1.25.10", "js-yaml": "^4.1.0", + "pdf-parse": "^1.1.1", "typescript": "^5.0.4", "uuid": "^9.0.1" } diff --git a/server-ce/test/project-list.spec.ts b/server-ce/test/project-list.spec.ts new file mode 100644 index 0000000000..54992f0751 --- /dev/null +++ b/server-ce/test/project-list.spec.ts @@ -0,0 +1,116 @@ +import { ensureUserExists, login } from './helpers/login' +import { createProject } from './helpers/project' +import { startWith } from './helpers/config' +import { v4 as uuid } from 'uuid' + +const WITHOUT_PROJECTS_USER = 'user-without-projects@example.com' +const REGULAR_USER = 'user@example.com' + +describe('Project List', () => { + startWith({ pro: true }) + + const findProjectRow = (projectName: string) => { + cy.log('find project row') + return cy.findByText(projectName).parent().parent() + } + + describe('user with no projects', () => { + ensureUserExists({ email: WITHOUT_PROJECTS_USER }) + + it("'Import from Github' is not displayed in the welcome page", () => { + login(WITHOUT_PROJECTS_USER) + cy.visit('/project') + cy.findByText('Create a new project').click() + cy.findByText(/Import from Github/i).should('not.exist') + }) + }) + + describe('user with projects', () => { + const projectName = `test-project-${uuid()}` + let projectId: string | undefined + ensureUserExists({ email: REGULAR_USER }) + + before(() => { + login(REGULAR_USER) + cy.visit('/project') + createProject(projectName, { type: 'Example Project' }).then( + id => (projectId = id) + ) + }) + + it('Can download project sources', () => { + login(REGULAR_USER) + cy.visit('/project') + + findProjectRow(projectName).within(() => + cy.get(`[aria-label="Download .zip file"]`).click() + ) + + cy.task('readFileInZip', { + pathToZip: `cypress/downloads/${projectName}.zip`, + fileToRead: 'main.tex', + }).should('contain', 'Your introduction goes here') + }) + + it('Can download project PDF', () => { + login(REGULAR_USER) + cy.visit('/project') + + findProjectRow(projectName).within(() => + cy.get(`[aria-label="Download PDF"]`).click() + ) + + const pdfName = projectName.replaceAll('-', '_') + cy.task('readPdf', `cypress/downloads/${pdfName}.pdf`).should( + 'contain', + 'Your introduction goes here' + ) + }) + + it('can assign and remove tags to projects', () => { + const tagName = uuid().slice(0, 7) // long tag names are truncated in the UI, which affects selectors + login(REGULAR_USER) + cy.visit('/project') + + cy.log('select project') + cy.get(`[id="select-project-${projectId}"`).click() + + cy.log('add tag to project') + cy.get('button[aria-label="Tags"]').click() + cy.findByText('Create new tag').click() + cy.get('input[name="new-tag-form-name"]').type(`${tagName}{enter}`) + cy.get(`button[aria-label="Select tag ${tagName}"]`) // tag label in project row + + cy.log('remove tag') + cy.get(`button[aria-label="Remove tag ${tagName}"]`) + .first() + .click({ force: true }) + cy.get(`button[aria-label="Select tag ${tagName}"]`).should('not.exist') + }) + + it('can filter by tag', () => { + cy.log('create a separate project to filter') + const nonTaggedProjectName = `project-${uuid()}` + login(REGULAR_USER) + cy.visit('/project') + createProject(nonTaggedProjectName) + cy.visit('/project') + + cy.log('select project') + cy.get(`[id="select-project-${projectId}"`).click() + + cy.log('add tag to project') + const tagName = uuid().slice(0, 7) // long tag names are truncated in the UI, which affects selectors + cy.get('button[aria-label="Tags"]').click() + cy.findByText('Create new tag').click() + cy.get('input[name="new-tag-form-name"]').type(`${tagName}{enter}`) + + cy.log( + 'check the non-tagged project is filtered out after clicking the tag' + ) + cy.findByText(nonTaggedProjectName).should('exist') + cy.get('button').contains(tagName).click({ force: true }) + cy.findByText(nonTaggedProjectName).should('not.exist') + }) + }) +})